PC 模拟器移植

PC 模拟器是 EmbeddedGUI 的主要开发和调试平台,基于 SDL2 提供显示、输入、录制和截图能力。它的目标不是完全模拟 MCU 环境,而是提供一个高迭代效率、可自动化验证的本地运行平台。

构建与运行

# 构建
make all APP=HelloSimple PORT=pc

# 运行
make run

# 或直接运行
./output/main.exe

切换 APPPORT 时,建议先执行一次 make clean,避免复用旧的 output/main.exe

make clean
make all APP=HelloUnitTest PORT=pc_test
./output/main.exe

核心文件

  • porting/pc/main.c PC 入口,负责主屏/副屏 descriptor 收集、线程启动和退出回收

  • porting/pc/sdl_port.c SDL window 管理、事件路由、录制、截图和 runtime info

  • porting/pc/sdl_port.h SDL 相关接口声明

  • porting/pc/egui_port_pc.c display driver、platform driver 和触摸驱动注册

运行时模型

当前 PC 端采用下面的线程模型:

  • SDL 主线程 负责窗口事件分发、录制动作推进、截图输出和退出管理

  • 每个 display/core 各自的 GUI 线程 只轮询本屏幕的 egui_polling_work(core)

这意味着 PC 端已经不是早期“单 GUI 线程串行轮询所有屏幕”的模型,而是 per-core GUI thread。

多屏启动流程

porting/pc/main.c 当前流程可以概括为:

  1. 初始化 SDL / platform

  2. 构造主屏 descriptor

  3. 调用 egui_port_get_additional_display_descriptors() 收集副屏 descriptor

  4. 逐个创建 display driver、touch driver 和 egui_core_t

  5. 调用 egui_setup_display()

  6. 为每个 core 启动 GUI 线程

  7. SDL 主线程进入事件循环

  8. 退出时通知所有 GUI 线程停止,并回收窗口和线程资源

新增副屏时,优先补 descriptor,而不是继续在 main.c 写死示例逻辑。

SDL 显示后端

PC display driver 会把 PFB 数据写入 SDL 纹理,再把纹理刷新到窗口。

典型流程:

  • draw_area 把局部像素块写入 SDL 后端缓冲

  • flush 提交当前帧并刷新窗口

窗口尺寸由应用配置决定,例如:

#define EGUI_CONFIG_SCREEN_WIDTH  240
#define EGUI_CONFIG_SCREEN_HEIGHT 320

副屏则由 descriptor 中的 screen_width / screen_height 决定。

输入路由

SDL 鼠标事件会映射到 EGUI 触摸事件:

SDL 事件

EGUI 事件

SDL_MOUSEBUTTONDOWN

EGUI_MOTION_EVENT_ACTION_DOWN

SDL_MOUSEMOTION

EGUI_MOTION_EVENT_ACTION_MOVE

SDL_MOUSEBUTTONUP

EGUI_MOTION_EVENT_ACTION_UP

多屏模式下,PC 端会按窗口把事件路由到对应 display_id

  • 主屏窗口进入 display 0 输入队列

  • 副屏窗口进入对应副屏输入队列

  • 不同屏幕的输入不会串屏

如果副屏 descriptor 注册了:

descriptors[i].touch_register = egui_port_register_touch_driver;

那么该副屏就可以独立接收触摸输入。

键盘事件

SDL 键盘事件会映射为 EGUI 按键事件,例如:

SDL 键

EGUI 键

SDLK_RETURN

EGUI_KEY_CODE_ENTER

SDLK_ESCAPE

EGUI_KEY_CODE_BACK

SDLK_UP/DOWN/LEFT/RIGHT

方向键

启用键盘支持:

#define EGUI_CONFIG_FUNCTION_SUPPORT_KEY 1

跨线程访问边界

PC 端最重要的约束是:SDL 主线程不能直接修改 foreign core 的 UI。

需要遵守:

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

  • 跨线程操作必须先投递到目标 core

当前可用的辅助接口:

  • egui_port_post_core_task() 把任务投递到目标 core 的 GUI 线程执行

  • egui_port_get_display_runtime_info() 同步读取 display 运行时状态,避免 SDL 线程直接访问目标 core 内部数据

这两个接口是 PC 多屏线程安全模型的基础,后续 port 如需跟进,也建议沿用同样的职责边界。

新 Port 对齐建议

如果后续 MCU / RTOS port 也要跟进本地多屏并行线程方案,建议至少保持下面几条不变:

  • display 的创建入口保持 descriptor 化,不再把“主屏 + 固定 display 1”写死在启动代码里

  • 每个 display/core 对应自己的 GUI 线程或等价轮询上下文,不要回退到单线程串行轮询全部 core

  • foreign core 的 UI 修改统一走“投递到目标 core”入口,不要让事件线程、录制线程或业务线程直接调用目标对象 API

  • 读取运行时状态时优先提供快照/查询接口,避免外部线程直接探测目标 core 内部结构

  • 退出顺序固定为“先停 GUI 线程,再销毁 display/window/driver,最后做 platform deinit”,这样更容易做自动化校验

PC 端当前已经按这个边界拆成了 egui_port_get_additional_display_descriptors()egui_port_post_core_task()egui_port_get_display_runtime_info() 和 shutdown 标记链路。新 port 如果复用相同职责划分,后续脚本和文档也更容易沿用。

Platform Driver

PC platform driver 主要依赖标准库和 SDL:

接口

PC 实现

堆分配

默认 malloc / free

日志 / 格式化

默认 vprintf / vsprintf

get_tick_ms

sdl_get_system_timestamp_ms()

delay

sdl_port_sleep()

内存清零 / 拷贝

默认 memset / memcpy

interrupt_disable/enable

由 PC port 提供最小同步能力,供输入等临界区使用

PC 端的 interrupt_disable/enable 并不是 MCU 意义上的真实中断屏蔽,而是为了给输入等共享队列提供统一的最小临界区包装。

录制与截图

PC 模拟器支持录制截图序列,用于 runtime check 和 GIF 生成:

./output/main.exe --record output_dir 30 10
./output/main.exe --record output_dir 30 10 --speed 2

启用录制测试:

#define EGUI_CONFIG_FUNCTION_RECORDING_TEST 1

应用可以实现:

bool egui_port_get_recording_action(int action_index, egui_sim_action_t *p_action);

多屏录制推荐直接指定目标屏幕:

  • 设置 p_action->display_id

  • 或使用 EGUI_SIM_SET_*_DISP()

例如:

EGUI_SIM_SET_CLICK_VIEW_DISP(p_action, target_view, 600, 1);

运行时验证

代码修改后,推荐至少执行:

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

多屏示例建议额外验证:

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 clean
make all APP=HelloMultiDisplay PORT=pc
python scripts/code_runtime_check.py --app HelloMultiDisplay --timeout 10 --keep-screenshots

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

其中 release_check.py --scope multi-display 适合先做一轮一键多屏回归;命令开头会直接打印 scoped profile 摘要、--only 可用 step、compile/runtime/doc 的 drill-down 命令和关键产物目录,summary 尾部也会把失败步骤对应的 python scripts/release_check.py --scope multi-display --only <step>、底层命令与产物位置再列一遍。如果需要看具体双屏录制输出,再按需打开单个示例截图细查。

输出截图位于 runtime_check_output/

当前多屏 runtime 摘要会带上:

  • checks=...

  • stages=...

  • shutdown=begin->threads:N->cleanup:1+M->deinit

分别用于确认示例内建自检、录制阶段序列,以及线程退出和窗口销毁顺序。

当前多屏示例里,HelloMultiDisplay 会额外验证“主屏点击不会误推进副屏 activity”以及“主副屏 activity 动画在重叠窗口内同时处于运行态”;HelloMultiDisplayHetero 会验证“主屏连续拖动时副屏 tick 不停摆”与“副屏点击后 tick 归零”。

文件系统资源加载

PC 平台通过标准文件 I/O 加载外部资源。默认情况下会读取当前目录中的:

app_egui_resource_merge.bin

也可以通过命令行指定资源文件路径:

./output/main.exe /path/to/app_egui_resource_merge.bin

调试能力

常用调试宏:

#define EGUI_CONFIG_DEBUG_LOG_LEVEL EGUI_LOG_IMPL_LEVEL_DBG
#define EGUI_CONFIG_DEBUG_PFB_REFRESH 1
#define EGUI_CONFIG_DEBUG_DIRTY_REGION_REFRESH 1
#define EGUI_CONFIG_DEBUG_PERF_MONITOR_SHOW 1
#define EGUI_CONFIG_DEBUG_MEM_MONITOR_SHOW 1

触摸轨迹调试:

make all APP=HelloBasic APP_SUB=slider PORT=pc USER_CFLAGS="-DEGUI_CONFIG_DEBUG_TOUCH_TRACE=1"

只要触摸支持开启,窗口和截图中就会显示红色触摸轨迹线。

常见问题

  • SDL2 未安装 需要先安装 SDL2 开发库

  • 窗口不显示 检查 EGUI_CONFIG_SCREEN_WIDTH/HEIGHT 和 SDL 初始化是否成功

  • 触摸无响应 确认 EGUI_CONFIG_FUNCTION_SUPPORT_TOUCH=1,并且目标 descriptor 已注册 touch_register

  • 多屏录制没有命中副屏 检查 display_id 是否设置正确,或是否使用了 _DISP

  • 运行了错误的 output/main.exe 切换 APP / PORT 后先执行 make clean