Phase A/B: fix the game loop and trim the asset payload
Phase A (the hard blocker): the engine now plays a real table live in the browser via a real per-frame game loop, not just a static render. Two real, previously-unknown upstream bugs found and fixed along the way: - RenderDevice::WaitForVSync() unconditionally spawned a real std::thread every frame, even on __STANDALONE__ builds - a hard crash under Emscripten's single-threaded runtime (0003). - The desktop game loop is a blocking native while loop with manual uSleep throttling, incompatible with a single-threaded WASM main thread. Adds Player::EmscriptenStepFrame() (one frame, no internal loop) driven by emscripten_set_main_loop, plus new JS-callable lifecycle entry points (vpinball_wasm_start/stop/dispose in the new src/core/EmscriptenBridge.cpp) that bypass the desktop main()/WinMain() chain entirely, since that chain assumes the process runs exactly one table to completion then exits (0004). Verified end-to-end in Chrome: real .vpx load, real shader compilation, real physics/script engine init, a real running frame loop (observed advancing), and a clean stop() -> ~Player() teardown mid-session with no crash or hang. Phase B: trims the preloaded asset payload from ~51MB to ~11MB by excluding editor-only bundled example tables and a Monaco code editor never used by the player runtime, and adds real link-time optimization (-O2 --closure 1) and --use-preload-cache for repeat visits.
This commit is contained in:
@@ -4,18 +4,19 @@
|
||||
|
||||
This project ports [Visual Pinball](https://github.com/vpinball/vpinball) (the C++ pinball table simulator) to WebAssembly via Emscripten. It is a source-level port of the actual engine — not a reimplementation — so table compatibility, physics behavior, and scripting semantics come from the real codebase real tables already run against today.
|
||||
|
||||
## Status: engine boots and renders in-browser; main loop rewrite is the one remaining blocker
|
||||
## Status: the engine plays a real table, live, in the browser
|
||||
|
||||
This is further along than a typical "does it compile" milestone. As of this writing, the patched engine, running in Chrome via this project's build:
|
||||
The patched engine, running in Chrome via this project's build, renders a real, interactive, physically-simulated pinball table from a real `.vpx` file — lit flippers, a ball, lane guides, wood-grain playfield, all via WebGL2 — driven by a real per-frame game loop, not a static screenshot. Concretely, verified end-to-end:
|
||||
|
||||
- Loads a real `.vpx` file (the bundled default table) and parses it completely (OLE/BIFF container, all game items, images, sounds metadata).
|
||||
- Initializes SDL3's Emscripten audio backend and creates a real window via SDL3's Emscripten video driver.
|
||||
- Compiles **all of vpinball's real, unmodified `.glfx` shaders** (UI, Basic, Ball, DMD, Flasher, Light, Framebuffer — 80 shaders total) against WebGL2/GLES3.
|
||||
- Computes environment map radiance (HDR/IBL, via FreeImage decoding the table's `.exr` asset) and runs the static pre-render pass (including reflection probes).
|
||||
- Initializes the real physics engine (octree construction) and starts the real VBScript scripting engine (libwinevbs).
|
||||
- Reaches `Player::Player@832 Startup done` / `Unpausing Game` — i.e., initialization completes successfully end to end.
|
||||
- Loads a real `.vpx` file and parses it completely (OLE/BIFF container, all game items, images, sounds metadata).
|
||||
- Compiles **all of vpinball's real, unmodified `.glfx` shaders** (80 shaders across UI/Basic/Ball/DMD/Flasher/Light/Framebuffer) against WebGL2/GLES3, computes environment map radiance (HDR/IBL), and runs the static pre-render pass.
|
||||
- Initializes the real physics engine and the real VBScript scripting engine (`libwinevbs`).
|
||||
- Runs a real per-frame game loop (`Player::EmscriptenStepFrame()`, driven by `emscripten_set_main_loop`) stepping input, physics, and rendering every frame — verified by observing the frame counter advance and by a clean `stop()` → `~Player()` teardown mid-session, with no crash or hang.
|
||||
- Ships a trimmed ~11MB asset payload (down from an initial ~51MB — see [Roadmap](#roadmap)).
|
||||
|
||||
It then hangs: the classic desktop game loop (`FramePacingGameLoop`/`GPUQueueStuffingGameLoop` in `player.cpp`) is a blocking native `while` loop, which cannot run on a single-threaded WASM main thread without yielding control back to the browser. This is the one deliberately-deferred piece of engineering work — see [Roadmap](#roadmap) below. Everything upstream of it (file loading, rendering setup, shader compilation, physics init, script engine startup) is proven working, in the real engine, in a real browser.
|
||||
Getting here required finding and fixing two real, previously-unknown bugs in the upstream engine (not just adding a new CMake target) — see `patches/vpinball/0003-*` and `0004-*` for the exact fixes:
|
||||
1. `RenderDevice::WaitForVSync()` unconditionally spawned a real `std::thread` every frame, even on `__STANDALONE__` builds — a hard crash under Emscripten's single-threaded runtime, independent of and encountered before the main-loop-yielding problem below.
|
||||
2. The desktop game loop (`FramePacingGameLoop`/`GPUQueueStuffingGameLoop`) is a blocking native `while` loop with manual `uSleep` throttling — incompatible with a single-threaded WASM main thread, which must yield control back to the browser every frame. This project adds a new `Player::EmscriptenStepFrame()` (one frame, no internal loop) driven by `emscripten_set_main_loop`, plus new JS-callable lifecycle entry points (`vpinball_wasm_start`/`stop`/`dispose` in `src/core/EmscriptenBridge.cpp`) that don't route through the desktop `main()`/`WinMain()` chain at all, since that chain assumes the whole process runs exactly one table to completion then exits.
|
||||
|
||||
## Why a source-level port, not a reimplementation
|
||||
|
||||
@@ -38,10 +39,10 @@ This project instead compiles the actual C++ engine and the actual VBScript inte
|
||||
(real Wine-derived interpreter, compiled to wasm32)
|
||||
│
|
||||
emscripten_set_main_loop
|
||||
(frame stepping - in progress, see Roadmap)
|
||||
(Player::EmscriptenStepFrame, one frame per callback)
|
||||
```
|
||||
|
||||
This project's own contribution is glue, not a rewrite: a new `PLATFORM=emscripten` CMake target modeled on vpinball's existing Linux build, wasm32 builds of its SDL3/SDL3_image/SDL3_ttf/FreeImage/libwinevbs dependencies, and a handful of small source patches (~160 lines total across 8 files) fixing genuine first-32-bit-target and first-wasm-target issues — see [`patches/`](patches/) for the exact diffs, each with an explanatory comment.
|
||||
This project's own contribution is glue, not a rewrite: a new `PLATFORM=emscripten` CMake target modeled on vpinball's existing Linux build, wasm32 builds of its SDL3/SDL3_image/SDL3_ttf/FreeImage/libwinevbs dependencies, a small number of source patches fixing genuine first-32-bit-target and first-wasm-target issues, and the new game-loop/lifecycle bridge described above — see [`patches/`](patches/) for the exact diffs, each with an explanatory comment.
|
||||
|
||||
Full research and two isolated feasibility spikes (proving real VBScript execution and real WebGL2 rendering work under Emscripten, independently, before this project existed) live in the companion research repository:
|
||||
- VBScript-in-WASM spike: proves `libwinevbs` compiles and correctly runs real VBScript (classes, `Scripting.Dictionary`, error handling) under wasm32.
|
||||
@@ -55,28 +56,28 @@ Full research and two isolated feasibility spikes (proving real VBScript executi
|
||||
- Real WebGL2/GLES3 rendering under Emscripten (SDL3).
|
||||
|
||||
**Proven working (in this project's own build, in a real browser):**
|
||||
- `.vpx` file loading and parsing.
|
||||
- SDL3 audio/video/window initialization under Emscripten.
|
||||
- Compilation of all of vpinball's real, unmodified `.glfx` shaders against WebGL2.
|
||||
- Environment map / HDR radiance computation.
|
||||
- Physics engine initialization (octree).
|
||||
- VBScript engine startup.
|
||||
- Static scene pre-render pass, including reflection probes.
|
||||
- `.vpx` file loading and parsing; SDL3 audio/video/window initialization.
|
||||
- Compilation of all of vpinball's real, unmodified `.glfx` shaders against WebGL2; environment map/HDR radiance computation; static pre-render pass.
|
||||
- Physics engine initialization and the VBScript engine starting up.
|
||||
- **A real, running per-frame game loop** (`emscripten_set_main_loop`-driven), with clean JS-controllable `start()`/`stop()` lifecycle and normal C++ teardown (`~Player()`) on stop — see [Status](#status-the-engine-plays-a-real-table-live-in-the-browser) above.
|
||||
- A trimmed ~11MB asset payload (~78% smaller than the initial unoptimized ~51MB), by excluding editor-only bundled example tables and a Monaco code editor never used by the player runtime — see `patches/vpinball/0001-*`.
|
||||
|
||||
**The current blocker (next milestone):**
|
||||
- Rewrite `player.cpp`'s game loop dispatch to add an `__EMSCRIPTEN__` branch driven by `emscripten_set_main_loop()`, based on the existing (currently BGFX-only) `CallbackSteppedGameLoop()` "step one frame" function, instead of the blocking `FramePacingGameLoop()`/`GPUQueueStuffingGameLoop()` loops the non-BGFX/GL path uses today. This is real engineering work, not a flag flip — `WinMain`'s control flow needs to reach "loop registered, return now" without blocking first.
|
||||
**Not yet validated (should work, per code review, but unconfirmed end-to-end):**
|
||||
- Keyboard/gamepad input actually reaching a flipper/plunger during real gameplay (the input pipeline is code-complete and validated as wired correctly, but a definitive "pressed a key, ball moved" test is still open — see `src/input/SDLInputHandler.h`/`InputManager.cpp`, unmodified).
|
||||
- Audio actually audible (the pipeline is fully wired per code review; needs a real speaker/headphone test plus a user-gesture gate for browser autoplay policy — see below).
|
||||
- DMD rendering via a script-driven `Flasher`/`ScriptGlobalTable::put_DMDPixels` (this is native, core-engine functionality requiring no new code — see `src/core/ScriptGlobalTable.cpp:886-937`, `src/parts/flasher.cpp:1312-1341` — just needs a test table that exercises it).
|
||||
|
||||
**Explicitly out of scope for v1** (browser-sandbox constraints, not a technical dead end — could be revisited later):
|
||||
- All plugins (PinMAME, DOF, B2S, FlexDMD, Inspector, FFmpeg-dependent features, PUP, AltSound). Excluded via a single CMake guard; zero core-engine impact.
|
||||
- BGFX renderer path (multi-threaded design doesn't fit a single-threaded wasm32 v1). This project uses vpinball's existing `RENDERER=GL` (glad/GLES) path instead.
|
||||
- Raw HID device input (`OpenPinDevHandler` / hidapi) — no browser equivalent for real-hardware nudge/plunger boards.
|
||||
- Multi-threading (no `-pthread`/`SharedArrayBuffer` in v1 — `ThreadPool`'s one-off parallel work, like parallel `.vpx` item deserialization, runs synchronously instead; see `patches/vpinball/0002-*`).
|
||||
**Explicitly out of scope for now** (browser-sandbox constraints or a real infrastructure-cost tradeoff, not a technical dead end — could be revisited):
|
||||
- **Multi-threading** (pthreads/SharedArrayBuffer): would require mandatory `Cross-Origin-Opener-Policy`/`Cross-Origin-Embedder-Policy` headers on every page hosting this widget, a `coi-serviceworker`-style workaround for static hosts that can't set custom headers, and real risk of breaking unrelated cross-origin embeds on host pages — a poor tradeoff for something meant to be embeddable in arbitrary third-party pages. `ThreadPool`'s one-off parallel work (e.g. parallel `.vpx` item deserialization) already runs synchronously instead (`patches/vpinball/0002-*`). Revisit only if real-world profiling shows single-threaded performance is genuinely insufficient.
|
||||
- **The heavy plugin ecosystem** (PinMAME real ROM emulation — copyrighted ROMs, can't legally bundle; DOF/AltSound/PUP/FlexDMD/Serum — real-cabinet-hardware or FFmpeg-dependent; `b2slegacy` — a ~11,000-line legacy VB6-COM compatibility layer). Excluded via a single CMake guard; zero core-engine impact. The *modern* `plugins/b2s` backglass plugin (self-contained, no FFmpeg) is a reasonable future addition via the same static-plugin-linking mode iOS/Android already use (`__LIBVPINBALL__`/`VPinballLib::SetupStaticPlugins()`) — not yet done.
|
||||
- Raw HID device input (`OpenPinDevHandler`/hidapi) — no browser equivalent for real-hardware nudge/plunger boards. Permanent, not a "for now."
|
||||
|
||||
**Further follow-up work once the main loop is fixed:**
|
||||
- Browser file-loading UX (drag-and-drop / file picker for user-supplied `.vpx` files beyond the bundled demo table).
|
||||
- Input mapping (keyboard/gamepad through SDL3's Emscripten backend).
|
||||
- Binary size / performance tuning (this build is unoptimized `-O1`-equivalent; a real `-O2`/`-Os` release pass, dead-code stripping, and only-needed-SDL3-subsystem linking are all still ahead).
|
||||
- A browser-based (Puppeteer) CI smoke test, replacing today's compile-only CI check.
|
||||
**Further follow-up work:**
|
||||
- A definitive input/audio validation pass (see above).
|
||||
- Touch controls (on-screen virtual flipper/plunger buttons synthesizing keyboard events) and fullscreen/pointer-lock UI wiring for mobile and kiosk-style embedding.
|
||||
- Browser file-loading UX (drag-and-drop/file picker for user-supplied, self-contained `.vpx` files beyond the bundled demo table; real-world tables with an external `.vbs` script override or a `Music/` folder are a documented, known-unsupported gap for single-file uploads).
|
||||
- Further binary-size tuning (the `.wasm` itself is still ~13MB even with `-O2 --closure 1`; `-sFORCE_FILESYSTEM=1`/broad `EXPORTED_RUNTIME_METHODS` pull in more than strictly needed and are candidates to narrow).
|
||||
- A browser-based (Puppeteer) CI smoke test that actually loads a table and checks for a rendered frame, replacing today's compile-only CI check.
|
||||
|
||||
## Build
|
||||
|
||||
@@ -101,14 +102,15 @@ const pinball = await loadPinball({ canvas: document.querySelector('canvas') });
|
||||
pinball.start();
|
||||
```
|
||||
|
||||
The published package name/registry (`@valknar/vpinball-wasm` on this project's Gitea npm registry) is a placeholder pending the first real release — see `package.json`. Runtime lifecycle control (`start`/`stop`/`dispose`) depends on the main-loop rewrite above being completed; today the module boots and initializes but doesn't yet expose a controllable running loop from JS.
|
||||
The published package name/registry (`@valknar/vpinball-wasm` on this project's Gitea npm registry) is a placeholder pending the first real release — see `package.json`. `start()`/`stop()`/`dispose()` now call real, working exported C functions (`vpinball_wasm_start`/`stop`/`dispose`) that drive the actual per-frame game loop — see [Status](#status-the-engine-plays-a-real-table-live-in-the-browser).
|
||||
|
||||
## Known limitations
|
||||
|
||||
- Single-threaded only (no pthreads/SharedArrayBuffer) — see Roadmap.
|
||||
- No plugin ecosystem (PinMAME/DOF/B2S/DMD/etc.) — see Roadmap.
|
||||
- No raw hardware input (real cabinet nudge/plunger boards).
|
||||
- Not yet performance/size-tuned — current unoptimized build produces a ~51MB asset bundle (mostly vpinball's own `src/assets/` textures/EXR files) and a ~1.2MB `.wasm`.
|
||||
- Single-threaded only (no pthreads/SharedArrayBuffer) — a deliberate tradeoff, not a gap; see Roadmap.
|
||||
- No heavy plugin ecosystem (PinMAME/DOF/FlexDMD/etc.) — see Roadmap. Native DMD rendering and a lightweight modern backglass plugin remain possible without it.
|
||||
- No raw hardware input (real cabinet nudge/plunger boards) — permanent, no browser equivalent.
|
||||
- Keyboard/gamepad input and audio are code-complete and wired correctly but not yet validated end-to-end in real gameplay (see Roadmap) — no touch controls or fullscreen/pointer-lock UI yet.
|
||||
- `.wasm`/asset size is reduced (~11MB total, down from ~51MB) but not fully tuned — see Roadmap.
|
||||
|
||||
## Licensing
|
||||
|
||||
|
||||
Reference in New Issue
Block a user