多屏方案(Multi-Display)

概述

EmbeddedGUI 支持在同一进程内驱动多块屏幕。当前实现的基本原则是:

  • 每块屏幕对应一个独立的 egui_core_t

  • 每块屏幕拥有自己的分辨率、PFB、输入状态、定时器、动画和 UI 树

  • uicode_dispN_init(core) 只负责在目标 core 上构建 UI,不负责创建 core、display driver 或线程

多屏场景的核心约束不是“先切 active core 再使用对象”,而是“对象从初始化开始就绑定到正确的 core 上”。

当前 PC 实现

PC 端已经把多屏启动入口收敛为 descriptor 流程,不再在 porting/pc/main.c 中写死“主屏 + 固定 display 1”的特殊逻辑。

当前启动过程是:

  1. 初始化 SDL / platform

  2. 构建主屏 descriptor

  3. 通过 egui_port_get_additional_display_descriptors() 收集额外屏幕 descriptor

  4. 为每个 descriptor 创建 egui_core_t、display driver 和 SDL window

  5. 调用 egui_setup_display()

  6. 为每个 core 启动独立 GUI 线程

这意味着:

  • porting/pc/main.c 现在是通用的

  • 应用层只需要提供副屏 descriptor

  • 新增副屏时,优先扩展 descriptor,而不是继续往 PC 入口里加 if/else

职责划分

uicode_disp0_init() / uicode_disp1_init()

这些函数只负责目标屏幕的 UI 初始化,例如:

  • 初始化 view / activity / dialog / toast

  • 把 view 挂到目标 core

  • 启动属于该屏幕的定时器

这些函数不应该负责:

  • 分配或保存 egui_core_t

  • 创建 display driver

  • 创建副屏窗口

  • 注册 GUI 线程

porting

porting 层负责:

  • 维护多屏 core 实例

  • 准备每块屏幕的 egui_display_setup_t

  • 创建主屏和副屏 display driver

  • 调用 egui_setup_display()

  • 启动并管理 GUI 线程

  • 路由 SDL 输入、录制和退出流程

推荐初始化接口

egui_setup_display()

所有屏幕都推荐通过 egui_setup_display() 初始化。它会统一完成:

  • egui_core_t 初始化

  • display driver 注册

  • 可选的 per-core render_config 应用

  • 可选的 touch driver 注册

  • 调用 uicode_dispN_init(core)

  • 调用 egui_screen_on(core)

egui_init_display() 仍然是底层接口,但新代码优先使用 egui_setup_display()

egui_port_get_additional_display_descriptors()

PC 多屏应用推荐通过这个钩子返回额外屏幕描述。每个 descriptor 至少包含:

  • screen_width / screen_height

  • pfb_width / pfb_height

  • pfb_buffers / pfb_buffer_count

  • 可选的 render_config

  • touch_register

  • uicode_init

主屏仍由 PC 入口直接构造,副屏由 descriptor 扩展。

render_config 适合描述“同一个应用里不同 core 的运行时渲染策略”,例如副屏需要 color_16_swap=1、主屏保持 0,或者只有某块屏幕启用软件旋转。几何尺寸仍建议保留在宏或 descriptor 的 width/height/PFB 字段里。

如果某块屏的策略需要在启动后继续变化,也可以直接对对应 core 调用 egui_core_set_render_config();框架会把该屏标记为整屏刷新,并在软件旋转策略改变当前逻辑宽高时同步刷新尺寸状态。

static egui_core_render_config_t disp1_render_config = {
    .color_16_swap = 1,
    .software_rotation = 1,
    .rotation_scratch = NULL,
};

descriptors[0].render_config = &disp1_render_config;

线程模型

当前 PC 端采用“1 个 SDL 主线程 + 每个 core 1 个 GUI 线程”的模型:

  • SDL 主线程负责窗口事件、录制驱动、截图和退出管理

  • 每个 display/core 拥有自己的 GUI 线程,只轮询自己的 egui_polling_work(core)

这比早期“单 GUI 线程串行轮询所有 core”更接近真实多屏运行模型。

跨线程访问约束

需要严格遵守下面的边界:

  • UI 树、动画、定时器、dirty region 只能在所属 core 的 GUI 线程中直接修改

  • SDL 主线程不能直接操作 foreign core 的 view/tree

  • 跨线程 UI 修改必须先投递到目标 core

当前 PC 端已经提供两个辅助接口:

  • egui_port_post_core_task():把操作投递到目标 core

  • egui_port_get_display_runtime_info():安全读取 display 运行时信息,避免 SDL 线程直接访问目标 core 内部状态

后续跨屏逻辑应优先走“投递到目标 core”模型,而不是在外部线程直接调用目标屏幕对象 API。

配置

多屏默认配置分为两层:

  • 单屏默认值位于 src/config/egui_config_default.h

  • 多屏补充值位于 src/config/egui_config_multi_default.h

当前常用宏如下:

#define EGUI_CONFIG_MAX_DISPLAY_COUNT 1

#define EGUI_CONFIG_SCREEN_1_WIDTH  EGUI_CONFIG_SCREEN_WIDTH
#define EGUI_CONFIG_SCREEN_1_HEIGHT EGUI_CONFIG_SCREEN_HEIGHT
#define EGUI_CONFIG_PFB_1_WIDTH    EGUI_CONFIG_PFB_WIDTH
#define EGUI_CONFIG_PFB_1_HEIGHT   EGUI_CONFIG_PFB_HEIGHT

注意:

  • 没有 EGUI_CONFIG_SCREEN_0_* / EGUI_CONFIG_PFB_0_*

  • display 0 直接使用 EGUI_CONFIG_SCREEN_WIDTHEGUI_CONFIG_SCREEN_HEIGHTEGUI_CONFIG_PFB_WIDTHEGUI_CONFIG_PFB_HEIGHT

  • display 1、display 2 默认 fallback 到主屏配置,可在 app_egui_config.h 中覆盖

启用双屏的最小配置:

#define EGUI_CONFIG_MAX_DISPLAY_COUNT 2

异构副屏示例:

#define EGUI_CONFIG_MAX_DISPLAY_COUNT 2

#define EGUI_CONFIG_SCREEN_1_WIDTH  128
#define EGUI_CONFIG_SCREEN_1_HEIGHT 64
#define EGUI_CONFIG_PFB_1_WIDTH    16
#define EGUI_CONFIG_PFB_1_HEIGHT   8

副屏输入

当前 PC 多屏已经支持副屏独立触摸输入。启用条件是副屏 descriptor 显式注册 touch_register

descriptors[0].touch_register = egui_port_register_touch_driver;

启用后:

  • SDL 会按窗口把鼠标/触摸事件路由到对应 display_id

  • 每个 display 维护自己的输入队列

  • 副屏可以独立点击、拖动,而不会串到主屏

录制与运行时验证

PC 多屏录制已经支持按 display_id 路由动作。推荐使用带 _DISP 后缀的宏:

  • EGUI_SIM_SET_CLICK_VIEW_DISP()

  • EGUI_SIM_SET_DRAG_VIEW_DISP()

  • EGUI_SIM_SET_SWIPE_VIEW_DISP()

也可以直接设置:

p_action->display_id = 1;

当前仓库内已经补齐两个多屏示例的副屏验证闭环:

  • HelloMultiDisplay 副屏按钮可独立点击,录制脚本会推进副屏 activity

  • HelloMultiDisplayHetero 副屏状态面板可独立点击,录制脚本会验证 tick 重置和页签颜色切换

推荐验证命令:

python scripts/release_check.py --scope multi-display

python scripts/code_compile_check.py --scope multi-display --case-jobs 2
python scripts/code_runtime_check.py --scope multi-display --jobs 2 --timeout 10 --keep-screenshots

make all APP=HelloMultiDisplay PORT=pc
python scripts/code_runtime_check.py --app HelloMultiDisplay --timeout 10 --keep-screenshots

make all APP=HelloMultiDisplayHetero PORT=pc
python scripts/code_runtime_check.py --app HelloMultiDisplayHetero --timeout 10 --keep-screenshots

其中 release_check.py --scope multi-display 适合一键串起多屏 compile/runtime/doc 回归;命令启动后会先打印 scoped profile 摘要、--only 可用 step、当前激活的 --only/--skip 过滤结果、compile/runtime/doc 的 drill-down 命令和关键产物目录,方便失败后直接拆看,summary 尾部也会把失败步骤对应的 python scripts/release_check.py --scope multi-display --only <step>、底层命令与产物位置再列出来;如果这一轮通过,summary 里还会把已完成步骤的关键产物目录再汇总一遍。runtime scope 会按 EGUI_CONFIG_MAX_DISPLAY_COUNT 校验主屏 frame_XXXX.png 和各个额外屏幕 frame_XXXX_dispN.png 成套输出,并校验多屏录制阶段标签,确认主屏/副屏关键交互后的稳定快照都已经产出。HelloMultiDisplay 还会在录制过程中自检“点主屏只推进主屏 activity”以及“主副屏 activity 动画在重叠窗口内同时处于运行态”;HelloMultiDisplayHetero 会自检“主屏连续拖动时副屏 tick 仍持续递增”以及“副屏点击后 tick 归零”。如果要细查单个示例截图,再继续跑下面两条单例命令。

截图输出位于:

  • runtime_check_output/HelloMultiDisplay/default/

  • runtime_check_output/HelloMultiDisplayHetero/default/

runtime scope 的终端摘要里还会带上 checks=...stages=...shutdown=begin->threads:N->cleanup:1+M->deinit,分别用于确认示例内建自检项、录制阶段序列,以及多屏退出阶段的线程回收和 SDL 窗口销毁顺序。

多屏示例建议至少覆盖:

  • 主屏交互

  • 副屏交互

  • 不同 display 的截图输出

  • 退出路径和线程回收

示例

示例

说明

HelloMultiDisplay

主屏和副屏同为 240x320,演示多屏 activity 切换与副屏独立输入

HelloMultiDisplayHetero

主屏 240x320,副屏 128x64,演示异构副屏状态面板和跨屏状态联动

注意事项

  1. display_id 必须正确设置,egui_setup_display() 会写入 core->id,PC 端依赖该值做窗口路由、输入分发和截图命名。

  2. 多屏 core 应由 porting 层维护,不要在应用层重复保存另一套副本。

  3. 每块屏幕都必须有自己的 PFB,副屏不能复用主屏 PFB。

  4. 显式接收 core 的 API 必须传目标屏幕自己的 core,例如 egui_timer_start_timer(core, ...)

  5. 不显式接收 core 的对象式 API 依赖对象初始化时绑定的 core,对象必须从一开始就构造在正确屏幕上。

  6. 对 foreign core 的跨线程 UI 操作不要直接调用对象 API,优先通过 egui_port_post_core_task() 投递。

  7. PC 当前已支持 descriptor 化扩展,但新增更多屏幕时仍需同步检查线程退出、窗口销毁、录制输出以及每块屏幕自己的 render_config 是否符合预期。