195 lines
6.6 KiB
Diff
195 lines
6.6 KiB
Diff
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
|