20 Commits
Author SHA1 Message Date
valknar 1c21d2d79b chore: bump version to 0.3.4
CI / Build wasm engine (push) Successful in 7m54s
2026-08-24 13:57:30 +02:00
valknar f607992885 fix(vpinball): skip SDL_SetWindowIcon on the Emscripten target
SDL3's Emscripten video backend emulates SetWindowIcon by pointing the
page's <link rel="icon"> at a blob: URL of the encoded surface - a
reasonable desktop-icon mapping in general, but unwanted for an
embeddable widget where the host page already has its own favicon.
Verified via examples/basic: after this patch, no rel=icon link is
ever created/mutated while a table runs (previously a consumer had to
work around this with a MutationObserver reverting it every time).
2026-08-24 13:56:48 +02:00
valknar 10f32789e6 Trigger CI to verify simplified embedded-cache config
CI / Build wasm engine (push) Successful in 8m48s
2026-08-24 12:01:25 +02:00
valknar bf818bdd29 Trigger CI to verify gitea/runner migration + cache config fix
CI / Build wasm engine (push) Canceled after 4m37s
2026-08-24 11:56:29 +02:00
valknarandClaude Sonnet 5 4946dd2033 Trigger CI again after force-recreating the stale runner daemon
CI / Build wasm engine (push) Canceled after 1m16s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 11:43:24 +02:00
valknarandClaude Sonnet 5 7e5793bb17 Trigger CI to test the dedicated cache-server setup
CI / Build wasm engine (push) Canceled after 4m13s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 11:39:56 +02:00
valknarandClaude Sonnet 5 f52fa0cb79 Trigger CI with debug logging to capture the exact cache URL
CI / Build wasm engine (push) Successful in 11m17s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 10:54:35 +02:00
valknarandClaude Sonnet 5 9c8afb7541 Trigger CI again to inspect the live job container's network
CI / Build wasm engine (push) Canceled after 5m3s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 10:49:51 +02:00
valknarandClaude Sonnet 5 02f17165f2 Trigger CI to verify Gitea Actions cache fix (act_runner network/host config)
CI / Build wasm engine (push) Successful in 11m23s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 10:33:08 +02:00
valknarandClaude Sonnet 5 0f945ed68e Bump version to 0.3.3 for the dispose() hang fix
CI / Build wasm engine (push) Successful in 11m16s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 10:12:51 +02:00
valknarandClaude Sonnet 5 43216a408e Fix PinMAME busy-wait freeze on Controller.Stop() (dispose() hang)
Controller::Stop() (real desktop vpinball code, unmodified until now)
busy-waits for PinmameIsRunning() to clear after calling PinmameStop(),
in a sleep loop meant for a real OS thread to notice the quit flag and
finish stopping on its own. Under Emscripten there is no such thread -
PinmameStop() there only sets the quit flag - so the loop spun forever
on the single available thread, with nothing left to ever clear it.

This fires from Player::~Player()'s GameEvents_Exit script event
(controller.vbs's default Exit handler calls Controller.Stop), which
runs synchronously inside dispose() - hanging any consumer's teardown
for a ROM-based table, most visibly React StrictMode/unmount cleanup
calling stop() then dispose() shortly after.

Fixed by driving one more (now-instant, since the quit flag is already
set) PinmameEmscriptenStep() call directly under __EMSCRIPTEN__ instead
of busy-waiting - it runs cpu_post_run() and OnStateChange(0)
synchronously right there.

Also adds a "Stop & Dispose" button to the basic example, exercising
the same stop()-then-wait-two-frames-then-dispose() sequence consumers
use, to make this kind of regression visible without a separate app.

Confirmed fixed by hands-on testing: dispose() on a running ROM-based
table now returns immediately instead of hanging the tab.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 10:12:25 +02:00
valknarandClaude Sonnet 5 6ff116ba02 Bump version to 0.3.2 for the MsgBox fix
CI / Build wasm engine (push) Successful in 10m55s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 19:31:05 +02:00
valknarandClaude Sonnet 5 1d7c4e7b7d Implement real MsgBox() for VBScript table scripts (browser confirm/alert)
libwinevbs's __LIBWINEVBS__ build of Global_MsgBox() only logged the
prompt text and never set a return value, since it has no host UI to
show a dialog through. Any table script that branches on
MsgBox(...) = vbYes (a common pattern - e.g. core.vbs's own trough
ball-count dialog) always took the "no" branch, since the result
stayed at its zero-initialized default.

Adds a msgbox callback to libwinevbs_callbacks_t, wired on the
vpinball-wasm side to a real window.confirm()/alert() call. Both are
synchronous browser APIs that block JS execution and return a value
immediately, matching what VBScript's MsgBox() needs (its result is
used by the calling statement right away) - the only way to answer it
asynchronously would require Asyncify or a busy-wait poll loop, which
this project has deliberately avoided elsewhere.

Confirmed fixed by hands-on testing: a table's ball-count confirm
dialog now correctly returns Yes/No based on the user's click, instead
of always silently taking the "no" branch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 19:30:25 +02:00
valknarandClaude Sonnet 5 c7f7cb6afa Bump version to 0.3.1 for the focus-loss freeze fix
CI / Build wasm engine (push) Successful in 10m50s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 15:17:21 +02:00
valknarandClaude Sonnet 5 99ae5b9eda Fix PinMAME busy-wait freeze on browser/window focus loss
core.vbs auto-wires Controller.Pause = True into every table's Paused
event, which Player::OnFocusChanged() fires on focus loss. PinMAME's
own pause handling in usrintrf.c is a "while (g_fPause) { ...;
draw_screen(); ... }" busy-wait written for a real-OS-thread host,
where a separate thread later flips g_fPause back to 0. Under our
single-threaded Emscripten cooperative-scheduling model there is no
second thread to clear it, so entering that loop spins forever at
100% CPU with the tab completely unresponsive.

cpu_run_emscripten_step() now checks g_fPause up front and skips
cpu_timeslice() (and therefore that loop) entirely while paused;
emulation simply doesn't advance until Controller.Pause is set back
to False and the step function is called again next frame.

Confirmed fixed by hands-on testing: unfocusing the browser during a
PinMAME ROM session no longer freezes the tab.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 15:15:48 +02:00
valknarandClaude Sonnet 5 5570955bc5 Fix patches/pinmame/0004: drop duplicate hunk conflicting with 0002
CI / Build wasm engine (push) Successful in 10m51s
0004 was generated via a plain `git diff` against pinmame's pristine
commit, which captured the *cumulative* diff for cpuexec.c - including
patch 0002's time_fence stub hunk, not just 0004's own new cpu_run
splitting code. Applying 0002 then 0004 in sequence (exactly what
setup.sh does) failed: 0004's duplicate hunk expected pre-0002 context
that no longer matched.

Regenerated by diffing the current tree against a baseline with 0001-0003
already applied (matching the vpinball/0006 patch's existing approach)
instead of against pristine directly. Verified: all 4 pinmame patches now
apply cleanly in sequence against a fresh pristine checkout, and the
result is byte-identical to the working tree that was actually built and
tested.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SoSarxLgY33Kax5UNXcafZ
2026-08-23 12:47:51 +02:00
valknarandClaude Sonnet 5 6c8d08e618 Bump version to 0.3.0 for the PinMAME integration release
CI / Build wasm engine (push) Failing after 2m19s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SoSarxLgY33Kax5UNXcafZ
2026-08-23 12:38:16 +02:00
valknarandClaude Sonnet 5 a7b85ec8a4 Confirm real PinMAME ROM-driven emulation actually running
Tested pinball.loadRom() against a correctly-matched ROM zip for a real
community table (Heavy Metal Meltdown, Bally 1987): PinMAME loads the ROM,
and its own video/audio subsystems come up and run with no errors
(osd_create_display: 60.00 fps; real INT16/44100Hz audio format), not just
Controller object creation succeeding as before. Updates the README to move
this from "not yet validated" to confirmed, narrowing the remaining gap to
on-screen DMD/backglass output and hands-on switch/solenoid/scoring
behavior during actual play, which nobody has watched yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SoSarxLgY33Kax5UNXcafZ
2026-08-23 12:34:25 +02:00
valknarandClaude Sonnet 5 9479cea280 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
2026-08-23 12:09:35 +02:00
valknarandClaude Sonnet 5 d91ef0c96d Rename package to scoped @valknar/vpinball-wasm
CI / Build wasm engine (push) Successful in 9m24s
Reverts the earlier unscoping: an unscoped package on a Gitea npm
registry has no `.npmrc` mapping a consumer can use other than pinning
the exact tarball URL, since Gitea's registry doesn't proxy npmjs.org
and only scoped packages support the standard `@scope:registry=`
config. Scoping restores that: consumers add one `@valknar:registry=`
line and depend on a normal semver range.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012hQoM3jJT1Lx7CMTciMzvD
2026-08-23 01:06:15 +02:00
18 changed files with 943 additions and 12 deletions
+29 -8
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 — real ROM-driven emulation confirmed running
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 with its real, correctly-matched ROM zip supplied via `pinball.loadRom()`: the Controller 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`), loads the ROM successfully, and PinMAME's own video and audio subsystems come up and run with no errors (`osd_create_display: 60.00 fps`; `OnAudioAvailable: format=INT16, channels=1, sampleRate=44100.00, framesPerSecond=60.00`) — this is real emulation actually executing, not just object creation succeeding. A missing/invalid ROM (verified separately, with two different non-matching ROM zips) fails cleanly instead — a logged error, the game just doesn't start, no page crash.
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. **Still not visually/interactively confirmed:** actual on-screen DMD/backglass output and hands-on switch/solenoid/scoring behavior during play — video/audio subsystem startup is confirmed (see above), but no one has yet watched a full game played against this integration. 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.
@@ -70,16 +81,18 @@ Full research and two isolated feasibility spikes (proving real VBScript executi
- **A full loading → play flow in a real page** (`examples/basic/index.html`): a byte-level download progress bar, a user-gesture "Start" button (required for both audio autoplay and fullscreen to work), a working fullscreen button, and responsive canvas sizing that fills its container correctly. - **A full loading → play flow in a real page** (`examples/basic/index.html`): a byte-level download progress bar, a user-gesture "Start" button (required for both audio autoplay and fullscreen to work), a working fullscreen button, and responsive canvas sizing that fills its container correctly.
- **Browser file-loading for self-contained tables**: picking a `.vpx` file, mounting it via `FS.writeFile`, and starting the engine against it — verified end-to-end (`PinTable::LoadGameFromFilename /tables/uploaded.vpx` in the boot log, followed by a normal render). - **Browser file-loading for self-contained tables**: picking a `.vpx` file, mounting it via `FS.writeFile`, and starting the engine against it — verified end-to-end (`PinTable::LoadGameFromFilename /tables/uploaded.vpx` in the boot log, followed by a normal render).
- 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.
- **Real PinMAME (`VPinMAME.Controller`) ROM-driven emulation actually running**, not just object creation — see Status above for the exact confirmation (real video/audio subsystem startup against a real, correctly-matched ROM zip supplied via `pinball.loadRom()`).
**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 on-screen DMD/backglass output and hands-on gameplay** — the emulation session itself is confirmed running (see Status above), but no one has yet watched real DMD frames render or played a full game against it to confirm switch/solenoid/scoring behavior end-to-end.
- 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 +108,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
``` ```
@@ -107,7 +120,7 @@ source emsdk/emsdk_env.sh
## npm usage ## npm usage
```ts ```ts
import { loadPinball, attachTouchControls } from 'vpinball-wasm'; import { loadPinball, attachTouchControls } from '@valknar/vpinball-wasm';
const canvas = document.querySelector('canvas'); const canvas = document.querySelector('canvas');
const pinball = await loadPinball({ const pinball = await loadPinball({
@@ -126,14 +139,21 @@ 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 `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`.
## 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 — confirmed actually running real ROM-driven emulation (video/audio subsystem startup against a real ROM), but on-screen DMD/backglass output and hands-on switch/solenoid/scoring behavior during play are not yet confirmed — 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 +161,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.
+46 -1
View File
@@ -39,6 +39,13 @@
background: rgba(255,255,255,0.15); color: #eee; cursor: pointer; background: rgba(255,255,255,0.15); color: #eee; cursor: pointer;
} }
#fullscreen-button.hidden { display: none; } #fullscreen-button.hidden { display: none; }
#dispose-button {
position: absolute; top: 12px; left: 12px; z-index: 10;
padding: 8px 14px; border-radius: 6px; border: none;
background: rgba(220,38,38,0.85); color: #fff; cursor: pointer;
}
#dispose-button.hidden { display: none; }
</style> </style>
</head> </head>
<body> <body>
@@ -48,10 +55,13 @@
<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>
<button id="fullscreen-button" class="hidden">Fullscreen</button> <button id="fullscreen-button" class="hidden">Fullscreen</button>
<button id="dispose-button" class="hidden">Stop &amp; Dispose</button>
</div> </div>
<script type="module"> <script type="module">
@@ -62,7 +72,9 @@
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');
const disposeButton = document.getElementById('dispose-button');
let uploadedTableData; let uploadedTableData;
fileInput.addEventListener('change', async () => { fileInput.addEventListener('change', async () => {
@@ -70,6 +82,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,13 +102,18 @@
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');
disposeButton.classList.remove('hidden');
// Touch controls are additive UI for touch-capable devices - not // Touch controls are additive UI for touch-capable devices - not
// required for desktop mouse+keyboard play. // required for desktop mouse+keyboard play.
@@ -103,6 +128,26 @@
// browser denies it) - nothing to recover from here. // browser denies it) - nothing to recover from here.
}); });
}); });
// Mirrors the stop()-then-wait-a-couple-frames-then-dispose() pattern
// consumers need: stop() only takes effect on the engine's next
// internal step, so disposing synchronously right after can race it.
disposeButton.addEventListener('click', () => {
console.log('[dispose test] calling stop()...');
pinball.stop();
requestAnimationFrame(() => {
requestAnimationFrame(() => {
console.log('[dispose test] calling dispose()...');
pinball.dispose();
console.log('[dispose test] dispose() returned - no hang');
disposeButton.classList.add('hidden');
fullscreenButton.classList.add('hidden');
overlay.classList.remove('hidden');
startButton.textContent = 'Disposed (reload page to restart)';
startButton.disabled = true;
});
});
});
</script> </script>
</body> </body>
</html> </html>
+30
View File
@@ -0,0 +1,30 @@
{
"name": "@valknar/vpinball-wasm",
"version": "0.3.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@valknar/vpinball-wasm",
"version": "0.3.4",
"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"
}
}
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "vpinball-wasm", "name": "@valknar/vpinball-wasm",
"version": "0.1.0", "version": "0.3.4",
"description": "Visual Pinball's engine compiled to WebAssembly - real .vpx tables, real VBScript, WebGL2 rendering, in the browser", "description": "Visual Pinball's engine compiled to WebAssembly - real .vpx tables, real VBScript, WebGL2 rendering, in the browser",
"license": "SEE LICENSE IN LICENSE", "license": "SEE LICENSE IN LICENSE",
"type": "module", "type": "module",
+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,64 @@
diff --git a/include/libwinevbs.h b/include/libwinevbs.h
index 3ba4df9..6cac7df 100644
--- a/include/libwinevbs.h
+++ b/include/libwinevbs.h
@@ -52,6 +52,14 @@ LIBWINEVBS_API const char* libwinevbs_hresult_name(HRESULT hr);
typedef struct {
void (*log)(libwinevbs_log_level_t level, const char* format, va_list args);
HRESULT (*create_object)(const WCHAR* progid, IClassFactory* cf, IUnknown** obj);
+ /* Answers a VBScript MsgBox() call. Must return synchronously (its result
+ is used by the calling script statement immediately) - e.g. a real,
+ blocking confirm()/alert() call under Emscripten. type is the raw
+ VBScript "buttons" argument (MB_OK=0, MB_OKCANCEL=1, MB_YESNO=4, ...);
+ title may be NULL. Return the matching button id (IDOK=1, IDCANCEL=2,
+ IDABORT=3, IDRETRY=4, IDIGNORE=5, IDYES=6, IDNO=7), matching vbOK..vbNo.
+ If unset, MsgBox always answers IDOK/vbOK without asking anything. */
+ int (*msgbox)(const char* prompt, int type, const char* title);
} libwinevbs_callbacks_t;
LIBWINEVBS_API void libwinevbs_init(const libwinevbs_callbacks_t* callbacks);
diff --git a/src/libwinevbs.c b/src/libwinevbs.c
index 7b3d6f0..91091e4 100644
--- a/src/libwinevbs.c
+++ b/src/libwinevbs.c
@@ -32,6 +32,14 @@ HRESULT libwinevbs_create_object(const WCHAR* progid, IClassFactory* cf, IUnknow
return CLASS_E_CLASSNOTAVAILABLE;
}
+int libwinevbs_msgbox(const char* prompt, int type, const char* title)
+{
+ if (g_callbacks.msgbox)
+ return g_callbacks.msgbox(prompt, type, title);
+
+ return 1; /* IDOK/vbOK - no host callback registered, so just proceed */
+}
+
const char* libwinevbs_hresult_name(HRESULT hr)
{
switch (hr) {
diff --git a/wine/dlls/vbscript/global.c b/wine/dlls/vbscript/global.c
index 0e0e144..351f967 100644
--- a/wine/dlls/vbscript/global.c
+++ b/wine/dlls/vbscript/global.c
@@ -33,6 +33,7 @@
#include <locale.h>
#include "scrrun_private.h"
HRESULT libwinevbs_create_object(const WCHAR *progid, IClassFactory* cf, IUnknown** obj);
+int libwinevbs_msgbox(const char *prompt, int type, const char *title);
extern HRESULT WINAPI WshShellFactory_CreateInstance(IClassFactory*,IUnknown*,REFIID,void**);
#endif
@@ -2834,9 +2835,12 @@ static HRESULT Global_MsgBox(BuiltinDisp *This, VARIANT *args, unsigned args_cnt
hres = show_msgbox(This->ctx, prompt, type, title, res);
#else
if(SUCCEEDED(hres)) {
- char buf[2048];
+ char buf[2048], title_buf[256] = {0};
WideCharToMultiByte(CP_ACP, 0, prompt, -1, buf, sizeof(buf) - 1, NULL, NULL);
+ if (title)
+ WideCharToMultiByte(CP_ACP, 0, title, -1, title_buf, sizeof(title_buf) - 1, NULL, NULL);
libwinevbs_log(LIBWINEVBS_LOG_INFO, "vbscript: MsgBox prompt=%s", buf);
+ hres = return_short(res, libwinevbs_msgbox(buf, type, title ? title_buf : NULL));
}
#endif
@@ -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,402 @@
diff --git a/src/cpuexec.c b/src/cpuexec.c
index 07db6a1..19da31a 100644
--- a/src/cpuexec.c
+++ b/src/cpuexec.c
@@ -466,6 +466,78 @@ 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)
+{
+ /* usrintrf.c's per-frame video update contains a "while (g_fPause) {
+ ...; draw_screen(); ... }" busy-wait (see the VPINMAME/LIBPINMAME
+ branch around updatescreen()) that assumes a real OS thread will
+ later flip g_fPause back to 0 from outside it. We're single-threaded
+ here, so entering that loop at all would spin forever burning 100%
+ CPU with no way out - nothing else can ever run to clear the flag.
+ Skip cpu_timeslice() (and therefore that loop) entirely while paused;
+ the emulation simply doesn't advance until Controller.Pause is set
+ back to False and this function is called again next frame. */
+ extern int g_fPause;
+ if (g_fPause)
+ return 1;
+
+ 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
+
/*************************************
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();
@@ -0,0 +1,60 @@
diff --git a/src/core/VPApp.cpp b/src/core/VPApp.cpp
index 7175dd2..f05a324 100644
--- a/src/core/VPApp.cpp
+++ b/src/core/VPApp.cpp
@@ -36,6 +36,10 @@
#include <libwinevbs/libwinevbs.h>
#endif
+#ifdef __EMSCRIPTEN__
+#include <emscripten.h>
+#endif
+
#include "parts/ball.h"
#include "parts/timer.h"
#include "parts/flipper.h"
@@ -273,6 +277,34 @@ int VPApp::GetLogicalNumberOfProcessors() const
return m_logicalNumberOfProcessors;
}
+#ifdef __EMSCRIPTEN__
+// VBScript's MsgBox() must return synchronously (its VARIANT result is used
+// right after the call, in the same script statement) - window.confirm()/
+// alert() are the only browser APIs that block JS execution and return a
+// value synchronously, so they're used here directly rather than routing
+// through any async/JS-callback mechanism the host page might otherwise want
+// to customize the look of (which would require Asyncify to suspend the
+// call, since single-threaded builds have no other way to block for it).
+// type is the raw VBScript "buttons" argument (MB_OK=0, MB_OKCANCEL=1,
+// MB_ABORTRETRYIGNORE=2, MB_YESNOCANCEL=3, MB_YESNO=4, MB_RETRYCANCEL=5).
+// The returned id matches vbOK(1)/vbCancel(2)/vbYes(6)/vbNo(7).
+static int EmscriptenMsgBox(const char* prompt, int type, const char* title)
+{
+ const string message = (title && title[0]) ? (string(title) + "\n\n" + prompt) : string(prompt);
+ switch (type & 0x0F)
+ {
+ case 1: // MB_OKCANCEL
+ return EM_ASM_INT({ return confirm(UTF8ToString($0)) ? 1 : 0; }, message.c_str()) ? 1 /* IDOK */ : 2 /* IDCANCEL */;
+ case 3: // MB_YESNOCANCEL - Cancel isn't distinguishable from No via confirm(), best effort
+ case 4: // MB_YESNO
+ return EM_ASM_INT({ return confirm(UTF8ToString($0)) ? 1 : 0; }, message.c_str()) ? 6 /* IDYES */ : 7 /* IDNO */;
+ default:
+ EM_ASM_({ alert(UTF8ToString($0)); }, message.c_str());
+ return 1; // IDOK
+ }
+}
+#endif
+
void VPApp::InitInstance()
{
std::filesystem::path iniFileName = m_commandLineCustomSettingsFileName;
@@ -325,6 +357,9 @@ void VPApp::InitInstance()
delete[] buffer;
}
};
+#ifdef __EMSCRIPTEN__
+ callbacks.msgbox = &EmscriptenMsgBox;
+#endif
libwinevbs_init(&callbacks);
#endif
@@ -0,0 +1,25 @@
diff --git a/plugins/pinmame/Controller.cpp b/plugins/pinmame/Controller.cpp
index 29c2405..a498fee 100644
--- a/plugins/pinmame/Controller.cpp
+++ b/plugins/pinmame/Controller.cpp
@@ -265,8 +265,20 @@ void Controller::Stop()
if (PinmameIsRunning())
{
PinmameStop();
+#ifdef __EMSCRIPTEN__
+ // PinmameStop() only sets a "please quit" flag here (see its own
+ // Emscripten branch) - there's no separate OS thread that will ever
+ // notice it and finish stopping on its own, so busy-waiting for
+ // PinmameIsRunning() to clear on this single thread would spin
+ // forever. Drive one more step directly instead: with the quit flag
+ // already set, it's a bounded, instant call that completes the
+ // pending teardown (calls cpu_post_run() and OnStateChange(0))
+ // synchronously right here.
+ PinmameEmscriptenStep();
+#else
while (PinmameIsRunning() != 0) // Wait until the machine is stopped
std::this_thread::sleep_for(std::chrono::milliseconds(75));
+#endif
if (m_onGameEndHandler)
m_onGameEndHandler(this);
}
@@ -0,0 +1,25 @@
diff --git a/src/renderer/Window.cpp b/src/renderer/Window.cpp
index 3dff060..92a96df 100644
--- a/src/renderer/Window.cpp
+++ b/src/renderer/Window.cpp
@@ -264,6 +264,12 @@ Window::Window(const string& title, const Settings& settings, VPXWindowId window
m_pixelDensity = 1.f;
}
+#ifndef __EMSCRIPTEN__
+ // SDL3's Emscripten video backend emulates SDL_SetWindowIcon by pointing
+ // the page's <link rel="icon"> at a blob: URL of the encoded surface -
+ // a reasonable desktop-icon mapping in general, but unwanted here where
+ // the embedding page already has its own favicon. Skip it on this target
+ // rather than fight it from the host page's JS.
if (auto icon = BaseTexture::CreateFromFile(g_app->m_fileLocator.GetAppPath(FileLocator::AppSubFolder::Assets, "vpinball.png")); icon)
{
SDL_Surface* pSurface = icon->ToSDLSurface();
@@ -276,6 +282,7 @@ Window::Window(const string& title, const Settings& settings, VPXWindowId window
else {
PLOGE << "Failed to load window icon: " << SDL_GetError();
}
+#endif
// Check if the platform allows positioning windows (as Wayland forbids it...)
{
+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