From 53c8ea03ac480868c806b1ca7b0b4fc55c834efc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Mon, 24 Aug 2026 09:52:30 +0200 Subject: [PATCH] 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 --- components/pinball/PinballCanvasImpl.tsx | 7 +++++++ lib/pinball/usePinballInstance.ts | 16 +++++++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/components/pinball/PinballCanvasImpl.tsx b/components/pinball/PinballCanvasImpl.tsx index e1229bb..8686ef5 100644 --- a/components/pinball/PinballCanvasImpl.tsx +++ b/components/pinball/PinballCanvasImpl.tsx @@ -74,6 +74,13 @@ export default function PinballCanvasImpl({ tableData, rom, tableInfo }: Pinball

)} + + {status === "starting" && ( +
+
+
STARTING TABLE
+
+ )}
); } diff --git a/lib/pinball/usePinballInstance.ts b/lib/pinball/usePinballInstance.ts index 352b88c..d5eee96 100644 --- a/lib/pinball/usePinballInstance.ts +++ b/lib/pinball/usePinballInstance.ts @@ -10,7 +10,7 @@ import { hasWebGL2 } from "./webglSupport"; // outside webpack's module graph (see scripts/copy-engine-assets.mjs). 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 { loadPinball(options: LoadPinballOptions): Promise; @@ -114,8 +114,18 @@ export function usePinballInstance( const start = useCallback(() => { if (!instanceRef.current) return; - instanceRef.current.start(); - setStatus("running"); + 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(); + setStatus("running"); + }); + }); }, []); return { status, progress, error, loadTimeMs, instanceRef, start };