core.vbs auto-wires Controller.Pause = True into every table's Paused
event, which Player::OnFocusChanged() fires on focus loss. PinMAME's
own pause handling in usrintrf.c is a "while (g_fPause) { ...;
draw_screen(); ... }" busy-wait written for a real-OS-thread host,
where a separate thread later flips g_fPause back to 0. Under our
single-threaded Emscripten cooperative-scheduling model there is no
second thread to clear it, so entering that loop spins forever at
100% CPU with the tab completely unresponsive.
cpu_run_emscripten_step() now checks g_fPause up front and skips
cpu_timeslice() (and therefore that loop) entirely while paused;
emulation simply doesn't advance until Controller.Pause is set back
to False and the step function is called again next frame.
Confirmed fixed by hands-on testing: unfocusing the browser during a
PinMAME ROM session no longer freezes the tab.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
403 lines
11 KiB
Diff
403 lines
11 KiB
Diff
diff --git a/src/cpuexec.c b/src/cpuexec.c
|
|
index 07db6a1..19da31a 100644
|
|
--- a/src/cpuexec.c
|
|
+++ b/src/cpuexec.c
|
|
@@ -466,6 +466,78 @@ void cpu_run(void)
|
|
#endif
|
|
}
|
|
|
|
+#if defined(LIBPINMAME) && defined(__EMSCRIPTEN__)
|
|
+/* Emscripten is single-threaded, so cpu_run()'s own blocking "while
|
|
+ (!time_to_quit) { ... }" loop above can't run on a real OS thread the way
|
|
+ it does everywhere else libpinmame runs (see PinmameRun() in
|
|
+ libpinmame.cpp). These three entry points split that same loop, using the
|
|
+ exact same globals/logic, into a one-shot init, a step callable once per
|
|
+ host video frame (bounded to roughly one frame of emulated time via
|
|
+ timer_get_time(), the same clock cpu_timeslice()'s own time_fence logic
|
|
+ above already uses), and a one-shot teardown - so the host's own
|
|
+ per-frame callback can drive it cooperatively instead. */
|
|
+
|
|
+void cpu_run_emscripten_init(void)
|
|
+{
|
|
+ time_to_quit = 0;
|
|
+ cpu_pre_run();
|
|
+ time_to_reset = 0;
|
|
+ time_fence_global_offset = -options.time_fence;
|
|
+}
|
|
+
|
|
+/* Returns 1 if the emulation is still running (call again next frame), or 0
|
|
+ once it has fully quit (cpu_post_run() has already been invoked). */
|
|
+int cpu_run_emscripten_step(void)
|
|
+{
|
|
+ /* usrintrf.c's per-frame video update contains a "while (g_fPause) {
|
|
+ ...; draw_screen(); ... }" busy-wait (see the VPINMAME/LIBPINMAME
|
|
+ branch around updatescreen()) that assumes a real OS thread will
|
|
+ later flip g_fPause back to 0 from outside it. We're single-threaded
|
|
+ here, so entering that loop at all would spin forever burning 100%
|
|
+ CPU with no way out - nothing else can ever run to clear the flag.
|
|
+ Skip cpu_timeslice() (and therefore that loop) entirely while paused;
|
|
+ the emulation simply doesn't advance until Controller.Pause is set
|
|
+ back to False and this function is called again next frame. */
|
|
+ extern int g_fPause;
|
|
+ if (g_fPause)
|
|
+ return 1;
|
|
+
|
|
+ const double frame_target = timer_get_time() + (1.0 / 60.0);
|
|
+
|
|
+ while (!time_to_quit && !time_to_reset && timer_get_time() < frame_target)
|
|
+ {
|
|
+ profiler_mark(PROFILER_EXTRA);
|
|
+
|
|
+ if (loadsave_schedule != LOADSAVE_NONE)
|
|
+ handle_loadsave();
|
|
+
|
|
+ cpu_timeslice();
|
|
+
|
|
+ extern int libpinmame_time_to_quit(void);
|
|
+ if (libpinmame_time_to_quit())
|
|
+ time_to_quit = 1;
|
|
+
|
|
+ profiler_mark(PROFILER_END);
|
|
+ }
|
|
+
|
|
+ if (time_to_reset && !time_to_quit)
|
|
+ {
|
|
+ cpu_post_run();
|
|
+ cpu_pre_run();
|
|
+ time_to_reset = 0;
|
|
+ time_fence_global_offset = -options.time_fence;
|
|
+ }
|
|
+
|
|
+ if (time_to_quit)
|
|
+ {
|
|
+ cpu_post_run();
|
|
+ return 0;
|
|
+ }
|
|
+
|
|
+ return 1;
|
|
+}
|
|
+#endif
|
|
+
|
|
|
|
|
|
/*************************************
|
|
diff --git a/src/libpinmame/libpinmame.cpp b/src/libpinmame/libpinmame.cpp
|
|
index 6966d77..755d762 100644
|
|
--- a/src/libpinmame/libpinmame.cpp
|
|
+++ b/src/libpinmame/libpinmame.cpp
|
|
@@ -1104,11 +1104,52 @@ PINMAMEAPI PINMAME_STATUS PinmameRun(const char* const p_name)
|
|
|
|
vp_init();
|
|
|
|
+#ifdef __EMSCRIPTEN__
|
|
+ // No real OS thread: Emscripten builds here are single-threaded (see
|
|
+ // vpinball-wasm's README on why pthreads/SharedArrayBuffer are out of
|
|
+ // scope), so run_game()'s own blocking call chain - which StartGame()
|
|
+ // otherwise runs on _p_gameThread - has been split in mame.c/cpuexec.c
|
|
+ // into a one-shot init (called synchronously right here) plus a step
|
|
+ // the host calls once per video frame via PinmameEmscriptenStep().
|
|
+ memset(_mechInit, 0, sizeof(_mechInit));
|
|
+ memset(_mechInfo, 0, sizeof(_mechInfo));
|
|
+
|
|
+ extern int run_game_emscripten_init(int game);
|
|
+ if (run_game_emscripten_init(gameNum) != 0)
|
|
+ {
|
|
+ OnStateChange(0);
|
|
+ return PINMAME_STATUS_GAME_NOT_FOUND;
|
|
+ }
|
|
+
|
|
+ OnStateChange(1);
|
|
+#else
|
|
_p_gameThread = new std::thread(StartGame, gameNum);
|
|
+#endif
|
|
|
|
return PINMAME_STATUS_OK;
|
|
}
|
|
|
|
+#ifdef __EMSCRIPTEN__
|
|
+/******************************************************
|
|
+ * PinmameEmscriptenStep
|
|
+ *
|
|
+ * Must be called once per host video frame (e.g. from the same
|
|
+ * requestAnimationFrame-driven callback that steps vpinball's own frame)
|
|
+ * while the emulator is running - see PinmameRun's __EMSCRIPTEN__ branch
|
|
+ * above for why this exists instead of a real thread.
|
|
+ ******************************************************/
|
|
+
|
|
+PINMAMEAPI void PinmameEmscriptenStep(void)
|
|
+{
|
|
+ if (!_isRunning)
|
|
+ return;
|
|
+
|
|
+ extern int run_game_emscripten_step(void);
|
|
+ if (!run_game_emscripten_step())
|
|
+ OnStateChange(0);
|
|
+}
|
|
+#endif
|
|
+
|
|
/******************************************************
|
|
* PinmameIsRunning
|
|
******************************************************/
|
|
@@ -1161,6 +1202,19 @@ PINMAMEAPI int PinmameIsPaused()
|
|
|
|
PINMAMEAPI void PinmameStop()
|
|
{
|
|
+#ifdef __EMSCRIPTEN__
|
|
+ // No game thread to join here (see PinmameRun's __EMSCRIPTEN__ branch) -
|
|
+ // just request the stop. The next PinmameEmscriptenStep() call notices
|
|
+ // libpinmame_time_to_quit() via cpu_run_emscripten_step(), runs the same
|
|
+ // teardown chain a real quit would, and calls OnStateChange(0) itself.
|
|
+ if (_isRunning)
|
|
+ {
|
|
+ g_fPause = 0;
|
|
+ _timeToQuit = 1;
|
|
+ }
|
|
+ return;
|
|
+#endif
|
|
+
|
|
if (!_p_gameThread) {
|
|
if (_isRunning) {
|
|
libpinmame_log_error("PinmameStop(): run state is %d but game thread handle is null; forcing stopped state.", _isRunning);
|
|
diff --git a/src/libpinmame/libpinmame.h b/src/libpinmame/libpinmame.h
|
|
index dc604df..d3aacc1 100644
|
|
--- a/src/libpinmame/libpinmame.h
|
|
+++ b/src/libpinmame/libpinmame.h
|
|
@@ -453,6 +453,11 @@ PINMAMEAPI PINMAME_STATUS PinmamePause(const int pause);
|
|
PINMAMEAPI int PinmameIsPaused();
|
|
PINMAMEAPI PINMAME_STATUS PinmameReset();
|
|
PINMAMEAPI void PinmameStop();
|
|
+#ifdef __EMSCRIPTEN__
|
|
+// Must be called once per host video frame while running - see PinmameRun's
|
|
+// __EMSCRIPTEN__ branch in libpinmame.cpp for why.
|
|
+PINMAMEAPI void PinmameEmscriptenStep(void);
|
|
+#endif
|
|
PINMAMEAPI PINMAME_HARDWARE_GEN PinmameGetHardwareGen();
|
|
PINMAMEAPI int PinmameGetSwitch(const int swNo);
|
|
PINMAMEAPI void PinmameSetSwitch(const int swNo, const int state);
|
|
diff --git a/src/mame.c b/src/mame.c
|
|
index d0aea1a..2f8c4d5 100644
|
|
--- a/src/mame.c
|
|
+++ b/src/mame.c
|
|
@@ -229,6 +229,16 @@ static void shutdown_machine(void);
|
|
static int run_machine(void);
|
|
static void run_machine_core(void);
|
|
|
|
+#if defined(LIBPINMAME) && defined(__EMSCRIPTEN__)
|
|
+/* See cpu_run_emscripten_init's comment in cpuexec.c for why these exist. */
|
|
+void cpu_run_emscripten_init(void);
|
|
+int cpu_run_emscripten_step(void);
|
|
+void run_machine_core_emscripten_init(void);
|
|
+int run_machine_core_emscripten_step(void);
|
|
+int run_machine_emscripten_init(void);
|
|
+int run_machine_emscripten_step(void);
|
|
+#endif
|
|
+
|
|
#ifdef MAME_DEBUG
|
|
static int validitychecks(void);
|
|
#endif
|
|
@@ -355,6 +365,71 @@ int run_game(int game)
|
|
return err;
|
|
}
|
|
|
|
+#if defined(LIBPINMAME) && defined(__EMSCRIPTEN__)
|
|
+/* Mirrors run_game() above, split around run_machine() the same way that's
|
|
+ split around run_machine_core(). This is the top of the whole init/step/
|
|
+ teardown chain PinmameRun()/PinmameEmscriptenStep() in libpinmame.cpp
|
|
+ drive - see cpu_run_emscripten_init's comment in cpuexec.c for why the
|
|
+ chain exists at all. Returns 0 on success, non-zero on failure (matching
|
|
+ run_game()'s own convention) - a failure here (e.g. ROM not found) means
|
|
+ no teardown call is needed, since nothing that needs undoing succeeded. */
|
|
+int run_game_emscripten_init(int game)
|
|
+{
|
|
+ begin_resource_tracking();
|
|
+
|
|
+ memset(Machine, 0, sizeof(*Machine));
|
|
+ Machine->gamedrv = gamedrv = drivers[game];
|
|
+ expand_machine_driver(gamedrv->drv, &internal_drv);
|
|
+ Machine->drv = &internal_drv;
|
|
+
|
|
+ if (init_game_options())
|
|
+ return 1;
|
|
+
|
|
+ cpu_loadsave_reset();
|
|
+ bailing = 0;
|
|
+
|
|
+ if (osd_init())
|
|
+ {
|
|
+ bail_and_print("Unable to initialize system");
|
|
+ return 1;
|
|
+ }
|
|
+
|
|
+ begin_resource_tracking();
|
|
+
|
|
+ if (init_machine())
|
|
+ {
|
|
+ bail_and_print("Unable to initialize machine emulation");
|
|
+ end_resource_tracking();
|
|
+ osd_exit();
|
|
+ return 1;
|
|
+ }
|
|
+
|
|
+ if (run_machine_emscripten_init())
|
|
+ {
|
|
+ bail_and_print("Unable to start machine emulation");
|
|
+ shutdown_machine();
|
|
+ end_resource_tracking();
|
|
+ osd_exit();
|
|
+ return 1;
|
|
+ }
|
|
+
|
|
+ return 0;
|
|
+}
|
|
+
|
|
+/* Returns 1 while still running, 0 once fully torn down. */
|
|
+int run_game_emscripten_step(void)
|
|
+{
|
|
+ if (run_machine_emscripten_step())
|
|
+ return 1;
|
|
+
|
|
+ shutdown_machine();
|
|
+ end_resource_tracking();
|
|
+ osd_exit();
|
|
+ end_resource_tracking();
|
|
+ return 0;
|
|
+}
|
|
+#endif
|
|
+
|
|
|
|
|
|
/*-------------------------------------------------
|
|
@@ -547,6 +622,71 @@ static int run_machine(void)
|
|
return res;
|
|
}
|
|
|
|
+#if defined(LIBPINMAME) && defined(__EMSCRIPTEN__)
|
|
+/* Mirrors run_machine() above, split around run_machine_core() the same way
|
|
+ that's split around cpu_run(). Returns 0 on success (matching
|
|
+ run_machine()'s own convention), non-zero on failure. */
|
|
+int run_machine_emscripten_init(void)
|
|
+{
|
|
+ if (vh_open())
|
|
+ {
|
|
+ bail_and_print("Unable to start video emulation");
|
|
+ return 1;
|
|
+ }
|
|
+
|
|
+ tilemap_init();
|
|
+
|
|
+ if (Machine->drv->video_start && (*Machine->drv->video_start)())
|
|
+ {
|
|
+ bail_and_print("Unable to start video emulation");
|
|
+ tilemap_close();
|
|
+ vh_close();
|
|
+ return 1;
|
|
+ }
|
|
+
|
|
+ if (sound_start())
|
|
+ {
|
|
+ bail_and_print("Unable to start audio emulation");
|
|
+ if (Machine->drv->video_stop)
|
|
+ (*Machine->drv->video_stop)();
|
|
+ tilemap_close();
|
|
+ vh_close();
|
|
+ return 1;
|
|
+ }
|
|
+
|
|
+ {
|
|
+ int region;
|
|
+ /* free memory regions allocated with REGIONFLAG_DISPOSE (typically gfx roms) */
|
|
+ for (region = 0; region < MAX_MEMORY_REGIONS; region++)
|
|
+ if (Machine->memory_region[region].flags & ROMREGION_DISPOSE)
|
|
+ {
|
|
+ size_t i;
|
|
+ for (i = 0; i < memory_region_length(region); i++)
|
|
+ memory_region(region)[i] = rand();
|
|
+ free(Machine->memory_region[region].base);
|
|
+ Machine->memory_region[region].base = 0;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ run_machine_core_emscripten_init();
|
|
+ return 0;
|
|
+}
|
|
+
|
|
+/* Returns 1 while still running, 0 once fully torn down. */
|
|
+int run_machine_emscripten_step(void)
|
|
+{
|
|
+ if (run_machine_core_emscripten_step())
|
|
+ return 1;
|
|
+
|
|
+ sound_stop();
|
|
+ if (Machine->drv->video_stop)
|
|
+ (*Machine->drv->video_stop)();
|
|
+ tilemap_close();
|
|
+ vh_close();
|
|
+ return 0;
|
|
+}
|
|
+#endif
|
|
+
|
|
|
|
|
|
/*-------------------------------------------------
|
|
@@ -620,6 +760,60 @@ void run_machine_core(void)
|
|
}
|
|
}
|
|
|
|
+#if defined(LIBPINMAME) && defined(__EMSCRIPTEN__)
|
|
+/* Mirrors run_machine_core() above (same globals, same calls), split around
|
|
+ cpu_run() - see cpu_run_emscripten_init/_step/PinmameRun's own comment for
|
|
+ why. The disclaimer/gamewarnings/gameinfo splash screens run_machine_core()
|
|
+ shows natively are skipped entirely here: gamewarnings is already excluded
|
|
+ for LIBPINMAME builds above, and disclaimer/gameinfo are native-UI-only
|
|
+ concerns with nothing meaningful to display in an embedded/headless
|
|
+ context like this one. */
|
|
+void run_machine_core_emscripten_init(void)
|
|
+{
|
|
+ artwork_enable(0);
|
|
+ init_user_interface();
|
|
+ artwork_enable(1);
|
|
+
|
|
+ if (!gamedrv->rom)
|
|
+ options.cheat = 0;
|
|
+ if (options.cheat)
|
|
+ InitCheat();
|
|
+
|
|
+ if (Machine->drv->nvram_handler)
|
|
+ {
|
|
+ mame_file *nvram_file = mame_fopen(Machine->gamedrv->name, 0, FILETYPE_NVRAM, 0);
|
|
+ (*Machine->drv->nvram_handler)(nvram_file, 0);
|
|
+ if (nvram_file)
|
|
+ mame_fclose(nvram_file);
|
|
+ }
|
|
+
|
|
+ cpu_run_emscripten_init();
|
|
+}
|
|
+
|
|
+/* Returns 1 while still running, 0 once cpu_run's own teardown has run. */
|
|
+int run_machine_core_emscripten_step(void)
|
|
+{
|
|
+ if (cpu_run_emscripten_step())
|
|
+ return 1;
|
|
+
|
|
+ if (Machine->drv->nvram_handler)
|
|
+ {
|
|
+ mame_file *nvram_file = mame_fopen(Machine->gamedrv->name, 0, FILETYPE_NVRAM, 1);
|
|
+ if (nvram_file != NULL)
|
|
+ {
|
|
+ (*Machine->drv->nvram_handler)(nvram_file, 1);
|
|
+ mame_fclose(nvram_file);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (options.cheat)
|
|
+ StopCheat();
|
|
+
|
|
+ save_input_port_settings();
|
|
+ return 0;
|
|
+}
|
|
+#endif
|
|
+
|
|
|
|
|
|
/*-------------------------------------------------
|