diff --git a/src/cpuexec.c b/src/cpuexec.c index 07db6a1..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 + /************************************* 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 + /*-------------------------------------------------