2. 嵌入式GUI基础

渲染管线,说白了就是GUI从“我要画什么”到“屏幕上显示出来”的完整路径。这条路走得好不好,直接决定了你的界面是60帧丝滑,还是卡成PPT。

我这些年调过的嵌入式GUI项目,十有八九的性能问题都出在渲染管线上。不是代码写得不对,而是没搞清楚底层到底怎么跑的。今天咱们就把这块掰开揉碎了讲。

绘制调用合并:减少CPU与GPU的“对话”次数

每次绘制调用(Draw Call),CPU都要打包一堆指令发给GPU。这个开销可不小。你想想看,CPU发一次指令,GPU才能开始干活。如果一帧里有几百次Draw Call,CPU光忙着发指令了,哪还有时间做别的?

核心思路:把多个小绘制请求合并成一个大请求。

合并前后的对比

指标合并前(10次Draw Call)合并后(1次Draw Call)
CPU耗时~500μs~50μs
GPU空闲等待高低
帧率稳定性波动大稳定

怎么合并?我常用的方法有这几种:

  • 纹理图集(Texture Atlas):把多个小图标拼成一张大图。这样原本需要多次切换纹理的绘制,一次就能搞定。
  • 顶点缓冲合并:把多个几何体的顶点数据攒到一起,一次性提交。
  • 状态排序:按渲染状态(纹理、着色器、混合模式)排序,相同状态的绘制请求挨在一起,减少状态切换。

我的经验:在LVGL项目中,我曾经把一帧的Draw Call从120次降到18次,帧率直接从22fps飙到55fps。关键就是用了纹理图集+状态排序。别小看这个优化,效果立竿见影。

Overdraw检测与消除:别画那些看不见的东西

Overdraw,就是同一个像素被反复绘制了多次。说白了,你画了一层又一层,但用户只能看到最上面那层。底下的全白画了。

为什么会这样?最常见的原因是:

  • 背景层叠太多,透明窗口互相覆盖
  • 控件背景色和父容器背景色重复绘制
  • 动画过程中,新旧帧的像素没有正确裁剪

检测方法:我习惯用两种方式。

  1. 颜色混合模式检测:把渲染模式改成“累加混合”,Overdraw越严重的地方越亮。亮到发白的地方,就是重灾区。
  2. 性能计数器:很多GPU驱动提供了“像素填充率”和“片段着色器调用次数”的计数器。这两个数值远高于屏幕分辨率时,说明Overdraw严重。

我曾经踩过的坑:在一个智能手表项目里,表盘背景用了三层渐变叠加。看起来效果不错,但每帧的Overdraw达到了4.5倍。也就是说,每个像素被画了4.5次。后来改成单层渐变+遮罩,Overdraw降到1.1倍,电池续航直接多了2小时。

消除策略:

  • 裁剪区域(Clip Region):只绘制脏区域,不重绘整个窗口。
  • 层级优化:不透明的控件放在底层,透明的放上层。避免透明层下面堆叠太多内容。
  • 背景合并:如果子控件完全覆盖父控件,就别画父控件的背景了。

脏矩形更新策略:只画变化的部分

这个策略,说白了就是“懒”。屏幕那么大,每次只更新变化的那一小块区域,其他部分保持不动。

基本原理:

  1. 标记所有发生变化的区域为“脏矩形”
  2. 合并相邻或重叠的脏矩形,减少绘制次数
  3. 只对合并后的脏矩形区域执行渲染
  4. 将渲染结果合成到帧缓冲中

我见过很多团队在这个环节翻车。最常见的问题是脏矩形合并算法写得不对,导致合并后的区域比实际变化区域大好几倍。那还不如全屏重绘呢。

脏矩形合并的黄金法则

  • 两个矩形如果水平或垂直间距小于4像素,直接合并
  • 合并后的矩形面积如果超过屏幕面积的60%,干脆全屏重绘
  • 动画过程中,脏矩形要包含动画的起始和结束位置

实际项目中,我一般用四叉树来管理脏矩形。每个节点记录自己的脏状态,更新时只遍历受影响的子树。这样复杂度从O(n)降到O(log n)。

// 伪代码:脏矩形合并
Rect mergeDirtyRects(Rect a, Rect b) {
    // 如果两个矩形不相交且距离较远,不合并
    if (distance(a, b) > THRESHOLD) return invalid;
    // 合并:取两个矩形的最小包围盒
    return Rect(
        min(a.x, b.x),
        min(a.y, b.y),
        max(a.x + a.w, b.x + b.w) - min(a.x, b.x),
        max(a.y + a.h, b.y + b.h) - min(a.y, b.y)
    );
}

硬件加速层利用:让GPU干它该干的活

很多嵌入式开发者对硬件加速有误解。觉得只要打开了硬件加速,性能就自动好了。其实不是这么回事。

硬件加速的本质:把CPU不适合干的活(大量并行计算、像素填充、纹理采样)交给GPU。但前提是,你得让GPU干得动。

我总结了几条硬件加速的使用原则:

  • 纹理格式要对:GPU对某些格式有硬件优化。比如RGBA8888比ARGB1555快得多,但占内存也大。得权衡。
  • 避免频繁创建/销毁纹理:GPU纹理的创建开销很大。我习惯用纹理池,复用已有的纹理对象。
  • 着色器要简单:复杂的片段着色器会让GPU的并行优势荡然无存。能用固定管线完成的,就别上着色器。
  • 帧缓冲对象(FBO)要少切换:每切换一次FBO,GPU都要清空管线。能在一个FBO里完成的,别拆成多个。

我的建议:在项目初期就确定好硬件加速的策略。别等到性能出问题了再临时加。我见过一个团队,产品都快量产了才发现GPU没使上劲,结果改了一个月才搞定。

本章知识体系总览

下面这张图,把渲染管线优化的四个核心方向串起来了。你可以把它当作调优时的检查清单。

渲染管线优化知识体系 渲染管线优化 绘制调用合并 Overdraw检测与消除 脏矩形更新策略 硬件加速层利用 纹理图集 顶点缓冲合并 裁剪区域 层级优化 矩形合并 四叉树管理 纹理格式 纹理池复用 着色器简化 FBO少切换 四个方向相互影响,建议按顺序逐一排查

这四个方向,每个都能单独拿出来做深度优化。但实际项目中,它们往往是相互影响的。比如Draw Call合并做得好,Overdraw自然就少了;脏矩形策略用对了,硬件加速的压力也小了。

我个人的习惯是:先抓Draw Call合并,这是性价比最高的。然后做Overdraw检测,把明显的浪费砍掉。接着优化脏矩形策略,让每次更新都精准。最后才是硬件加速的调优,因为这块涉及硬件特性,改起来比较麻烦。

嗯,渲染管线这块就讲这么多。记住一句话:别让GPU闲着,也别让CPU累着。找到那个平衡点,你的GUI性能就稳了。