docs 22: what D5 actually costs (a lifetime change, not a mechanism change)

The per-frame park is how a synchronous C++ frame waits; a frame that must not
park can only RETURN. So D5 = loop body on a context + DoRun returns + main()
returns without tearing wx down (EXIT_RUNTIME=0). Step 3 is the real cost and
needs its own gate - teardown, ~wxTopLevelWindowWasm, the S6 shutdown latch
and the v0.1.28 quit-notify fix all assume DoRun returning means the app ends.

Checked rather than assumed that no smaller fix exists: the scheduler stack IS
the main stack, so while it is parked in the rAF yield any pump entry re-enters
a logically suspended stack, and the loop parks every frame.

Recommends doing D5 first and alone (wx-only, cheap to iterate) before
re-enabling the already-written D/C/B/E on top.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EarUW9DS1c1sSW4ZNrkGQS
This commit is contained in:
Gergő Törcsvári 2026-08-07 01:31:15 +02:00
commit ef58c6ebce
No known key found for this signature in database
GPG key ID: 8E75F2CDE64E5322

View file

@ -230,6 +230,44 @@ Revised flip contents: **D5 + D + C + B + E**, landed together. That is a bigger
commit than doc 22 §5 planned, and it is the honest consequence of the measurement — commit than doc 22 §5 planned, and it is the honest consequence of the measurement —
the alternative (landing D without D5) is the partial migration this document forbids. the alternative (landing D without D5) is the partial migration this document forbids.
### What D5 actually costs (analysed 2026-08-07, before writing any of it)
`wxGUIEventLoop::DoRun`'s top-level body is:
```
while( !m_shouldExit ) { wxWasmScheduleProcessEvents(); wxWasmYieldToBrowser(); }
```
The park is not incidental — it is how a *synchronous C++ frame* waits for the next
frame. A frame that must not park has only one other way to wait: **return**. So D5 is
not "run the loop on a context" bolted onto the current shape; it is:
1. the loop body moves to a main-loop context whose per-frame wait is `yield_park`;
2. `DoRun` **returns** to `main()`, and the app is thereafter driven by JS ticks calling
the pump (a rAF loop marking the main-loop context ready, then `drain_all()`);
3. therefore `main()` must return without tearing wx down — the runtime stays alive
(`EXIT_RUNTIME=0`, no wx cleanup on that path).
Step 3 is the real cost: it changes application lifetime, and the existing comments
record that this area was deliberately built to avoid exactly that
(`simulate_infinite_loop=1`'s unwind throw is fatal under native wasm-EH —
docs/features/wasm-exceptions/08+09). Returning normally is NOT that throw and should be
safe, but it needs its own gate: teardown paths, `~wxTopLevelWindowWasm`, the shutdown
latch in S6, and the "quit notify only from IsMainFrame" fix from v0.1.28 all live on
the assumption that DoRun returning means the app is ending.
**No smaller fix exists, and this was checked rather than assumed:** the scheduler
stack IS the main stack, so while the main stack is Asyncify-parked in the rAF yield,
any pump entry re-enters wasm on a stack that is logically suspended — which is the
`overlapped-wake` above. Since the loop parks every frame, there is no window in which
pumping is safe. Hence the park has to go, and hence the return.
**Recommended next move:** do D5 first and ALONE on the sub-branch, gated by the wx
battery + coroutine trio (no KiCad build needed — it is wx-only), because it is the
step most likely to be wrong and the cheapest to iterate on. Only once the main stack is
provably scheduler-only should D/C/B/E be re-enabled on top; they are already written
and committed here (`WIP star-flip`), currently sitting behind this blocker.
### Phase E — bridges on contexts (1 wk) ### Phase E — bridges on contexts (1 wk)
Doc 21 §1's K1K7 and W4/W5 through one `wasm_await_promise`-style helper. After this **no Doc 21 §1's K1K7 and W4/W5 through one `wasm_await_promise`-style helper. After this **no