PixelUI’s Coroutine is a cooperative state machine built from a fixed context and macros. It is not a C++20 language coroutine. It is useful for short flows such as “start a series of animations one by one; or async wait, then continue.”
1 | Coroutine intro_{[this](CoroutineContext& ctx) { |
Registration and cleanup
1 | void onEnter(ExitCallback cb) override { |
A Coroutine should normally be an App member. Object state, scheduler registration, deadlines, and resume points are independent concerns; the API sections below explain each rule.
API details
Include this header to use the APIs:
1 |
CoroutineContext
Each Coroutine contains a context that stores execution position and waiting state across calls:
1 | struct CoroutineContext { |
| Field | Meaning |
|---|---|
pc |
Execution position of the macro state machine. Maintained by CORO_*; application code should not modify it. |
waitUntil |
Absolute timestamp used by CORO_DELAY. Maintained by the macro. |
localData[8] |
Eight persistent uint32_t slots for simple application state across resume calls. |
state |
Current state: CREATED, RUNNING, SUSPENDED, or FINISHED. |
waitReason |
Suspension reason: NONE, DELAY, or ANIMATION. The scheduler uses it to calculate the next wakeup. |
reset() does not clear localData. Explicitly clear used slots when a new run should not inherit old values:
1 | intro_.reset(); |
Apart from localData, normally read the context only for diagnostics. Manually changing pc, state, waitUntil, or waitReason can make macro state and scheduler decisions inconsistent.
Coroutine
Constructor
1 | Coroutine(CoroutineFunction func); |
The actual CoroutineFunction type is:
1 | etl::inplace_function<void(CoroutineContext&), CALLBACK_STORAGE_SIZE> |
The callback is stored inside the object without heap allocation. A lambda capture larger than the configured capacity fails at compile time; see Resource limits. Construction creates the Coroutine but does not execute its function.
start()
1 | void start(); |
When state is CREATED, start() changes it to RUNNING and places execution at the beginning. It does not call the Coroutine function immediately; the scheduler runs it during a later update. It has no effect in RUNNING, SUSPENDED, or FINISHED. Call reset() before restarting a completed Coroutine.
reset()
1 | void reset(); |
Restores CREATED state and resets pc, waitUntil, and waitReason. It neither clears localData nor removes or re-registers the Coroutine with the scheduler.
State queries
1 | bool isFinished() const; |
isFinished() only checks whether context state is FINISHED. getContext() provides state diagnostics and access to localData; prefer the const overload for diagnostics.
Low-level scheduling interface
1 | void resume(uint32_t currentTime, bool animationActive = false); |
CoroutineScheduler normally calls these methods; regular Apps do not need to drive them. resume() executes only a RUNNING Coroutine or a SUSPENDED one whose resume condition is satisfied. shouldRun() tests whether it may run now. nextWakeupMs() returns the milliseconds until the next run, or PixelUITime::NO_WAKEUP when no timed wakeup is currently needed.
When calling them manually, currentTime and animationActive must match the real state of the same PixelUI instance, or delay and Animation-wait semantics will be wrong.
Coroutine macros
Use every macro inside the same Coroutine function between CORO_BEGIN(ctx) and CORO_END(ctx).
CORO_BEGIN(ctx) / CORO_END(ctx)
1 | CORO_BEGIN(ctx); |
CORO_BEGIN opens a state machine based on ctx.pc. CORO_END sets state to FINISHED and returns. A normal execution path must not bypass CORO_END, or the Coroutine remains RUNNING and will be scheduled again.
CORO_YIELD(ctx, line)
1 | CORO_YIELD(ctx, 1); |
Saves the execution position and returns immediately without entering a timed wait. State remains RUNNING, so the scheduler continues at the next processing opportunity rather than during the same function call. Use it to split a longer flow into short steps, not as a substitute for real-time delay.
CORO_DELAY(ctx, ui, ms, line)
1 | CORO_DELAY(ctx, ui_, 250U, 2); |
Records ui.getCurrentTime() + ms as the deadline, sets state to SUSPENDED, and continues after the macro when due. Deadline comparison is safe across unsigned wraparound, but each wait must be shorter than 2^31 ms. The deadline participates in tickless earliest-wakeup calculation and does not require continuous drawing.
CORO_WAIT_ANIMATION(ctx, ui, line)
1 | CORO_WAIT_ANIMATION(ctx, ui_, 3); |
Suspends until ui.activeAnimationCount() reaches zero. This observes every Animation belonging to that PixelUI, not only Animations started by the current App or Coroutine. Other modules that continuously create Animations can therefore extend the wait.
Rules for the line parameter
line becomes a C++ switch case label, so it must be a compile-time integer constant and unique inside one Coroutine function. CORO_BEGIN already uses 0, which cannot be reused. Use manual numbering or __LINE__:
1 | CORO_DELAY(ctx, ui_, 100U, __LINE__); |
Because resume points are case labels, do not depend on ordinary local variables remaining initialized across them. Store persistent state in App members or ctx.localData:
1 | Coroutine worker_{[this](CoroutineContext& ctx) { |
PixelUI registration interface
addCoroutine()
1 | void addCoroutine(Coroutine* coroutine); |
Adds a non-owning pointer to the scheduler; nullptr is ignored. It does not call start() or check duplicate registration. Call reset() and start() first, register each pointer only once, and design for the PIXELUI_MAX_COROUTINE_NUM capacity. The API has no failure return value.
The object must remain alive until it is removed or the scheduler is cleared. An App member is safer than a function-local Coroutine.
removeCoroutine()
1 | void removeCoroutine(Coroutine* coroutine); |
Removes all registrations matching the pointer. It does not change the Coroutine’s state. The object can later be reset, started, and registered again. Passing an unregistered pointer has no effect.
clearAllCoroutines()
1 | void clearAllCoroutines(); |
Clears every non-owning pointer from this PixelUI scheduler without resetting the corresponding objects. It affects all Apps and modules, so regular Apps should remove their own Coroutines instead. ViewManager performs global cleanup before destroying an App to prevent dangling registrations.
getActiveCoroutineCount()
1 | size_t getActiveCoroutineCount(); |
Returns the current number of scheduler registrations, including waiting Coroutines and registered CREATED Coroutines that have not started. This is a registration count, not the number executing in this frame. The scheduler automatically removes completed Coroutines during update.
When PIXELUI_USE_COROUTINE is 0, these four PixelUI methods remain callable: add, remove, and clear are no-ops, and getActiveCoroutineCount() always returns zero. Code that declares a Coroutine directly must still keep its header and build configuration consistent.
CoroutineScheduler (framework level)
1 | explicit CoroutineScheduler(PixelUI& ui); |
PixelUI already owns and drives the scheduler; Apps normally should not construct or call one. update() removes finished entries, resumes each eligible Coroutine, and removes newly completed entries again at the end of the pass. It re-reads global Animation state before each Coroutine, so an Animation started by an earlier Coroutine can affect a later Coroutine’s CORO_WAIT_ANIMATION decision. nextWakeupMs() supplies the earliest deadline to PixelUI’s event-driven and tickless scheduler.
