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:
@@ -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
|
||||
Reference in New Issue
Block a user