Compare commits

..
2 Commits
Author SHA1 Message Date
valknarandClaude Sonnet 5 44504c718d Fix eject hanging the tab on ROM-based tables
Bump @valknar/vpinball-wasm to 0.3.3, which fixes a genuine infinite
busy-wait in the PinMAME plugin's Controller::Stop() under Emscripten
(no OS thread ever clears the flag it spins on) — this fired from
dispose()'s synchronous script Exit event on any ROM-based table,
hanging the tab permanently.

Sequence unmount cleanup as stop() then two animation frames then
dispose(), instead of dispose() alone, matching the consumer pattern
the engine fix expects (stop() only takes effect on its next internal
step, so disposing immediately after can still race it).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 10:27:35 +02:00
valknarandClaude Sonnet 5 53c8ea03ac Show a spinner while the table is starting
instance.start() runs synchronously and blocks the main thread for a
noticeable moment on heavy WASM init, so pressing "Tap to Start" would
freeze on the old screen with no feedback until the HUD suddenly
appeared. Add a "starting" status between "ready" and "running", and
defer the actual start() call by a nested rAF so the browser gets a
chance to paint the spinner before the blocking call runs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 09:52:30 +02:00
6 changed files with 42 additions and 16 deletions
+2 -5
View File
@@ -64,11 +64,8 @@ export default function CoinDoorHud({ instanceRef, tableInfo, loadTimeMs }: Coin
}; };
const handleEject = () => { const handleEject = () => {
// dispose() (called by usePinballInstance's unmount cleanup, triggered by // usePinballInstance's unmount cleanup (triggered by this navigation)
// this navigation) tears the instance down immediately. Calling stop() // handles stop()+dispose() itself, sequenced to avoid hanging the tab.
// here too raced it — stop() defers teardown to the engine's next
// internal step, and dispose() freeing WASM memory before that step runs
// hung the tab.
router.push("/"); router.push("/");
}; };
+7
View File
@@ -74,6 +74,13 @@ export default function PinballCanvasImpl({ tableData, rom, tableInfo }: Pinball
</p> </p>
</div> </div>
)} )}
{status === "starting" && (
<div className="absolute inset-0 flex flex-col items-center justify-center gap-4 bg-ink/90 text-paper">
<div className="h-12 w-12 animate-spin rounded-full border-4 border-chrome/20 border-t-marquee" />
<div className="font-display text-2xl tracking-wide text-marquee">STARTING TABLE</div>
</div>
)}
</div> </div>
); );
} }
+24 -2
View File
@@ -10,7 +10,7 @@ import { hasWebGL2 } from "./webglSupport";
// outside webpack's module graph (see scripts/copy-engine-assets.mjs). // outside webpack's module graph (see scripts/copy-engine-assets.mjs).
const ENGINE_BASE_URL = "/vendor/vpinball-wasm"; const ENGINE_BASE_URL = "/vendor/vpinball-wasm";
export type PinballStatus = "idle" | "loading" | "ready" | "running" | "error"; export type PinballStatus = "idle" | "loading" | "ready" | "starting" | "running" | "error";
interface EngineModule { interface EngineModule {
loadPinball(options: LoadPinballOptions): Promise<PinballInstance>; loadPinball(options: LoadPinballOptions): Promise<PinballInstance>;
@@ -104,8 +104,20 @@ export function usePinballInstance(
return () => { return () => {
disposedRef.current = true; disposedRef.current = true;
instanceRef.current?.dispose(); const instance = instanceRef.current;
instanceRef.current = null; instanceRef.current = null;
if (instance) {
// stop() halts the engine's render loop, but only takes effect on
// its next internal step rather than immediately. Calling dispose()
// synchronously right after — freeing WASM memory before that step
// runs — is what hangs the tab. Give it a couple of frames first.
instance.stop();
requestAnimationFrame(() => {
requestAnimationFrame(() => {
instance.dispose();
});
});
}
}; };
// Intentionally run once: PlayView mounts a fresh PinballCanvasImpl // Intentionally run once: PlayView mounts a fresh PinballCanvasImpl
// (via `key`) per table rather than swapping tableData in place. // (via `key`) per table rather than swapping tableData in place.
@@ -114,8 +126,18 @@ export function usePinballInstance(
const start = useCallback(() => { const start = useCallback(() => {
if (!instanceRef.current) return; if (!instanceRef.current) return;
setStatus("starting");
// instance.start() runs synchronously and blocks the main thread for a
// noticeable moment (heavy WASM init) — a nested rAF lets the browser
// paint the "starting" spinner from the state update above before that
// block hits, instead of the UI freezing on the old screen mid-click.
requestAnimationFrame(() => {
requestAnimationFrame(() => {
if (disposedRef.current || !instanceRef.current) return;
instanceRef.current.start(); instanceRef.current.start();
setStatus("running"); setStatus("running");
});
});
}, []); }, []);
return { status, progress, error, loadTimeMs, instanceRef, start }; return { status, progress, error, loadTimeMs, instanceRef, start };
+1 -1
View File
@@ -12,7 +12,7 @@
"lint": "eslint" "lint": "eslint"
}, },
"dependencies": { "dependencies": {
"@valknar/vpinball-wasm": "0.3.2", "@valknar/vpinball-wasm": "0.3.3",
"next": "16.3.2", "next": "16.3.2",
"react": "19.2.8", "react": "19.2.8",
"react-dom": "19.2.8" "react-dom": "19.2.8"
+5 -5
View File
@@ -9,8 +9,8 @@ importers:
.: .:
dependencies: dependencies:
'@valknar/vpinball-wasm': '@valknar/vpinball-wasm':
specifier: 0.3.2 specifier: 0.3.3
version: 0.3.2 version: 0.3.3
next: next:
specifier: 16.3.2 specifier: 16.3.2
version: 16.3.2(@babel/core@7.29.7(supports-color@7.2.0))(@types/node@20.19.43)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) version: 16.3.2(@babel/core@7.29.7(supports-color@7.2.0))(@types/node@20.19.43)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
@@ -754,8 +754,8 @@ packages:
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@valknar/vpinball-wasm@0.3.2': '@valknar/vpinball-wasm@0.3.3':
resolution: {integrity: sha512-52vkjVzev92khiO8a50N2afYIviX1hHCMzTilN24Pe/7pu5m9LlSQVS3o11rRdItSF5h1RVmTLpoh1ENYrLQJw==, tarball: https://dev.pivoine.art/api/packages/valknar/npm/%40valknar%2Fvpinball-wasm/-/0.3.2/vpinball-wasm-0.3.2.tgz} resolution: {integrity: sha512-It2VO8FrmBa7u+aom4m+JCWaVv6ZOhRn0x8BqqrFbGo2ptTb64G8SgqRXuGU8GldhIvgl3/ug+PsSg3EhETZpg==, tarball: https://dev.pivoine.art/api/packages/valknar/npm/%40valknar%2Fvpinball-wasm/-/0.3.3/vpinball-wasm-0.3.3.tgz}
acorn-jsx@5.3.2: acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
@@ -2645,7 +2645,7 @@ snapshots:
'@unrs/resolver-binding-win32-x64-msvc@1.12.2': '@unrs/resolver-binding-win32-x64-msvc@1.12.2':
optional: true optional: true
'@valknar/vpinball-wasm@0.3.2': {} '@valknar/vpinball-wasm@0.3.3': {}
acorn-jsx@5.3.2(acorn@8.18.0): acorn-jsx@5.3.2(acorn@8.18.0):
dependencies: dependencies:
+1 -1
View File
@@ -2,5 +2,5 @@ allowBuilds:
sharp: false sharp: false
unrs-resolver: false unrs-resolver: false
minimumReleaseAgeExclude: minimumReleaseAgeExclude:
- '@valknar/vpinball-wasm@0.2.0 || 0.3.1 || 0.3.2' - '@valknar/vpinball-wasm@0.2.0 || 0.3.1 || 0.3.2 || 0.3.3'
updateNotifier: false updateNotifier: false