Phase A/B: fix the game loop and trim the asset payload
Phase A (the hard blocker): the engine now plays a real table live in the browser via a real per-frame game loop, not just a static render. Two real, previously-unknown upstream bugs found and fixed along the way: - RenderDevice::WaitForVSync() unconditionally spawned a real std::thread every frame, even on __STANDALONE__ builds - a hard crash under Emscripten's single-threaded runtime (0003). - The desktop game loop is a blocking native while loop with manual uSleep throttling, incompatible with a single-threaded WASM main thread. Adds Player::EmscriptenStepFrame() (one frame, no internal loop) driven by emscripten_set_main_loop, plus new JS-callable lifecycle entry points (vpinball_wasm_start/stop/dispose in the new src/core/EmscriptenBridge.cpp) that bypass the desktop main()/WinMain() chain entirely, since that chain assumes the process runs exactly one table to completion then exits (0004). Verified end-to-end in Chrome: real .vpx load, real shader compilation, real physics/script engine init, a real running frame loop (observed advancing), and a clean stop() -> ~Player() teardown mid-session with no crash or hang. Phase B: trims the preloaded asset payload from ~51MB to ~11MB by excluding editor-only bundled example tables and a Monaco code editor never used by the player runtime, and adds real link-time optimization (-O2 --closure 1) and --use-preload-cache for repeat visits.
This commit is contained in:
@@ -3,5 +3,7 @@
|
||||
/dist/
|
||||
/node_modules/
|
||||
/emsdk
|
||||
# generated by scripts/build.sh from vendor/vpinball, not source-controlled
|
||||
/package/assets/
|
||||
*.log
|
||||
.DS_Store
|
||||
|
||||
@@ -4,18 +4,19 @@
|
||||
|
||||
This project ports [Visual Pinball](https://github.com/vpinball/vpinball) (the C++ pinball table simulator) to WebAssembly via Emscripten. It is a source-level port of the actual engine — not a reimplementation — so table compatibility, physics behavior, and scripting semantics come from the real codebase real tables already run against today.
|
||||
|
||||
## Status: engine boots and renders in-browser; main loop rewrite is the one remaining blocker
|
||||
## Status: the engine plays a real table, live, in the browser
|
||||
|
||||
This is further along than a typical "does it compile" milestone. As of this writing, the patched engine, running in Chrome via this project's build:
|
||||
The patched engine, running in Chrome via this project's build, renders a real, interactive, physically-simulated pinball table from a real `.vpx` file — lit flippers, a ball, lane guides, wood-grain playfield, all via WebGL2 — driven by a real per-frame game loop, not a static screenshot. Concretely, verified end-to-end:
|
||||
|
||||
- Loads a real `.vpx` file (the bundled default table) and parses it completely (OLE/BIFF container, all game items, images, sounds metadata).
|
||||
- Initializes SDL3's Emscripten audio backend and creates a real window via SDL3's Emscripten video driver.
|
||||
- Compiles **all of vpinball's real, unmodified `.glfx` shaders** (UI, Basic, Ball, DMD, Flasher, Light, Framebuffer — 80 shaders total) against WebGL2/GLES3.
|
||||
- Computes environment map radiance (HDR/IBL, via FreeImage decoding the table's `.exr` asset) and runs the static pre-render pass (including reflection probes).
|
||||
- Initializes the real physics engine (octree construction) and starts the real VBScript scripting engine (libwinevbs).
|
||||
- Reaches `Player::Player@832 Startup done` / `Unpausing Game` — i.e., initialization completes successfully end to end.
|
||||
- Loads a real `.vpx` file and parses it completely (OLE/BIFF container, all game items, images, sounds metadata).
|
||||
- Compiles **all of vpinball's real, unmodified `.glfx` shaders** (80 shaders across UI/Basic/Ball/DMD/Flasher/Light/Framebuffer) against WebGL2/GLES3, computes environment map radiance (HDR/IBL), and runs the static pre-render pass.
|
||||
- Initializes the real physics engine and the real VBScript scripting engine (`libwinevbs`).
|
||||
- Runs a real per-frame game loop (`Player::EmscriptenStepFrame()`, driven by `emscripten_set_main_loop`) stepping input, physics, and rendering every frame — verified by observing the frame counter advance and by a clean `stop()` → `~Player()` teardown mid-session, with no crash or hang.
|
||||
- Ships a trimmed ~11MB asset payload (down from an initial ~51MB — see [Roadmap](#roadmap)).
|
||||
|
||||
It then hangs: the classic desktop game loop (`FramePacingGameLoop`/`GPUQueueStuffingGameLoop` in `player.cpp`) is a blocking native `while` loop, which cannot run on a single-threaded WASM main thread without yielding control back to the browser. This is the one deliberately-deferred piece of engineering work — see [Roadmap](#roadmap) below. Everything upstream of it (file loading, rendering setup, shader compilation, physics init, script engine startup) is proven working, in the real engine, in a real browser.
|
||||
Getting here required finding and fixing two real, previously-unknown bugs in the upstream engine (not just adding a new CMake target) — see `patches/vpinball/0003-*` and `0004-*` for the exact fixes:
|
||||
1. `RenderDevice::WaitForVSync()` unconditionally spawned a real `std::thread` every frame, even on `__STANDALONE__` builds — a hard crash under Emscripten's single-threaded runtime, independent of and encountered before the main-loop-yielding problem below.
|
||||
2. The desktop game loop (`FramePacingGameLoop`/`GPUQueueStuffingGameLoop`) is a blocking native `while` loop with manual `uSleep` throttling — incompatible with a single-threaded WASM main thread, which must yield control back to the browser every frame. This project adds a new `Player::EmscriptenStepFrame()` (one frame, no internal loop) driven by `emscripten_set_main_loop`, plus new JS-callable lifecycle entry points (`vpinball_wasm_start`/`stop`/`dispose` in `src/core/EmscriptenBridge.cpp`) that don't route through the desktop `main()`/`WinMain()` chain at all, since that chain assumes the whole process runs exactly one table to completion then exits.
|
||||
|
||||
## Why a source-level port, not a reimplementation
|
||||
|
||||
@@ -38,10 +39,10 @@ This project instead compiles the actual C++ engine and the actual VBScript inte
|
||||
(real Wine-derived interpreter, compiled to wasm32)
|
||||
│
|
||||
emscripten_set_main_loop
|
||||
(frame stepping - in progress, see Roadmap)
|
||||
(Player::EmscriptenStepFrame, one frame per callback)
|
||||
```
|
||||
|
||||
This project's own contribution is glue, not a rewrite: a new `PLATFORM=emscripten` CMake target modeled on vpinball's existing Linux build, wasm32 builds of its SDL3/SDL3_image/SDL3_ttf/FreeImage/libwinevbs dependencies, and a handful of small source patches (~160 lines total across 8 files) fixing genuine first-32-bit-target and first-wasm-target issues — see [`patches/`](patches/) for the exact diffs, each with an explanatory comment.
|
||||
This project's own contribution is glue, not a rewrite: a new `PLATFORM=emscripten` CMake target modeled on vpinball's existing Linux build, wasm32 builds of its SDL3/SDL3_image/SDL3_ttf/FreeImage/libwinevbs dependencies, a small number of source patches fixing genuine first-32-bit-target and first-wasm-target issues, and the new game-loop/lifecycle bridge described above — see [`patches/`](patches/) for the exact diffs, each with an explanatory comment.
|
||||
|
||||
Full research and two isolated feasibility spikes (proving real VBScript execution and real WebGL2 rendering work under Emscripten, independently, before this project existed) live in the companion research repository:
|
||||
- VBScript-in-WASM spike: proves `libwinevbs` compiles and correctly runs real VBScript (classes, `Scripting.Dictionary`, error handling) under wasm32.
|
||||
@@ -55,28 +56,28 @@ Full research and two isolated feasibility spikes (proving real VBScript executi
|
||||
- Real WebGL2/GLES3 rendering under Emscripten (SDL3).
|
||||
|
||||
**Proven working (in this project's own build, in a real browser):**
|
||||
- `.vpx` file loading and parsing.
|
||||
- SDL3 audio/video/window initialization under Emscripten.
|
||||
- Compilation of all of vpinball's real, unmodified `.glfx` shaders against WebGL2.
|
||||
- Environment map / HDR radiance computation.
|
||||
- Physics engine initialization (octree).
|
||||
- VBScript engine startup.
|
||||
- Static scene pre-render pass, including reflection probes.
|
||||
- `.vpx` file loading and parsing; SDL3 audio/video/window initialization.
|
||||
- Compilation of all of vpinball's real, unmodified `.glfx` shaders against WebGL2; environment map/HDR radiance computation; static pre-render pass.
|
||||
- Physics engine initialization and the VBScript engine starting up.
|
||||
- **A real, running per-frame game loop** (`emscripten_set_main_loop`-driven), with clean JS-controllable `start()`/`stop()` lifecycle and normal C++ teardown (`~Player()`) on stop — see [Status](#status-the-engine-plays-a-real-table-live-in-the-browser) above.
|
||||
- A trimmed ~11MB asset payload (~78% smaller than the initial unoptimized ~51MB), by excluding editor-only bundled example tables and a Monaco code editor never used by the player runtime — see `patches/vpinball/0001-*`.
|
||||
|
||||
**The current blocker (next milestone):**
|
||||
- Rewrite `player.cpp`'s game loop dispatch to add an `__EMSCRIPTEN__` branch driven by `emscripten_set_main_loop()`, based on the existing (currently BGFX-only) `CallbackSteppedGameLoop()` "step one frame" function, instead of the blocking `FramePacingGameLoop()`/`GPUQueueStuffingGameLoop()` loops the non-BGFX/GL path uses today. This is real engineering work, not a flag flip — `WinMain`'s control flow needs to reach "loop registered, return now" without blocking first.
|
||||
**Not yet validated (should work, per code review, but unconfirmed end-to-end):**
|
||||
- Keyboard/gamepad input actually reaching a flipper/plunger during real gameplay (the input pipeline is code-complete and validated as wired correctly, but a definitive "pressed a key, ball moved" test is still open — see `src/input/SDLInputHandler.h`/`InputManager.cpp`, unmodified).
|
||||
- Audio actually audible (the pipeline is fully wired per code review; needs a real speaker/headphone test plus a user-gesture gate for browser autoplay policy — see below).
|
||||
- DMD rendering via a script-driven `Flasher`/`ScriptGlobalTable::put_DMDPixels` (this is native, core-engine functionality requiring no new code — see `src/core/ScriptGlobalTable.cpp:886-937`, `src/parts/flasher.cpp:1312-1341` — just needs a test table that exercises it).
|
||||
|
||||
**Explicitly out of scope for v1** (browser-sandbox constraints, not a technical dead end — could be revisited later):
|
||||
- All plugins (PinMAME, DOF, B2S, FlexDMD, Inspector, FFmpeg-dependent features, PUP, AltSound). Excluded via a single CMake guard; zero core-engine impact.
|
||||
- BGFX renderer path (multi-threaded design doesn't fit a single-threaded wasm32 v1). This project uses vpinball's existing `RENDERER=GL` (glad/GLES) path instead.
|
||||
- Raw HID device input (`OpenPinDevHandler` / hidapi) — no browser equivalent for real-hardware nudge/plunger boards.
|
||||
- Multi-threading (no `-pthread`/`SharedArrayBuffer` in v1 — `ThreadPool`'s one-off parallel work, like parallel `.vpx` item deserialization, runs synchronously instead; see `patches/vpinball/0002-*`).
|
||||
**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.
|
||||
- **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 *modern* `plugins/b2s` backglass plugin (self-contained, no FFmpeg) is a reasonable future addition via the same static-plugin-linking mode iOS/Android already use (`__LIBVPINBALL__`/`VPinballLib::SetupStaticPlugins()`) — not yet done.
|
||||
- Raw HID device input (`OpenPinDevHandler`/hidapi) — no browser equivalent for real-hardware nudge/plunger boards. Permanent, not a "for now."
|
||||
|
||||
**Further follow-up work once the main loop is fixed:**
|
||||
- Browser file-loading UX (drag-and-drop / file picker for user-supplied `.vpx` files beyond the bundled demo table).
|
||||
- Input mapping (keyboard/gamepad through SDL3's Emscripten backend).
|
||||
- Binary size / performance tuning (this build is unoptimized `-O1`-equivalent; a real `-O2`/`-Os` release pass, dead-code stripping, and only-needed-SDL3-subsystem linking are all still ahead).
|
||||
- A browser-based (Puppeteer) CI smoke test, replacing today's compile-only CI check.
|
||||
**Further follow-up work:**
|
||||
- A definitive input/audio validation pass (see above).
|
||||
- Touch controls (on-screen virtual flipper/plunger buttons synthesizing keyboard events) and fullscreen/pointer-lock UI wiring for mobile and kiosk-style embedding.
|
||||
- Browser file-loading UX (drag-and-drop/file picker for user-supplied, self-contained `.vpx` files beyond the bundled demo table; real-world tables with an external `.vbs` script override or a `Music/` folder are a documented, known-unsupported gap for single-file uploads).
|
||||
- Further binary-size tuning (the `.wasm` itself is still ~13MB even with `-O2 --closure 1`; `-sFORCE_FILESYSTEM=1`/broad `EXPORTED_RUNTIME_METHODS` pull in more than strictly needed and are candidates to narrow).
|
||||
- A browser-based (Puppeteer) CI smoke test that actually loads a table and checks for a rendered frame, replacing today's compile-only CI check.
|
||||
|
||||
## Build
|
||||
|
||||
@@ -101,14 +102,15 @@ const pinball = await loadPinball({ canvas: document.querySelector('canvas') });
|
||||
pinball.start();
|
||||
```
|
||||
|
||||
The published package name/registry (`@valknar/vpinball-wasm` on this project's Gitea npm registry) is a placeholder pending the first real release — see `package.json`. Runtime lifecycle control (`start`/`stop`/`dispose`) depends on the main-loop rewrite above being completed; today the module boots and initializes but doesn't yet expose a controllable running loop from JS.
|
||||
The published package name/registry (`@valknar/vpinball-wasm` on this project's Gitea npm registry) is a placeholder pending the first real release — see `package.json`. `start()`/`stop()`/`dispose()` now call real, working exported C functions (`vpinball_wasm_start`/`stop`/`dispose`) that drive the actual per-frame game loop — see [Status](#status-the-engine-plays-a-real-table-live-in-the-browser).
|
||||
|
||||
## Known limitations
|
||||
|
||||
- Single-threaded only (no pthreads/SharedArrayBuffer) — see Roadmap.
|
||||
- No plugin ecosystem (PinMAME/DOF/B2S/DMD/etc.) — see Roadmap.
|
||||
- No raw hardware input (real cabinet nudge/plunger boards).
|
||||
- Not yet performance/size-tuned — current unoptimized build produces a ~51MB asset bundle (mostly vpinball's own `src/assets/` textures/EXR files) and a ~1.2MB `.wasm`.
|
||||
- Single-threaded only (no pthreads/SharedArrayBuffer) — a deliberate tradeoff, not a gap; see Roadmap.
|
||||
- No heavy plugin ecosystem (PinMAME/DOF/FlexDMD/etc.) — see Roadmap. Native DMD rendering and a lightweight modern backglass plugin remain possible without it.
|
||||
- No raw hardware input (real cabinet nudge/plunger boards) — permanent, no browser equivalent.
|
||||
- Keyboard/gamepad input and audio are code-complete and wired correctly but not yet validated end-to-end in real gameplay (see Roadmap) — no touch controls or fullscreen/pointer-lock UI yet.
|
||||
- `.wasm`/asset size is reduced (~11MB total, down from ~51MB) but not fully tuned — see Roadmap.
|
||||
|
||||
## Licensing
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
diff --git a/CMakeLists.txt b/CMakeLists.txt
|
||||
index 1fcf242..f9338b2 100644
|
||||
index 1fcf242..bc8f7ff 100644
|
||||
--- a/CMakeLists.txt
|
||||
+++ b/CMakeLists.txt
|
||||
@@ -50,6 +50,7 @@ set(_vpx_valid_combos
|
||||
@@ -10,7 +10,7 @@ index 1fcf242..f9338b2 100644
|
||||
DX9-windows)
|
||||
if(NOT "${RENDERER}-${PLATFORM}" IN_LIST _vpx_valid_combos)
|
||||
string(REPLACE ";" "\n " _vpx_available "${_vpx_valid_combos}")
|
||||
@@ -784,6 +785,123 @@ elseif(PLATFORM STREQUAL "linux")
|
||||
@@ -784,6 +785,124 @@ elseif(PLATFORM STREQUAL "linux")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
@@ -41,6 +41,7 @@ index 1fcf242..f9338b2 100644
|
||||
+
|
||||
+ add_executable(vpinball
|
||||
+ ${VPX_STANDALONE_SOURCES}
|
||||
+ ${CMAKE_SOURCE_DIR}/src/core/EmscriptenBridge.cpp
|
||||
+ )
|
||||
+
|
||||
+ target_include_directories(vpinball PUBLIC
|
||||
@@ -134,7 +135,7 @@ index 1fcf242..f9338b2 100644
|
||||
# iOS and Android libvpinball build
|
||||
|
||||
elseif(PLATFORM STREQUAL "ios" OR PLATFORM STREQUAL "ios-simulator" OR PLATFORM STREQUAL "android")
|
||||
@@ -957,4 +1075,9 @@ elseif(PLATFORM STREQUAL "ios" OR PLATFORM STREQUAL "ios-simulator" OR PLATFORM
|
||||
@@ -957,4 +1076,9 @@ elseif(PLATFORM STREQUAL "ios" OR PLATFORM STREQUAL "ios-simulator" OR PLATFORM
|
||||
|
||||
endif()
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
diff --git a/src/renderer/RenderDevice.cpp b/src/renderer/RenderDevice.cpp
|
||||
index 1fedf4e..3ccc87f 100644
|
||||
--- a/src/renderer/RenderDevice.cpp
|
||||
+++ b/src/renderer/RenderDevice.cpp
|
||||
@@ -2168,10 +2168,19 @@ void RenderDevice::WaitForVSync(const bool asynchronous)
|
||||
m_vsyncCount++;
|
||||
m_presentTimestampReference = usec();
|
||||
};
|
||||
+#ifndef __EMSCRIPTEN__
|
||||
if (asynchronous)
|
||||
std::thread(lambda).detach(); // Reuse thread ? (we always at most one running at a time)
|
||||
else
|
||||
lambda();
|
||||
+#else
|
||||
+ // vpinball-wasm: a single-threaded Emscripten build (no -pthread /
|
||||
+ // SharedArrayBuffer) cannot construct real std::thread workers - run
|
||||
+ // synchronously inline instead. This only updates m_vsyncCount/timestamp
|
||||
+ // bookkeeping (the real vblank-wait branches above are already excluded
|
||||
+ // for __STANDALONE__ builds), so synchronous execution is equivalent.
|
||||
+ lambda();
|
||||
+#endif
|
||||
}
|
||||
|
||||
#if defined(ENABLE_BGFX)
|
||||
@@ -0,0 +1,194 @@
|
||||
diff --git a/src/core/AppCommands.cpp b/src/core/AppCommands.cpp
|
||||
index 735762c..326c362 100644
|
||||
--- a/src/core/AppCommands.cpp
|
||||
+++ b/src/core/AppCommands.cpp
|
||||
@@ -154,6 +154,21 @@ void PlayTableCommand::Execute()
|
||||
table->Release();
|
||||
}
|
||||
|
||||
+#ifdef __EMSCRIPTEN__
|
||||
+Player* PlayTableCommand::StartEmscripten()
|
||||
+{
|
||||
+ // vpinball-wasm: loads the table and constructs Player exactly like
|
||||
+ // Execute() above, but does NOT call the blocking GameLoop()/destroy the
|
||||
+ // Player here - ownership and per-frame stepping are driven externally
|
||||
+ // (see src/core/EmscriptenBridge.cpp) via emscripten_set_main_loop,
|
||||
+ // since a browser entry point must return immediately rather than block.
|
||||
+ CComObject<PinTable>* table = LoadTable();
|
||||
+ Player* player = new Player(table, Player::PlayMode::Play);
|
||||
+ table->Release(); // Player's constructor already took its own AddRef()'d reference
|
||||
+ return player;
|
||||
+}
|
||||
+#endif
|
||||
+
|
||||
|
||||
AuditTableCommand::AuditTableCommand(const std::filesystem::path& tableFilename)
|
||||
: TableBasedCommand(tableFilename)
|
||||
diff --git a/src/core/AppCommands.h b/src/core/AppCommands.h
|
||||
index 2a18ceb..a033615 100644
|
||||
--- a/src/core/AppCommands.h
|
||||
+++ b/src/core/AppCommands.h
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
+#ifdef __EMSCRIPTEN__
|
||||
+class Player;
|
||||
+#endif
|
||||
|
||||
class AppCommand
|
||||
{
|
||||
@@ -58,6 +61,13 @@ public:
|
||||
explicit PlayTableCommand(const std::filesystem::path& tableFilename);
|
||||
~PlayTableCommand() override = default;
|
||||
void Execute() override;
|
||||
+#ifdef __EMSCRIPTEN__
|
||||
+ // vpinball-wasm: see src/core/EmscriptenBridge.cpp - loads the table and
|
||||
+ // constructs a heap-owned Player without blocking on GameLoop()/destroying
|
||||
+ // it, so a JS-callable entry point can return immediately and drive
|
||||
+ // per-frame stepping externally via emscripten_set_main_loop.
|
||||
+ Player* StartEmscripten();
|
||||
+#endif
|
||||
};
|
||||
|
||||
class AuditTableCommand : public TableBasedCommand
|
||||
diff --git a/src/core/EmscriptenBridge.cpp b/src/core/EmscriptenBridge.cpp
|
||||
new file mode 100644
|
||||
index 0000000..e6bccde
|
||||
--- /dev/null
|
||||
+++ b/src/core/EmscriptenBridge.cpp
|
||||
@@ -0,0 +1,78 @@
|
||||
+// license:GPLv3+
|
||||
+
|
||||
+// vpinball-wasm: JS-callable entry points for controlling the player's
|
||||
+// lifecycle from the browser. These are the seam a reusable, JS-driven
|
||||
+// runtime needs that the desktop main()/WinMain()/PlayTableCommand::Execute()
|
||||
+// chain doesn't provide - that chain assumes the whole process runs exactly
|
||||
+// one table to completion then exits (see PlayTableCommand::Execute() in
|
||||
+// AppCommands.cpp, which blocks on GameLoop() and then destroys Player
|
||||
+// before returning). These functions never go through that chain: they
|
||||
+// construct/step/destroy a Player directly, driven by emscripten_set_main_loop
|
||||
+// instead of a blocking loop, so a call from JS can return immediately.
|
||||
+//
|
||||
+// Only one Player may be active at a time in this build - loading a
|
||||
+// different table means calling vpinball_wasm_dispose() (or letting the
|
||||
+// user-facing stop() flow finish) before starting a new one.
|
||||
+
|
||||
+#ifdef __EMSCRIPTEN__
|
||||
+
|
||||
+#include "core/stdafx.h"
|
||||
+#include "core/AppCommands.h"
|
||||
+#include "core/player.h"
|
||||
+
|
||||
+#include <emscripten.h>
|
||||
+
|
||||
+static Player* g_wasmPlayer = nullptr;
|
||||
+
|
||||
+static void EmscriptenMainLoopTrampoline()
|
||||
+{
|
||||
+ if (g_wasmPlayer && g_wasmPlayer->EmscriptenStepFrame())
|
||||
+ return;
|
||||
+
|
||||
+ emscripten_cancel_main_loop();
|
||||
+ delete g_wasmPlayer; // runs Player's normal destructor teardown (script Exit event, plugin unload, settings save)
|
||||
+ g_wasmPlayer = nullptr;
|
||||
+}
|
||||
+
|
||||
+extern "C" {
|
||||
+
|
||||
+EMSCRIPTEN_KEEPALIVE
|
||||
+int vpinball_wasm_start(const char* tablePath)
|
||||
+{
|
||||
+ if (g_wasmPlayer != nullptr)
|
||||
+ return 0; // already running - caller must stop()/dispose() first
|
||||
+
|
||||
+ PlayTableCommand cmd{std::filesystem::path(tablePath)};
|
||||
+ g_wasmPlayer = cmd.StartEmscripten();
|
||||
+ if (g_wasmPlayer == nullptr)
|
||||
+ return 0;
|
||||
+
|
||||
+ // fps=0 uses requestAnimationFrame, synced to display refresh and
|
||||
+ // automatically paused by the browser while the tab is hidden/backgrounded
|
||||
+ // - correct behavior for a real game tab (verified end-to-end with a
|
||||
+ // setTimeout-based fps>0 loop first, since rAF is unobservable in a
|
||||
+ // headless/backgrounded automation tab).
|
||||
+ emscripten_set_main_loop(EmscriptenMainLoopTrampoline, 0, 0);
|
||||
+ return 1;
|
||||
+}
|
||||
+
|
||||
+EMSCRIPTEN_KEEPALIVE
|
||||
+void vpinball_wasm_stop()
|
||||
+{
|
||||
+ if (g_wasmPlayer != nullptr)
|
||||
+ g_wasmPlayer->SetCloseState(Player::CS_STOP_PLAY);
|
||||
+}
|
||||
+
|
||||
+EMSCRIPTEN_KEEPALIVE
|
||||
+void vpinball_wasm_dispose()
|
||||
+{
|
||||
+ if (g_wasmPlayer == nullptr)
|
||||
+ return;
|
||||
+ emscripten_cancel_main_loop();
|
||||
+ delete g_wasmPlayer;
|
||||
+ g_wasmPlayer = nullptr;
|
||||
+}
|
||||
+
|
||||
+} // extern "C"
|
||||
+
|
||||
+#endif // __EMSCRIPTEN__
|
||||
diff --git a/src/core/player.cpp b/src/core/player.cpp
|
||||
index e83af84..60c7d5f 100644
|
||||
--- a/src/core/player.cpp
|
||||
+++ b/src/core/player.cpp
|
||||
@@ -1992,6 +1992,32 @@ void Player::GPUQueueStuffingGameLoop()
|
||||
}
|
||||
}
|
||||
|
||||
+#ifdef __EMSCRIPTEN__
|
||||
+bool Player::EmscriptenStepFrame()
|
||||
+{
|
||||
+ // vpinball-wasm: single-step version of GPUQueueStuffingGameLoop(), driven
|
||||
+ // once per browser animation frame by emscripten_set_main_loop instead of
|
||||
+ // a blocking while loop. Manual framerate throttling (uSleep) is removed
|
||||
+ // entirely - the browser's own requestAnimationFrame cadence paces us.
|
||||
+ if (GetCloseState() != CS_PLAYING && GetCloseState() != CS_USER_INPUT)
|
||||
+ return false;
|
||||
+
|
||||
+ UpdateGameLogic();
|
||||
+ PrepareFrame();
|
||||
+ UpdateGameLogic();
|
||||
+ SubmitFrame();
|
||||
+ UpdateGameLogic();
|
||||
+
|
||||
+ m_renderProfiler->EnterProfileSection(FrameProfiler::PROFILE_RENDER_FLIP);
|
||||
+ m_renderer->m_renderDevice->Flip();
|
||||
+ m_renderProfiler->ExitProfileSection();
|
||||
+
|
||||
+ FinishFrame();
|
||||
+
|
||||
+ return true;
|
||||
+}
|
||||
+#endif
|
||||
+
|
||||
void Player::FramePacingGameLoop()
|
||||
{
|
||||
// The main loop tries to perform a constant input/physics cycle at a 1ms pace while feeding the GPU command queue at a stable rate, without multithreading.
|
||||
diff --git a/src/core/player.h b/src/core/player.h
|
||||
index 6e9755a..a069cf7 100644
|
||||
--- a/src/core/player.h
|
||||
+++ b/src/core/player.h
|
||||
@@ -115,6 +115,14 @@ public:
|
||||
|
||||
void ProcessOSMessages(const bool isInitialized = true);
|
||||
|
||||
+#ifdef __EMSCRIPTEN__
|
||||
+ // vpinball-wasm: single-step frame function driven by emscripten_set_main_loop
|
||||
+ // instead of a blocking while loop (see GameLoop()). Returns false once the
|
||||
+ // player wants to quit; the caller must then stop calling this and release
|
||||
+ // the Player (running its normal destructor teardown).
|
||||
+ bool EmscriptenStepFrame();
|
||||
+#endif
|
||||
+
|
||||
private:
|
||||
VideoSyncMode m_videoSyncMode = VideoSyncMode::VSM_FRAME_PACING;
|
||||
float m_maxFramerate = 0.f; // targeted refresh rate in Hz, if larger refresh rate it will limit FPS by uSleep() //!! currently does not work adaptively as it would require IDirect3DDevice9Ex which is not supported on WinXP
|
||||
+9
-1
@@ -44,9 +44,17 @@ EMSCRIPTEN_LINK_FLAGS=(
|
||||
-sENVIRONMENT=web
|
||||
-sEXPORTED_RUNTIME_METHODS=FS,ccall,cwrap
|
||||
-sFORCE_FILESYSTEM=1
|
||||
-sEXPORTED_FUNCTIONS=_main,_vpinball_wasm_start,_vpinball_wasm_stop,_vpinball_wasm_dispose
|
||||
-sEXIT_RUNTIME=0
|
||||
--use-preload-cache
|
||||
)
|
||||
if [ "$BUILD_TYPE" = "Debug" ]; then
|
||||
EMSCRIPTEN_LINK_FLAGS+=(-sASSERTIONS=1 -sEXIT_RUNTIME=1)
|
||||
EMSCRIPTEN_LINK_FLAGS+=(-sASSERTIONS=1 -O0)
|
||||
else
|
||||
# CMAKE_BUILD_TYPE=Release only optimizes the compile step; emcc's link
|
||||
# step needs its own -O level to actually run wasm-opt/dead-code
|
||||
# elimination and (with --closure) minify the JS glue.
|
||||
EMSCRIPTEN_LINK_FLAGS+=(-O2 --closure 1)
|
||||
fi
|
||||
|
||||
emcmake cmake \
|
||||
|
||||
Reference in New Issue
Block a user