From ab6e44e57fe66e28b38dfe66ddf6de0b18e3799c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20T=C3=B6rcsv=C3=A1ri?= Date: Fri, 7 Aug 2026 22:33:41 +0200 Subject: [PATCH] docs 22 + wx bump: DOM entries on the dispatch context; canvas tools green at D-on The increment doc 22 ordered last round. Every entry that can reach a tool coroutine - the four DOM callbacks and the mailbox tick - now goes through the scheduler, so a coroutine is never entered by a star transfer from one path and a direct symmetric swap from another. At D-on all four canvas-tool specs are GREEN and the KiCad suite is 136/3 (was 135/5 before the sleep work, with the tools red throughout). context_sleep's wake learned the mirror lesson: now that the mailbox runs ON a context, it must NOT call drain_all from there (drain refuses re-entry, and should) - it marks ready and lets the outer drain_all perform the entry, with an armed pump as a backstop. Recorded honestly, not papered over: two of the three remaining D-on failures are the timer-park and quasimodal-strand levers, each failing ONE assertion - "scheduler shim observed the concurrent-park window", expected >0, got 0 - while fired/done/parked/errors all pass. That counter needs TWO concurrent in-place Asyncify parks, and the lever stages "timer park x MAIN-LOOP YIELD PARK"; D5 removed the main loop's Asyncify park, so the overlap cannot occur. Re-pinning those levers to the post-migration invariant is a Phase F decision alongside fiber-resume-park's red->green flip, NOT an assert to relax now. Landing state: STAR_DISPATCH=0, kicad 139 passed / 1 (pre-existing occ-probe). Next: Phase E - the K1-K7 bridges are the only in-place parks left under a context, and are exactly what the current()!=0 fallback still tolerates. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01LBjomQfKyRa3jBdeAKpmTw --- .../features/async/22-absorbing-libcontext.md | 54 +++++++++++++++++++ wasm/shims/context_sleep.cpp | 26 +++++++-- wxwidgets | 2 +- 3 files changed, 78 insertions(+), 4 deletions(-) diff --git a/docs/features/async/22-absorbing-libcontext.md b/docs/features/async/22-absorbing-libcontext.md index 9f832e1..3d0c6b5 100644 --- a/docs/features/async/22-absorbing-libcontext.md +++ b/docs/features/async/22-absorbing-libcontext.md @@ -794,6 +794,60 @@ main stack, `resolve_root_identity()` always answers "the running context". **Landing state: `wxWASM_STAR_DISPATCH` back to 0**, context-sleep and its pump-ownership guard kept (inert at D-off, verified 139/1). +### DOM entries on the dispatch context — THE CANVAS TOOLS GO GREEN (2026-08-07) + +The increment the section above ordered, built and measured: **every entry that can +reach a tool coroutine now goes through the scheduler.** + +- `wxWasmRunOnDispatchContext(fn, arg)` (evtloop.cpp) hands work to a dispatch context + and pumps. It is SYNCHRONOUS in the common case — `drain_all` returns once the + context parks at idle, i.e. after the job completed — so callers that need an answer + still get one. It falls back to running inline when already on a dispatch context + (same-stack recursion, as `wxYield` does) or when the registry says another context + is running, which is the in-place-parked-bridge window Phase E closes. +- All four DOM callbacks (`MouseCallback`, `WheelCallback`, `TouchCallback`, + `KeyCallback`) and the **mailbox tick** now route through it. Jobs are heap-owned + with ownership going to *whoever finishes last*, because a job may park for a + dialog's lifetime: the job deletes itself if the caller has given up, else the caller + deletes it and reads its result. Keys keep their synchronous `preventDefault` (the + job writes it before it can park; a job that parks anyway leaves the default — the + browser cannot wait for a modal). +- The context-sleep wake had to learn the same lesson in reverse: now that the mailbox + runs ON a context, `wake_sleeper` must not call `drain_all` from there (drain refuses + re-entry, correctly). It marks ready and lets the outer `drain_all` — still pumping + on the scheduler stack — perform the entry, with an armed pump as a backstop. + +**Result at D-on: KiCad 136 passed / 3, and all four canvas-tool specs are GREEN** +(eeschema draw-wires, pcbnew draw-lines, pcbnew move-with-m, presence-locks move). +The mixed-mode rewind class — a coroutine entered by a star transfer from the tick and +by a direct symmetric swap from a DOM handler — is gone, which was Phase D's blocker. + +**The 3 remaining, and 2 of them are pins measuring a world D5 deleted.** Besides the +pre-existing `occ-probe`, `timer-park-repro` and `quasimodal-strand`'s *staging* test +fail on ONE assertion: "scheduler shim observed the concurrent-park window", expected +`> 0`, got `0`. Everything else in those specs passes — `fired=true done=true +parked=true errors=0`, i.e. the parking timer handler parks, rewinds and survives. + +That counter fires when `handleSleep` is entered while `currData` is non-null: **two +concurrent in-place Asyncify parks.** The lever stages "timer park × MAIN-LOOP YIELD +PARK" (its own header says so), and D5 removed the main loop's Asyncify park — so the +overlap cannot occur any more. The spec's comment even anticipates the shape of this: +"that assert fails only if the lever itself never created the overlap (a broken repro, +not a passing one)" — here the repro is not broken, its ingredient was deliberately +eliminated. + +**Do NOT paper over this by relaxing the assert.** It is a Phase F decision, alongside +`fiber-resume-park.spec.ts`'s red→green flip: each lever gets re-pinned to the +post-migration invariant (the runtime survives AND no concurrent-park window is +observable) with the evidence recorded, or a replacement lever is built that stages a +still-possible overlap. Until then the honest statement is: at D-on those two specs +cannot stage their scenario, and the runtime behaviour they guard is green. + +**Landing state: `wxWASM_STAR_DISPATCH` back to 0** (all of the above is inert there), +everything kept. **Next: Phase E** — the K1–K7 bridges are now the only in-place parks +left under a context, and they are what the `current() != 0` fallback above still +tolerates. + 1. **pthreads.** Doc 21 §2 settled that every Asyncify park is main-thread and the lib bridge's worker path is a blocking proxy. Phase A must re-check that libcontext is never driven from a worker before assuming the scheduler is main-thread-only. diff --git a/wasm/shims/context_sleep.cpp b/wasm/shims/context_sleep.cpp index 448f50c..04ab2e5 100644 --- a/wasm/shims/context_sleep.cpp +++ b/wasm/shims/context_sleep.cpp @@ -39,6 +39,8 @@ #include +#include + #include // The scheduler mailbox (wx/wasm/private/mailbox.h). A timer message is the @@ -62,10 +64,28 @@ void wake_sleeper( void* aArg ) const pcbjam_sched::ContextId id = static_cast( reinterpret_cast( aArg ) ); - // mark_ready never resumes inline (doc 13 §1.4); drain_all performs the - // entry from this clean mailbox-tick stack. - if( pcbjam_sched::mark_ready( id, 0 ) ) + // mark_ready never resumes inline (doc 13 §1.4). WHO performs the entry + // depends on where this delivery landed: + if( !pcbjam_sched::mark_ready( id, 0 ) ) + return; + + if( pcbjam_sched::current() == 0 ) + { + // On the scheduler stack: drain here. pcbjam_sched::drain_all(); + return; + } + + // Delivered ON a context — the mailbox tick itself now runs on a dispatch + // context (doc 22 §10). drain() would refuse re-entry, and it should: the + // outer drain_all that entered this context keeps pumping once it parks, + // and picks up the context we just marked ready. Arm a pump anyway so a + // delivery that reached here by some other route cannot strand it. + EM_ASM( { + setTimeout( function() { + if( Module["_wxWasmSchedPump"] ) Module["_wxWasmSchedPump"](); + }, 0 ); + } ); } } // namespace diff --git a/wxwidgets b/wxwidgets index b4fb50f..8f61561 160000 --- a/wxwidgets +++ b/wxwidgets @@ -1 +1 @@ -Subproject commit b4fb50faa869a827a69bf35832055279b265c3c8 +Subproject commit 8f61561f061f1a38e98be5ac4b9c7b0fd2bbd234