Two root causes fixed end to end:
1. Exactness race: phaseFields' velocity pass wrote its own coarse blocks
and then read halo blocks no neighbour had committed yet — pool runs
diverged at random frames (2/4 runs). The velocity pass now runs as a
block-owner section at the END of phaseStrains; phaseFields only reads
committed velocities. 8/8 pool runs bit-exact through f1239; tests 9/9
(including 1600-frame x 2-worker runs across the section boundary).
2. Browser boot stall: beginPhase's hot spin (Atomics.wait throws on a
window) froze pending web-worker script loads — Chromium routes worker
startup through the busy main thread, so the show stalled 30s and
threw 'region pool stalled'. The web shell now acks ('pool') after
makeRegion (mail[63] is a real boot counter), the world runs cold until
every worker acks (step/stepTo skip frames; post-warm catch-up is
unbounded), and loadSongData awaits player.warm() before the show.
Node pools are untouched (native threads answer barriers directly).
Also: Player.build/rewind dispose the old pool (no more leaked idle
worlds); region-core.js is the shared worker body; region-worker.web.js
the browser shell (vite bundles it via the literal new Worker(new URL)
spelling — dist emits a self-contained worker asset instead of a broken
base64 data URL).
Circuit breaker in the demo: >=2 consecutive sub-25fps windows trip;
>=30 frames behind the audio clock for >=750ms trips (dormant while
stepTo catches up unconditionally — the fps path is live). 12/12 mirror
scenarios pass.
Bench: node serial 74.2ms/f, 2w 38.9 (1.91x), 4w 24.6 (3.02x). Browser:
steady 30fps including a 64s soak to 20k organisms at 640x360.