PixelUI 的 Coroutine 是固定 context 加宏的协作式状态机,不是 C++20 language coroutine。它适合编排“启动动画 -> 等待 -> 继续”这类短流程。
1 | Coroutine intro_{[this](CoroutineContext& ctx) { |
注册与清理
1 | void onEnter(ExitCallback cb) override { |
Coroutine 通常应是 App 成员。对象状态、scheduler 注册、deadline 和恢复点是彼此独立的规则,下面按 API 说明。
API 详解
使用这些 API 需要包含:
1 |
CoroutineContext
每个 Coroutine 都内置一个 context,用它保存跨多次调用的执行位置和等待状态:
1 | struct CoroutineContext { |
| 字段 | 含义 |
|---|---|
pc |
宏状态机的执行位置。由 CORO_* 宏维护,业务代码不要直接修改。 |
waitUntil |
CORO_DELAY 使用的绝对时间戳。由宏维护。 |
localData[8] |
提供给业务代码的 8 个持久化 uint32_t 槽位,可在多次 resume 之间保存简单状态。 |
state |
当前状态:CREATED、RUNNING、SUSPENDED 或 FINISHED。 |
waitReason |
挂起原因:NONE、DELAY 或 ANIMATION。调度器据此计算下次唤醒时间。 |
reset() 不会清空 localData。如果一次新的流程不应继承旧值,需要在启动前显式清零所用槽位:
1 | intro_.reset(); |
除 localData 外,通常只读取 context 进行诊断,不要手动改写 pc、state、waitUntil 或 waitReason,否则宏状态和调度器判断可能不一致。
Coroutine
构造函数
1 | Coroutine(CoroutineFunction func); |
CoroutineFunction 的实际类型是:
1 | etl::inplace_function<void(CoroutineContext&), CALLBACK_STORAGE_SIZE> |
回调存放在对象内部,不使用堆。lambda 捕获超过配置容量会在编译期失败;具体上限见资源上限。构造只创建 coroutine,不会执行回调。
start()
1 | void start(); |
仅当状态为 CREATED 时,将状态改为 RUNNING 并把执行位置设为开头。它不会立即调用 coroutine 函数;函数会在 scheduler 下一次更新时运行。对 RUNNING、SUSPENDED 或 FINISHED 状态调用没有效果。若要重新运行已完成的 coroutine,先调用 reset()。
reset()
1 | void reset(); |
将状态恢复为 CREATED,并重置 pc、waitUntil 和 waitReason。它不会清空 localData,也不会自动从 scheduler 移除或重新注册 coroutine。
状态查询
1 | bool isFinished() const; |
isFinished() 只判断 context 是否为 FINISHED。getContext() 可用于读取状态或访问 localData;优先使用 const 版本进行诊断。
低层调度接口
1 | void resume(uint32_t currentTime, bool animationActive = false); |
这些接口主要由 CoroutineScheduler 调用,普通 App 不需要手动驱动。resume() 只会执行 RUNNING 或已经满足恢复条件的 SUSPENDED coroutine;shouldRun() 判断当前是否可运行;nextWakeupMs() 返回距离下一次运行的毫秒数,暂时没有定时唤醒需求时返回 PixelUITime::NO_WAKEUP。
手动调用时,currentTime 和 animationActive 必须与同一 PixelUI 实例的真实状态一致,否则延时或动画等待语义会出错。
Coroutine 宏
所有宏都必须在同一个 coroutine 函数的 CORO_BEGIN(ctx) 与 CORO_END(ctx) 之间使用。
CORO_BEGIN(ctx) / CORO_END(ctx)
1 | CORO_BEGIN(ctx); |
CORO_BEGIN 打开基于 ctx.pc 的状态机;CORO_END 将状态设为 FINISHED 并返回。正常执行路径不能绕过 CORO_END,否则 coroutine 会保持 RUNNING 并被继续调度。
CORO_YIELD(ctx, line)
1 | CORO_YIELD(ctx, 1); |
保存执行位置并立即返回,但不进入定时等待。coroutine 仍为 RUNNING,所以 scheduler 会在下一次处理机会继续执行,而不是在同一次函数调用中继续。它适合把较长流程主动拆成多个短步骤,不能代替基于真实时间的延时。
CORO_DELAY(ctx, ui, ms, line)
1 | CORO_DELAY(ctx, ui_, 250U, 2); |
按 ui.getCurrentTime() + ms 记录 deadline,将状态设为 SUSPENDED,到期后从宏的下一行继续。deadline 使用无符号回绕安全比较,但单次等待必须小于 2^31 ms。它会参与 tickless 的最早唤醒时间计算,不需要开启 continuous draw。
CORO_WAIT_ANIMATION(ctx, ui, line)
1 | CORO_WAIT_ANIMATION(ctx, ui_, 3); |
挂起到 ui.activeAnimationCount() 变为 0。这里观察的是该 PixelUI 的全部动画,不只是当前 App 或当前 coroutine 启动的动画。若其他模块持续创建动画,等待会相应延长。
line 参数规则
line 最终会成为 C++ switch 的 case 标签,因此必须是编译期整数常量,并且在同一个 coroutine 函数内唯一;0 已由 CORO_BEGIN 使用,不能再传。可以使用人工编号,也可以用 __LINE__ 自动取得当前源码行号:
1 | CORO_DELAY(ctx, ui_, 100U, __LINE__); |
由于恢复点是 case 标签,不要让需要跨恢复点存活的普通局部变量依赖栈上的初始化。把持久状态放在 App 成员或 ctx.localData 中:
1 | Coroutine worker_{[this](CoroutineContext& ctx) { |
PixelUI 注册接口
addCoroutine()
1 | void addCoroutine(Coroutine* coroutine); |
把非拥有指针加入 scheduler;nullptr 会被忽略。它不会调用 start(),也不会检查重复注册。调用前应先 reset()/start(),并确保同一个指针只注册一次。容量由 PIXELUI_MAX_COROUTINE_NUM 决定;接口没有失败返回值,因此调用方必须在设计上保证不超过固定容量。
对象本身必须活到被移除或 scheduler 清理之后。最稳妥的做法是把 Coroutine 作为 App 成员,而不是函数局部变量。
removeCoroutine()
1 | void removeCoroutine(Coroutine* coroutine); |
移除所有与该指针相同的注册项。它不会改变 coroutine 自身的状态;之后可以先 reset()、start(),再重新注册。传入未注册的指针不会产生效果。
clearAllCoroutines()
1 | void clearAllCoroutines(); |
清空当前 PixelUI scheduler 中的全部非拥有指针,但不会 reset 对应对象。这个 API 影响所有 App/模块,不适合作为普通 App 的局部清理手段;退出时优先对自己的 coroutine 调用 removeCoroutine()。ViewManager 在销毁 App 前会执行全局清理,以避免遗留悬空指针。
getActiveCoroutineCount()
1 | size_t getActiveCoroutineCount(); |
返回 scheduler 当前保存的注册项数量,包括等待中的 coroutine,以及被注册但尚未 start() 的 CREATED coroutine。它是注册数量,不是“本帧正在执行”的数量。完成的 coroutine 会在 scheduler 更新中自动移除。
当 PIXELUI_USE_COROUTINE 为 0 时,以上四个 PixelUI 接口仍然可以调用:注册、移除和清理操作为空操作,getActiveCoroutineCount() 固定返回 0。业务代码若直接声明 Coroutine,仍需确保对应头文件和构建配置一致。
CoroutineScheduler(框架级)
1 | explicit CoroutineScheduler(PixelUI& ui); |
PixelUI 已经持有并驱动 scheduler,App 通常不应自行构造或调用它。update() 会先清理已完成项,再依次恢复满足条件的 coroutine,并在本轮末尾再次清理刚完成的项。每个 coroutine 执行前都会重新读取全局动画状态,因此前一个 coroutine 新启动的动画会影响后一个 coroutine 的 CORO_WAIT_ANIMATION 判断。nextWakeupMs() 供 PixelUI 的 event-driven/tickless 调度计算最早唤醒时间。
