diff --git a/CHANGES.md b/CHANGES.md index ee70123441de..ac81d8ba49da 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,6 +12,7 @@ - CLOUDSYNC: Enable WebDAV support for Android - CLOUDSYNC: Speed up cloudsync on Apple - DATABASE: Improve multidisk game scanning +- EMSCRIPTEN: Support core switching - EMSCRIPTEN: Support suspend screensaver - EMSCRIPTEN/RWEBCAM: Fix camera driver - EMSCRIPTEN/RWEBINPUT: Add accelerometer/gyroscope support diff --git a/Makefile.emscripten b/Makefile.emscripten index d6d3751b8cad..61bef8f596d6 100644 --- a/Makefile.emscripten +++ b/Makefile.emscripten @@ -161,7 +161,7 @@ ifeq ($(HAVE_SDL2), 1) endif LDFLAGS := -L. --no-heap-copy -s STACK_SIZE=$(STACK_SIZE) -s INITIAL_MEMORY=$(INITIAL_HEAP) \ - -s EXPORTED_RUNTIME_METHODS=$(EXPORTS) \ + -s EXPORTED_RUNTIME_METHODS=$(EXPORTS) -s EXIT_RUNTIME=1 \ -s ALLOW_MEMORY_GROWTH=1 -s EXPORTED_FUNCTIONS="$(EXPORTED_FUNCTIONS)" \ -s MODULARIZE=1 -s EXPORT_ES6=1 -s EXPORT_NAME="libretro_$(subst -,_,$(LIBRETRO))" \ -s DISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR=0 \ diff --git a/audio/drivers/audioworklet.c b/audio/drivers/audioworklet.c index 9b6a26f132cf..41846a566ca1 100644 --- a/audio/drivers/audioworklet.c +++ b/audio/drivers/audioworklet.c @@ -431,6 +431,19 @@ bool audioworklet_external_block(void) } #endif +/* called on program exit */ +void audioworklet_close(void) +{ + audioworklet_data_t *audioworklet = audioworklet_static_data; + + if (!audioworklet) + return; + + MAIN_THREAD_EM_ASM({ + emscriptenGetAudioObject($0).close(); + }, audioworklet->context); +} + static bool audioworklet_stop(void *data) { audioworklet_data_t *audioworklet = (audioworklet_data_t*)data; diff --git a/command.c b/command.c index 0ba94dc42c49..f99f6816feee 100644 --- a/command.c +++ b/command.c @@ -66,6 +66,7 @@ #include "verbosity.h" #include "version.h" #include "version_git.h" +#include "tasks/task_content.h" #define CMD_BUF_SIZE 4096 @@ -902,6 +903,14 @@ bool command_version(command_t *cmd, const char* arg) return true; } +bool command_load_core(command_t *cmd, const char* arg) +{ + content_ctx_info_t content_info = {0}; + task_push_load_new_core(arg, NULL, + &content_info, CORE_TYPE_PLAIN, NULL, NULL); + return true; +} + static const rarch_memory_descriptor_t* command_memory_get_descriptor(const rarch_memory_map_t* mmap, unsigned address, size_t* offset) { const rarch_memory_descriptor_t* desc = mmap->descriptors; diff --git a/command.h b/command.h index 6f4564acb4b6..ed02dea53006 100644 --- a/command.h +++ b/command.h @@ -429,6 +429,7 @@ bool command_write_ram(command_t *cmd, const char *arg); #endif bool command_read_memory(command_t *cmd, const char *arg); bool command_write_memory(command_t *cmd, const char *arg); +bool command_load_core(command_t *cmd, const char* arg); static const struct cmd_action_map action_map[] = { #if defined(HAVE_CG) || defined(HAVE_GLSL) || defined(HAVE_SLANG) || defined(HAVE_HLSL) @@ -452,6 +453,8 @@ static const struct cmd_action_map action_map[] = { { "SAVE_FILES", command_save_savefiles, "No argument"}, { "LOAD_FILES", command_load_savefiles, "No argument"}, + + { "LOAD_CORE", command_load_core, ""}, }; static const struct cmd_map map[] = { diff --git a/emscripten/library_platform_emscripten.js b/emscripten/library_platform_emscripten.js index 4edfb92a0c5c..73ff84645269 100644 --- a/emscripten/library_platform_emscripten.js +++ b/emscripten/library_platform_emscripten.js @@ -4,12 +4,21 @@ var LibraryPlatformEmscripten = { $RPE: { canvasWidth: 0, canvasHeight: 0, + battery: null, + observer: null, + memoryUsageTimeout: null, sentinelPromise: null, command_queue: [], command_reply_queue: [] }, - PlatformEmscriptenWatchCanvasSizeAndDpr__deps: ["platform_emscripten_update_canvas_dimensions_cb"], + $PlatformEmscriptenOnWindowResize: function() { + RPE.observer.unobserve(Module.canvas); + RPE.observer.observe(Module.canvas); + }, + + PlatformEmscriptenWatchCanvasSizeAndDpr__deps: ["platform_emscripten_update_canvas_dimensions_cb", "$PlatformEmscriptenOnWindowResize"], + PlatformEmscriptenWatchCanvasSizeAndDpr__proxy: "sync", PlatformEmscriptenWatchCanvasSizeAndDpr: function(dpr) { if (RPE.observer) { RPE.observer.unobserve(Module.canvas); @@ -34,31 +43,49 @@ var LibraryPlatformEmscripten = { _platform_emscripten_update_canvas_dimensions_cb(width, height, dpr); }); RPE.observer.observe(Module.canvas); - window.addEventListener("resize", function() { - RPE.observer.unobserve(Module.canvas); - RPE.observer.observe(Module.canvas); - }, false); + window.addEventListener("resize", PlatformEmscriptenOnWindowResize, false); + }, + + $PlatformEmscriptenOnCanvasPointerDown: function() { + Module.canvas.focus(); }, - PlatformEmscriptenWatchWindowVisibility__deps: ["platform_emscripten_update_window_hidden_cb"], + $PlatformEmscriptenOnCanvasContextMenu: function(e) { + e.preventDefault(); + }, + + PlatformEmscriptenCanvasListenersInit__deps: ["$PlatformEmscriptenOnCanvasPointerDown", "$PlatformEmscriptenOnCanvasContextMenu"], + PlatformEmscriptenCanvasListenersInit__proxy: "sync", + PlatformEmscriptenCanvasListenersInit: function() { + Module.canvas.addEventListener("pointerdown", PlatformEmscriptenOnCanvasPointerDown, false); + Module.canvas.addEventListener("contextmenu", PlatformEmscriptenOnCanvasContextMenu, false); + }, + + $PlatformEmscriptenOnVisibilityChange__deps: ["platform_emscripten_update_window_hidden_cb"], + $PlatformEmscriptenOnVisibilityChange: function() { + _platform_emscripten_update_window_hidden_cb(document.visibilityState == "hidden"); + }, + + PlatformEmscriptenWatchWindowVisibility__deps: ["$PlatformEmscriptenOnVisibilityChange"], + PlatformEmscriptenWatchWindowVisibility__proxy: "sync", PlatformEmscriptenWatchWindowVisibility: function() { - document.addEventListener("visibilitychange", function() { - _platform_emscripten_update_window_hidden_cb(document.visibilityState == "hidden"); - }, false); + document.addEventListener("visibilitychange", PlatformEmscriptenOnVisibilityChange, false); }, - $PlatformEmscriptenPowerStateChange__deps: ["platform_emscripten_update_power_state_cb"], - $PlatformEmscriptenPowerStateChange: function(e) { + $PlatformEmscriptenOnPowerStateChange__deps: ["platform_emscripten_update_power_state_cb"], + $PlatformEmscriptenOnPowerStateChange: function(e) { _platform_emscripten_update_power_state_cb(true, Number.isFinite(e.target.dischargingTime) ? e.target.dischargingTime : 0x7FFFFFFF, e.target.level, e.target.charging); }, - PlatformEmscriptenPowerStateInit__deps: ["$PlatformEmscriptenPowerStateChange"], + PlatformEmscriptenPowerStateInit__deps: ["$PlatformEmscriptenOnPowerStateChange"], + PlatformEmscriptenPowerStateInit__proxy: "sync", PlatformEmscriptenPowerStateInit: function() { if (!navigator.getBattery) return; navigator.getBattery().then(function(battery) { - battery.addEventListener("chargingchange", PlatformEmscriptenPowerStateChange); - battery.addEventListener("levelchange", PlatformEmscriptenPowerStateChange); - PlatformEmscriptenPowerStateChange({target: battery}); + RPE.battery = battery; + battery.addEventListener("chargingchange", PlatformEmscriptenOnPowerStateChange); + battery.addEventListener("levelchange", PlatformEmscriptenOnPowerStateChange); + PlatformEmscriptenOnPowerStateChange({target: battery}); }); }, @@ -66,7 +93,7 @@ var LibraryPlatformEmscripten = { $PlatformEmscriptenUpdateMemoryUsage: function() { // unfortunately this will be inaccurate in threaded (worker) builds _platform_emscripten_update_memory_usage_cb(BigInt(performance.memory.usedJSHeapSize || 0), BigInt(performance.memory.jsHeapSizeLimit || 0)); - setTimeout(PlatformEmscriptenUpdateMemoryUsage, 5000); + RPE.memoryUsageTimeout = setTimeout(PlatformEmscriptenUpdateMemoryUsage, 5000); }, PlatformEmscriptenMemoryUsageInit__deps: ["$PlatformEmscriptenUpdateMemoryUsage"], @@ -75,22 +102,33 @@ var LibraryPlatformEmscripten = { PlatformEmscriptenUpdateMemoryUsage(); }, - PlatformEmscriptenWatchFullscreen__deps: ["platform_emscripten_update_fullscreen_state_cb"], + $PlatformEmscriptenOnFullscreenChange__deps: ["platform_emscripten_update_fullscreen_state_cb"], + $PlatformEmscriptenOnFullscreenChange: function() { + _platform_emscripten_update_fullscreen_state_cb(!!document.fullscreenElement); + }, + + PlatformEmscriptenWatchFullscreen__deps: ["$PlatformEmscriptenOnFullscreenChange"], + PlatformEmscriptenWatchFullscreen__proxy: "sync", PlatformEmscriptenWatchFullscreen: function() { - document.addEventListener("fullscreenchange", function() { - _platform_emscripten_update_fullscreen_state_cb(!!document.fullscreenElement); - }, false); + document.addEventListener("fullscreenchange", PlatformEmscriptenOnFullscreenChange, false); }, - PlatformEmscriptenGLContextEventInit__deps: ["platform_emscripten_gl_context_lost_cb", "platform_emscripten_gl_context_restored_cb"], + $PlatformEmscriptenOnGLContextLost__deps: ["platform_emscripten_gl_context_lost_cb"], + $PlatformEmscriptenOnGLContextLost: function(e) { + e.preventDefault(); + _platform_emscripten_gl_context_lost_cb(); + }, + + $PlatformEmscriptenOnGLContextRestored__deps: ["platform_emscripten_gl_context_restored_cb"], + $PlatformEmscriptenOnGLContextRestored: function() { + _platform_emscripten_gl_context_restored_cb(); + }, + + PlatformEmscriptenGLContextEventInit__deps: ["$PlatformEmscriptenOnGLContextLost", "$PlatformEmscriptenOnGLContextRestored"], + PlatformEmscriptenGLContextEventInit__proxy: "sync", PlatformEmscriptenGLContextEventInit: function() { - Module.canvas.addEventListener("webglcontextlost", function(e) { - e.preventDefault(); - _platform_emscripten_gl_context_lost_cb(); - }); - Module.canvas.addEventListener("webglcontextrestored", function() { - _platform_emscripten_gl_context_restored_cb(); - }); + Module.canvas.addEventListener("webglcontextlost", PlatformEmscriptenOnGLContextLost); + Module.canvas.addEventListener("webglcontextrestored", PlatformEmscriptenOnGLContextRestored); }, $PlatformEmscriptenDoSetCanvasSize: async function(width, height) { @@ -156,6 +194,34 @@ var LibraryPlatformEmscripten = { $EmscriptenReceiveCommandReply: function() { return RPE.command_reply_queue.shift(); + }, + + $PlatformEmscriptenFreeBrowser__proxy: "sync", + $PlatformEmscriptenFreeBrowser__deps: ["$PlatformEmscriptenDoSetWakeLock", "$PlatformEmscriptenOnCanvasPointerDown", "$PlatformEmscriptenOnCanvasContextMenu", "$PlatformEmscriptenOnWindowResize", "$PlatformEmscriptenOnVisibilityChange", "$PlatformEmscriptenOnFullscreenChange", "$PlatformEmscriptenOnPowerStateChange"], + $PlatformEmscriptenFreeBrowser: function() { + if (RPE.memoryUsageTimeout) clearTimeout(RPE.memoryUsageTimeout); + PlatformEmscriptenDoSetWakeLock(false); + if (RPE.observer) { + RPE.observer.unobserve(Module.canvas); + RPE.observer = null; + } + Module.canvas.removeEventListener("pointerdown", PlatformEmscriptenOnCanvasPointerDown); + Module.canvas.removeEventListener("contextmenu", PlatformEmscriptenOnCanvasContextMenu); + window.removeEventListener("resize", PlatformEmscriptenOnWindowResize); + document.removeEventListener("visibilitychange", PlatformEmscriptenOnVisibilityChange); + document.removeEventListener("fullscreenchange", PlatformEmscriptenOnFullscreenChange); + if (RPE.battery) { + RPE.battery.removeEventListener("chargingchange", PlatformEmscriptenOnPowerStateChange); + RPE.battery.removeEventListener("levelchange", PlatformEmscriptenOnPowerStateChange); + RPE.battery = null; + } + }, + + PlatformEmscriptenFree__deps: ["$PlatformEmscriptenFreeBrowser"], + PlatformEmscriptenFree: function() { + Module.canvas.removeEventListener("webglcontextlost", PlatformEmscriptenOnGLContextLost); + Module.canvas.removeEventListener("webglcontextrestored", PlatformEmscriptenOnGLContextRestored); + PlatformEmscriptenFreeBrowser(); } }; diff --git a/frontend/drivers/platform_emscripten.c b/frontend/drivers/platform_emscripten.c index 392918690f78..d54fbcf6fd77 100644 --- a/frontend/drivers/platform_emscripten.c +++ b/frontend/drivers/platform_emscripten.c @@ -76,6 +76,7 @@ void emscripten_mainloop(void); /* javascript library functions */ void PlatformEmscriptenWatchCanvasSizeAndDpr(double *dpr); +void PlatformEmscriptenCanvasListenersInit(void); void PlatformEmscriptenWatchWindowVisibility(void); void PlatformEmscriptenPowerStateInit(void); void PlatformEmscriptenMemoryUsageInit(void); @@ -84,6 +85,7 @@ void PlatformEmscriptenGLContextEventInit(void); void PlatformEmscriptenSetCanvasSize(int width, int height); void PlatformEmscriptenSetWakeLock(bool state); uint32_t PlatformEmscriptenGetSystemInfo(void); +void PlatformEmscriptenFree(void); typedef struct { @@ -95,8 +97,10 @@ typedef struct uint64_t memory_used; uint64_t memory_limit; double device_pixel_ratio; + double device_pixel_ratio_temp; enum platform_emscripten_browser browser; enum platform_emscripten_os os; + enum frontend_fork fork_mode; int raf_interval; int canvas_width; int canvas_height; @@ -383,7 +387,7 @@ size_t platform_emscripten_command_read(char **into, size_t max_len) var next_command = RPE.command_queue.shift(); var length = lengthBytesUTF8(next_command); if (length > $2) { - console.error("[CMD] Command too long, skipping", next_command); + err("[CMD] Command too long, skipping", next_command); return 0; } stringToUTF8(next_command, $1, $2); @@ -552,6 +556,11 @@ static void frontend_emscripten_get_env(int *argc, char *argv[], char bundle_path[PATH_MAX]; const char *home = getenv("HOME"); + /* Try to set core library path so the frontend knows what core is currently loaded. + * It's not an issue if left unspecified, but turning off the "Always Reload Core on + * Run Content" option will only work if the frontend knows the current core. */ + path_set(RARCH_PATH_CORE, getenv("LIBRARY_PATH")); + if (home) { size_t _len = strlcpy(base_path, home, sizeof(base_path)); @@ -686,6 +695,51 @@ static uint64_t frontend_emscripten_get_free_mem(void) return (PLATFORM_GETVAL(u64, &emscripten_platform_data->memory_limit) - used); } +#ifdef HAVE_AUDIOWORKLET +void audioworklet_close(void); +#endif + +static void frontend_emscripten_exec_browser(void *path) +{ + const char *core = emscripten_platform_data->fork_mode == FRONTEND_FORK_NONE ? 0 : path; + const char *content = emscripten_platform_data->fork_mode == FRONTEND_FORK_CORE_WITH_ARGS ? path_get(RARCH_PATH_CONTENT) : 0; + +#ifdef HAVE_AUDIOWORKLET + audioworklet_close(); +#endif + + EM_ASM({ +#ifdef PROXY_TO_PTHREAD + /* undo OffscreenCanvas */ + let newCanvas = Module.canvas.cloneNode(); + Module.canvas.replaceWith(newCanvas); + Module.canvas = newCanvas; +#endif + if (typeof Module.retroArchExit == "function") + setTimeout(Module.retroArchExit, 0, $0 && UTF8ToString($0), $1 && UTF8ToString($1)); + else + out("[INFO] Exiting, but Module.retroArchExit was not provided"); + }, core, content); + emscripten_force_exit(0); +} + +static void frontend_emscripten_exec(const char *path, bool should_load_content) +{ + PlatformEmscriptenFree(); + platform_emscripten_run_on_browser_thread_sync(frontend_emscripten_exec_browser, (void *)path); +} + +static void frontend_emscripten_exitspawn(char *s, size_t len, char *args) +{ + frontend_emscripten_exec(s, false); +} + +static bool frontend_emscripten_set_fork(enum frontend_fork fork_mode) +{ + emscripten_platform_data->fork_mode = fork_mode; + return true; +} + /* program entry and startup */ #ifdef HAVE_EXTRA_WASMFS @@ -890,14 +944,6 @@ int main(int argc, char *argv[]) if (!Module.canvas.getAttribute("tabindex")) Module.canvas.setAttribute("tabindex", "-1"); Module.canvas.focus(); - Module.canvas.addEventListener("pointerdown", function() { - Module.canvas.focus(); - }, false); - - /* disable browser right click menu */ - Module.canvas.addEventListener("contextmenu", function(e) { - e.preventDefault(); - }, false); /* background should be black */ Module.canvas.style.backgroundColor = "#000000"; @@ -908,7 +954,7 @@ int main(int argc, char *argv[]) /* ensure canvas size is constrained by CSS, otherwise infinite resizing may occur */ if (window.getComputedStyle(Module.canvas).display == "inline") { - console.warn("[WARN] Canvas should not use display: inline!"); + err("[WARN] Canvas should not use display: inline!"); Module.canvas.style.display = "inline-block"; } var oldWidth = Module.canvas.clientWidth; @@ -916,13 +962,14 @@ int main(int argc, char *argv[]) Module.canvas.width = 64; Module.canvas.height = 64; if (oldWidth != Module.canvas.clientWidth || oldHeight != Module.canvas.clientHeight) { - console.warn("[WARN] Canvas size should be set using CSS properties!"); + err("[WARN] Canvas size should be set using CSS properties!"); Module.canvas.style.width = oldWidth + "px"; Module.canvas.style.height = oldHeight + "px"; } }); - PlatformEmscriptenWatchCanvasSizeAndDpr(malloc(sizeof(double))); + PlatformEmscriptenWatchCanvasSizeAndDpr(&emscripten_platform_data->device_pixel_ratio_temp); + PlatformEmscriptenCanvasListenersInit(); PlatformEmscriptenWatchWindowVisibility(); PlatformEmscriptenPowerStateInit(); PlatformEmscriptenMemoryUsageInit(); @@ -956,10 +1003,10 @@ frontend_ctx_driver_t frontend_ctx_emscripten = { frontend_emscripten_get_env, /* environment_get */ NULL, /* init */ NULL, /* deinit */ - NULL, /* exitspawn */ + frontend_emscripten_exitspawn, /* exitspawn */ NULL, /* process_args */ - NULL, /* exec */ - NULL, /* set_fork */ + frontend_emscripten_exec, /* exec */ + frontend_emscripten_set_fork, /* set_fork */ NULL, /* shutdown */ NULL, /* get_name */ NULL, /* get_os */ diff --git a/frontend/frontend_driver.c b/frontend/frontend_driver.c index 433f7bcac9ba..654638579601 100644 --- a/frontend/frontend_driver.c +++ b/frontend/frontend_driver.c @@ -212,6 +212,9 @@ size_t frontend_driver_get_core_extension(char *s, size_t len) if (envIsHomebrew()) return strlcpy(s, "3dsx", len); return strlcpy(s, "cia", len); +#elif defined(EMSCRIPTEN) + /* may not contain the core */ + return strlcpy(s, "core", len); #else return 0; #endif diff --git a/pkg/emscripten/libretro-thread/core_list.js b/pkg/emscripten/libretro-thread/core_list.js new file mode 100644 index 000000000000..a691eff5ceb6 --- /dev/null +++ b/pkg/emscripten/libretro-thread/core_list.js @@ -0,0 +1,91 @@ +const libretroCores = { + "2048": "2048", + "anarch": "Anarch", + "ardens": "Arduboy (Ardens)", + "arduous": "Arduboy (Arduous)", + "bk": "Elektronika - BK-0010/BK-0011 (BK)", + "chailove": "ChaiLove", + "craft": "Minecraft (Craft)", + "DoubleCherryGB": "Nintendo - Game Boy / Color (DoubleCherryGB)", + "ecwolf": "Wolfenstein 3D (ECWolf)", + "fbalpha2012": "Arcade (FB Alpha 2012)", + "fbalpha2012_cps1": "Arcade (FB Alpha 2012 CPS1)", + "fbalpha2012_cps2": "Arcade (FB Alpha 2012 CPS2)", + "fbalpha2012_neogeo": "Arcade (FB Alpha 2012 NeoGeo)", + "fceumm": "Nintendo - NES / Famicom (FCEUmm)", + "freechaf": "Fairchild ChannelF (FreeChaF)", + "galaksija": "Galaksija", + "gambatte": "Nintendo - Game Boy / Color (Gambatte)", + "gme": "Game Music Emu", + "gearboy": "Nintendo - Game Boy / Color (GearBoy)", + "gearcoleco": "Coleco - ColecoVision (GearColeco)", + "gearsystem": "Sega - MS/GG/SG-1000 (GearSystem)", + "genesis_plus_gx": "Sega - MS/GG/MD/CD (Genesis Plus GX)", + "genesis_plus_gx_wide": "Sega - MS/GG/MD/CD (Genesis Plus GX Wide)", + "gong": "Gong", + "gw": "Handheld Electronic (GW)", + "handy": "Atari - Lynx (Handy)", + "jaxe": "CHIP-8/S-CHIP/XO-CHIP (JAXE)", + "jumpnbump": "Jump 'n Bump", + "lowresnx": "LowResNX", + "lutro": "Lua Engine (Lutro)", + "m2000": "Philips - P2000T (M2000)", + "mame2000": "Arcade - MAME 2000", + "mame2003": "Arcade - MAME 2003", + "mame2003_plus": "Arcade - MAME 2003-Plus", + "mednafen_lynx": "Atari - Lynx (Beetle Lynx)", + "mednafen_ngp": "SNK - Neo Geo Pocket / Color (Beetle Neo Geo Pop)", + "mednafen_pce_fast": "NEC - PC Engine / CD (Beetle PC Engine Fast)", + "mednafen_vb": "Nintendo - Virtual Boy (Beetle VB)", + "mednafen_wswan": "Bandai - WonderSwan/Color (Beetle WonderSwan)", + "mgba": "Nintendo - Game Boy Advance (mGBA)", + "minivmac": "Mac II (MiniVmac)", + "mu": "Palm OS(Mu)", + "mrboom": "Bomberman (Mr.Boom)", + "neocd": "SNK - Neo Geo CD (NeoCD)", + "nestopia": "Nintendo - NES / Famicom (Nestopia)", + "numero": "Texas Instruments TI-83 (Numero)", + "nxengine": "Cave Story (NX Engine)", + "o2em": "Magnavox - Odyssey2 / Philips Videopac+ (O2EM)", + "opera": "The 3DO Company - 3DO (Opera)", + "pcsx_rearmed": "Sony - PlayStation (PCSX ReARMed)", + "picodrive": "Sega - MS/GG/MD/CD/32X (PicoDrive)", + "pocketcdg": "PocketCDG", + "prboom": "Doom (PrBoom)", + "quasi88": "NEC - PC-8000 / PC-8800 series (QUASI88)", + "quicknes": "Nintendo - NES / Famicom (QuickNES)", + "retro8": "PICO-8 (Retro8)", + "scummvm": "ScummVM", + "snes9x2002": "Nintendo - SNES / SFC (Snes9x 2002)", + "snes9x2005": "Nintendo - SNES / SFC (Snes9x 2005)", + "snes9x2010": "Nintendo - SNES / SFC (Snes9x 2010)", + "snes9x": "Nintendo - SNES / SFC (Snes9x)", + "squirreljme": "Java ME (SquirrelJME)", + "tamalibretro": "Bandai - Tamagothci P1 (TamaLIBretro)", + "tgbdual": "Nintendo - Game Boy / Color (TGB Dual)", + "theodore": "Theodore (Thomson TO8/TO9)", + "tic80": "TIC-80", + "tyrquake": "Quake (TyrQuake)", + "uw8": "MicroW8 (UW8)", + "uzem": "Uzebox (Uzem)", + "vaporspec": "Vaporspec", + "vba_next": "Nintendo - Game Boy Advance (VBA Next)", + "vecx": "GCE - Vectrex (Vecx)", + "vice_x64": "Commodore - C64 (VICE x64, fast)", + "vice_x64sc": "Commodore - C64 (VICE x64sc, accurate)", + "vice_x128": "Commodore - C128 (VICE x128)", + "vice_xcbm2": "Commodore - CBM-II 6x0/7x0 (VICE xcbm2)", + "vice_xcbm5x0": "Commodore - CBM-II 5x0 (xcbm5x0)", + "vice_xpet": "Commodore - PET (VICE xpet)", + "vice_xplus4": "Commodore - PLUS/4 (VICE xplus4)", + "vice_xscpu64": "Commodore - C64 SuperCPU (VICE xscpu4)", + "vice_xvic": "Commodore - VIC-20 (VICE xvic)", + "virtualxt": "VirtualXT", + "vitaquake2": "Quake II (vitaQuake 2)", + "vitaquake2-rogue": "Quake II - Ground Zero (vitaQuake2 (rogue))", + "vitaquake2-xatrix": "Quake II - The Reckoning (vitaQuake2 (xatrix))", + "vitaquake2-zaero": "Quake II - Zaero (vitaQuake2 (zaero))", + "wasm4": "WASM4", + "x1": "Sharp X1 (X Millenium)", + "xrick": "Rick Dangerous (XRick)" +}; diff --git a/pkg/emscripten/libretro-thread/index.html b/pkg/emscripten/libretro-thread/index.html index 07856601df93..29180a0e19cd 100644 --- a/pkg/emscripten/libretro-thread/index.html +++ b/pkg/emscripten/libretro-thread/index.html @@ -26,97 +26,7 @@ Core Selection  - +
  •   Run @@ -197,6 +107,7 @@

      Quick Menu

    + diff --git a/pkg/emscripten/libretro-thread/libretro.js b/pkg/emscripten/libretro-thread/libretro.js index 7ca538b7eced..066b4b884e4a 100644 --- a/pkg/emscripten/libretro-thread/libretro.js +++ b/pkg/emscripten/libretro-thread/libretro.js @@ -4,7 +4,11 @@ * This provides the basic JavaScript for the RetroArch web player. */ -const canvas = document.getElementById("canvas"); +const defaultCore = "gambatte"; +var autoStart = true; + +let currentCore; +let canvas = document.getElementById("canvas"); const webplayerPreview = document.getElementById("webplayer-preview"); const menuBar = document.getElementById("navbar"); const menuHider = document.getElementById("menuhider"); @@ -64,7 +68,10 @@ const disableKeys = { 123: "F12" }; +let Module; let fsLoadPromise; +let reloadTimeout; +let retroArchRunning = false; // all methods provided by the worker that we may require const workerHandlers = {FS: ["init", "writeFile", "readFile", "mkdirTree", "readdir", "readdirTree", "rm", "stat"], helper: ["loadFS", "zipDirs"]}; @@ -151,11 +158,18 @@ function openModal(which) { modalClose.addEventListener("click", function() { modalContainer.style.display = "none"; + setTimeout(function() { + canvas.focus(); + }, 0); }); -var Module = { +function modulePreRun(module) { + module.ENV["OPFS_MOUNT"] = "/home/web_user"; + module.ENV["LIBRARY_PATH"] = module.corePath; +} + +const ModuleBase = { noInitialRun: true, - arguments: ["-v", "--menu"], noImageDecoding: true, noAudioDecoding: true, @@ -165,11 +179,9 @@ var Module = { retroArchRecv: function() { return this.EmscriptenReceiveCommandReply(); }, - preRun: [ - function(module) { - module.ENV["OPFS_MOUNT"] = "/home/web_user"; - } - ], + retroArchExit: function(core, content) { + relaunch(core, content); + }, locateFile: function(path, prefix) { if (path.endsWith(".js")) return typeof this.mainScriptUrlOrBlob == "string" ? this.mainScriptUrlOrBlob : URL.createObjectURL(this.mainScriptUrlOrBlob); return path; @@ -183,11 +195,7 @@ var Module = { printErr: function(text) { console.log("stderr:", text); }, - canvas: canvas, - totalDependencies: 0, - monitorRunDependencies: function(left) { - this.totalDependencies = Math.max(this.totalDependencies, left); - } + canvas: canvas }; // read File object to an ArrayBuffer @@ -301,7 +309,7 @@ function appIsSmallScreen() { // used for the menu hider function adjustMenuHeight() { const actualMenuHeight = menuHider.checked ? 0 : 65; - document.body.style.setProperty("--actualmenuheight", actualMenuHeight + "px", "important") + document.body.style.setProperty("--actualmenuheight", actualMenuHeight + "px", "important"); } function startRetroArch() { @@ -328,10 +336,15 @@ function startRetroArch() { // refocus the canvas so that keyboard events work menuBar.addEventListener("pointerdown", function() { setTimeout(function() { - Module.canvas.focus(); + canvas.focus(); }, 0); }, false); + // subsequent relaunches will start automatically + ModuleBase.noInitialRun = false; + ModuleBase.onRuntimeInitialized = null; + + retroArchRunning = true; Module.callMain(Module.arguments); } @@ -340,39 +353,107 @@ async function appInitialized() { console.log("WASM runtime initialized"); await fsLoadPromise; console.log("FS initialized"); + + // ensure the current core exists even if it's not in the core list + await FS.writeFile("/retroarch/cores/" + currentCore + "_libretro.core", new Uint8Array()); setProgress("main"); setProgressText("main"); icnRun.classList.remove("fa-spinner", "fa-spin"); icnRun.classList.add("fa-play"); - // Make the Preview image clickable to start RetroArch. - webplayerPreview.classList.add("loaded"); - webplayerPreview.addEventListener("click", function() { - startRetroArch(); - }); - btnRun.classList.remove("disabled"); - btnRun.addEventListener("click", function() { + + if (autoStart) { startRetroArch(); - }); + } else { + // Make the Preview image clickable to start RetroArch. + webplayerPreview.classList.add("loaded"); + webplayerPreview.addEventListener("click", function() { + startRetroArch(); + }); + btnRun.classList.remove("disabled"); + btnRun.addEventListener("click", function() { + startRetroArch(); + }); + } } async function downloadScript(src) { let resp = await fetch(src); + if (resp.status >= 400) throw "HTTP " + resp.status; let blob = await resp.blob(); return blob; } -async function loadCore(core) { +function loadCoreFallback(currentCore) { + URL.revokeObjectURL(ModuleBase.mainScriptUrlOrBlob); + if (currentCore == defaultCore) { + alert("Error: could not load default core!"); + return; + } + loadCore(defaultCore); +} + +function loadCoreFromUrl(url, core, args) { + ModuleBase.arguments = args || ["-v", "--menu"]; + ModuleBase.preRun = [modulePreRun]; + ModuleBase.mainScriptUrlOrBlob = url; + ModuleBase.canvas = canvas; + ModuleBase.corePath = "/home/web_user/retroarch/cores/" + core + "_libretro.core"; + import(url).then(script => { + script.default(Object.assign({}, ModuleBase)).then(mod => { + Module = mod; + }).catch(err => { + console.error("Couldn't instantiate module", err); + loadCoreFallback(core); + throw err; + }); + }).catch(err => { + console.error("Couldn't load script", err); + loadCoreFallback(core); + throw err; + }); +} + +function loadCore(core, args) { // Make the core the selected core in the UI. - const coreTitle = document.querySelector('#core-selector a[data-core="' + core + '"]')?.textContent; + const coreTitle = libretroCores[core]; if (coreTitle) coreSelectorCurrent.textContent = coreTitle; const fileExt = (core == "retroarch") ? ".js" : "_libretro.js"; - const url = URL.createObjectURL(await downloadScript("./" + core + fileExt)); - Module.mainScriptUrlOrBlob = url; - import(url).then(script => { - script.default(Module).then(mod => { - Module = mod; - }).catch(err => { console.error("Couldn't instantiate module", err); throw err; }); - }).catch(err => { console.error("Couldn't load script", err); throw err; }); + downloadScript("./" + core + fileExt).then(blob => { + loadCoreFromUrl(URL.createObjectURL(blob), core, args); + }).catch(err => { + console.error("Couldn't download script", err); + loadCoreFallback(core); + throw err; + }); +} + +// exit/exitspawn hook +function relaunch(core, content) { + // get the new canvas element + canvas = Module.canvas; + + // force restart on exit + if (!core) core = ModuleBase.corePath; + + if (!content) content = "--menu"; + + Module = null; + if (reloadTimeout) { + clearTimeout(reloadTimeout); + reloadTimeout = null; + } + + // parse core name from full path ("/retroarch/cores/NAME_libretro.core") + currentCore = core.slice(0, -14).split("/").slice(-1)[0]; + + // don't download the core again when restarting + if (core == ModuleBase.corePath) { + loadCoreFromUrl(ModuleBase.mainScriptUrlOrBlob, currentCore, ["-v", content]); + } else { + localStorage.setItem("core", currentCore); + URL.revokeObjectURL(ModuleBase.mainScriptUrlOrBlob); + loadCore(currentCore, ["-v", content]); + } } // When the browser has loaded everything. @@ -387,6 +468,17 @@ document.addEventListener("DOMContentLoaded", async function() { if (!coreSelector.parentElement.contains(e.target)) dropdownBox.checked = false; }); + // create core list + var coreArray = Object.entries(libretroCores); + var coreNames = Object.values(libretroCores).sort(); + for (let name of coreNames) { + let a = document.createElement("a"); + a.href = "."; + a.dataset.core = coreArray.find(i => i[1] == name)[0]; + a.textContent = name; + coreSelector.appendChild(a); + } + // disable default right click action canvas.addEventListener("contextmenu", function(e) { e.preventDefault(); @@ -421,11 +513,25 @@ document.addEventListener("DOMContentLoaded", async function() { // Switch the core when selecting one. coreSelector.addEventListener("click", function(e) { - const coreChoice = e.target.dataset?.core; - if (coreChoice) localStorage.setItem("core", coreChoice); + e.preventDefault(); + const core = e.target.dataset?.core; + if (!core) return; + dropdownBox.checked = false; + localStorage.setItem("core", core); + if (Module && retroArchRunning) { + Module.retroArchSend("LOAD_CORE /home/web_user/retroarch/cores/" + core + "_libretro.core"); + + // maybe RetroArch crashed? reload if RetroArch doesn't exit within a second. + if (reloadTimeout) clearTimeout(reloadTimeout); + reloadTimeout = setTimeout(function() { + location.reload(); + }, 1000); + } else { + location.reload(); + } }); // Find which core to load. - const core = localStorage.getItem("core") || "gambatte"; - loadCore(core); + currentCore = localStorage.getItem("core") || defaultCore; + loadCore(currentCore); }); diff --git a/pkg/emscripten/libretro-thread/libretro.worker.js b/pkg/emscripten/libretro-thread/libretro.worker.js index 881ed8f7059c..1c80bd5f431c 100644 --- a/pkg/emscripten/libretro-thread/libretro.worker.js +++ b/pkg/emscripten/libretro-thread/libretro.worker.js @@ -160,6 +160,20 @@ FS.stat = async function(path) { } } +/* create fake core files for RetroArch */ + +async function listInstalledCores() { + loadScripts("core_list.js"); + const cores = Object.keys(libretroCores); + const coreFiles = (await FS.readdir("/retroarch/cores") || []).map(i => i.slice(0, -14)); // remove "_libretro.core" + for (let core of cores) { + if (!coreFiles.includes(core)) await FS.writeFile("/retroarch/cores/" + core + "_libretro.core", new Uint8Array()); + } + for (let core of coreFiles) { + if (!cores.includes(core)) await FS.rm("/retroarch/cores/" + core + "_libretro.core"); + } +} + /* data migration */ function idbExists(dbName, objStoreName) { @@ -350,6 +364,7 @@ async function tryLoadBundle() { /* helper functions */ helper.loadFS = async function() { + await listInstalledCores(); await tryMigrateFromIdbfs(); await tryLoadBundle(); } diff --git a/pkg/emscripten/libretro/core_list.js b/pkg/emscripten/libretro/core_list.js new file mode 100644 index 000000000000..a691eff5ceb6 --- /dev/null +++ b/pkg/emscripten/libretro/core_list.js @@ -0,0 +1,91 @@ +const libretroCores = { + "2048": "2048", + "anarch": "Anarch", + "ardens": "Arduboy (Ardens)", + "arduous": "Arduboy (Arduous)", + "bk": "Elektronika - BK-0010/BK-0011 (BK)", + "chailove": "ChaiLove", + "craft": "Minecraft (Craft)", + "DoubleCherryGB": "Nintendo - Game Boy / Color (DoubleCherryGB)", + "ecwolf": "Wolfenstein 3D (ECWolf)", + "fbalpha2012": "Arcade (FB Alpha 2012)", + "fbalpha2012_cps1": "Arcade (FB Alpha 2012 CPS1)", + "fbalpha2012_cps2": "Arcade (FB Alpha 2012 CPS2)", + "fbalpha2012_neogeo": "Arcade (FB Alpha 2012 NeoGeo)", + "fceumm": "Nintendo - NES / Famicom (FCEUmm)", + "freechaf": "Fairchild ChannelF (FreeChaF)", + "galaksija": "Galaksija", + "gambatte": "Nintendo - Game Boy / Color (Gambatte)", + "gme": "Game Music Emu", + "gearboy": "Nintendo - Game Boy / Color (GearBoy)", + "gearcoleco": "Coleco - ColecoVision (GearColeco)", + "gearsystem": "Sega - MS/GG/SG-1000 (GearSystem)", + "genesis_plus_gx": "Sega - MS/GG/MD/CD (Genesis Plus GX)", + "genesis_plus_gx_wide": "Sega - MS/GG/MD/CD (Genesis Plus GX Wide)", + "gong": "Gong", + "gw": "Handheld Electronic (GW)", + "handy": "Atari - Lynx (Handy)", + "jaxe": "CHIP-8/S-CHIP/XO-CHIP (JAXE)", + "jumpnbump": "Jump 'n Bump", + "lowresnx": "LowResNX", + "lutro": "Lua Engine (Lutro)", + "m2000": "Philips - P2000T (M2000)", + "mame2000": "Arcade - MAME 2000", + "mame2003": "Arcade - MAME 2003", + "mame2003_plus": "Arcade - MAME 2003-Plus", + "mednafen_lynx": "Atari - Lynx (Beetle Lynx)", + "mednafen_ngp": "SNK - Neo Geo Pocket / Color (Beetle Neo Geo Pop)", + "mednafen_pce_fast": "NEC - PC Engine / CD (Beetle PC Engine Fast)", + "mednafen_vb": "Nintendo - Virtual Boy (Beetle VB)", + "mednafen_wswan": "Bandai - WonderSwan/Color (Beetle WonderSwan)", + "mgba": "Nintendo - Game Boy Advance (mGBA)", + "minivmac": "Mac II (MiniVmac)", + "mu": "Palm OS(Mu)", + "mrboom": "Bomberman (Mr.Boom)", + "neocd": "SNK - Neo Geo CD (NeoCD)", + "nestopia": "Nintendo - NES / Famicom (Nestopia)", + "numero": "Texas Instruments TI-83 (Numero)", + "nxengine": "Cave Story (NX Engine)", + "o2em": "Magnavox - Odyssey2 / Philips Videopac+ (O2EM)", + "opera": "The 3DO Company - 3DO (Opera)", + "pcsx_rearmed": "Sony - PlayStation (PCSX ReARMed)", + "picodrive": "Sega - MS/GG/MD/CD/32X (PicoDrive)", + "pocketcdg": "PocketCDG", + "prboom": "Doom (PrBoom)", + "quasi88": "NEC - PC-8000 / PC-8800 series (QUASI88)", + "quicknes": "Nintendo - NES / Famicom (QuickNES)", + "retro8": "PICO-8 (Retro8)", + "scummvm": "ScummVM", + "snes9x2002": "Nintendo - SNES / SFC (Snes9x 2002)", + "snes9x2005": "Nintendo - SNES / SFC (Snes9x 2005)", + "snes9x2010": "Nintendo - SNES / SFC (Snes9x 2010)", + "snes9x": "Nintendo - SNES / SFC (Snes9x)", + "squirreljme": "Java ME (SquirrelJME)", + "tamalibretro": "Bandai - Tamagothci P1 (TamaLIBretro)", + "tgbdual": "Nintendo - Game Boy / Color (TGB Dual)", + "theodore": "Theodore (Thomson TO8/TO9)", + "tic80": "TIC-80", + "tyrquake": "Quake (TyrQuake)", + "uw8": "MicroW8 (UW8)", + "uzem": "Uzebox (Uzem)", + "vaporspec": "Vaporspec", + "vba_next": "Nintendo - Game Boy Advance (VBA Next)", + "vecx": "GCE - Vectrex (Vecx)", + "vice_x64": "Commodore - C64 (VICE x64, fast)", + "vice_x64sc": "Commodore - C64 (VICE x64sc, accurate)", + "vice_x128": "Commodore - C128 (VICE x128)", + "vice_xcbm2": "Commodore - CBM-II 6x0/7x0 (VICE xcbm2)", + "vice_xcbm5x0": "Commodore - CBM-II 5x0 (xcbm5x0)", + "vice_xpet": "Commodore - PET (VICE xpet)", + "vice_xplus4": "Commodore - PLUS/4 (VICE xplus4)", + "vice_xscpu64": "Commodore - C64 SuperCPU (VICE xscpu4)", + "vice_xvic": "Commodore - VIC-20 (VICE xvic)", + "virtualxt": "VirtualXT", + "vitaquake2": "Quake II (vitaQuake 2)", + "vitaquake2-rogue": "Quake II - Ground Zero (vitaQuake2 (rogue))", + "vitaquake2-xatrix": "Quake II - The Reckoning (vitaQuake2 (xatrix))", + "vitaquake2-zaero": "Quake II - Zaero (vitaQuake2 (zaero))", + "wasm4": "WASM4", + "x1": "Sharp X1 (X Millenium)", + "xrick": "Rick Dangerous (XRick)" +}; diff --git a/pkg/emscripten/libretro/index.html b/pkg/emscripten/libretro/index.html index 07e6803a1d61..b7157316faab 100644 --- a/pkg/emscripten/libretro/index.html +++ b/pkg/emscripten/libretro/index.html @@ -5,12 +5,12 @@ RetroArch Web Player - + - + - - + + @@ -24,97 +24,7 @@