Files
vpinball-wasm/patches/pinmame/0004-emscripten-cooperative-scheduling.patch
T

425 lines
12 KiB
Diff
Raw Normal View History

diff --git a/src/cpuexec.c b/src/cpuexec.c
index ca7401f..0bfcbe7 100644
--- a/src/cpuexec.c
+++ b/src/cpuexec.c
@@ -466,6 +466,65 @@ 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)
+{
+ 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
+
/*************************************
@@ -943,6 +1002,34 @@ void time_fence_exit()
}
}
+#elif defined(__EMSCRIPTEN__)
+
+// No cross-thread wait primitive is used here: this build is single-threaded
+// (see vpinball-wasm's README on why pthreads/SharedArrayBuffer are out of
+// scope), and time_fence is purely an *optional* external-clock-sync feature
+// (mirrored by Controller.TimeFence in controller.vbs) - reporting it as
+// unsupported just means the emulator paces itself on its own internal
+// timing instead of syncing to the host's clock, which is what every other
+// platform this library runs on outside of this fence do anyway.
+int time_fence_is_supported()
+{
+ return 0;
+}
+
+void time_fence_post()
+{
+}
+
+int time_fence_wait(double secs)
+{
+ (void)secs;
+ return 0;
+}
+
+void time_fence_exit()
+{
+}
+
#else
#include <semaphore.h>
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
+
/*-------------------------------------------------