Virtual Stage¶
概览¶
virtual_stage 面向“大画布 + 大量绝对定位节点 + 少量活跃交互控件”的页面场景。
它解决的不是长列表滚动复用,而是这类固定大页面问题:
一个页面上同时摆放几十到上百个节点
大多数节点只负责显示,不值得长期常驻完整控件实例
少量节点需要交互、焦点、输入态、展开态或短时动画
设备 SRAM 紧张,不能让每个节点都长期持有一份
view
和 virtual_viewport / virtual_list 的区别:
virtual_viewport解决“滚动窗口里的列表项复用”virtual_stage解决“固定页面中的节点按需 materialize”
推荐阅读顺序:
先看
example/HelloVirtual/virtual_stage_basic/,这是推荐入门 case,聚焦最小接法和 helper API。再看
example/HelloVirtual/virtual_stage_showcase/,这是 showcase 风格的对比 case,适合和HelloShowcase并排理解“render-only panel + 真实 widget”怎么混合组织。最后看
example/HelloVirtual/virtual_stage/,这是复杂 cockpit 场景,覆盖更多节点类型、keepalive 和状态恢复。回到本文,对照
adapter、slot、pin、save/restore 的设计细节。
适用场景¶
工业驾驶舱、产线看板、设备总览页
大尺寸配置页、工位页、状态大盘
节点很多,但真正需要交互的节点很少
希望把状态尽量放到业务层,而不是全压在控件实例内部
不太适合的场景:
主要矛盾是长列表滚动,优先考虑
virtual_viewport页面上绝大多数节点都要同时可编辑并长期活跃
控件动画强依赖内部瞬时状态,但业务层又不愿意外置保存
核心模型¶
节点描述 egui_virtual_stage_node_desc_t¶
每个节点由一个轻量描述结构体定义:
字段 |
说明 |
|---|---|
|
节点在 page 内的绝对区域 |
|
稳定 ID,用于 pin、状态恢复、定向刷新 |
|
需要 materialize 时创建的控件类型;render-only 节点填 |
|
命中测试与绘制顺序 |
|
交互、保活、隐藏等标记 |
建议:
stable_id一旦对外暴露,就保持稳定,不要跟着数组重排变化render-only 节点统一走
draw_node只有真的要交互的节点才分配
view_type
Adapter¶
virtual_stage 不理解业务语义,所有业务接入都通过 adapter 完成。
常用回调:
回调 |
是否建议实现 |
用途 |
|---|---|---|
|
必需 |
返回节点数 |
|
必需 |
返回指定节点描述 |
|
交互节点必需 |
按 |
|
交互节点必需 |
销毁真实控件 |
|
必需 |
把业务数据绑定到 live view |
|
强烈建议 |
render-only 节点绘制入口 |
|
可选 |
自定义命中区域 |
|
推荐 |
保存 textinput / slider / combobox 等状态 |
|
推荐 |
恢复交互状态 |
|
推荐 |
控制展开态、焦点态、动画态是否保活 |
如果节点集合本身就是固定数组,推荐直接用 array bridge:
EGUI_VIEW_VIRTUAL_STAGE_DESC_ARRAY_SOURCE_INIT(...)EGUI_VIEW_VIRTUAL_STAGE_NODE_ARRAY_SOURCE_INIT(...)EGUI_VIEW_VIRTUAL_STAGE_ARRAY_OPS_SIMPLE_INIT(...)EGUI_VIEW_VIRTUAL_STAGE_ARRAY_OPS_SIMPLE_CONST_INIT(...)EGUI_VIEW_VIRTUAL_STAGE_ARRAY_OPS_INTERACTIVE_INIT(...)EGUI_VIEW_VIRTUAL_STAGE_ARRAY_OPS_INTERACTIVE_CONST_INIT(...)EGUI_VIEW_VIRTUAL_STAGE_ARRAY_OPS_STATEFUL_INIT(...)EGUI_VIEW_VIRTUAL_STAGE_ARRAY_OPS_STATEFUL_CONST_INIT(...)EGUI_VIEW_VIRTUAL_STAGE_NODE_ARRAY_SIMPLE_BRIDGE_INIT_WITH_LIMIT(...)EGUI_VIEW_VIRTUAL_STAGE_NODE_ARRAY_INTERACTIVE_BRIDGE_INIT_WITH_LIMIT(...)EGUI_VIEW_VIRTUAL_STAGE_NODE_ARRAY_STATEFUL_BRIDGE_INIT_WITH_LIMIT(...)EGUI_VIEW_VIRTUAL_STAGE_DESC_ARRAY_BRIDGE_INIT(...)EGUI_VIEW_VIRTUAL_STAGE_NODE_ARRAY_BRIDGE_INIT(...)EGUI_VIEW_VIRTUAL_STAGE_ARRAY_BRIDGE_INIT(...)EGUI_VIEW_VIRTUAL_STAGE_NODE_ARRAY_BRIDGE_INIT_WITH_LIMIT(...)EGUI_VIEW_VIRTUAL_STAGE_NODE_ARRAY_SCREEN_BRIDGE_INIT_WITH_LIMIT(...)EGUI_VIEW_VIRTUAL_STAGE_AS_VIEW(...)EGUI_VIEW_VIRTUAL_STAGE_INIT_ARRAY_BRIDGE(...)EGUI_VIEW_VIRTUAL_STAGE_ADD_ROOT(...)EGUI_VIEW_VIRTUAL_STAGE_SET_BACKGROUND(...)EGUI_VIEW_VIRTUAL_STAGE_REQUEST_LAYOUT(...)EGUI_VIEW_VIRTUAL_STAGE_NOTIFY_NODE(...)EGUI_VIEW_VIRTUAL_STAGE_NOTIFY_IDS(...)EGUI_VIEW_VIRTUAL_STAGE_NOTIFY_NODES(...)EGUI_VIEW_VIRTUAL_STAGE_NOTIFY_BOUNDS_IDS(...)EGUI_VIEW_VIRTUAL_STAGE_TOGGLE_PIN(...)EGUI_VIEW_VIRTUAL_STAGE_SLOT_COUNT(...)egui_view_virtual_stage_init_with_array_bridge(...)
这样可以省掉重复的 get_count / get_desc 样板,也不用再手工拼 params + node_source + adapter + setup,只保留 create / bind / draw / hit_test / should_keep_alive 这些真正和业务有关的部分。
第一次接入时可以先按这条最小判断来写:
必需:
create_view、bind_viewrender-only 节点推荐实现:
draw_node需要跨回收恢复状态时再补:
save_state、restore_state需要展开态/焦点态短时保活时再补:
should_keep_alive命中区域不是普通矩形时再补:
hit_test
缺省行为也可以先记住:
不写
hit_test时,默认按节点矩形区域命中不写
should_keep_alive时,默认不额外保活不写
destroy_view/unbind_view/save_state/restore_state时,框架不会替你补业务状态逻辑如果 stage 就是整屏页面,优先用
NODE_ARRAY_SCREEN_*_BRIDGE_INIT_WITH_LIMIT(...),少写一组屏幕尺寸参数
Slot 池模型¶
virtual_stage 内部维护固定数量的 live slot,真正占 SRAM 的不是“页面节点数”,而是“当前活跃 slot 数”。
典型 slot 状态:
ACTIVE:当前命中或当前正在交互KEEPALIVE:因为 pin / focus / adapter keepalive 而继续保留POOLED:暂未绑定目标节点,但可以复用UNUSED:空闲 slot
内存思路:
传统方案:
100 节点 * 100 份 view 实例virtual_stage:live_slot_limit * 平均 view 大小 + 业务状态
所以节点越多、交互节点占比越低,收益越明显。
生命周期¶
render-only 节点¶
如果节点只是显示:
view_type = EGUI_VIEW_VIRTUAL_STAGE_VIEW_TYPE_NONE不创建真实控件
由
adapter->draw_node()直接绘制
这种节点几乎不占控件实例 SRAM,适合:
KPI 卡片
badge / chip / status
复杂业务摘要卡
图表缩略态
交互节点按需 materialize¶
如果节点需要交互:
用户点击命中节点
virtual_stage依据view_type选择或创建 slot调用
bind_view如有历史状态,再调
restore_state将事件派发给真实控件
释放与状态保存¶
当节点不再需要常驻时:
virtual_stage调用save_state如实现了
unbind_view,先做解绑slot 被回收或复用
建议把这些状态保存在业务层:
文本内容
当前选项
当前数值
当前展开项
动画进度或业务计时
公开接口逐项说明¶
以下接口定义见 src/widget/egui_view_virtual_stage.h。
egui_view_virtual_stage_apply_params¶
作用:把 egui_view_virtual_stage_params_t 应用到一个已初始化的 virtual_stage。
典型用途:
更新 page 区域
更新初始
live_slot_limit在二次初始化前复用同一个对象
注意:
它只应用参数,不负责完整初始化内部结构
正常首次接入更推荐直接使用
init_with_params
egui_view_virtual_stage_init_with_params¶
作用:初始化 virtual_stage,并同时应用 region、live_slot_limit。
最常用接入方式:
static egui_view_virtual_stage_t page;
EGUI_VIEW_VIRTUAL_STAGE_PARAMS_INIT(page_params, 16, 124, 768, 660);
egui_view_virtual_stage_init_with_params(EGUI_VIEW_OF(&page), core, &page_params);
建议:
首次创建 page 时优先用它
初始化后再设置 adapter,再挂到根视图
egui_view_virtual_stage_set_adapter¶
作用:注册 adapter 和 adapter 上下文。
它决定了:
页面有多少节点
每个节点描述是什么
哪些节点需要真实控件
render-only 节点如何绘制
状态如何保存与恢复
典型顺序:
egui_view_virtual_stage_init_with_params(EGUI_VIEW_OF(&page), core, &page_params);
egui_view_virtual_stage_set_adapter(EGUI_VIEW_OF(&page), &my_adapter, &my_ctx);
注意:
业务层数据通常通过
adapter_context传入如果换了数据源,通常要配合
notify_data_changed
egui_view_virtual_stage_set_live_slot_limit¶
作用:设置 page 允许同时保留的 live slot 数。
适合:
根据 SRAM 预算压缩 slot 数
根据页面交互复杂度提高或降低上限
经验值:
纯展示页面:
2~3有少量输入 / 展开交互:
3~4多个复杂交互需要并存:再按需增加
注意:
它影响的是“真实控件实例上限”,不是节点总数
上限过小会增加反复 materialize / release 的频率
如果在一次交互尚未结束时降低上限,capture 结束后也会立即按新上限重新裁剪 slot
egui_view_virtual_stage_notify_data_changed¶
作用:通知整个数据源发生了全量变化。
适合:
整体换页
批量重排
大量节点联动刷新
代价:
这是最重的一种刷新通知
没必要时不要把所有更新都退化成它
如果一次 capture 尚未结束就通过它把当前节点移除或替换掉,
virtual_stage会先向旧 view 发送cancel,再回收或复用 slot,避免 pooled view 残留 pressed 等临时交互状态
egui_view_virtual_stage_notify_node_changed¶
作用:通知某个 stable_id 对应节点内容变化,但区域不变。
适合:
文本变化
状态变化
render-only 节点重绘
已 materialize 节点重新 bind
建议:
业务层最好维护依赖节点集合
一次交互只刷新真正受影响的
stable_id
egui_view_virtual_stage_notify_node_bounds_changed¶
作用:通知某个节点的区域或描述边界相关信息发生变化。
适合:
节点位置变化
节点尺寸变化
hidden/view_type/region变化
和 notify_node_changed 的区别:
notify_node_changed:内容变了,区域通常不变notify_node_bounds_changed:布局边界变了,需要更新 old/new region
egui_view_virtual_stage_pin_node¶
作用:强制让指定节点保留 live slot,不要被普通回收策略释放。
适合:
关键设备卡
需要持续刷新诊断内容的节点
需要长时间保留运行态的局部控件
返回值:
成功 pin 返回真
失败通常意味着 slot 预算不足或节点不支持 materialize
egui_view_virtual_stage_unpin_node¶
作用:撤销 pin_node 的强制常驻。
调用后:
节点不再享受 pin 保活
是否立即释放,取决于当前是否仍被 focus / keepalive 占用
建议:
对成对 pin / unpin 的业务动作做明确封装
不要把 pin 当作长期默认行为滥用
egui_view_virtual_stage_get_slot_count¶
作用:返回当前 virtual_stage 实际持有的 slot 数。
常见用途:
调试 slot 预算
统计当前活跃实例数量
在示例里估算 live SRAM 占用
egui_view_virtual_stage_get_slot¶
作用:读取指定 slot 的运行时信息。
可拿到:
stateview_typestable_idviewrender_region
适合:
调试和单测
业务示例做可视化统计
不建议:
业务逻辑强依赖 slot 下标
把它当作稳定业务接口长期使用
新增便捷 helper¶
为了让 virtual_stage 更适合真实业务接入,当前还补了几组高频 helper:
EGUI_VIEW_VIRTUAL_STAGE_NOTIFY_NODES(...)EGUI_VIEW_VIRTUAL_STAGE_NOTIFY_IDS(...)EGUI_VIEW_VIRTUAL_STAGE_NOTIFY_NODE_BOUNDS(...)EGUI_VIEW_VIRTUAL_STAGE_NOTIFY_BOUNDS_IDS(...)EGUI_VIEW_VIRTUAL_STAGE_NOTIFY_NODES_BOUNDS(...)EGUI_VIEW_VIRTUAL_STAGE_ARRAY_SETUP_INIT(...)egui_view_virtual_stage_array_adapter_init()egui_view_virtual_stage_apply_setup()/egui_view_virtual_stage_init_with_setup()egui_view_virtual_stage_apply_array_setup()/egui_view_virtual_stage_init_with_array_setup()EGUI_VIEW_VIRTUAL_STAGE_APPLY_ARRAY_BRIDGE(...)/EGUI_VIEW_VIRTUAL_STAGE_INIT_ARRAY_BRIDGE(...)egui_view_virtual_stage_is_node_pinned()EGUI_VIEW_VIRTUAL_STAGE_TOGGLE_PIN(...)egui_view_virtual_stage_find_slot_by_stable_id()egui_view_virtual_stage_find_view_by_stable_id()egui_view_virtual_stage_resolve_node_by_view()EGUI_VIEW_VIRTUAL_STAGE_RESOLVE_ID_BY_VIEW(...)egui_view_virtual_stage_notify_nodes_changed()/egui_view_virtual_stage_notify_nodes_bounds_changed()
推荐用法:
初始化阶段优先使用
init_with_setup(),一次性收口params + adapter + context固定节点数组且回调就在当前文件时,优先直接用一步式
NODE_ARRAY_*_BRIDGE_INIT_WITH_LIMIT(...) + INIT_ARRAY_BRIDGE(...);如果 stage 是整屏页,进一步用NODE_ARRAY_SCREEN_*_BRIDGE_INIT_WITH_LIMIT(...);如果ops需要跨文件导出、复用,或者你已经把params/node_source单独拆出来了,再回退到ARRAY_OPS_* + NODE_ARRAY_BRIDGE_INIT* + INIT_ARRAY_BRIDGE(...)array ops分层 helper 继续保留给复用场景:只需要create/bind/draw用ARRAY_OPS_SIMPLE_INIT(...);还要hit_test + should_keep_alive用ARRAY_OPS_INTERACTIVE_INIT(...);还要save_state + restore_state时再用ARRAY_OPS_STATEFUL_INIT(...);如果ops需要跨文件导出,改用对应的*_CONST_INIT(...)如果你手上拿的是
egui_view_virtual_stage_t *,优先用 bridge 头里的 typed convenience 宏,例如INIT_ARRAY_BRIDGE(...)、ADD_ROOT(...)、SET_BACKGROUND(...)、REQUEST_LAYOUT(...)、NOTIFY_NODE(...)、NOTIFY_IDS(...)、NOTIFY_BOUNDS_IDS(...)、PIN_IDS(...)、UNPIN_IDS(...)、TOGGLE_PIN(...)、SLOT_COUNT(...),继续少写一层EGUI_VIEW_OF(...)事件回调里如果只需要业务身份,优先使用
RESOLVE_ID_BY_VIEW(...);需要 index 和 desc 时再用RESOLVE_NODE_BY_VIEW(...)调试和摘要面板里优先使用
find_slot_by_stable_id()/find_view_by_stable_id()观察 live slot一次联动少量
stable_id时优先使用NOTIFY_IDS(...)/NOTIFY_BOUNDS_IDS(...)这类 inline helper;如果stable_id数组需要复用,再继续使用NOTIFY_NODES(...)/NOTIFY_NODES_BOUNDS(...)业务 pin 状态判断或开关按钮直接用
IS_PINNED(...)/TOGGLE_PIN(...),不要重复维护一份“框架侧是否已 pin”的影子状态
固定节点数组场景的简化接法¶
如果 stage 里的节点本身就是一组固定数组,推荐直接用下面这条接法:
typedef struct app_stage_node
{
egui_virtual_stage_node_desc_t desc;
uint8_t business_state;
} app_stage_node_t;
static egui_view_virtual_stage_t stage_view;
static app_stage_context_t stage_ctx;
static app_stage_node_t stage_nodes[12];
EGUI_VIEW_VIRTUAL_STAGE_NODE_ARRAY_INTERACTIVE_BRIDGE_INIT_WITH_LIMIT(stage_bridge, 8, 80, 304, 200, 3, stage_nodes, app_stage_node_t, desc,
app_stage_create_view, app_stage_destroy_view, app_stage_bind_view,
app_stage_draw_node, app_stage_hit_test, app_stage_should_keep_alive, &stage_ctx);
EGUI_VIEW_VIRTUAL_STAGE_INIT_ARRAY_BRIDGE(&stage_view, &stage_bridge);
EGUI_VIEW_VIRTUAL_STAGE_ADD_ROOT(&stage_view);
如果你只是在固定节点数组上接一层真实控件,这里通常直接记住三档 ops helper 就够了:
EGUI_VIEW_VIRTUAL_STAGE_ARRAY_OPS_SIMPLE_INIT(...):只有create_view / destroy_view / bind_view / draw_nodeEGUI_VIEW_VIRTUAL_STAGE_ARRAY_OPS_INTERACTIVE_INIT(...):再加hit_test / should_keep_aliveEGUI_VIEW_VIRTUAL_STAGE_ARRAY_OPS_STATEFUL_INIT(...):再加save_state / restore_state上面三档如果要跨文件导出
const ops,分别改用..._SIMPLE_CONST_INIT(...)、..._INTERACTIVE_CONST_INIT(...)、..._STATEFUL_CONST_INIT(...)
只有当你还要自定义 unbind_view 或其他更少见的组合时,再回退到手写 egui_view_virtual_stage_array_ops_t。
这条路适合:
节点总数固定,或者按场景初始化后基本不变
你已经有一组本地节点数组
你只想关心节点描述、真实控件创建和业务绑定
三个可直接照抄的模板¶
固定矩形 stage:
EGUI_VIEW_VIRTUAL_STAGE_NODE_ARRAY_INTERACTIVE_BRIDGE_INIT_WITH_LIMIT(stage_bridge, 8, 80, 304, 200, 3, stage_nodes, app_stage_node_t, desc,
app_stage_create_view, app_stage_destroy_view, app_stage_bind_view,
app_stage_draw_node, app_stage_hit_test, app_stage_should_keep_alive, &stage_ctx);
EGUI_VIEW_VIRTUAL_STAGE_INIT_ARRAY_BRIDGE(&stage_view, &stage_bridge);
EGUI_VIEW_VIRTUAL_STAGE_ADD_ROOT(&stage_view);
整屏 stage:
EGUI_VIEW_VIRTUAL_STAGE_NODE_ARRAY_SCREEN_STATEFUL_BRIDGE_INIT_WITH_LIMIT(stage_bridge, 2, stage_nodes, app_stage_node_t, desc, app_stage_create_view,
app_stage_destroy_view, app_stage_bind_view, app_stage_save_state,
app_stage_restore_state, app_stage_draw_node, app_stage_hit_test,
app_stage_should_keep_alive, &stage_ctx);
EGUI_VIEW_VIRTUAL_STAGE_INIT_ARRAY_BRIDGE(&stage_view, &stage_bridge);
EGUI_VIEW_VIRTUAL_STAGE_ADD_ROOT(&stage_view);
已有 params:
EGUI_VIEW_VIRTUAL_STAGE_PARAMS_INIT_WITH_LIMIT(stage_params, 8, 80, 304, 200, 3);
EGUI_VIEW_VIRTUAL_STAGE_ARRAY_OPS_SIMPLE_INIT(stage_ops, app_stage_create_view, app_stage_destroy_view, app_stage_bind_view, app_stage_draw_node);
EGUI_VIEW_VIRTUAL_STAGE_NODE_ARRAY_BRIDGE_INIT(stage_bridge, &stage_params, stage_nodes, app_stage_node_t, desc, &stage_ops, &stage_ctx);
EGUI_VIEW_VIRTUAL_STAGE_INIT_ARRAY_BRIDGE(&stage_view, &stage_bridge);
EGUI_VIEW_VIRTUAL_STAGE_ADD_ROOT(&stage_view);
如果节点数量会频繁变化,或者节点描述来自外部动态模型,再继续使用 raw adapter 即可。
keepalive / pin / focus¶
virtual_stage 常见有三种“暂时不要释放”的机制。
focus 保活¶
输入类控件只要仍在焦点态,就应继续占用 slot。
典型控件:
textinput正在编辑的复合控件
adapter keepalive¶
通过 should_keep_alive() 告诉框架:当前节点还不能释放。
典型场景:
combobox展开正在播放的控件动画
控件内部手势尚未结束
pin 保活¶
通过 PIN(...) / UNPIN(...) 由业务显式控制常驻。
典型场景:
关键设备卡
持续诊断卡
局部长驻操作面板
时序图¶
点击交互节点 -> materialize -> bind -> restore -> dispatch¶
User
|
| tap interactive node
v
virtual_stage
|
| hit_test / locate stable_id
| choose or allocate slot
| create_view(view_type) [if slot has no reusable view]
| bind_view(index, stable_id)
| restore_state(...) [if adapter implements it]
| dispatch input event
v
live view
输入框失焦 -> save_state -> release¶
Focused textinput
|
| blur / clear focus
v
virtual_stage
|
| check keepalive conditions
| save_state(textinput text, cursor related business state)
| unbind_view(...) [optional]
| recycle slot
v
adapter context / business state
业务层 notify_node_changed -> cache refresh -> rebind / redraw¶
Business logic
|
| update external state
| notify_node_changed(stable_id)
v
virtual_stage
|
| refresh cached desc for target node
| if target is live slot -> mark needs_bind and rebind target slot
| if target is render-only -> invalidate target region and redraw
v
screen
最小接入步骤¶
1. 先拆业务状态¶
优先分成两层:
节点描述层:位置、类型、
z_order、stable_id业务数据层:文本、数值、选中项、展开态、运行态
2. 约定 view_type 和 stable_id¶
enum
{
MY_VIEW_TYPE_BUTTON = 1,
MY_VIEW_TYPE_TEXTINPUT = 2,
MY_VIEW_TYPE_COMBOBOX = 3,
};
stable_id 最好按区域或业务模块分段,便于做定向刷新。
3. 动态数据源时实现 raw adapter¶
static const egui_view_virtual_stage_adapter_t my_adapter = {
.get_count = my_get_count,
.get_desc = my_get_desc,
.create_view = my_create_view,
.destroy_view = my_destroy_view,
.bind_view = my_bind_view,
.save_state = my_save_state,
.restore_state = my_restore_state,
.draw_node = my_draw_node,
.hit_test = my_hit_test,
.should_keep_alive = my_should_keep_alive,
};
4. 初始化 page¶
static egui_view_virtual_stage_t page;
EGUI_VIEW_VIRTUAL_STAGE_NODE_ARRAY_BRIDGE_INIT_WITH_LIMIT(page_bridge, 0, 0, 320, 240, 3, stage_nodes, app_stage_node_t, desc, &stage_ops, &my_context);
EGUI_VIEW_VIRTUAL_STAGE_INIT_ARRAY_BRIDGE(&page, &page_bridge);
EGUI_VIEW_VIRTUAL_STAGE_ADD_ROOT(&page);
5. 业务变化时显式通知¶
const uint32_t changed_ids[] = {
MY_NODE_SPEED_BAR_ID,
MY_NODE_SUMMARY_ID,
};
my_ctx.slider_value = 68;
EGUI_VIEW_VIRTUAL_STAGE_NOTIFY_NODES(&page, changed_ids);
如果只是一次性联动少量 id,也可以直接写成:
EGUI_VIEW_VIRTUAL_STAGE_NOTIFY_IDS(&page, MY_NODE_SPEED_BAR_ID, MY_NODE_SUMMARY_ID);
6. 需要长期活跃的节点做保活¶
EGUI_VIEW_VIRTUAL_STAGE_TOGGLE_PIN(&page, MY_NODE_CORE_CARD_ID);
接入建议¶
render-only 优先¶
如果节点不需要真实复杂交互,优先做成 render-only。
判断标准:
只是显示文本、数字、图标、趋势
交互只需要页面级点击
视觉复杂但状态简单
输入态外置¶
不要把输入类控件的关键值只留在实例里。
推荐外置:
textinput文本slider数值picker当前值combobox当前索引roller当前索引segmented当前索引
动画态两种做法¶
如果节点有动画:
简单方案:把动画进度也外置,render-only 按业务时钟直接绘制
保守方案:动画期间通过
should_keep_alive()保活,结束后再释放
命中区域复杂时实现 hit_test¶
默认命中通常就是矩形区域。
如果节点:
有镂空区
只有局部可点
需要更严格的热点控制
就实现 adapter->hit_test()。
virtual_stage 在 DOWN 命中节点后,后续 MOVE/UP 也会继续按 hit_test() 判断“当前是否仍在热点内”。
这意味着拖出热点会像普通控件那样取消按压,拖回热点后又能恢复。
HelloVirtual 示例¶
参考:
src/widget/egui_view_virtual_stage.hsrc/widget/egui_view_virtual_stage.cexample/HelloVirtual/virtual_stage_basic/test.cexample/HelloVirtual/virtual_stage_showcase/test.cexample/HelloVirtual/virtual_stage/test.cexample/HelloVirtual/virtual_stage/demo_virtual_stage_internal.hexample/HelloVirtual/virtual_stage/demo_virtual_stage_adapter.cexample/HelloUnitTest/test/test_virtual_stage.c
推荐先看 HelloVirtual/virtual_stage_basic,它演示的是:
固定页面上的最小节点集
Image + ProgressBar + Button + Combobox四类基础 live widget固定节点数组 +
NODE_ARRAY_BRIDGE_INIT_WITH_LIMIT(...)外置业务状态
INIT_ARRAY_BRIDGE + RESOLVE_ID_BY_VIEW + TOGGLE_PIN + batch notify用
find_slot_by_stable_id()/find_view_by_stable_id()做运行态观测
然后看 HelloVirtual/virtual_stage_showcase,它演示的是:
更接近
HelloShowcase的深色分区布局panel / summary / badge 继续走 render-only
8 个代表性控件直接使用真实 widget,而不是画布重画
当前 slot ceiling 为
8时,如何做一个便于对比的 stage 页面render-only 区域的动画由外部 tick 重绘驱动,所以观感可能比
HelloShowcase更“静”;如果要保留原生控件自驱动画,就要把对应控件也提升为 live widget
HelloShowcase vs HelloVirtual/virtual_stage_showcase¶
如果你关心的是“显示效果不变,但 SRAM 能省多少”,这两个例程就是最直接的对照组。
这里不建议直接用整个 ELF 的 data + bss 做对比,因为那会混入:
PFB平台移植层自身 SRAM
框架公共静态 SRAM
更适合衡量 virtual_stage 收益的口径,是“示例自身对象文件的 .data* + .bss*”。
对应目录分别是:
output/obj/HelloShowcase_stm32g0/example/HelloShowcase/output/obj/HelloVirtual_virtual_stage_showcase_stm32g0/example/HelloVirtual/virtual_stage_showcase/
以 PORT=stm32g0、COMPILE_OPT_LEVEL=-Os 为例:
示例 |
示例自身静态 SRAM |
|---|---|
|
13248 B |
|
7128 B |
可以直接得到这组 tradeoff:
HelloVirtual/virtual_stage_showcase比HelloShowcase少占6120字节示例自身静态 SRAM,约46.2%收益来自
virtual_stage不再常驻整页控件树,而是保留stage + scratch + 外置状态 + 少量 live slotHelloShowcase的主要静态 SRAM 消耗集中在整页真实控件,例如wg_keyboard、wg_list、wg_tablevirtual_stage_showcase的主要静态 SRAM 消耗集中在showcase_keyboard_view、showcase_scratch、showcase_stage_view、showcase_ctx这个口径不包含运行时栈/堆,所以它衡量的是“示例自身静态占用”,不是整机运行时总 SRAM 峰值
QEMU 实测 heap¶
如果你还关心“运行时 heap 会不会涨”,要单独看 QEMU 实测。
推荐命令:
python scripts/perf_analysis/compare_virtual_showcase_heap_qemu.py --mode app-recording
app-recording 模式会直接复用各自示例的 egui_port_get_recording_action(),比统一假动作更接近真实页面交互。
以 2026-03-22 的 QEMU 实测结果为例:
示例 |
idle current heap |
interaction total peak heap |
|---|---|---|
|
0 B |
0 B |
|
2488 B |
3328 B |
这组数据和前面的静态 SRAM 结论并不矛盾:
HelloShowcase主要是整页静态全局控件,页面自身几乎不走运行时mallocvirtual_stage_showcase省下的是“整页真实控件树的静态 SRAM”同时它会引入一部分“按需 materialize”的运行时 heap
为什么 virtual_stage_showcase 会有这部分 heap:
stage 节点缓存本身会分配 heap:
node_cache和draw_order在egui_view_virtual_stage_reload_cache()首次装载或容量增长时通过egui_malloc()创建;同容量刷新会复用现有缓存,避免每次 reload 都重复 malloc/free正常模式下页面默认会保留少量 live widget,尤其
List节点带KEEPALIVE,所以 idle 阶段就会出现少量常驻 heap交互过程中,
Button、TextInput、Slider、Combobox、List等真实控件会在showcase_adapter_create_view()里按view_type动态egui_malloc(),释放后再复用或销毁当前实测里
virtual_stage_showcase的 idle 阶段有3次分配,交互阶段额外出现24次分配和24次释放,说明 heap 大头来自“少量常驻 slot + 交互时反复 materialize live widget”
怎么判断这 3.25 KB heap 值不值:
如果目标板最紧的是静态 SRAM,这个 tradeoff 是成立的,因为它换回了约
46.2%的示例自身静态 SRAM如果目标板对 heap 更敏感,那就要继续压 live widget 数量和 stage 运行时元数据
后续优化方向:
继续减少默认 keepalive 节点,优先检查
List、展开态Combobox这类会长期占 slot 的控件严格控制
live_slot_limit,只给真正需要并存的交互控件留预算能做成 render-only 的节点不要升级成 live widget,尤其是纯展示卡片、说明文字、装饰层
把常用 view_type 改成更轻的专用 live view,而不是直接保留较重控件的完整实例
如果目标是“heap 可控”而不是“heap 绝对最小”,可以把 stage cache / pin 表 / live view 改成固定池或专用 arena,避免碎片并锁定峰值
对于重量级控件,优先评估“展开时临时创建,收起后立即释放”,不要长期 keepalive
因此可以这样选:
想要最低接入成本、最直接复用整页控件树:优先
HelloShowcase想保住
HelloShowcase的视觉和交互,但目标板 SRAM 更紧:优先HelloVirtual/virtual_stage_showcase
最后再看 HelloVirtual/virtual_stage,它演示了:
800x800大画布100个节点同时呈现默认 render-only
交互节点按需 materialize
stable_id + save_state/restore_statepin + focus + should_keep_alivenotify_node_changed / notify_node_bounds_changedadapter/helper 拆分后的更易复用组织方式
验证建议¶
代码改动后建议至少执行:
make all APP=HelloVirtual APP_SUB=virtual_stage_basic PORT=pc
python scripts/code_runtime_check.py --app HelloVirtual --keep-screenshots
python scripts/code_runtime_check.py --app HelloVirtual --app-sub virtual_stage_basic --keep-screenshots
python scripts/checks/hello_basic_render_workflow.py --app HelloVirtual --widgets virtual_stage_basic --skip-unit-tests --bits64
make all APP=HelloVirtual APP_SUB=virtual_stage_showcase PORT=pc
python scripts/code_runtime_check.py --app HelloVirtual --app-sub virtual_stage_showcase --keep-screenshots
python scripts/checks/showcase_stage_parity_check.py --timeout 35 --bits64
python scripts/checks/hello_basic_render_workflow.py --app HelloVirtual --widgets virtual_stage_showcase --skip-unit-tests --bits64
make -j1 all APP=HelloShowcase PORT=stm32g0
make -j1 all APP=HelloVirtual APP_SUB=virtual_stage_showcase PORT=stm32g0
python scripts/perf_analysis/compare_virtual_showcase_ram.py --skip-build
python scripts/perf_analysis/compare_virtual_showcase_heap_qemu.py --mode app-recording
make all APP=HelloVirtual APP_SUB=virtual_stage PORT=pc
python scripts/code_runtime_check.py --app HelloVirtual --app-sub virtual_stage --keep-screenshots
python scripts/checks/hello_basic_render_workflow.py --app HelloVirtual --widgets virtual_stage,virtual_stage_showcase,virtual_stage_basic --skip-unit-tests --bits64
如果改了框架层,再执行:
make all APP=HelloUnitTest PORT=pc_test
output\main.exe
常见陷阱¶
stable_id不稳定,导致 pin 和状态恢复错位节点 bounds 变了,却只调用了
notify_node_changed输入框失焦后没有把文本写回业务层
combobox展开态没保活,导致点击下拉项时控件已被释放所有联动都退化成
notify_data_changed,导致整页重绑
如果页面本身是“很多节点同时可见,但只有少量节点真正活跃”,优先考虑 virtual_stage,不要为每个节点都常驻一份控件实例。