Files
vpinball-wasm/README.md
T
valknar 0a63835689
CI / Build wasm engine (push) Failing after 3m30s
CI / Publish to npm registry (push) Skipped
Scaffold vpinball-wasm: Emscripten port of Visual Pinball
Build scripts, CMake/source patches, npm packaging, and a Gitea CI
workflow to compile the real Visual Pinball engine (SDL3 + WebGL2 +
libwinevbs for real VBScript) to WebAssembly.

The patches are validated end-to-end: the patched engine boots in a
real browser, loads a real .vpx table, compiles its real GLSL shaders,
computes environment map radiance, initializes physics, and starts the
VBScript engine, before hanging on the one deliberately-deferred piece
of work (game loop rewrite around emscripten_set_main_loop), documented
in the README's roadmap.
2026-08-22 14:30:14 +02:00

122 lines
10 KiB
Markdown

# vpinball-wasm
**Visual Pinball's real engine, compiled to WebAssembly — real `.vpx` tables, real VBScript table logic, real WebGL2 rendering, in the browser.**
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
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:
- 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.
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.
## Why a source-level port, not a reimplementation
Projects like [`vpx-js`](https://github.com/vpdb/vpx-js) reimplement Visual Pinball's physics and a VBScript-to-JavaScript transpiler from scratch in TypeScript. That approach has to independently re-derive correct behavior for every physics quirk and every VBScript language feature — and in `vpx-js`'s case, its own test suite documents that it never achieved compatibility with `core.vbs`, the shared script library nearly all real tables depend on.
This project instead compiles the actual C++ engine and the actual VBScript interpreter (`libwinevbs`, extracted from Wine, the same interpreter real tables already run against on macOS/Linux/iOS/Android today) to WebAssembly. Table compatibility and scripting correctness come from code that's already correct, not from independently re-deriving it.
## Architecture
```
.vpx file (OLE/BIFF) → vpinball's own loader (POLE-based, unmodified)
┌──────────────────────┴───────────────────────┐
│ │
Physics engine Rendering
(unmodified C++) SDL3 → WebGL2/GLES3 (glad)
│ real .glfx shaders
│ │
VBScript table logic ←────────────────────── libwinevbs
(real Wine-derived interpreter, compiled to wasm32)
emscripten_set_main_loop
(frame stepping - in progress, see Roadmap)
```
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.
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.
- SDL3/WebGL2 spike: proves SDL3's Emscripten backend creates a real WebGL2 context and GLSL ES 3.00 rendering works.
- Feasibility reports covering the native engine, `vpx-js`, and `libwinevbs` specifically.
## Roadmap
**Proven working (independent spikes, before this project's own build existed):**
- Real VBScript execution under wasm32 (libwinevbs).
- 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.
**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.
**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-*`).
**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.
## Build
Requires: `bison` ≥ 3.8.2, `curl`, `git`, `cmake` ≥ 3.25, `python3` (for the dev server), a POSIX shell. Emscripten itself is installed automatically by `setup.sh`. Expect a multi-gigabyte `emsdk/` + `build/` footprint and a first build in the tens of minutes (subsequent builds are much faster, especially with CI caching).
```bash
./scripts/setup.sh # installs emsdk + bison check, fetches vpinball + libwinevbs at pinned commits, applies patches/
source emsdk/emsdk_env.sh
./scripts/build-deps.sh # builds SDL3/SDL3_image/SDL3_ttf/FreeImage/libwinevbs for wasm32
./scripts/build.sh # configures + builds vpinball itself -> dist/vpinball.{js,wasm,data}
./scripts/dev-server.sh # serves dist/ locally for manual testing
```
`scripts/build.sh --debug` produces a build with assertions and runtime-exit-on-return enabled, useful for diagnosing engine startup issues (this is exactly how the current game-loop blocker above was diagnosed).
## npm usage
```ts
import { loadPinball } from '@valknar/vpinball-wasm';
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.
## 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`.
## Licensing
Visual Pinball itself is under a **mixed license**: the project has been migrating file-by-file from a legacy "old MAME"-like license to GPLv3+ since October 2020; each GPLv3+ file is marked `// license:GPLv3+` at its top, and any file without that marking remains under the legacy license. See `vendor/vpinball/LICENSE` (fetched by `setup.sh`) for the authoritative text — **do not treat this README as a substitute for reading it** before redistributing built artifacts. `libwinevbs` is LGPL-2.1 (Wine-derived) with a handful of small ATL header stubs of less certain provenance (see its own `README.md`). This project's own glue code (CMake integration, patches, npm wrapper) has no license conflict with either, but the combined built artifact's distribution terms are governed by vpinball's and libwinevbs's licenses, not just this repository's.
## Attribution
- [Visual Pinball](https://github.com/vpinball/vpinball) — the engine this project ports.
- [libwinevbs](https://github.com/vpinball/libwinevbs) — the real VBScript interpreter (Wine-derived), compiled here to wasm32.
- [Wine](https://www.winehq.org/) — original source of the VBScript/OLE Automation engine libwinevbs extracts and packages.