Files
vpinball-wasm/patches/vpinball/0004-game-loop-rewrite.patch
T

195 lines
6.6 KiB
Diff
Raw Normal View History

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