脏矩形细粒度优化指南¶
概述¶
在 EmbeddedGUI 中,默认的 egui_view_invalidate() 会把整个控件的 region_screen 标记为脏区域。这对布局变化最安全,但在“控件内部只有一小块内容发生变化”的场景里,会带来额外的重绘面积、PFB 分块遍历和 on_draw() 计算开销。
这类场景后续可以继续做细粒度优化:由控件自己定义一组稳定的子区域,把它们当成控件内部的“自定义脏矩形矩阵”来使用。这里的“矩阵”本质上仍然是一组矩形,只是这组矩形由控件作者按业务语义手工拆分,而不是让框架自动推导任意形状。
当前框架已经具备以下能力:
egui_view_invalidate_region():只让控件内部的一个局部区域失效egui_view_invalidate_sub_region():通过预定义的子区域表失效固定分区egui_canvas_is_region_active():在on_draw()内判断当前子元素是否落在当前 PFB tile 中,避免无关绘制计算EGUI_CONFIG_DEBUG_DIRTY_REGION_STATS:输出每帧脏区面积和 PFB tile 统计scripts/perf_analysis/dirty_region_stats_report.py:把日志自动汇总成 Markdown / CSV 报告
注意:这套方法优化的是脏区面积和绘制计算量。最终帧时间仍应通过 QEMU 性能测试确认,PC 录制结果更适合用来观察脏区覆盖是否缩小。
什么时候值得做¶
高频、小面积变化:例如光标闪烁、按钮按压态、单个网格项选中态
控件内部结构稳定:例如上下按钮区、中间数值区、日期格子区
子元素边界容易计算:例如固定 cell、固定 icon、固定 badge、固定 progress 段
on_draw()内部本身存在明显分区,可以按区域跳过计算
什么时候不建议做¶
布局重新计算:尺寸、位置、padding、换行范围发生变化时
文本重排:内容长度变化导致整体 text layout 变化时
大范围翻页/切月/切主题:旧区域和新区域整体差异较大时
控件内部几何关系难以稳定描述,维护成本高于收益时
这类场景优先继续使用 egui_view_invalidate() 做整控件失效。
可用工具¶
工具 |
作用 |
适用场景 |
|---|---|---|
|
让整个控件失效 |
布局变化、文本重排、全量刷新 |
|
让控件局部区域失效 |
光标、局部高亮、单个子元素变化 |
|
通过固定索引失效子区域 |
网格、分段按钮、仪表盘固定分区 |
|
判断子区域是否与当前 PFB tile 相交 |
|
|
输出每帧脏区统计 |
观察优化是否真正缩小了脏区 |
|
汇总日志为 Markdown / CSV |
多个场景横向对比、留档 |
推荐实现模式¶
模式 1:直接计算局部脏区¶
适合少量、动态但仍可计算的位置。
static void my_widget_invalidate_icon(egui_view_t *self, egui_dim_t x, egui_dim_t y)
{
EGUI_REGION_DEFINE(dirty_region, x, y, 16, 16);
egui_view_invalidate_region(self, &dirty_region);
}
要点:
dirty_region使用控件本地坐标框架会自动转换到屏幕坐标并裁剪到
region_screen如果旧状态和新状态位置不同,需要同时失效旧区域和新区域
模式 2:预定义子区域表¶
适合固定分区明显的控件,例如九宫格、分栏按钮、步骤条、日历格子。
enum
{
MY_WIDGET_REGION_LEFT,
MY_WIDGET_REGION_CENTER,
MY_WIDGET_REGION_RIGHT,
};
static egui_sub_region_t s_regions[] = {
{ .region = {{0, 0}, {40, 32}} },
{ .region = {{40, 0}, {80, 32}} },
{ .region = {{120, 0}, {40, 32}} },
};
static const egui_sub_region_table_t s_region_table = {
.regions = s_regions,
.count = sizeof(s_regions) / sizeof(s_regions[0]),
};
状态变化时:
egui_view_invalidate_sub_region(self, &s_region_table, MY_WIDGET_REGION_CENTER);
推荐把子区域表当成控件内部的“脏矩形矩阵”:每个分区都保持语义稳定、边界固定,后续维护成本最低。
模式 3:绘制阶段配合 PFB 早裁剪¶
仅缩小 dirty area 还不够,如果 on_draw() 仍然无条件遍历所有子元素,CPU 计算开销还是会留下来。
EGUI_REGION_DEFINE(icon_screen_region,
region.location.x + icon_x,
region.location.y + icon_y,
16,
16);
if (egui_canvas_is_region_active(&icon_screen_region))
{
draw_icon(...);
}
推荐只对以下“计算不便宜”的子元素做检查:
文本测量、字符串格式化
渐变、阴影、圆弧等较重的绘制逻辑
大量循环中的单元格/节点/子项
对于非常便宜的直线、分隔线,通常可以不必过度拆分。
设计原则¶
1. 按业务语义拆分,不按像素极限拆分¶
优先拆成“光标区 / 文本区 / 日期格子区 / 上下按钮区”这种稳定的语义块,而不是为了追求最小面积把控件拆成大量零碎矩形。矩形越多,维护成本越高,合并和遍历成本也越高。
2. 同时考虑“失效”和“绘制”¶
只做 invalidate_region() 而不做 egui_canvas_is_region_active(),通常只能减少 dirty area,不能完全减少 on_draw() 内部的无效计算。最佳实践是两边一起做。
3. 旧状态和新状态都要覆盖¶
选择态、焦点态、拖动位置、光标位置这类状态切换,经常不是只刷“新位置”就够了,旧位置留下的视觉残影也必须失效。
4. 布局变化时及时回退到全量 invalidate¶
以下情况应优先回退为 egui_view_invalidate():
文本长度变化导致换行或滚动范围变化
月份/年份切换导致整个网格内容变化
控件尺寸变化、padding 变化、字体变化
多个子区域会一起失效,局部优化已经不划算
5. 统计值只作为“覆盖率证据”¶
dirty_area、dirty_ratio_percent、pfb_tiles 证明的是“刷新的面积是不是变小了”,不是最终 FPS。真正的时间收益仍然要看 QEMU 上的性能基准。
当前优化案例¶
下面的数据来自 perf_output/dirty_region_stats/hello_basic_targets.md,反映的是脏区覆盖率,不是 QEMU 帧时间:
控件 |
触发场景 |
细粒度策略 |
观测结果 |
|---|---|---|---|
|
光标闪烁 |
只失效光标区域,并在 |
最优局部帧 |
|
按压态 / 数值更新 |
拆成上、中、下三区域 |
局部刷新平均占全屏 |
|
日期切换 |
只失效旧日期格和新日期格 |
局部刷新约占全屏 |
这些案例说明:一旦控件内部存在稳定的局部分区,手工定义脏矩形矩阵通常比整控件失效更有效。
观测与验证流程¶
1. 开启脏区统计¶
构建时打开:
make all APP=HelloBasic APP_SUB=textinput PORT=pc \
USER_CFLAGS="-DEGUI_CONFIG_FUNCTION_RECORDING_TEST=1 -DEGUI_CONFIG_DEBUG_DIRTY_REGION_STATS=1"
2. 录制运行日志¶
运行 PC 录制,让日志里输出 DIRTY_REGION_STATS:。
3. 生成汇总报告¶
python scripts/perf_analysis/dirty_region_stats_report.py \
--input textinput=perf_output/dirty_region_logs/textinput.log \
--input number_picker=perf_output/dirty_region_logs/number_picker.log \
--input mini_calendar=perf_output/dirty_region_logs/mini_calendar.log \
--output-prefix perf_output/dirty_region_stats/hello_basic_targets
脚本会生成:
<prefix>.md<prefix>.csv
脚本支持 PC 录制产生的 UTF-8 / UTF-16 日志,适合直接留档和横向比较。
4. 检查三类指标¶
dirty_area / screen_area:局部刷新面积是否真的缩小pfb_tiles:同一帧涉及的 PFB 分块数量是否下降best partial frame / worst partial frame:最好和最差局部帧是否符合预期
5. 最终仍用 QEMU 验证性能¶
如果要确认“时间上到底快了多少”,仍然要回到 QEMU 基准测试。PC 录制适合看脏区覆盖,QEMU 适合看真实耗时。
开发检查清单¶
是否真的只有控件内部局部区域在变化
是否优先选择了稳定、可维护的子区域划分
是否同时处理了旧状态和新状态的失效
on_draw()内是否为高成本子元素增加了egui_canvas_is_region_active()判断是否保留了回退到
egui_view_invalidate()的路径是否检查了运行截图,确认没有残影、缺刷和布局错乱
是否输出了
DIRTY_REGION_STATS并生成对比报告是否在 QEMU 上做了最终性能确认