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

10 KiB

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 (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 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 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/ 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).

./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

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 — the engine this project ports.
  • libwinevbs — the real VBScript interpreter (Wine-derived), compiled here to wasm32.
  • Wine — original source of the VBScript/OLE Automation engine libwinevbs extracts and packages.