Add real PinMAME (VPinMAME.Controller) integration for ROM-based tables

Statically links the real PinMAME emulation core (libpinmame) instead of
leaving VPinMAME.Controller creation to fail, which was crashing table
scripts on ROM-based tables before they could spawn a ball. PinMAME's own
run_game()->cpu_run() loop is split into a one-shot init, a per-frame step,
and a one-shot teardown (patches/pinmame/0004) so it runs cooperatively on
the same frame callback as vpinball's own loop instead of on a real
std::thread, which hard-aborts under Emscripten's single-threaded runtime -
three smaller wasm32 portability fixes to libpinmame itself round out the
patch set (0001-0003). Adds pinball.loadRom() to supply a ROM zip, written
to the table-relative pinmame/roms/ path vpinball's own plugin already
checks. Confirmed against a real community ROM-based table: Controller
creation and game identification succeed, and a missing ROM now fails
cleanly instead of crashing the page - actual ROM-driven gameplay is still
unconfirmed since no ROM was available (or sought out) to test with.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SoSarxLgY33Kax5UNXcafZ
This commit is contained in:
2026-08-23 12:09:35 +02:00
co-authored by Claude Sonnet 5
parent d91ef0c96d
commit 9479cea280
13 changed files with 756 additions and 8 deletions
+26 -6
View File
@@ -21,6 +21,17 @@ Getting here required finding and fixing four real, previously-unknown bugs in t
3. The window's OpenGL back buffer was created with the window's *logical* (CSS) pixel size instead of its *device* pixel size — on any browser tab with `devicePixelRatio != 1` (essentially all HiDPI displays), the GL viewport only covered a fraction of the canvas's actual backing buffer, rendering anchored to one corner (GL's viewport origin) instead of filling the canvas. 3. The window's OpenGL back buffer was created with the window's *logical* (CSS) pixel size instead of its *device* pixel size — on any browser tab with `devicePixelRatio != 1` (essentially all HiDPI displays), the GL viewport only covered a fraction of the canvas's actual backing buffer, rendering anchored to one corner (GL's viewport origin) instead of filling the canvas.
4. `Window::OnResized()` (called on every SDL resize event, including entering/leaving browser fullscreen) updated the window's own tracked pixel size but never propagated it to the window's back buffer render target — so toggling fullscreen resized the canvas but rendering stayed pinned to the pre-resize viewport size. 4. `Window::OnResized()` (called on every SDL resize event, including entering/leaving browser fullscreen) updated the window's own tracked pixel size but never propagated it to the window's back buffer render target — so toggling fullscreen resized the canvas but rendering stayed pinned to the pre-resize viewport size.
## Status: real PinMAME (`VPinMAME.Controller`) integration — cooperative scheduling confirmed, gameplay unconfirmed (no ROM to test with)
Real-hardware ("SS"/ROM-based) tables `CreateObject("VPinMAME.Controller")` against the actual PinMAME emulation core (`libpinmame`, from the real [vpinball/pinmame](https://github.com/vpinball/pinmame) project — not a stub, not a mock), compiled to wasm32 and statically linked in. Confirmed by hands-on testing against a real community ROM-based table: the Controller correctly identifies the requested game from PinMAME's actual ~2,900-entry built-in driver database (`PinMAME::Controller::SetGameName``Game found: name=hvymetal, description=Heavy Metal Meltdown, manufacturer=Bally, year=1987`), and a missing/invalid ROM now fails cleanly (a logged error, the game just doesn't start) instead of crashing the page.
Getting a real OS-thread-based emulator core running inside a single-threaded WASM build required the same class of fix as the vsync/game-loop bugs above, just one level deeper — this time inside PinMAME's own CPU-execution loop, not vpinball's:
1. `PinmameRun()` spawned PinMAME's actual emulation main loop (`run_game()``run_machine()``run_machine_core()``cpu_run()`, MAME's real CPU-cycle scheduler) on a real `std::thread` — the same hard-abort-under-Emscripten bug as `RenderDevice::WaitForVSync()` above, just for an entire emulation session instead of one vsync wait. Fixed by splitting that whole call chain into a one-shot init, a step bounded to roughly one host video frame of emulated time (`timer_get_time()`-bounded, called once per frame from `Player::EmscriptenStepFrame()` itself), and a one-shot teardown — see `patches/pinmame/0004-emscripten-cooperative-scheduling.patch`.
2. Two small wasm32 portability bugs in libpinmame itself, unrelated to threading: an x86-only compiler intrinsic (`__rolq`/`__rorq`) reached by a portability guard that didn't exclude wasm32, and a bundled-zlib include path only wired up for the Windows build — see `patches/pinmame/0001-*` and `0003-*`.
3. An optional external-clock-sync feature (`time_fence`, mirrored by `Controller.TimeFence` in table scripts) used POSIX semaphores unavailable in this build; it now reports itself as unsupported instead, the same way every other optional sync/timing feature becomes a no-op under this build's single-threaded model — see `patches/pinmame/0002-*`.
A ROM zip can be supplied via `pinball.loadRom(gameName, romZipBytes)`, written to `/tables/pinmame/roms/<gameName>.zip` — the same table-relative convention vpinball's own PinMAME plugin already checks before any global settings path, so no extra configuration is needed. **Not yet validated: actual gameplay** (real switch/solenoid state, sound, scoring) against a real ROM — none was available to test against, and ROM files are copyrighted, so none was sought out. Table script compatibility for tables that don't need real ROM emulation — most tables, including the bundled default — is unaffected either way.
## Why a source-level port, not a reimplementation ## 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. 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.
@@ -72,14 +83,15 @@ Full research and two isolated feasibility spikes (proving real VBScript executi
- A JS-callable `evalScript()` debug/utility hook (`pinball.evalScript(script)`) that runs arbitrary VBScript against the live table via the same entry point the interpreter's own debug console uses. - A JS-callable `evalScript()` debug/utility hook (`pinball.evalScript(script)`) that runs arbitrary VBScript against the live table via the same entry point the interpreter's own debug console uses.
**Not yet validated (should work, per code review and by analogy to already-proven mechanisms, but unconfirmed end-to-end):** **Not yet validated (should work, per code review and by analogy to already-proven mechanisms, but unconfirmed end-to-end):**
- **PinMAME (`VPinMAME.Controller`) real gameplay** — Controller creation, game identification, and graceful missing-ROM handling are confirmed (see Status above), but actual ROM-driven switch/solenoid/sound/scoring behavior is unconfirmed end-to-end, since no ROM file was available to test against (and none was sought out — they're copyrighted).
- Gamepad/joystick input during real gameplay (keyboard input uses the identical `InputManager` pipeline and is confirmed working; gamepad support is code-complete but untested on real hardware). - Gamepad/joystick input during real gameplay (keyboard input uses the identical `InputManager` pipeline and is confirmed working; gamepad support is code-complete but untested on real hardware).
- Touch-control overlay (`attachTouchControls`) actually flipping a flipper on a real touch device — implemented and included in the example (synthesizes the real default keyboard scancodes on `window`), not yet confirmed on physical touch hardware. - Touch-control overlay (`attachTouchControls`) actually flipping a flipper on a real touch device — implemented and included in the example (synthesizes the real default keyboard scancodes on `window`), not yet confirmed on physical touch hardware.
- DMD rendering via a script-driven `Flasher`/`ScriptGlobalTable::put_DMDPixels`. This is native, core-engine functionality requiring no new code (`src/core/ScriptGlobalTable.cpp:886-937`, `src/parts/flasher.cpp:1312-1341`), and rides the exact same `ScriptInterpreter::Evaluate()` VBScript-dispatch path already confirmed working by the flipper test above — but the bundled default table has no DMD-configured `Flasher`, so a full *visual* confirmation needs a real DMD-equipped table (uploadable via the file picker) or authoring one, neither done yet. - DMD rendering via a script-driven `Flasher`/`ScriptGlobalTable::put_DMDPixels`. This is native, core-engine functionality requiring no new code (`src/core/ScriptGlobalTable.cpp:886-937`, `src/parts/flasher.cpp:1312-1341`), and rides the exact same `ScriptInterpreter::Evaluate()` VBScript-dispatch path already confirmed working by the flipper test above — but the bundled default table has no DMD-configured `Flasher`, so a full *visual* confirmation needs a real DMD-equipped table (uploadable via the file picker) or authoring one, neither done yet.
**Explicitly out of scope for now** (browser-sandbox constraints or a real infrastructure-cost tradeoff, not a technical dead end — could be revisited): **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. - **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 rest of the plugin ecosystem beyond PinMAME** (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. (PinMAME itself is no longer in this category — see Status above.)
- **The modern `plugins/b2s` backglass plugin** — explicitly assessed and declined for now, not just left undone. `B2SServer` itself has no browser-incompatible networking (it's built on vpinball's own in-process plugin messaging bus, not real sockets), but our emscripten build currently excludes the *entire* plugin subsystem outright (`CMakeLists_plugins.txt` isn't included at all), so wiring B2S in means new static-linking plumbing modeled on iOS/Android's `__LIBVPINBALL__` path, which this project's `__STANDALONE__` build doesn't share — a genuinely multi-hour undertaking, and one with no way to confirm it actually renders anything without a real `.directb2s`-equipped test table, which isn't available. Revisit if/when such a table is available to validate against. - **The modern `plugins/b2s` backglass plugin** — still not wired in, but meaningfully cheaper now than when this was last assessed: PinMAME's own static-linking plumbing (`patches/vpinball/0006-*`, see Status above) already established the exact pattern B2S would need — registering a statically-linked plugin via `MsgPluginManager::RegisterPlugin` instead of desktop VP's dynamic `/plugins` folder scan — so this is now mostly "repeat the same wiring for a second plugin" rather than new infrastructure. Still no `.directb2s`-equipped test table available to confirm it actually renders anything against. Revisit if/when one is available.
- Raw HID device input (`OpenPinDevHandler`/hidapi) — no browser equivalent for real-hardware nudge/plunger boards. Permanent, not a "for now." - Raw HID device input (`OpenPinDevHandler`/hidapi) — no browser equivalent for real-hardware nudge/plunger boards. Permanent, not a "for now."
**Further follow-up work:** **Further follow-up work:**
@@ -95,9 +107,9 @@ Full research and two isolated feasibility spikes (proving real VBScript executi
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). 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 ```bash
./scripts/setup.sh # installs emsdk + bison check, fetches vpinball + libwinevbs at pinned commits, applies patches/ ./scripts/setup.sh # installs emsdk + bison check, fetches vpinball + libwinevbs + pinmame at pinned commits, applies patches/
source emsdk/emsdk_env.sh source emsdk/emsdk_env.sh
./scripts/build-deps.sh # builds SDL3/SDL3_image/SDL3_ttf/FreeImage/libwinevbs for wasm32 ./scripts/build-deps.sh # builds SDL3/SDL3_image/SDL3_ttf/FreeImage/libwinevbs/libpinmame for wasm32
./scripts/build.sh # configures + builds vpinball itself -> dist/vpinball.{js,wasm,data} ./scripts/build.sh # configures + builds vpinball itself -> dist/vpinball.{js,wasm,data}
./scripts/dev-server.sh # serves dist/ locally for manual testing ./scripts/dev-server.sh # serves dist/ locally for manual testing
``` ```
@@ -126,6 +138,13 @@ startButton.addEventListener('click', () => {
// Optional debug/utility hook: runs arbitrary VBScript against the live // Optional debug/utility hook: runs arbitrary VBScript against the live
// table, via the same entry point the interpreter's own debug console uses. // table, via the same entry point the interpreter's own debug console uses.
pinball.evalScript('DMDWidth = 128 : DMDHeight = 32'); pinball.evalScript('DMDWidth = 128 : DMDHeight = 32');
// Optional: supply a ROM zip for a real-hardware ("SS") table's
// CreateObject("VPinMAME.Controller")/Controller.Run() before start() -
// ROM files are copyrighted, so bring your own legally-obtained one.
// gameName is the short ROM name the table's script passes to LoadVPM
// (e.g. "hvymetal"), not the table's display title.
pinball.loadRom('hvymetal', romZipArrayBuffer);
``` ```
See `examples/basic/index.html` for a complete working page (loading progress, file upload, touch controls, fullscreen). Published as `@valknar/vpinball-wasm` on this project's Gitea npm registry — see `package.json`. See `examples/basic/index.html` for a complete working page (loading progress, file upload, touch controls, fullscreen). Published as `@valknar/vpinball-wasm` on this project's Gitea npm registry — see `package.json`.
@@ -133,7 +152,7 @@ See `examples/basic/index.html` for a complete working page (loading progress, f
## Known limitations ## Known limitations
- Single-threaded only (no pthreads/SharedArrayBuffer) — a deliberate tradeoff, not a gap; see Roadmap. - Single-threaded only (no pthreads/SharedArrayBuffer) — a deliberate tradeoff, not a gap; see Roadmap.
- No heavy plugin ecosystem (PinMAME/DOF/FlexDMD/etc.), and the modern `plugins/b2s` backglass plugin specifically assessed and declined for now (no plugin subsystem wired for this build, no test table to validate against) — see Roadmap. - Real PinMAME (`VPinMAME.Controller`) integration for ROM-based tables — Controller creation, game identification, and graceful missing-ROM handling confirmed, but actual ROM-driven gameplay unconfirmed without a real ROM (copyrighted, none sought out) — see Status and Roadmap. The rest of the plugin ecosystem (DOF/FlexDMD/etc., and the modern `plugins/b2s` backglass plugin) remains unwired — see Roadmap.
- No raw hardware input (real cabinet nudge/plunger boards) — permanent, no browser equivalent. - No raw hardware input (real cabinet nudge/plunger boards) — permanent, no browser equivalent.
- Keyboard input, audio, and VBScript-driven gameplay are confirmed working end-to-end by hands-on manual testing (not just code review). Gamepad input and the touch-control overlay are code-complete and use the identical input pipeline, but are not yet confirmed on real gamepad/touch hardware — see Roadmap. - Keyboard input, audio, and VBScript-driven gameplay are confirmed working end-to-end by hands-on manual testing (not just code review). Gamepad input and the touch-control overlay are code-complete and use the identical input pipeline, but are not yet confirmed on real gamepad/touch hardware — see Roadmap.
- DMD rendering is native, code-complete functionality riding the same proven VBScript-dispatch path, but not yet visually confirmed since the bundled default table has no DMD-configured `Flasher` — see Roadmap. - DMD rendering is native, code-complete functionality riding the same proven VBScript-dispatch path, but not yet visually confirmed since the bundled default table has no DMD-configured `Flasher` — see Roadmap.
@@ -141,10 +160,11 @@ See `examples/basic/index.html` for a complete working page (loading progress, f
## Licensing ## 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. 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`). PinMAME is under a similar **mixed license** to vpinball's own — migrating file-by-file from the same inherited "old MAME" license to 3-Clause BSD, with each migrated file marked `// license:BSD-3-Clause`; see `vendor/pinmame/LICENSE` for the authoritative text. Separately, and unrelated to source licensing: PinMAME requires ROM images dumped from the real arcade/pinball hardware to actually run a game, which this project does not and cannot bundle — see `pinball.loadRom()` above. This project's own glue code (CMake integration, patches, npm wrapper) has no license conflict with any of the above, but the combined built artifact's distribution terms are governed by vpinball's, libwinevbs's, and PinMAME's licenses, not just this repository's.
## Attribution ## Attribution
- [Visual Pinball](https://github.com/vpinball/vpinball) — the engine this project ports. - [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. - [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. - [Wine](https://www.winehq.org/) — original source of the VBScript/OLE Automation engine libwinevbs extracts and packages.
- [PinMAME](https://github.com/vpinball/pinmame) — the real ROM/hardware emulation core `VPinMAME.Controller` wraps, compiled here to wasm32.
+16 -1
View File
@@ -48,6 +48,8 @@
<div id="progress-track"><div id="progress-bar"></div></div> <div id="progress-track"><div id="progress-bar"></div></div>
<div id="file-picker"> <div id="file-picker">
<label>Optional: play your own table (self-contained .vpx only) <input type="file" id="table-file" accept=".vpx" /></label> <label>Optional: play your own table (self-contained .vpx only) <input type="file" id="table-file" accept=".vpx" /></label>
<br />
<label>Optional: ROM zip for a real-hardware table (e.g. hvymetal.zip) <input type="file" id="rom-file" accept=".zip" /></label>
</div> </div>
<button id="start-button" disabled>Loading...</button> <button id="start-button" disabled>Loading...</button>
</div> </div>
@@ -62,6 +64,7 @@
const progressBar = document.getElementById('progress-bar'); const progressBar = document.getElementById('progress-bar');
const startButton = document.getElementById('start-button'); const startButton = document.getElementById('start-button');
const fileInput = document.getElementById('table-file'); const fileInput = document.getElementById('table-file');
const romInput = document.getElementById('rom-file');
const fullscreenButton = document.getElementById('fullscreen-button'); const fullscreenButton = document.getElementById('fullscreen-button');
let uploadedTableData; let uploadedTableData;
@@ -70,6 +73,14 @@
if (file) uploadedTableData = await file.arrayBuffer(); if (file) uploadedTableData = await file.arrayBuffer();
}); });
// ROM zips are copyrighted - only pick one you're legally entitled to
// use. Named after its own short ROM/game name by convention (e.g.
// hvymetal.zip), which loadRom() below relies on to derive gameName.
let uploadedRomFile;
romInput.addEventListener('change', () => {
uploadedRomFile = romInput.files?.[0];
});
const pinball = await loadPinball({ const pinball = await loadPinball({
canvas, canvas,
baseUrl: '../../dist', baseUrl: '../../dist',
@@ -82,10 +93,14 @@
startButton.disabled = false; startButton.disabled = false;
startButton.textContent = 'Start'; startButton.textContent = 'Start';
startButton.addEventListener('click', () => { startButton.addEventListener('click', async () => {
if (uploadedTableData) { if (uploadedTableData) {
pinball.loadTable(uploadedTableData); pinball.loadTable(uploadedTableData);
} }
if (uploadedRomFile) {
const gameName = uploadedRomFile.name.replace(/\.zip$/i, '');
pinball.loadRom(gameName, await uploadedRomFile.arrayBuffer());
}
pinball.start(); pinball.start();
overlay.classList.add('hidden'); overlay.classList.add('hidden');
fullscreenButton.classList.remove('hidden'); fullscreenButton.classList.remove('hidden');
+30
View File
@@ -0,0 +1,30 @@
{
"name": "@valknar/vpinball-wasm",
"version": "0.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@valknar/vpinball-wasm",
"version": "0.2.0",
"license": "SEE LICENSE IN LICENSE",
"devDependencies": {
"typescript": "^5.6.0"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
}
}
}
+17 -1
View File
@@ -12,7 +12,16 @@ const UPLOADED_TABLE_PATH = '/tables/uploaded.vpx';
* canvas and returns a handle to control its lifecycle. * canvas and returns a handle to control its lifecycle.
*/ */
export async function loadPinball(options: LoadPinballOptions): Promise<PinballInstance> { export async function loadPinball(options: LoadPinballOptions): Promise<PinballInstance> {
const baseUrl = options.baseUrl ?? '.'; // Resolved to an absolute URL against the *page's* location up front,
// rather than used as-is: a bare relative baseUrl works for
// Module.locateFile and wireDownloadProgress's fetch() (both resolve
// relative to the page), but not for the dynamic import() below, whose
// relative-specifier resolution is against *this module's own* URL
// (dist/index.js) instead - the two disagree for any page location that
// doesn't happen to cancel the difference out (as examples/basic/'s
// '../../dist' accidentally does). Resolving once here, to an absolute
// URL, makes every use of it agree.
const baseUrl = new URL(options.baseUrl ?? '.', document.baseURI).href.replace(/\/$/, '');
let tablePath = DEFAULT_TABLE_PATH; let tablePath = DEFAULT_TABLE_PATH;
const moduleArgs: Record<string, unknown> = { const moduleArgs: Record<string, unknown> = {
@@ -43,6 +52,10 @@ export async function loadPinball(options: LoadPinballOptions): Promise<PinballI
module.FS.writeFile(UPLOADED_TABLE_PATH, new Uint8Array(vpxBytes)); module.FS.writeFile(UPLOADED_TABLE_PATH, new Uint8Array(vpxBytes));
tablePath = UPLOADED_TABLE_PATH; tablePath = UPLOADED_TABLE_PATH;
}, },
loadRom(gameName: string, romZipBytes: ArrayBuffer) {
module.FS.mkdirTree(PINMAME_ROMS_DIR);
module.FS.writeFile(`${PINMAME_ROMS_DIR}/${gameName}.zip`, new Uint8Array(romZipBytes));
},
start() { start() {
module.ccall?.('vpinball_wasm_start', 'number', ['string'], [tablePath]); module.ccall?.('vpinball_wasm_start', 'number', ['string'], [tablePath]);
}, },
@@ -116,6 +129,9 @@ async function wireDownloadProgress(
interface EmscriptenModule { interface EmscriptenModule {
FS: { FS: {
writeFile(path: string, data: Uint8Array): void; writeFile(path: string, data: Uint8Array): void;
mkdirTree(path: string): void;
}; };
ccall?: (name: string, returnType: string | null, argTypes: string[], args: unknown[]) => unknown; ccall?: (name: string, returnType: string | null, argTypes: string[], args: unknown[]) => unknown;
} }
const PINMAME_ROMS_DIR = '/tables/pinmame/roms';
+13
View File
@@ -35,6 +35,19 @@ export interface PinballInstance {
* session with a different table). * session with a different table).
*/ */
loadTable(vpxBytes: ArrayBuffer): void; loadTable(vpxBytes: ArrayBuffer): void;
/**
* Makes a ROM zip available to PinMAME (VPinMAME.Controller) for a
* ROM-based ("SS"/real-hardware) table's Controller.Run() call - written
* to /tables/pinmame/roms/<gameName>.zip, the table-relative convention
* PinMAME's plugin already looks for before any global settings path.
* gameName is the short ROM/game name the table's own script passes to
* LoadVPM (e.g. "hvymetal"), not the table's display title. Must be
* called before start() (or loadTable(), if switching tables) since
* Controller.Run() reads the filesystem synchronously at table-init
* time. ROM files are copyrighted - only supply ones you're legally
* entitled to use.
*/
loadRom(gameName: string, romZipBytes: ArrayBuffer): void;
/** Start the simulation's main loop. No-op if already running. */ /** Start the simulation's main loop. No-op if already running. */
start(): void; start(): void;
/** Stop the simulation; the engine tears down (script Exit event, settings save) on its next step. */ /** Stop the simulation; the engine tears down (script Exit event, settings save) on its next step. */
@@ -0,0 +1,22 @@
diff --git a/src/common.h b/src/common.h
index f0249b6..854bd16 100644
--- a/src/common.h
+++ b/src/common.h
@@ -533,7 +533,7 @@ INLINE unsigned long long rotl_64(const unsigned long long x, const unsigned int
{
#ifdef _MSC_VER
return _rotl64(x, count);
-#elif !defined(__arm__) && !defined(__aarch64__) && (defined(__INTEL_COMPILER) || (defined(__GNUC__) && (__GNUC__ > 3)) || defined(__clang__))
+#elif !defined(__arm__) && !defined(__aarch64__) && !defined(__EMSCRIPTEN__) && !defined(__wasm__) && (defined(__INTEL_COMPILER) || (defined(__GNUC__) && (__GNUC__ > 3)) || defined(__clang__))
return __rolq(x, count);
#else
return (x<<count) | (x>>( (unsigned int)(-(int)count)&63 )); // -count&63 instead of 64-count to handle count==0
@@ -555,7 +555,7 @@ INLINE unsigned long long rotr_64(const unsigned long long x, const unsigned int
{
#ifdef _MSC_VER
return _rotr64(x, count);
-#elif !defined(__arm__) && !defined(__aarch64__) && (defined(__INTEL_COMPILER) || (defined(__GNUC__) && (__GNUC__ > 3)) || defined(__clang__))
+#elif !defined(__arm__) && !defined(__aarch64__) && !defined(__EMSCRIPTEN__) && !defined(__wasm__) && (defined(__INTEL_COMPILER) || (defined(__GNUC__) && (__GNUC__ > 3)) || defined(__clang__))
return __rorq(x, count);
#else
return (x>>count) | (x<<( (unsigned int)(-(int)count)&63 )); // -count&63 instead of 64-count to handle count==0
@@ -0,0 +1,39 @@
diff --git a/src/cpuexec.c b/src/cpuexec.c
index ca7401f..07db6a1 100644
--- a/src/cpuexec.c
+++ b/src/cpuexec.c
@@ -943,6 +943,34 @@ void time_fence_exit()
}
}
+#elif defined(__EMSCRIPTEN__)
+
+// No cross-thread wait primitive is used here: this build is single-threaded
+// (see vpinball-wasm's README on why pthreads/SharedArrayBuffer are out of
+// scope), and time_fence is purely an *optional* external-clock-sync feature
+// (mirrored by Controller.TimeFence in controller.vbs) - reporting it as
+// unsupported just means the emulator paces itself on its own internal
+// timing instead of syncing to the host's clock, which is what every other
+// platform this library runs on outside of this fence do anyway.
+int time_fence_is_supported()
+{
+ return 0;
+}
+
+void time_fence_post()
+{
+}
+
+int time_fence_wait(double secs)
+{
+ (void)secs;
+ return 0;
+}
+
+void time_fence_exit()
+{
+}
+
#else
#include <semaphore.h>
@@ -0,0 +1,20 @@
diff --git a/cmake/libpinmame/CMakeLists.txt b/cmake/libpinmame/CMakeLists.txt
index a2dfd1a..119e3ec 100644
--- a/cmake/libpinmame/CMakeLists.txt
+++ b/cmake/libpinmame/CMakeLists.txt
@@ -745,6 +745,7 @@ set(PINMAME_INCLUDE_DIRS
src/cpu/m68000/generated_by_m68kmake
src/unix
src/unix/sysdep
+ ext/zlib
)
@@ -752,7 +753,6 @@ if(PLATFORM STREQUAL "win" OR PLATFORM STREQUAL "win-mingw")
list(APPEND PINMAME_INCLUDE_DIRS
src/vc
src/windows
- ext/zlib
)
endif()
@@ -0,0 +1,424 @@
diff --git a/src/cpuexec.c b/src/cpuexec.c
index ca7401f..0bfcbe7 100644
--- a/src/cpuexec.c
+++ b/src/cpuexec.c
@@ -466,6 +466,65 @@ void cpu_run(void)
#endif
}
+#if defined(LIBPINMAME) && defined(__EMSCRIPTEN__)
+/* Emscripten is single-threaded, so cpu_run()'s own blocking "while
+ (!time_to_quit) { ... }" loop above can't run on a real OS thread the way
+ it does everywhere else libpinmame runs (see PinmameRun() in
+ libpinmame.cpp). These three entry points split that same loop, using the
+ exact same globals/logic, into a one-shot init, a step callable once per
+ host video frame (bounded to roughly one frame of emulated time via
+ timer_get_time(), the same clock cpu_timeslice()'s own time_fence logic
+ above already uses), and a one-shot teardown - so the host's own
+ per-frame callback can drive it cooperatively instead. */
+
+void cpu_run_emscripten_init(void)
+{
+ time_to_quit = 0;
+ cpu_pre_run();
+ time_to_reset = 0;
+ time_fence_global_offset = -options.time_fence;
+}
+
+/* Returns 1 if the emulation is still running (call again next frame), or 0
+ once it has fully quit (cpu_post_run() has already been invoked). */
+int cpu_run_emscripten_step(void)
+{
+ const double frame_target = timer_get_time() + (1.0 / 60.0);
+
+ while (!time_to_quit && !time_to_reset && timer_get_time() < frame_target)
+ {
+ profiler_mark(PROFILER_EXTRA);
+
+ if (loadsave_schedule != LOADSAVE_NONE)
+ handle_loadsave();
+
+ cpu_timeslice();
+
+ extern int libpinmame_time_to_quit(void);
+ if (libpinmame_time_to_quit())
+ time_to_quit = 1;
+
+ profiler_mark(PROFILER_END);
+ }
+
+ if (time_to_reset && !time_to_quit)
+ {
+ cpu_post_run();
+ cpu_pre_run();
+ time_to_reset = 0;
+ time_fence_global_offset = -options.time_fence;
+ }
+
+ if (time_to_quit)
+ {
+ cpu_post_run();
+ return 0;
+ }
+
+ return 1;
+}
+#endif
+
/*************************************
@@ -943,6 +1002,34 @@ void time_fence_exit()
}
}
+#elif defined(__EMSCRIPTEN__)
+
+// No cross-thread wait primitive is used here: this build is single-threaded
+// (see vpinball-wasm's README on why pthreads/SharedArrayBuffer are out of
+// scope), and time_fence is purely an *optional* external-clock-sync feature
+// (mirrored by Controller.TimeFence in controller.vbs) - reporting it as
+// unsupported just means the emulator paces itself on its own internal
+// timing instead of syncing to the host's clock, which is what every other
+// platform this library runs on outside of this fence do anyway.
+int time_fence_is_supported()
+{
+ return 0;
+}
+
+void time_fence_post()
+{
+}
+
+int time_fence_wait(double secs)
+{
+ (void)secs;
+ return 0;
+}
+
+void time_fence_exit()
+{
+}
+
#else
#include <semaphore.h>
diff --git a/src/libpinmame/libpinmame.cpp b/src/libpinmame/libpinmame.cpp
index 6966d77..755d762 100644
--- a/src/libpinmame/libpinmame.cpp
+++ b/src/libpinmame/libpinmame.cpp
@@ -1104,11 +1104,52 @@ PINMAMEAPI PINMAME_STATUS PinmameRun(const char* const p_name)
vp_init();
+#ifdef __EMSCRIPTEN__
+ // No real OS thread: Emscripten builds here are single-threaded (see
+ // vpinball-wasm's README on why pthreads/SharedArrayBuffer are out of
+ // scope), so run_game()'s own blocking call chain - which StartGame()
+ // otherwise runs on _p_gameThread - has been split in mame.c/cpuexec.c
+ // into a one-shot init (called synchronously right here) plus a step
+ // the host calls once per video frame via PinmameEmscriptenStep().
+ memset(_mechInit, 0, sizeof(_mechInit));
+ memset(_mechInfo, 0, sizeof(_mechInfo));
+
+ extern int run_game_emscripten_init(int game);
+ if (run_game_emscripten_init(gameNum) != 0)
+ {
+ OnStateChange(0);
+ return PINMAME_STATUS_GAME_NOT_FOUND;
+ }
+
+ OnStateChange(1);
+#else
_p_gameThread = new std::thread(StartGame, gameNum);
+#endif
return PINMAME_STATUS_OK;
}
+#ifdef __EMSCRIPTEN__
+/******************************************************
+ * PinmameEmscriptenStep
+ *
+ * Must be called once per host video frame (e.g. from the same
+ * requestAnimationFrame-driven callback that steps vpinball's own frame)
+ * while the emulator is running - see PinmameRun's __EMSCRIPTEN__ branch
+ * above for why this exists instead of a real thread.
+ ******************************************************/
+
+PINMAMEAPI void PinmameEmscriptenStep(void)
+{
+ if (!_isRunning)
+ return;
+
+ extern int run_game_emscripten_step(void);
+ if (!run_game_emscripten_step())
+ OnStateChange(0);
+}
+#endif
+
/******************************************************
* PinmameIsRunning
******************************************************/
@@ -1161,6 +1202,19 @@ PINMAMEAPI int PinmameIsPaused()
PINMAMEAPI void PinmameStop()
{
+#ifdef __EMSCRIPTEN__
+ // No game thread to join here (see PinmameRun's __EMSCRIPTEN__ branch) -
+ // just request the stop. The next PinmameEmscriptenStep() call notices
+ // libpinmame_time_to_quit() via cpu_run_emscripten_step(), runs the same
+ // teardown chain a real quit would, and calls OnStateChange(0) itself.
+ if (_isRunning)
+ {
+ g_fPause = 0;
+ _timeToQuit = 1;
+ }
+ return;
+#endif
+
if (!_p_gameThread) {
if (_isRunning) {
libpinmame_log_error("PinmameStop(): run state is %d but game thread handle is null; forcing stopped state.", _isRunning);
diff --git a/src/libpinmame/libpinmame.h b/src/libpinmame/libpinmame.h
index dc604df..d3aacc1 100644
--- a/src/libpinmame/libpinmame.h
+++ b/src/libpinmame/libpinmame.h
@@ -453,6 +453,11 @@ PINMAMEAPI PINMAME_STATUS PinmamePause(const int pause);
PINMAMEAPI int PinmameIsPaused();
PINMAMEAPI PINMAME_STATUS PinmameReset();
PINMAMEAPI void PinmameStop();
+#ifdef __EMSCRIPTEN__
+// Must be called once per host video frame while running - see PinmameRun's
+// __EMSCRIPTEN__ branch in libpinmame.cpp for why.
+PINMAMEAPI void PinmameEmscriptenStep(void);
+#endif
PINMAMEAPI PINMAME_HARDWARE_GEN PinmameGetHardwareGen();
PINMAMEAPI int PinmameGetSwitch(const int swNo);
PINMAMEAPI void PinmameSetSwitch(const int swNo, const int state);
diff --git a/src/mame.c b/src/mame.c
index d0aea1a..2f8c4d5 100644
--- a/src/mame.c
+++ b/src/mame.c
@@ -229,6 +229,16 @@ static void shutdown_machine(void);
static int run_machine(void);
static void run_machine_core(void);
+#if defined(LIBPINMAME) && defined(__EMSCRIPTEN__)
+/* See cpu_run_emscripten_init's comment in cpuexec.c for why these exist. */
+void cpu_run_emscripten_init(void);
+int cpu_run_emscripten_step(void);
+void run_machine_core_emscripten_init(void);
+int run_machine_core_emscripten_step(void);
+int run_machine_emscripten_init(void);
+int run_machine_emscripten_step(void);
+#endif
+
#ifdef MAME_DEBUG
static int validitychecks(void);
#endif
@@ -355,6 +365,71 @@ int run_game(int game)
return err;
}
+#if defined(LIBPINMAME) && defined(__EMSCRIPTEN__)
+/* Mirrors run_game() above, split around run_machine() the same way that's
+ split around run_machine_core(). This is the top of the whole init/step/
+ teardown chain PinmameRun()/PinmameEmscriptenStep() in libpinmame.cpp
+ drive - see cpu_run_emscripten_init's comment in cpuexec.c for why the
+ chain exists at all. Returns 0 on success, non-zero on failure (matching
+ run_game()'s own convention) - a failure here (e.g. ROM not found) means
+ no teardown call is needed, since nothing that needs undoing succeeded. */
+int run_game_emscripten_init(int game)
+{
+ begin_resource_tracking();
+
+ memset(Machine, 0, sizeof(*Machine));
+ Machine->gamedrv = gamedrv = drivers[game];
+ expand_machine_driver(gamedrv->drv, &internal_drv);
+ Machine->drv = &internal_drv;
+
+ if (init_game_options())
+ return 1;
+
+ cpu_loadsave_reset();
+ bailing = 0;
+
+ if (osd_init())
+ {
+ bail_and_print("Unable to initialize system");
+ return 1;
+ }
+
+ begin_resource_tracking();
+
+ if (init_machine())
+ {
+ bail_and_print("Unable to initialize machine emulation");
+ end_resource_tracking();
+ osd_exit();
+ return 1;
+ }
+
+ if (run_machine_emscripten_init())
+ {
+ bail_and_print("Unable to start machine emulation");
+ shutdown_machine();
+ end_resource_tracking();
+ osd_exit();
+ return 1;
+ }
+
+ return 0;
+}
+
+/* Returns 1 while still running, 0 once fully torn down. */
+int run_game_emscripten_step(void)
+{
+ if (run_machine_emscripten_step())
+ return 1;
+
+ shutdown_machine();
+ end_resource_tracking();
+ osd_exit();
+ end_resource_tracking();
+ return 0;
+}
+#endif
+
/*-------------------------------------------------
@@ -547,6 +622,71 @@ static int run_machine(void)
return res;
}
+#if defined(LIBPINMAME) && defined(__EMSCRIPTEN__)
+/* Mirrors run_machine() above, split around run_machine_core() the same way
+ that's split around cpu_run(). Returns 0 on success (matching
+ run_machine()'s own convention), non-zero on failure. */
+int run_machine_emscripten_init(void)
+{
+ if (vh_open())
+ {
+ bail_and_print("Unable to start video emulation");
+ return 1;
+ }
+
+ tilemap_init();
+
+ if (Machine->drv->video_start && (*Machine->drv->video_start)())
+ {
+ bail_and_print("Unable to start video emulation");
+ tilemap_close();
+ vh_close();
+ return 1;
+ }
+
+ if (sound_start())
+ {
+ bail_and_print("Unable to start audio emulation");
+ if (Machine->drv->video_stop)
+ (*Machine->drv->video_stop)();
+ tilemap_close();
+ vh_close();
+ return 1;
+ }
+
+ {
+ int region;
+ /* free memory regions allocated with REGIONFLAG_DISPOSE (typically gfx roms) */
+ for (region = 0; region < MAX_MEMORY_REGIONS; region++)
+ if (Machine->memory_region[region].flags & ROMREGION_DISPOSE)
+ {
+ size_t i;
+ for (i = 0; i < memory_region_length(region); i++)
+ memory_region(region)[i] = rand();
+ free(Machine->memory_region[region].base);
+ Machine->memory_region[region].base = 0;
+ }
+ }
+
+ run_machine_core_emscripten_init();
+ return 0;
+}
+
+/* Returns 1 while still running, 0 once fully torn down. */
+int run_machine_emscripten_step(void)
+{
+ if (run_machine_core_emscripten_step())
+ return 1;
+
+ sound_stop();
+ if (Machine->drv->video_stop)
+ (*Machine->drv->video_stop)();
+ tilemap_close();
+ vh_close();
+ return 0;
+}
+#endif
+
/*-------------------------------------------------
@@ -620,6 +760,60 @@ void run_machine_core(void)
}
}
+#if defined(LIBPINMAME) && defined(__EMSCRIPTEN__)
+/* Mirrors run_machine_core() above (same globals, same calls), split around
+ cpu_run() - see cpu_run_emscripten_init/_step/PinmameRun's own comment for
+ why. The disclaimer/gamewarnings/gameinfo splash screens run_machine_core()
+ shows natively are skipped entirely here: gamewarnings is already excluded
+ for LIBPINMAME builds above, and disclaimer/gameinfo are native-UI-only
+ concerns with nothing meaningful to display in an embedded/headless
+ context like this one. */
+void run_machine_core_emscripten_init(void)
+{
+ artwork_enable(0);
+ init_user_interface();
+ artwork_enable(1);
+
+ if (!gamedrv->rom)
+ options.cheat = 0;
+ if (options.cheat)
+ InitCheat();
+
+ if (Machine->drv->nvram_handler)
+ {
+ mame_file *nvram_file = mame_fopen(Machine->gamedrv->name, 0, FILETYPE_NVRAM, 0);
+ (*Machine->drv->nvram_handler)(nvram_file, 0);
+ if (nvram_file)
+ mame_fclose(nvram_file);
+ }
+
+ cpu_run_emscripten_init();
+}
+
+/* Returns 1 while still running, 0 once cpu_run's own teardown has run. */
+int run_machine_core_emscripten_step(void)
+{
+ if (cpu_run_emscripten_step())
+ return 1;
+
+ if (Machine->drv->nvram_handler)
+ {
+ mame_file *nvram_file = mame_fopen(Machine->gamedrv->name, 0, FILETYPE_NVRAM, 1);
+ if (nvram_file != NULL)
+ {
+ (*Machine->drv->nvram_handler)(nvram_file, 1);
+ mame_fclose(nvram_file);
+ }
+ }
+
+ if (options.cheat)
+ StopCheat();
+
+ save_input_port_settings();
+ return 0;
+}
+#endif
+
/*-------------------------------------------------
@@ -0,0 +1,112 @@
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 745b91b..edb4112 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -810,8 +810,29 @@ elseif(PLATFORM STREQUAL "emscripten")
src/input/OpenPinDevHandler.cpp
)
+ # PinMAME VPX-side plugin glue (registers VPinMAME.Controller for real
+ # ROM-driven scoring/switches/solenoids) - statically linked and
+ # registered from player.cpp instead of desktop VP's dynamic /plugins
+ # folder scan, same as the built-in "vpx" plugin already is. The real
+ # emulation core it drives (libpinmame.a) is staged into
+ # VPINBALL_WASM_DEPS_DIR by vpinball-wasm's own build-deps.sh (see
+ # patches/pinmame/ for the 3 portability fixes that wasm32 build needed).
+ set(VPX_PINMAME_PLUGIN_SOURCES
+ plugins/pinmame/common.cpp
+ plugins/pinmame/PinMAMEPlugin.cpp
+ plugins/pinmame/Controller.cpp
+ plugins/pinmame/ControllerSettings.cpp
+ plugins/pinmame/Game.cpp
+ plugins/pinmame/Games.cpp
+ plugins/pinmame/GameSettings.cpp
+ plugins/pinmame/Rom.cpp
+ plugins/pinmame/Roms.cpp
+ plugins/pinmame/Settings.cpp
+ )
+
add_executable(vpinball
${VPX_STANDALONE_SOURCES}
+ ${VPX_PINMAME_PLUGIN_SOURCES}
${CMAKE_SOURCE_DIR}/src/core/EmscriptenBridge.cpp
)
@@ -824,6 +845,7 @@ elseif(PLATFORM STREQUAL "emscripten")
${VPINBALL_WASM_DEPS_DIR}/include/libwinevbs/wine/include
src
plugins
+ plugins/pinmame
)
target_compile_definitions(vpinball PRIVATE
@@ -863,6 +885,7 @@ elseif(PLATFORM STREQUAL "emscripten")
freetype
freeimage
winevbs
+ pinmame
glad
)
diff --git a/src/core/player.cpp b/src/core/player.cpp
index 60c7d5f..ec9ebdd 100644
--- a/src/core/player.cpp
+++ b/src/core/player.cpp
@@ -79,6 +79,19 @@ using namespace VPX;
// leave as-is as e.g. VPM relies on this
#define WIN32_PLAYER_WND_CLASSNAME _T("VPPlayer")
+#ifdef __EMSCRIPTEN__
+// Statically-linked player plugin entry points (see the __EMSCRIPTEN__
+// branch in Player::Player below) - same declaration shape used for the
+// iOS/Android static plugin table in lib/src/VPinballLib.cpp, just declared
+// at file scope here since MSGPI_EXPORT's __attribute__((visibility(...)))
+// isn't accepted on a block-scope declaration.
+MSGPI_EXPORT void MSGPIAPI PinMAMEPluginLoad(const uint32_t sessionId, const MsgPluginAPI* api);
+MSGPI_EXPORT void MSGPIAPI PinMAMEPluginUnload();
+
+// For PinmameEmscriptenStep(), called once per frame in EmscriptenStepFrame()
+// below - see that function's own comment.
+#include "pinmame/libpinmame.h"
+#endif
Player::Player(PinTable *const table, const PlayMode playMode)
: m_ptable(table)
@@ -128,6 +141,24 @@ Player::Player(PinTable *const table, const PlayMode playMode)
#ifdef __LIBVPINBALL__
VPinballLib::VPinballLib::SetupStaticPlugins(m_pluginManager);
+#elif defined(__EMSCRIPTEN__)
+ // A browser sandbox has no dynamic-library-loading /plugins folder to
+ // scan (see the ScanPluginFolder call in the #else branch below), so
+ // player plugins have to be statically linked into this same wasm binary
+ // and registered by function pointer instead - the same technique
+ // VPinballLib::SetupStaticPlugins already uses for the iOS/Android
+ // library build (lib/src/VPinballLib.cpp), just scoped here to the one
+ // plugin vpinball-wasm actually ships: PinMAME, which is what lets
+ // table scripts get a real (non-Nothing) VPinMAME.Controller for ROM-
+ // driven scoring/switches/solenoids instead of failing on
+ // CreateObject("VPinMAME.Controller") - see patches/pinmame/ for the
+ // wasm32 portability fixes its emulation core (libpinmame) needed.
+ //
+ // Registered only, not Load()-ed here: the per-plugin enable/Load loop
+ // just below (shared with the dynamic-scan #else branch) already does
+ // that for every entry in m_pluginManager.GetPlugins(), so calling Load()
+ // here too would just load it twice.
+ m_pluginManager.RegisterPlugin("PinMAME", "PinMAME", "PinMAME", "", "", "https://github.com/vpinball/pinmame", &PinMAMEPluginLoad, &PinMAMEPluginUnload);
#else
class SDLModuleLoader final : public MsgPI::MsgModuleLoader
{
@@ -2002,6 +2033,12 @@ bool Player::EmscriptenStepFrame()
if (GetCloseState() != CS_PLAYING && GetCloseState() != CS_USER_INPUT)
return false;
+ // PinMAME (VPinMAME.Controller for ROM-driven tables) is likewise
+ // single-threaded here instead of running on its own real thread - see
+ // PinmameEmscriptenStep's own comment in libpinmame.cpp. A no-op when no
+ // ROM-based table has called Controller.Run().
+ PinmameEmscriptenStep();
+
UpdateGameLogic();
PrepareFrame();
UpdateGameLogic();
+24
View File
@@ -133,6 +133,30 @@ cp -r "$VENDOR_DIR/libwinevbs/wine/include/"* "$DEPS_DIR/include/libwinevbs/wine
cp -r "$VENDOR_DIR/libwinevbs/atl/include/"* "$DEPS_DIR/include/libwinevbs/atl/include/" cp -r "$VENDOR_DIR/libwinevbs/atl/include/"* "$DEPS_DIR/include/libwinevbs/atl/include/"
cp -r "$VENDOR_DIR/libwinevbs/atlmfc/include/"* "$DEPS_DIR/include/libwinevbs/atlmfc/include/" cp -r "$VENDOR_DIR/libwinevbs/atlmfc/include/"* "$DEPS_DIR/include/libwinevbs/atlmfc/include/"
# --- libpinmame (the real ROM/hardware emulation core VPinMAME.Controller --
# --- wraps on every other platform; validated end-to-end for wasm32 by this
# --- project's own pinmame-wasm spike - see patches/pinmame/ for the 3
# --- portability fixes it needed. ARCH=wasm32 (not x86/x64/arm64/aarch64)
# --- makes its own CMakeLists.txt skip the asmjit-based ARM7 JIT backend and
# --- fall back to its portable interpreter, exactly as it already does for
# --- 32-bit arm targets - no patch needed for that part. libpinmame's
# --- CMakeLists.txt lives at cmake/libpinmame/ but its source paths are all
# --- relative to the repo root, so upstream's own CI copies it there first;
# --- mirrored here rather than patched, to stay a plain file copy diff-free.
if ! cache_check "$VENDOR_DIR/pinmame" "built-${PINMAME_SHA}"; then
echo "== Building libpinmame for wasm32 =="
cp "$VENDOR_DIR/pinmame/cmake/libpinmame/CMakeLists.txt" "$VENDOR_DIR/pinmame/CMakeLists.txt"
( cd "$VENDOR_DIR/pinmame" && emcmake cmake -DPLATFORM=linux -DARCH=wasm32 \
-DBUILD_STATIC=ON -DBUILD_SHARED=OFF -DCMAKE_BUILD_TYPE=Release -B build \
&& cmake --build build --target pinmame_static -- -j"$NUM_PROCS" )
echo "built-${PINMAME_SHA}" > "$VENDOR_DIR/pinmame/.cache-sha"
fi
cp "$VENDOR_DIR/pinmame/build/libpinmame.a" "$DEPS_DIR/lib/"
mkdir -p "$DEPS_DIR/include/pinmame"
cp "$VENDOR_DIR/pinmame/src/libpinmame/libpinmame.h" "$DEPS_DIR/include/pinmame/"
cp "$VENDOR_DIR/pinmame/src/libpinmame/PinMAMEPlugin.h" "$DEPS_DIR/include/pinmame/"
echo "" echo ""
echo "== build-deps.sh complete: $DEPS_DIR ==" echo "== build-deps.sh complete: $DEPS_DIR =="
ls -la "$DEPS_DIR/lib" ls -la "$DEPS_DIR/lib"
+2
View File
@@ -80,9 +80,11 @@ apply_patches() {
mkdir -p "$VENDOR_DIR" mkdir -p "$VENDOR_DIR"
fetch_pinned "vpinball" "https://github.com/vpinball/vpinball" "$VPINBALL_SHA" "$VENDOR_DIR/vpinball" fetch_pinned "vpinball" "https://github.com/vpinball/vpinball" "$VPINBALL_SHA" "$VENDOR_DIR/vpinball"
fetch_pinned "libwinevbs" "https://github.com/vpinball/libwinevbs" "$LIBWINEVBS_SHA" "$VENDOR_DIR/libwinevbs" fetch_pinned "libwinevbs" "https://github.com/vpinball/libwinevbs" "$LIBWINEVBS_SHA" "$VENDOR_DIR/libwinevbs"
fetch_pinned "pinmame" "https://github.com/vpinball/pinmame" "$PINMAME_SHA" "$VENDOR_DIR/pinmame"
apply_patches "vpinball" "$VENDOR_DIR/vpinball" apply_patches "vpinball" "$VENDOR_DIR/vpinball"
apply_patches "libwinevbs" "$VENDOR_DIR/libwinevbs" apply_patches "libwinevbs" "$VENDOR_DIR/libwinevbs"
apply_patches "pinmame" "$VENDOR_DIR/pinmame"
echo "" echo ""
echo "== Setup complete ==" echo "== Setup complete =="
+11
View File
@@ -20,6 +20,17 @@ SDL_IMAGE_SHA=bec9134a26c7d0f31b36d6083c25296e04cabff5
SDL_TTF_SHA=a1ce3670aec736ecbf0936c43f2f0cc53aa61e5b SDL_TTF_SHA=a1ce3670aec736ecbf0936c43f2f0cc53aa61e5b
FREEIMAGE_SHA=b1613452a0c3849d43ac877b154cf51ff9e078d3 FREEIMAGE_SHA=b1613452a0c3849d43ac877b154cf51ff9e078d3
# PinMAME (the real ROM/hardware emulation core VPinMAME.Controller wraps on
# every other platform). Mirrors vpinball's own platforms/config.sh pin
# exactly (like LIBWINEVBS_SHA/SDL_SHA above) - vpinball's plugins/pinmame/
# glue code is written against this specific libpinmame API surface (e.g.
# PinmameSetMsgAPI), so a newer/older pinmame commit than this one won't
# necessarily still match it. Validated end-to-end for wasm32 by this
# project's own pinmame-wasm spike (compiles + runs the full driver database
# under Emscripten - see patches/pinmame/ for the 3 portability fixes that
# spike needed; re-verified against this exact pinned commit).
PINMAME_SHA=23321ec7de6dfd563a1c64153bc75f26e5059b9f
# Emscripten SDK version already validated by both spikes # Emscripten SDK version already validated by both spikes
# (spikes/libwinevbs-wasm and spikes/sdl3-gles3-wasm). # (spikes/libwinevbs-wasm and spikes/sdl3-gles3-wasm).
EMSDK_VERSION=6.0.8 EMSDK_VERSION=6.0.8