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>
vpinball
Play real .vpx pinball tables in your browser. Drop in your own table or try the bundled demo —
no plugins, no native install. Powered by @valknar/vpinball-wasm,
a WebAssembly build of the real Visual Pinball engine
(real physics, real VBScript table scripting, real WebGL2 rendering).
Features
- Play any self-contained
.vpxtable — drag a file onto the landing page, or click to browse. - ROM support for real-hardware ("SS") tables — attach the table's PinMAME ROM
.zipalongside the.vpxupload; most tables don't need this. - A local table library — uploads are saved in the browser (IndexedDB) so you don't have to re-upload the same table (and its ROM) on your next visit. Nothing leaves your browser; there's no server-side storage.
- Demo mode — a bundled table plays instantly with no upload required.
- Fullscreen, a live FPS/stats readout, and a game info panel (table name, size, load time) in an in-game HUD styled after a pinball cabinet's coin door.
- Installable as a PWA — add it to your home screen or desktop; the engine is cached after your first game so it's available offline afterward.
- Deployable as a static site via Docker Compose — no Node server at runtime, just nginx serving prebuilt files.
Prerequisites
- Node.js ≥ 20, pnpm ≥ 11
- Docker + Docker Compose, for the containerized deploy
Install
pnpm install --frozen-lockfile --trust-lockfile
vpinball-wasm is published to a private Gitea npm registry
(https://dev.pivoine.art/api/packages/valknar/npm/) under the @valknar scope. .npmrc maps
that scope to the registry; the package is publicly readable, so no auth token is needed to
install it.
To bump the engine to a newer published version:
pnpm add @valknar/vpinball-wasm@<version>
Development
pnpm dev
pnpm dev copies the engine's build output into public/vendor/vpinball-wasm/ first (via the
predev script) — it's gitignored and regenerated from node_modules, so this only needs
pnpm install to have completed. Re-run pnpm run copy-engine-assets by hand if you update the
dependency without restarting the dev server.
The dev server runs at http://localhost:3000.
Build
pnpm build
Produces a fully static export in out/ (next.config.ts sets output: "export") — no Node
server needed to serve it.
Docker deploy
No secrets or registry authentication are required to build the image.
The compose file is set up for a Coolify/Traefik deployment: it joins an
external Docker network and routes entirely through Traefik labels, with no host port published.
Copy .env.example to .env and set:
TRAEFIK_ENABLED=trueTRAEFIK_HOST— the domain to route to this serviceNETWORK_NAME— the external Docker network Traefik and this service both join (Coolify provisions this automatically for projects deployed through it)
docker compose up -d --build
There's no local-only mode — without Traefik routing to it, the container isn't reachable from
the host. To preview the static build locally instead, run pnpm build and serve out/ with any
static file server (e.g. npx serve out).
PWA
The app can be installed from the browser's install prompt. A hand-rolled service worker
(public/sw.js) precaches the app shell; the engine's .wasm/.data files (~34 MB) are cached
on first visit to /play, not upfront, so installing the app doesn't force a large download
before anyone has actually played. After that first visit, offline reloads work.
When you ship a release that changes the app shell or updates the vendored engine build, bump the cache version so returning visitors actually get the update:
pnpm run sw:bump
Controls
| Key | Action |
|---|---|
| 4 | Insert coin |
| 1 | Start game |
| Enter | Launch ball |
| Left Shift | Left flipper |
| Right Shift | Right flipper |
On touch devices, an on-screen control overlay appears automatically once a table is running.
Limitations
Inherited from the underlying engine:
- Only self-contained
.vpxtables are supported — tables that reference an external.vbsscript override or aMusic/folder won't load correctly. - PinMAME (
VPinMAME.Controller) ROM-driven emulation runs, but on-screen DMD/backglass output and hands-on switch/solenoid/scoring behavior aren't yet confirmed end-to-end — see the engine's own README for the current state. No DOF, FlexDMD, or backglass (.directb2s) plugin support. ROM files are copyrighted; only supply ones you're legally entitled to use. - The ROM
gameNamePinMAME needs is derived from the uploaded zip's filename (e.g.hvymetal.zip→hvymetal) — rename the zip to match if a table doesn't recognize its ROM. - Single-threaded (no
SharedArrayBuffer) — performance depends on your device's single-core WebGL2 throughput. - Initial engine download is ~34 MB (cached after the first visit).
- Gamepad input and DMD rendering are implemented but not yet confirmed on real hardware/tables.
- Raw cabinet hardware input (real nudge/plunger boards) has no browser equivalent.
- The table library lives in the browser's IndexedDB storage — clearing site data removes it, and it doesn't sync across browsers/devices.
License
This app's own source has no license file yet — add one before distributing it. Separately, the
built output bundles the @valknar/vpinball-wasm engine and its default table, which redistribute
Visual Pinball source under a mixed legacy/GPLv3+ license (migrating file-by-file to GPLv3+
since October 2020) — see that package's own LICENSE for the authoritative terms before
distributing a build.