Controller::Stop() (real desktop vpinball code, unmodified until now)
busy-waits for PinmameIsRunning() to clear after calling PinmameStop(),
in a sleep loop meant for a real OS thread to notice the quit flag and
finish stopping on its own. Under Emscripten there is no such thread -
PinmameStop() there only sets the quit flag - so the loop spun forever
on the single available thread, with nothing left to ever clear it.
This fires from Player::~Player()'s GameEvents_Exit script event
(controller.vbs's default Exit handler calls Controller.Stop), which
runs synchronously inside dispose() - hanging any consumer's teardown
for a ROM-based table, most visibly React StrictMode/unmount cleanup
calling stop() then dispose() shortly after.
Fixed by driving one more (now-instant, since the quit flag is already
set) PinmameEmscriptenStep() call directly under __EMSCRIPTEN__ instead
of busy-waiting - it runs cpu_post_run() and OnStateChange(0)
synchronously right there.
Also adds a "Stop & Dispose" button to the basic example, exercising
the same stop()-then-wait-two-frames-then-dispose() sequence consumers
use, to make this kind of regression visible without a separate app.
Confirmed fixed by hands-on testing: dispose() on a running ROM-based
table now returns immediately instead of hanging the tab.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>