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* 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..6e20009 --- /dev/null +++ b/src/core/EmscriptenBridge.cpp @@ -0,0 +1,93 @@ +// 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 + +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; +} + +// Runs arbitrary VBScript against the running table's script interpreter, +// via the same ScriptInterpreter::Evaluate() entry point the interpreter's +// own debug console uses. Useful both as a debug/console hook for consumers +// and, e.g., to exercise script-driven APIs (like the DMD pixel API) against +// a table that doesn't itself call them, without needing to author a new +// .vpx table. +EMSCRIPTEN_KEEPALIVE +int vpinball_wasm_eval_script(const char* script) +{ + if (g_wasmPlayer == nullptr || g_wasmPlayer->m_scriptInterpreter == nullptr) + return 0; + g_wasmPlayer->m_scriptInterpreter->Evaluate(script, false); + return 1; +} + +} // 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