diff --git a/CHANGES.md b/CHANGES.md index a9ad792ab4..1cb87c7e44 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,8 @@ # Future +- EMSCRIPTEN: Support suspend screensaver +- EMSCRIPTEN/RWEBCAM: Fix camera driver +- EMSCRIPTEN/RWEBINPUT: Add accelerometer/gyroscope support +- EMSCRIPTEN/RWEBPAD: Add rumble support # 1.21.0 - 3DS: Fix unique IDs for newer cores diff --git a/Makefile.emscripten b/Makefile.emscripten index 0d44cb2580..7f3fc7b437 100644 --- a/Makefile.emscripten +++ b/Makefile.emscripten @@ -11,7 +11,7 @@ TARGET_BASE := $(subst .js,,$(TARGET)) OS = Emscripten OBJ := -DEFINES := -DRARCH_INTERNAL -DHAVE_MAIN -DEMSCRIPTEN -DNO_CANVAS_RESIZE +DEFINES := -DRARCH_INTERNAL -DHAVE_MAIN -DEMSCRIPTEN DEFINES += -DHAVE_FILTERS_BUILTIN -DHAVE_ONLINE_UPDATER -DHAVE_UPDATE_ASSETS -DHAVE_UPDATE_CORE_INFO HAVE_PATCH = 1 @@ -106,6 +106,9 @@ ASYNC ?= 0 LTO ?= 0 PTHREAD_POOL_SIZE ?= 4 +ASYNCIFY_ADD ?= dynCall_*,emscripten_mainloop +ASYNCIFY_REMOVE ?= threaded_worker + STACK_SIZE ?= 4194304 INITIAL_HEAP ?= 134217728 @@ -123,7 +126,7 @@ INITIAL_HEAP ?= 134217728 OBJDIR := obj-emscripten -EXPORTED_FUNCTIONS = _main,_malloc,_free,_cmd_savefiles,_cmd_save_state,_cmd_load_state,_cmd_undo_save_state,_cmd_undo_load_state,_cmd_take_screenshot,\ +EXPORTED_FUNCTIONS = _main,_malloc,_free,_cmd_savefiles,_cmd_save_state,_cmd_load_state,_cmd_undo_save_state,_cmd_undo_load_state,_cmd_toggle_fullscreen,_cmd_take_screenshot,\ _cmd_toggle_menu,_cmd_reload_config,_cmd_toggle_grab_mouse,_cmd_toggle_game_focus,_cmd_reset,_cmd_toggle_pause,_cmd_pause,_cmd_unpause,\ _cmd_set_volume,_cmd_set_shader,_cmd_cheat_set_code,_cmd_cheat_get_code,_cmd_cheat_toggle_index,_cmd_cheat_get_code_state,_cmd_cheat_realloc,\ _cmd_cheat_get_size,_cmd_cheat_apply_cheats,EmscriptenSendCommand,EmscriptenReceiveCommandReply @@ -168,7 +171,7 @@ LDFLAGS := -L. --no-heap-copy -s STACK_SIZE=$(STACK_SIZE) -s INITIAL_MEMORY=$(IN -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 \ - -s ENVIRONMENT=web,worker \ + -s ENVIRONMENT=web,worker -s WASM_BIGINT=1 \ --extern-pre-js emscripten/pre.js \ --js-library emscripten/library_rwebcam.js \ --js-library emscripten/library_platform_emscripten.js @@ -240,7 +243,7 @@ ifeq ($(HAVE_THREADS), 1) endif ifeq ($(WASM_WORKERS), 1) - LDFLAGS += -s WASM_WORKERS=2 + LDFLAGS += -s WASM_WORKERS=1 endif ifeq ($(ASYNC), 1) @@ -251,7 +254,7 @@ ifeq ($(ASYNC), 1) endif else ifeq ($(MIN_ASYNC), 1) DEFINES += -DEMSCRIPTEN_ASYNCIFY -DEMSCRIPTEN_MIN_ASYNCIFY - LDFLAGS += -s ASYNCIFY=1 -s ASYNCIFY_STACK_SIZE=8192 -s ASYNCIFY_IGNORE_INDIRECT=1 -s ASYNCIFY_ADD='dynCall_*,emscripten_mainloop' -s ASYNCIFY_REMOVE='threaded_worker' + LDFLAGS += -s ASYNCIFY=1 -s ASYNCIFY_STACK_SIZE=8192 -s ASYNCIFY_IGNORE_INDIRECT=1 -s ASYNCIFY_ADD='$(ASYNCIFY_ADD)' -s ASYNCIFY_REMOVE='$(ASYNCIFY_REMOVE)' ifeq ($(DEBUG), 1) LDFLAGS += -s ASYNCIFY_ADVISE #-s ASYNCIFY_DEBUG=1 endif @@ -297,21 +300,11 @@ all: $(TARGET) $(libretro_new): ; mv_libretro: - mv -f $(libretro) $(libretro_new) || true + $(Q)mv -f $(libretro) $(libretro_new) || true -# until emscripten adds something like WASM_WORKERS=2 but for audio worklets, DIY. -ifeq ($(HAVE_AUDIOWORKLET), 1) $(TARGET): $(RARCH_OBJ) $(libretro_new) mv_libretro @$(if $(Q), $(shell echo echo "LD $@ \ $(libretro_new) $(LIBS) $(LDFLAGS)"),) $(Q)$(LD) -o $@ $(RARCH_OBJ) $(libretro_new) $(LIBS) $(LDFLAGS) - $(Q)tr -d '\n' < "$(TARGET_BASE).aw.js" | sed -e "s/[\/&]/\\\\&/g" -e "s/'/\\\\\\\\&/g" > _audioworklet.js - $(Q)sed -i.bak -e "s/\"$(TARGET_BASE)\.aw\.js\"/URL.createObjectURL(new Blob(['$$(cat _audioworklet.js)'],{type:'text\/javascript'}))/" -- "$@" - $(Q)rm -f "$(TARGET_BASE).aw.js" _audioworklet.js "$@".bak -else -$(TARGET): $(RARCH_OBJ) $(libretro_new) mv_libretro - @$(if $(Q), $(shell echo echo "LD $@ \ $(libretro_new) $(LIBS) $(LDFLAGS)"),) - $(Q)$(LD) -o $@ $(RARCH_OBJ) $(libretro_new) $(LIBS) $(LDFLAGS) -endif $(OBJDIR)/%.o: %.c @mkdir -p $(dir $@) diff --git a/audio/drivers/audioworklet.c b/audio/drivers/audioworklet.c index d5425ff88a..adcaa67c96 100644 --- a/audio/drivers/audioworklet.c +++ b/audio/drivers/audioworklet.c @@ -382,6 +382,9 @@ bool audioworklet_external_block(void) { audioworklet_data_t *audioworklet = audioworklet_static_data; + if (!audioworklet) + return false; + #ifdef EMSCRIPTEN_AUDIO_FAKE_BLOCK if (!audioworklet->block_requested) return false; diff --git a/camera/drivers/rwebcam.c b/camera/drivers/rwebcam.c index 5a08e951ac..292be416e7 100644 --- a/camera/drivers/rwebcam.c +++ b/camera/drivers/rwebcam.c @@ -14,6 +14,7 @@ * If not, see . */ +#include #include #include @@ -23,7 +24,7 @@ #include "../../retroarch.h" /* forward declarations */ -void *RWebCamInit(uint64_t caps, unsigned width, unsigned height); +void *RWebCamInit(uint64_t caps, unsigned width, unsigned height, bool debug); void RWebCamFree(void *data); bool RWebCamStart(void *data); void RWebCamStop(void *data); @@ -34,7 +35,7 @@ static void *rwebcam_init(const char *device, uint64_t caps, unsigned width, unsigned height) { (void)device; - return RWebCamInit(caps, width, height); + return RWebCamInit(caps, width, height, !!getenv("RWEBCAM_DEBUG")); } static void rwebcam_free(void *data) diff --git a/config.def.h b/config.def.h index d31f27feab..0bfb35bd9b 100644 --- a/config.def.h +++ b/config.def.h @@ -1290,11 +1290,7 @@ #endif /* Pause gameplay when window loses focus. */ -#if defined(EMSCRIPTEN) -#define DEFAULT_PAUSE_NONACTIVE false -#else #define DEFAULT_PAUSE_NONACTIVE true -#endif /* Pause gameplay when controller disconnects. */ #define DEFAULT_PAUSE_ON_DISCONNECT false diff --git a/emscripten/library_platform_emscripten.js b/emscripten/library_platform_emscripten.js index 3d015a5649..4edfb92a0c 100644 --- a/emscripten/library_platform_emscripten.js +++ b/emscripten/library_platform_emscripten.js @@ -2,23 +2,14 @@ var LibraryPlatformEmscripten = { $RPE: { - powerStateChange: function(e) { - _platform_emscripten_update_power_state(true, Number.isFinite(e.target.dischargingTime) ? e.target.dischargingTime : 0x7FFFFFFF, e.target.level, e.target.charging); - }, - - updateMemoryUsage: function() { - // unfortunately this will be innacurate in threaded (worker) builds - var used = BigInt(performance.memory.usedJSHeapSize || 0); - var limit = BigInt(performance.memory.jsHeapSizeLimit || 0); - // emscripten currently only supports passing 32 bit ints, so pack it - _platform_emscripten_update_memory_usage(Number(used & 0xFFFFFFFFn), Number(used >> 32n), Number(limit & 0xFFFFFFFFn), Number(limit >> 32n)); - setTimeout(RPE.updateMemoryUsage, 5000); - }, + canvasWidth: 0, + canvasHeight: 0, + sentinelPromise: null, command_queue: [], command_reply_queue: [] }, - PlatformEmscriptenWatchCanvasSizeAndDpr__deps: ["platform_emscripten_update_canvas_dimensions"], + PlatformEmscriptenWatchCanvasSizeAndDpr__deps: ["platform_emscripten_update_canvas_dimensions_cb"], PlatformEmscriptenWatchCanvasSizeAndDpr: function(dpr) { if (RPE.observer) { RPE.observer.unobserve(Module.canvas); @@ -36,9 +27,11 @@ var LibraryPlatformEmscripten = { width = Math.round(entry.contentRect.width * window.devicePixelRatio); height = Math.round(entry.contentRect.height * window.devicePixelRatio); } + RPE.canvasWidth = width; + RPE.canvasHeight = height; // doubles are too big to pass as an argument to exported functions {{{ makeSetValue("dpr", "0", "window.devicePixelRatio", "double") }}}; - _platform_emscripten_update_canvas_dimensions(width, height, dpr); + _platform_emscripten_update_canvas_dimensions_cb(width, height, dpr); }); RPE.observer.observe(Module.canvas); window.addEventListener("resize", function() { @@ -47,27 +40,112 @@ var LibraryPlatformEmscripten = { }, false); }, - PlatformEmscriptenWatchWindowVisibility__deps: ["platform_emscripten_update_window_hidden"], + PlatformEmscriptenWatchWindowVisibility__deps: ["platform_emscripten_update_window_hidden_cb"], PlatformEmscriptenWatchWindowVisibility: function() { document.addEventListener("visibilitychange", function() { - _platform_emscripten_update_window_hidden(document.visibilityState == "hidden"); + _platform_emscripten_update_window_hidden_cb(document.visibilityState == "hidden"); }, false); }, - PlatformEmscriptenPowerStateInit__deps: ["platform_emscripten_update_power_state"], + $PlatformEmscriptenPowerStateChange__deps: ["platform_emscripten_update_power_state_cb"], + $PlatformEmscriptenPowerStateChange: 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: function() { if (!navigator.getBattery) return; navigator.getBattery().then(function(battery) { - battery.addEventListener("chargingchange", RPE.powerStateChange); - battery.addEventListener("levelchange", RPE.powerStateChange); - RPE.powerStateChange({target: battery}); + battery.addEventListener("chargingchange", PlatformEmscriptenPowerStateChange); + battery.addEventListener("levelchange", PlatformEmscriptenPowerStateChange); + PlatformEmscriptenPowerStateChange({target: battery}); }); }, - PlatformEmscriptenMemoryUsageInit__deps: ["platform_emscripten_update_memory_usage"], + $PlatformEmscriptenUpdateMemoryUsage__deps: ["platform_emscripten_update_memory_usage_cb"], + $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); + }, + + PlatformEmscriptenMemoryUsageInit__deps: ["$PlatformEmscriptenUpdateMemoryUsage"], PlatformEmscriptenMemoryUsageInit: function() { if (!performance.memory) return; - RPE.updateMemoryUsage(); + PlatformEmscriptenUpdateMemoryUsage(); + }, + + PlatformEmscriptenWatchFullscreen__deps: ["platform_emscripten_update_fullscreen_state_cb"], + PlatformEmscriptenWatchFullscreen: function() { + document.addEventListener("fullscreenchange", function() { + _platform_emscripten_update_fullscreen_state_cb(!!document.fullscreenElement); + }, false); + }, + + PlatformEmscriptenGLContextEventInit__deps: ["platform_emscripten_gl_context_lost_cb", "platform_emscripten_gl_context_restored_cb"], + 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(); + }); + }, + + $PlatformEmscriptenDoSetCanvasSize: async function(width, height) { + // window.resizeTo is terrible; it uses window.outerWidth/outerHeight dimensions, + // which are on their own scale entirely, which also changes with the zoom level independently from the DPR. + // instead try 2 hardcoded sizes first and interpolate to find the correct size. + var expAX = 600; + var expAY = 450; + var expBX = expAX + 100; + var expBY = expAY + 100; + await new Promise(r => setTimeout(r, 0)); + window.resizeTo(expAX, expAY); + await new Promise(r => setTimeout(r, 50)); + var oldWidth = RPE.canvasWidth; + var oldHeight = RPE.canvasHeight; + window.resizeTo(expBX, expBY); + await new Promise(r => setTimeout(r, 50)); + var projX = (expBX - expAX) * (width - oldWidth) / (RPE.canvasWidth - oldWidth) + expAX; + var projY = (expBY - expAY) * (height - oldHeight) / (RPE.canvasHeight - oldHeight) + expAY; + window.resizeTo(Math.round(projX), Math.round(projY)); + }, + + PlatformEmscriptenSetCanvasSize__proxy: "sync", + PlatformEmscriptenSetCanvasSize__deps: ["$PlatformEmscriptenDoSetCanvasSize"], + PlatformEmscriptenSetCanvasSize: function(width, height) { + // c-accessible library functions should not be async + PlatformEmscriptenDoSetCanvasSize(width, height); + }, + + $PlatformEmscriptenDoSetWakeLock: async function(state) { + if (state && !RPE.sentinelPromise && navigator?.wakeLock?.request) { + try { + RPE.sentinelPromise = navigator.wakeLock.request("screen"); + } catch (e) {} + } else if (!state && RPE.sentinelPromise) { + try { + var sentinel = await RPE.sentinelPromise; + sentinel.release(); + } catch (e) {} + RPE.sentinelPromise = null; + } + }, + + PlatformEmscriptenSetWakeLock__proxy: "sync", + PlatformEmscriptenSetWakeLock__deps: ["$PlatformEmscriptenDoSetWakeLock"], + PlatformEmscriptenSetWakeLock: function(state) { + PlatformEmscriptenDoSetWakeLock(state); + }, + + PlatformEmscriptenGetSystemInfo: function() { + var userAgent = navigator?.userAgent?.toLowerCase?.(); + if (!userAgent) return 0; + var browser = 1 + ["chrom", "firefox", "safari"].findIndex(i => userAgent.includes(i)); + var os = 1 + [/windows/, /linux|cros|android/, /iphone|ipad/, /mac os/].findIndex(i => i.test(userAgent)); + return browser | (os << 16); }, $EmscriptenSendCommand__deps: ["platform_emscripten_command_raise_flag"], @@ -82,4 +160,4 @@ var LibraryPlatformEmscripten = { }; autoAddDeps(LibraryPlatformEmscripten, '$RPE'); -mergeInto(LibraryManager.library, LibraryPlatformEmscripten); +addToLibrary(LibraryPlatformEmscripten); diff --git a/emscripten/library_rwebaudio.js b/emscripten/library_rwebaudio.js index dfbca1e228..e7fa3dd76f 100644 --- a/emscripten/library_rwebaudio.js +++ b/emscripten/library_rwebaudio.js @@ -168,4 +168,4 @@ var LibraryRWebAudio = { }; autoAddDeps(LibraryRWebAudio, '$RA'); -mergeInto(LibraryManager.library, LibraryRWebAudio); +addToLibrary(LibraryRWebAudio); diff --git a/emscripten/library_rwebcam.js b/emscripten/library_rwebcam.js index 2aaef6e44d..a3517dce0b 100644 --- a/emscripten/library_rwebcam.js +++ b/emscripten/library_rwebcam.js @@ -5,133 +5,273 @@ var LibraryRWebCam = { RETRO_CAMERA_BUFFER_OPENGL_TEXTURE: 0, RETRO_CAMERA_BUFFER_RAW_FRAMEBUFFER: 1, tmp: null, - contexts: [], - counter: 0, - - ready: function(data) { - return RWC.contexts[data].runMode == 2 && !RWC.contexts[data].videoElement.paused && RWC.contexts[data].videoElement.videoWidth != 0 && RWC.contexts[data].videoElement.videoHeight != 0; - } + counter: 0 }, - RWebCamInit__deps: ['malloc'], - RWebCamInit: function(caps1, caps2, width, height) { - if (!navigator) return 0; - if (!navigator.mediaDevices.getUserMedia) return 0; + $RWebCamReady: function(c) { + try { + /* try to start video it was paused */ + if (RWC.contexts[c].videoElement.paused) RWC.contexts[c].videoElement.play(); + } catch (e) {} + return RWC.contexts[c].cameraRunning && !RWC.contexts[c].videoElement.paused && RWC.contexts[c].videoElement.videoWidth != 0 && RWC.contexts[c].videoElement.videoHeight != 0; + }, + + $RWebCamInitBrowser__proxy: "sync", + $RWebCamInitBrowser: function(width, height, glTex, rawFb, debug, proxied) { + if (!navigator?.mediaDevices?.getUserMedia) return 0; var c = ++RWC.counter; + RWC.contexts[c] = {width: width, height: height, glTex: glTex, rawFb: rawFb, debug: debug, proxied: proxied, idealWidth: width, idealHeight: height}; + return c; + }, - RWC.contexts[c] = []; - RWC.contexts[c].videoElement = document.createElement("video"); - RWC.contexts[c].videoElement.classList.add("retroarchWebcamVideo"); - if (width !== 0 && height !== 0) { - RWC.contexts[c].videoElement.width = width; - RWC.contexts[c].videoElement.height = height; + RWebCamInit__deps: ["malloc", "$RWebCamInitBrowser"], + RWebCamInit: function(caps, width, height, debug) { + var glTex = Number(caps) & (1 << RWC.RETRO_CAMERA_BUFFER_OPENGL_TEXTURE); + var rawFb = Number(caps) & (1 << RWC.RETRO_CAMERA_BUFFER_RAW_FRAMEBUFFER); + var c = RWebCamInitBrowser(width, height, glTex, rawFb, debug, ENVIRONMENT_IS_PTHREAD); + if (!c) return 0; + + if (debug) console.log("RWebCamInit", c); + + if (ENVIRONMENT_IS_PTHREAD) { + RWC.contexts[c] = {}; + RWC.contexts[c].debug = debug; + RWC.contexts[c].glTex = glTex; + RWC.contexts[c].rawFb = rawFb; } - RWC.contexts[c].runMode = 1; - RWC.contexts[c].glTex = caps1 & (1 << RWC.RETRO_CAMERA_BUFFER_OPENGL_TEXTURE); - RWC.contexts[c].rawFb = caps1 & (1 << RWC.RETRO_CAMERA_BUFFER_RAW_FRAMEBUFFER); - navigator.mediaDevices.getUserMedia({video: true, audio: false}).then(function(stream) { - RWC.contexts[c].videoElement.autoplay = true; - RWC.contexts[c].videoElement.srcObject = stream; - RWC.contexts[c].runMode = 2; - }).catch(function (err) { - console.log("webcam request failed", err); - RWC.runMode = 0; - }); - - // for getting/storing texture id in GL mode + /* for getting/storing texture id in GL mode */ if (!RWC.tmp) RWC.tmp = _malloc(4); return c; }, - RWebCamFree: function(data) { - RWC.contexts[data].videoElement.pause(); - RWC.contexts[data].videoElement = null; - RWC.contexts[data] = null; + $RWebCamFreeBrowser__proxy: "sync", + $RWebCamFreeBrowser: function(c) { + RWC.contexts[c] = null; }, - RWebCamStart__deps: ['glGenTextures', 'glBindTexture', 'glGetIntegerv', 'glTexParameteri', 'malloc'], - RWebCamStart: function(data) { - var ret = 0; - if (RWC.contexts[data].glTex) { + RWebCamFree__deps: ["$RWebCamFreeBrowser", "RWebCamStop"], + RWebCamFree: function(c) { + if (RWC.contexts[c].debug) console.log("RWebCamFree", c); + if (RWC.contexts[c].running) _RWebCamStop(c); /* need more checks in RA */ + RWebCamFreeBrowser(c); + if (ENVIRONMENT_IS_PTHREAD) RWC.contexts[c] = null; + }, + + $RWebCamStartBrowser__proxy: "sync", + $RWebCamStartBrowser: function(c) { + RWC.contexts[c].videoElement = document.createElement("video"); + RWC.contexts[c].videoElement.classList.add("retroarchWebcamVideo"); + if (RWC.contexts[c].debug) document.body.appendChild(RWC.contexts[c].videoElement); + + if (RWC.contexts[c].rawFb || RWC.contexts[c].proxied) { + RWC.contexts[c].rawFbCanvas = document.createElement("canvas"); + RWC.contexts[c].rawFbCanvas.classList.add("retroarchWebcamCanvas"); + if (RWC.contexts[c].debug) document.body.appendChild(RWC.contexts[c].rawFbCanvas); + } + + var videoOpts = true; + if (RWC.contexts[c].idealWidth && RWC.contexts[c].idealHeight) { + /* save us some cropping/scaling, only honored by some browsers */ + videoOpts = {width: RWC.contexts[c].idealWidth, height: RWC.contexts[c].idealHeight, aspectRatio: RWC.contexts[c].idealWidth / RWC.contexts[c].idealHeight}; + } + + navigator.mediaDevices.getUserMedia({audio: false, video: videoOpts}).then(function(stream) { + if (!RWC.contexts[c]?.running) { + /* too late */ + for (var track of stream.getVideoTracks()) { + track.stop(); + } + return; + } + RWC.contexts[c].videoElement.autoplay = true; + RWC.contexts[c].videoElement.srcObject = stream; + RWC.contexts[c].cameraRunning = true; + }).catch(function(e) { + console.log("[rwebcam] webcam request failed", e); + }); + + RWC.contexts[c].running = true; + }, + + RWebCamStart__deps: ["glGenTextures", "glBindTexture", "glGetIntegerv", "glTexParameteri", "malloc", "$RWebCamStartBrowser"], + RWebCamStart: function(c) { + if (RWC.contexts[c].debug) console.log("RWebCamStart", c); + if (RWC.contexts[c].running) return; + if (RWC.contexts[c].glTex) { _glGenTextures(1, RWC.tmp); - RWC.contexts[data].glTexId = {{{ makeGetValue('RWC.tmp', '0', 'i32') }}}; - if (RWC.contexts[data].glTexId !== 0) { - // save previous texture + RWC.contexts[c].glTexId = {{{ makeGetValue('RWC.tmp', 0, 'i32') }}}; + if (RWC.contexts[c].glTexId !== 0) { + /* save previous texture */ _glGetIntegerv(0x8069 /* GL_TEXTURE_BINDING_2D */, RWC.tmp); - var prev = {{{ makeGetValue('RWC.tmp', '0', 'i32') }}}; - _glBindTexture(0x0DE1 /* GL_TEXTURE_2D */, RWC.contexts[data].glTexId); + var prev = {{{ makeGetValue('RWC.tmp', 0, 'i32') }}}; + _glBindTexture(0x0DE1 /* GL_TEXTURE_2D */, RWC.contexts[c].glTexId); /* NPOT textures in WebGL must have these filters and clamping settings */ _glTexParameteri(0x0DE1 /* GL_TEXTURE_2D */, 0x2800 /* GL_TEXTURE_MAG_FILTER */, 0x2601 /* GL_LINEAR */); _glTexParameteri(0x0DE1 /* GL_TEXTURE_2D */, 0x2801 /* GL_TEXTURE_MIN_FILTER */, 0x2601 /* GL_LINEAR */); _glTexParameteri(0x0DE1 /* GL_TEXTURE_2D */, 0x2802 /* GL_TEXTURE_WRAP_S */, 0x812F /* GL_CLAMP_TO_EDGE */); _glTexParameteri(0x0DE1 /* GL_TEXTURE_2D */, 0x2803 /*GL_TEXTURE_WRAP_T */, 0x812F /* GL_CLAMP_TO_EDGE */); _glBindTexture(0x0DE1 /* GL_TEXTURE_2D */, prev); - RWC.contexts[data].glFirstFrame = true; - ret = 1; + RWC.contexts[c].glFirstFrame = true; } } - if (RWC.contexts[data].rawFb) { - RWC.contexts[data].rawFbCanvas = document.createElement("canvas"); - RWC.contexts[data].rawFbCanvas.classList.add("retroarchWebcamCanvas"); - ret = 1; - } - return ret; + RWebCamStartBrowser(c); + RWC.contexts[c].running = true; + return 1; }, - RWebCamStop__deps: ['glDeleteTextures', 'free'], - RWebCamStop: function(data) { - if (RWC.contexts[data].glTexId) { - _glDeleteTextures(1, RWC.contexts[data].glTexId); - } - - if (RWC.contexts[data].rawFbCanvas) { - if (RWC.contexts[data].rawBuffer) { - _free(RWC.contexts[data].rawBuffer); - RWC.contexts[data].rawBuffer = 0; - RWC.contexts[data].rawFbCanvasCtx = null + $RWebCamStopBrowser__proxy: "sync", + $RWebCamStopBrowser: function(c) { + if (RWC.contexts[c].debug && RWC.contexts[c].rawFbCanvas) document.body.removeChild(RWC.contexts[c].rawFbCanvas); + RWC.contexts[c].rawFbCanvasCtx = null; + RWC.contexts[c].rawFbCanvas = null; + RWC.contexts[c].videoElement.pause(); + if (RWC.contexts[c].cameraRunning) { + for (var track of RWC.contexts[c].videoElement.srcObject.getVideoTracks()) { + track.stop(); } - RWC.contexts[data].rawFbCanvas = null; } + if (RWC.contexts[c].debug) document.body.removeChild(RWC.contexts[c].videoElement); + RWC.contexts[c].videoElement = null; + RWC.contexts[c].running = false; }, - RWebCamPoll__deps: ['glBindTexture', 'glGetIntegerv'], - RWebCamPoll: function(data, frame_raw_cb, frame_gl_cb) { - if (!RWC.ready(data)) return 0; + RWebCamStop__deps: ["free", "glDeleteTextures", "$RWebCamStopBrowser"], + RWebCamStop: function(c) { + if (RWC.contexts[c].debug) console.log("RWebCamStop", c); + if (!RWC.contexts[c].running) return; + + if (RWC.contexts[c].glTexId) { + _glDeleteTextures(1, RWC.contexts[c].glTexId); + } + + if (RWC.contexts[c].rawBuffer) { + _free(RWC.contexts[c].rawBuffer); + RWC.contexts[c].rawBuffer = 0; + } + + RWebCamStopBrowser(c); + RWC.contexts[c].running = false; + }, + + $RWebCamCheckDimensions: function(c) { + if (!RWC.contexts[c].width) RWC.contexts[c].width = RWC.contexts[c].videoElement.videoWidth; + if (!RWC.contexts[c].height) RWC.contexts[c].height = RWC.contexts[c].videoElement.videoHeight; + }, + + $RWebCamStoreDimensions__proxy: "sync", + $RWebCamStoreDimensions__deps: ["$RWebCamReady", "$RWebCamCheckDimensions"], + $RWebCamStoreDimensions: function(c, ptr) { + if (!RWebCamReady(c)) return 0; + RWebCamCheckDimensions(c); + {{{ makeSetValue('ptr', 0, 'RWC.contexts[c].width', 'i32') }}}; + {{{ makeSetValue('ptr', 4, 'RWC.contexts[c].height', 'i32') }}}; + return 1; + }, + + $RWebCamGetImageData__deps: ["$RWebCamReady"], + $RWebCamGetImageData: function(c) { + if (!RWebCamReady(c)) return 0; + if (!RWC.contexts[c].rawFbCanvasCtx) { + RWC.contexts[c].rawFbCanvas.width = RWC.contexts[c].width; + RWC.contexts[c].rawFbCanvas.height = RWC.contexts[c].height; + RWC.contexts[c].rawFbCanvasCtx = RWC.contexts[c].rawFbCanvas.getContext("2d", {willReadFrequently: true}); + } + /* crop to desired aspect ratio if necessary */ + var oldAspect = RWC.contexts[c].videoElement.videoWidth / RWC.contexts[c].videoElement.videoHeight; + var newAspect = RWC.contexts[c].rawFbCanvas.width / RWC.contexts[c].rawFbCanvas.height; + var width = RWC.contexts[c].videoElement.videoWidth; + var height = RWC.contexts[c].videoElement.videoHeight; + var offsetX = 0; + var offsetY = 0; + if (oldAspect > newAspect) { + width = height * newAspect; + offsetX = (RWC.contexts[c].videoElement.videoWidth - width) / 2; + } else if (oldAspect < newAspect) { + height = width / newAspect; + offsetY = (RWC.contexts[c].videoElement.videoHeight - height) / 2; + } + RWC.contexts[c].rawFbCanvasCtx.drawImage(RWC.contexts[c].videoElement, + offsetX, offsetY, width, height, + 0, 0, RWC.contexts[c].rawFbCanvas.width, RWC.contexts[c].rawFbCanvas.height); + return RWC.contexts[c].rawFbCanvasCtx.getImageData(0, 0, RWC.contexts[c].rawFbCanvas.width, RWC.contexts[c].rawFbCanvas.height).data; + }, + + $RWebCamStoreImageData__proxy: "sync", + $RWebCamStoreImageData__deps: ["$RWebCamGetImageData"], + $RWebCamStoreImageData: function(c, ptr) { + var data = RWebCamGetImageData(c); + if (!data) return 0; + HEAPU8.set(data, ptr); + return 1; + }, + + RWebCamPoll__deps: ["$RWebCamReady", "glBindTexture", "glGetIntegerv", "malloc", "free", "$RWebCamStoreDimensions", "$RWebCamCheckDimensions", "$RWebCamStoreImageData"], + RWebCamPoll: function(c, frame_raw_cb, frame_gl_cb) { + if (RWC.contexts[c].debug) console.log("RWebCamPoll", c, ENVIRONMENT_IS_PTHREAD, RWC.contexts[c].rawBuffer, RWC.contexts[c].running); + if (!RWC.contexts[c].running) return 0; /* need more checks in RA */ var ret = 0; - if (RWC.contexts[data].glTexId !== 0 && frame_gl_cb !== 0) { - _glGetIntegerv(0x8069 /* GL_TEXTURE_BINDING_2D */, RWC.tmp); - var prev = {{{ makeGetValue('RWC.tmp', '0', 'i32') }}}; - _glBindTexture(0x0DE1 /* GL_TEXTURE_2D */, RWC.contexts[data].glTexId); - if (RWC.contexts[data].glFirstFrame) { - Module.ctx.texImage2D(Module.ctx.TEXTURE_2D, 0, Module.ctx.RGB, Module.ctx.RGB, Module.ctx.UNSIGNED_BYTE, RWC.contexts[data].videoElement); - RWC.contexts[data].glFirstFrame = false; + if ((RWC.contexts[c].rawFb && frame_raw_cb !== 0) || ENVIRONMENT_IS_PTHREAD) { + if (!RWC.contexts[c].rawBuffer) { + if (ENVIRONMENT_IS_PTHREAD) { + /* pull dimensions into this thread */ + var dimensions = _malloc(8); + var res = RWebCamStoreDimensions(c, dimensions); /* also checks ready(c) */ + if (!res) { + _free(dimensions); + return 0; + } + RWC.contexts[c].width = {{{ makeGetValue('dimensions', 0, 'i32') }}}; + RWC.contexts[c].height = {{{ makeGetValue('dimensions', 4, 'i32') }}}; + _free(dimensions); + RWC.contexts[c].length = RWC.contexts[c].width * RWC.contexts[c].height * 4; + } else { + if (!RWebCamReady(c)) return 0; + RWebCamCheckDimensions(c); + } + RWC.contexts[c].rawBuffer = _malloc(RWC.contexts[c].width * RWC.contexts[c].height * 4 + 1); + } + /* store at +1 so we can read data as XRGB */ + if (!RWebCamStoreImageData(c, RWC.contexts[c].rawBuffer + 1)) return 0; + } + + if (RWC.contexts[c].glTexId !== 0 && frame_gl_cb !== 0) { + var imageSrcGL; + if (ENVIRONMENT_IS_PTHREAD) { + imageSrcGL = HEAPU8.subarray(RWC.contexts[c].rawBuffer + 1, RWC.contexts[c].rawBuffer + RWC.contexts[c].length + 1); } else { - Module.ctx.texSubImage2D(Module.ctx.TEXTURE_2D, 0, 0, 0, Module.ctx.RGB, Module.ctx.UNSIGNED_BYTE, RWC.contexts[data].videoElement); + if (!RWebCamReady(c)) return 0; + imageSrcGL = RWC.contexts[c].videoElement; + } + + _glGetIntegerv(0x8069 /* GL_TEXTURE_BINDING_2D */, RWC.tmp); + var prev = {{{ makeGetValue('RWC.tmp', 0, 'i32') }}}; + _glBindTexture(0x0DE1 /* GL_TEXTURE_2D */, RWC.contexts[c].glTexId); + if (RWC.contexts[c].glFirstFrame) { + if (ENVIRONMENT_IS_PTHREAD) + Module.ctx.texImage2D(Module.ctx.TEXTURE_2D, 0, Module.ctx.RGBA, RWC.contexts[c].width, RWC.contexts[c].height, 0, Module.ctx.RGBA, Module.ctx.UNSIGNED_BYTE, imageSrcGL); + else + Module.ctx.texImage2D(Module.ctx.TEXTURE_2D, 0, Module.ctx.RGB, Module.ctx.RGB, Module.ctx.UNSIGNED_BYTE, imageSrcGL); + RWC.contexts[c].glFirstFrame = false; + } else { + if (ENVIRONMENT_IS_PTHREAD) + Module.ctx.texSubImage2D(Module.ctx.TEXTURE_2D, 0, 0, 0, RWC.contexts[c].width, RWC.contexts[c].height, Module.ctx.RGBA, Module.ctx.UNSIGNED_BYTE, imageSrcGL); + else + Module.ctx.texSubImage2D(Module.ctx.TEXTURE_2D, 0, 0, 0, Module.ctx.RGB, Module.ctx.UNSIGNED_BYTE, imageSrcGL); } _glBindTexture(0x0DE1 /* GL_TEXTURE_2D */, prev); - Runtime.dynCall('viii', frame_gl_cb, [RWC.contexts[data].glTexId, 0x0DE1 /* GL_TEXTURE_2D */, 0]); + {{{ makeDynCall('viip', 'frame_gl_cb') }}}(RWC.contexts[c].glTexId, 0x0DE1 /* GL_TEXTURE_2D */, 0); ret = 1; } - if (RWC.contexts[data].rawFbCanvas && frame_raw_cb !== 0) + if (RWC.contexts[c].rawFb && frame_raw_cb !== 0) { - if (!RWC.contexts[data].rawFbCanvasCtx) { - RWC.contexts[data].rawFbCanvas.width = RWC.contexts[data].videoElement.videoWidth; - RWC.contexts[data].rawFbCanvas.height = RWC.contexts[data].videoElement.videoHeight; - RWC.contexts[data].rawFbCanvasCtx = RWC.contexts[data].rawFbCanvas.getContext("2d"); - RWC.contexts[data].rawBuffer = _malloc(RWC.contexts[data].videoElement.videoWidth * RWC.contexts[data].videoElement.videoHeight * 4); - } - RWC.contexts[data].rawFbCanvasCtx.drawImage(RWC.contexts[data].videoElement, 0, 0, RWC.contexts[data].rawFbCanvas.width, RWC.contexts[data].rawFbCanvas.height); - var image = RWC.contexts[data].rawFbCanvasCtx.getImageData(0, 0, RWC.contexts[data].videoElement.videoWidth, RWC.contexts[data].videoElement.videoHeight); - Module.HEAPU8.set(image.data, RWC.contexts[data].rawBuffer); - Runtime.dynCall('viiii', frame_raw_cb, [RWC.contexts[data].rawBuffer, RWC.contexts[data].videoElement.videoWidth, RWC.contexts[data].videoElement.videoHeight, RWC.contexts[data].videoElement.videoWidth * 4]); - + {{{ makeDynCall('vpiii', 'frame_raw_cb') }}}(RWC.contexts[c].rawBuffer, RWC.contexts[c].width, RWC.contexts[c].height, RWC.contexts[c].width * 4); ret = 1; } @@ -140,4 +280,4 @@ var LibraryRWebCam = { }; autoAddDeps(LibraryRWebCam, '$RWC'); -mergeInto(LibraryManager.library, LibraryRWebCam); +addToLibrary(LibraryRWebCam); diff --git a/emscripten/template.html b/emscripten/template.html deleted file mode 100644 index 1ee0732476..0000000000 --- a/emscripten/template.html +++ /dev/null @@ -1,134 +0,0 @@ - - - - - - RetroArch Web Player - - - - - -
- -
- - - -
-
-
- - -
-
-
- -
- -
- Controls:
-
- A button (OK in menu): X
- B button (Back in menu): Z
- X Button: S
- Y Button: A
- L Button: Q
- R Button: W
- D-pad: Arrow Keys
- Start Button: Enter
- Select Button: Shift
- Toggle Menu: F1
- Fast forward: Spacebar (toggle)
- Slow motion: E (hold)
- Save state: F2
- Load state: F4 -
- - - - - - diff --git a/frontend/drivers/platform_emscripten.c b/frontend/drivers/platform_emscripten.c index ff2cb3ec6f..778622808f 100644 --- a/frontend/drivers/platform_emscripten.c +++ b/frontend/drivers/platform_emscripten.c @@ -17,6 +17,7 @@ #include #include +#include #include #include #include @@ -60,7 +61,6 @@ #ifdef PROXY_TO_PTHREAD #include -#include #include #include #define PLATFORM_SETVAL(type, addr, val) emscripten_atomic_store_##type(addr, val) @@ -73,10 +73,17 @@ #include "platform_emscripten.h" void emscripten_mainloop(void); + +/* javascript library functions */ void PlatformEmscriptenWatchCanvasSizeAndDpr(double *dpr); void PlatformEmscriptenWatchWindowVisibility(void); void PlatformEmscriptenPowerStateInit(void); void PlatformEmscriptenMemoryUsageInit(void); +void PlatformEmscriptenWatchFullscreen(void); +void PlatformEmscriptenGLContextEventInit(void); +void PlatformEmscriptenSetCanvasSize(int width, int height); +void PlatformEmscriptenSetWakeLock(bool state); +uint32_t PlatformEmscriptenGetSystemInfo(void); typedef struct { @@ -88,12 +95,18 @@ typedef struct uint64_t memory_used; uint64_t memory_limit; double device_pixel_ratio; + enum platform_emscripten_browser browser; + enum platform_emscripten_os os; int raf_interval; int canvas_width; int canvas_height; int power_state_discharge_time; float power_state_level; bool has_async_atomics; + bool enable_set_canvas_size; + bool disable_detect_enter_fullscreen; + bool fullscreen; + bool gl_context_lost; volatile bool power_state_charging; volatile bool power_state_supported; volatile bool window_hidden; @@ -102,6 +115,22 @@ typedef struct static emscripten_platform_data_t *emscripten_platform_data = NULL; +/* defined here since implementation is frontend dependent */ +void retro_sleep(unsigned msec) +{ +#if defined(EMSCRIPTEN_ASYNCIFY) && !defined(HAVE_THREADS) + emscripten_sleep(msec); +#elif defined(EMSCRIPTEN_ASYNCIFY) + if (emscripten_is_main_browser_thread()) + emscripten_sleep(msec); + else + emscripten_thread_sleep(msec); +#else + /* also works on main thread, but uses busy wait */ + emscripten_thread_sleep(msec); +#endif +} + /* begin exported functions */ /* saves and states */ @@ -133,6 +162,11 @@ void cmd_undo_load_state(void) /* misc */ +void cmd_toggle_fullscreen(void) +{ + command_event(CMD_EVENT_FULLSCREEN_TOGGLE, NULL); +} + void cmd_take_screenshot(void) { command_event(CMD_EVENT_TAKE_SCREENSHOT, NULL); @@ -232,7 +266,7 @@ void cmd_cheat_apply_cheats(void) /* javascript callbacks */ -void platform_emscripten_update_canvas_dimensions(int width, int height, double *dpr) +void platform_emscripten_update_canvas_dimensions_cb(int width, int height, double *dpr) { printf("[INFO] Setting real canvas size: %d x %d\n", width, height); emscripten_set_canvas_element_size("#canvas", width, height); @@ -243,14 +277,14 @@ void platform_emscripten_update_canvas_dimensions(int width, int height, double PLATFORM_SETVAL(f64, &emscripten_platform_data->device_pixel_ratio, *dpr); } -void platform_emscripten_update_window_hidden(bool hidden) +void platform_emscripten_update_window_hidden_cb(bool hidden) { if (!emscripten_platform_data) return; emscripten_platform_data->window_hidden = hidden; } -void platform_emscripten_update_power_state(bool supported, int discharge_time, float level, bool charging) +void platform_emscripten_update_power_state_cb(bool supported, int discharge_time, float level, bool charging) { if (!emscripten_platform_data) return; @@ -260,12 +294,40 @@ void platform_emscripten_update_power_state(bool supported, int discharge_time, PLATFORM_SETVAL(f32, &emscripten_platform_data->power_state_level, level); } -void platform_emscripten_update_memory_usage(uint32_t used1, uint32_t used2, uint32_t limit1, uint32_t limit2) +void platform_emscripten_update_memory_usage_cb(uint64_t used, uint64_t limit) { if (!emscripten_platform_data) return; - PLATFORM_SETVAL(u64, &emscripten_platform_data->memory_used, used1 | ((uint64_t)used2 << 32)); - PLATFORM_SETVAL(u64, &emscripten_platform_data->memory_limit, limit1 | ((uint64_t)limit2 << 32)); + PLATFORM_SETVAL(u64, &emscripten_platform_data->memory_used, used); + PLATFORM_SETVAL(u64, &emscripten_platform_data->memory_limit, limit); +} + +static void fullscreen_update(void *data) +{ + command_event(CMD_EVENT_FULLSCREEN_TOGGLE, NULL); +} + +void platform_emscripten_update_fullscreen_state_cb(bool state) +{ + if (state == emscripten_platform_data->fullscreen || (state && emscripten_platform_data->disable_detect_enter_fullscreen)) + return; + + emscripten_platform_data->fullscreen = state; + platform_emscripten_run_on_program_thread_async(fullscreen_update, NULL); +} + +void platform_emscripten_gl_context_lost_cb(void) +{ + printf("[WARN] WebGL context lost!\n"); + emscripten_platform_data->gl_context_lost = true; +} + +void platform_emscripten_gl_context_restored_cb(void) +{ + printf("[INFO] WebGL context restored.\n"); + emscripten_platform_data->gl_context_lost = false; + /* there might be a better way to do this, but for now this works */ + command_event(CMD_EVENT_REINIT, NULL); } void platform_emscripten_command_raise_flag() @@ -315,7 +377,7 @@ void platform_emscripten_command_reply(const char *msg, size_t len) size_t platform_emscripten_command_read(char **into, size_t max_len) { - if (!emscripten_platform_data || !emscripten_platform_data->command_flag) + if (!emscripten_platform_data->command_flag) return 0; return MAIN_THREAD_EM_ASM_INT({ var next_command = RPE.command_queue.shift(); @@ -334,16 +396,12 @@ size_t platform_emscripten_command_read(char **into, size_t max_len) void platform_emscripten_get_canvas_size(int *width, int *height) { - if (!emscripten_platform_data) - goto error; - *width = PLATFORM_GETVAL(u32, &emscripten_platform_data->canvas_width); *height = PLATFORM_GETVAL(u32, &emscripten_platform_data->canvas_height); if (*width != 0 || *height != 0) return; -error: *width = 800; *height = 600; RARCH_ERR("[EMSCRIPTEN]: Could not get screen dimensions!\n"); @@ -354,6 +412,15 @@ double platform_emscripten_get_dpr(void) return PLATFORM_GETVAL(f64, &emscripten_platform_data->device_pixel_ratio); } +unsigned platform_emscripten_get_min_sleep_ms(void) +{ + if (emscripten_platform_data->browser == PLATFORM_EMSCRIPTEN_BROWSER_FIREFOX && + emscripten_platform_data->os == PLATFORM_EMSCRIPTEN_OS_WINDOWS) + return 16; + + return 5; +} + bool platform_emscripten_has_async_atomics(void) { return emscripten_platform_data->has_async_atomics; @@ -366,7 +433,7 @@ bool platform_emscripten_is_window_hidden(void) bool platform_emscripten_should_drop_iter(void) { - return (emscripten_platform_data->window_hidden && emscripten_platform_data->raf_interval); + return (emscripten_platform_data->gl_context_lost || (emscripten_platform_data->window_hidden && emscripten_platform_data->raf_interval)); } #ifdef PROXY_TO_PTHREAD @@ -413,6 +480,58 @@ void platform_emscripten_set_main_loop_interval(int interval) #endif } +void platform_emscripten_set_pointer_visibility(bool state) +{ + MAIN_THREAD_EM_ASM({ + if ($0) { + Module.canvas.style.removeProperty("cursor"); + } else { + Module.canvas.style.setProperty("cursor", "none"); + } + }, state); +} + +static void platform_emscripten_do_set_fullscreen_state(void *data) +{ + bool ran_user_cb = EM_ASM_INT({ + var func = $0 ? "fullscreenEnter" : "fullscreenExit"; + if (Module[func]) { + Module[func](); + return 1; + } + return 0; + }, emscripten_platform_data->fullscreen); + if (ran_user_cb) + return; + + if (emscripten_platform_data->fullscreen) + emscripten_request_fullscreen("#canvas", false); + else + emscripten_exit_fullscreen(); +} + +void platform_emscripten_set_fullscreen_state(bool state) +{ + if (state == emscripten_platform_data->fullscreen) + return; + + emscripten_platform_data->fullscreen = state; + platform_emscripten_run_on_browser_thread_sync(platform_emscripten_do_set_fullscreen_state, NULL); +} + +void platform_emscripten_set_wake_lock(bool state) +{ + PlatformEmscriptenSetWakeLock(state); +} + +void platform_emscripten_set_canvas_size(int width, int height) +{ + if (!emscripten_platform_data->enable_set_canvas_size) + return; + + PlatformEmscriptenSetCanvasSize(width, height); +} + /* frontend driver impl */ static void frontend_emscripten_get_env(int *argc, char *argv[], @@ -575,7 +694,7 @@ static void platform_emscripten_mount_filesystems(void) path_parent_dir(parent, strlen(parent)); if (!path_mkdir(parent)) { - printf("mkdir error %d\n", errno); + printf("[OPFS] mkdir error %d\n", errno); abort(); } free(parent); @@ -704,6 +823,7 @@ static int thread_main(int argc, char *argv[]) platform_emscripten_mount_filesystems(); #endif + PlatformEmscriptenGLContextEventInit(); emscripten_set_main_loop(emscripten_mainloop, 0, 0); #ifdef PROXY_TO_PTHREAD emscripten_set_main_loop_timing(EM_TIMING_SETIMMEDIATE, 0); @@ -737,6 +857,7 @@ static void raf_signaler(void) int main(int argc, char *argv[]) { int ret = 0; + uint32_t system_info; #ifdef PROXY_TO_PTHREAD pthread_attr_t attr; pthread_t thread; @@ -744,14 +865,58 @@ int main(int argc, char *argv[]) /* this never gets freed */ emscripten_platform_data = (emscripten_platform_data_t *)calloc(1, sizeof(emscripten_platform_data_t)); + system_info = PlatformEmscriptenGetSystemInfo(); + emscripten_platform_data->browser = system_info & 0xFFFF; + emscripten_platform_data->os = system_info >> 16; + + emscripten_platform_data->enable_set_canvas_size = !!getenv("ENABLE_SET_CANVAS_SIZE"); + emscripten_platform_data->disable_detect_enter_fullscreen = !!getenv("DISABLE_DETECT_ENTER_FULLSCREEN"); emscripten_platform_data->has_async_atomics = EM_ASM_INT({ return Atomics?.waitAsync?.toString().includes("[native code]"); }); + EM_ASM({ + /* keyboard events won't work without the canvas being focused */ + 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"; + + /* border and padding may interfere with pointer event coordinates */ + Module.canvas.style.setProperty("padding", "0px", "important"); + Module.canvas.style.setProperty("border", "none", "important"); + + /* 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!"); + Module.canvas.style.display = "inline-block"; + } + var oldWidth = Module.canvas.clientWidth; + var oldHeight = Module.canvas.clientHeight; + 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!"); + Module.canvas.style.width = oldWidth + "px"; + Module.canvas.style.height = oldHeight + "px"; + } + }); + PlatformEmscriptenWatchCanvasSizeAndDpr(malloc(sizeof(double))); PlatformEmscriptenWatchWindowVisibility(); PlatformEmscriptenPowerStateInit(); PlatformEmscriptenMemoryUsageInit(); + PlatformEmscriptenWatchFullscreen(); emscripten_platform_data->raf_interval = 1; #ifdef PROXY_TO_PTHREAD diff --git a/frontend/drivers/platform_emscripten.h b/frontend/drivers/platform_emscripten.h index add2ad03c6..58dfa27946 100644 --- a/frontend/drivers/platform_emscripten.h +++ b/frontend/drivers/platform_emscripten.h @@ -20,6 +20,23 @@ #include +enum platform_emscripten_browser +{ + PLATFORM_EMSCRIPTEN_BROWSER_OTHER = 0, + PLATFORM_EMSCRIPTEN_BROWSER_CHROMIUM, + PLATFORM_EMSCRIPTEN_BROWSER_FIREFOX, + PLATFORM_EMSCRIPTEN_BROWSER_SAFARI +}; + +enum platform_emscripten_os +{ + PLATFORM_EMSCRIPTEN_OS_OTHER = 0, + PLATFORM_EMSCRIPTEN_OS_WINDOWS, + PLATFORM_EMSCRIPTEN_OS_LINUX, + PLATFORM_EMSCRIPTEN_OS_IOS, + PLATFORM_EMSCRIPTEN_OS_MACOS +}; + /** * Synchronously run a function on the browser thread. * @@ -77,6 +94,15 @@ void platform_emscripten_get_canvas_size(int *width, int *height); */ double platform_emscripten_get_dpr(void); +/** + * Get the minimum amount of time that setTimeout (retro_sleep on browser thread) can + * sleep for in a loop. + * This may vary between browsers: usually 5 ms, but much higher for firefox on windows. + * + * @return Minimum sleep in milliseconds. + */ +unsigned platform_emscripten_get_min_sleep_ms(void); + /** * Check if the browser supports Atomics.waitAsync. * @@ -128,4 +154,34 @@ void platform_emscripten_exit_fake_block(void); */ void platform_emscripten_set_main_loop_interval(int interval); +/** + * Hide or show the cursor. + * + * @param state True to show, false to hide. + */ +void platform_emscripten_set_pointer_visibility(bool state); + +/** + * Try to enter or exit fullscreen. + * + * @param state True to enter, false to exit. + */ +void platform_emscripten_set_fullscreen_state(bool state); + +/** + * Try to prevent the screen from dimming. + * + * @param state True to request, false to release. + */ +void platform_emscripten_set_wake_lock(bool state); + +/** + * Try to set the real screen dimensions of the canvas. + * Will only work if explicitly enabled by the embedder. + * + * @param width New width + * @param height New height + */ +void platform_emscripten_set_canvas_size(int width, int height); + #endif diff --git a/gfx/drivers_context/emscriptenegl_ctx.c b/gfx/drivers_context/emscriptenegl_ctx.c index 2cfbfd066c..c12866202a 100644 --- a/gfx/drivers_context/emscriptenegl_ctx.c +++ b/gfx/drivers_context/emscriptenegl_ctx.c @@ -63,16 +63,6 @@ static void gfx_ctx_emscripten_check_window(void *data, bool *quit, *quit = false; } -static void gfx_ctx_emscripten_swap_buffers(void *data) -{ -#ifdef HAVE_EGL - /* Doesn't really do anything in WebGL, but it might - * if we use WebGL workers in the future */ - emscripten_ctx_data_t *emscripten = (emscripten_ctx_data_t*)data; - egl_swap_buffers(&emscripten->egl); -#endif -} - static void gfx_ctx_emscripten_get_video_size(void *data, unsigned *width, unsigned *height) { @@ -181,11 +171,11 @@ error: } static bool gfx_ctx_emscripten_set_video_mode(void *data, - unsigned width, unsigned height, - bool fullscreen) + unsigned width, unsigned height, bool fullscreen) { - if (g_egl_inited) - return false; + platform_emscripten_set_fullscreen_state(fullscreen); + if (!fullscreen) + platform_emscripten_set_canvas_size(width, height); g_egl_inited = true; return true; @@ -212,9 +202,20 @@ static void gfx_ctx_emscripten_input_driver(void *data, *input_data = rwebinput; } -static bool gfx_ctx_emscripten_has_focus(void *data) { return g_egl_inited; } +static bool gfx_ctx_emscripten_has_focus(void *data) { + return g_egl_inited && !platform_emscripten_is_window_hidden(); +} -static bool gfx_ctx_emscripten_suppress_screensaver(void *data, bool enable) { return false; } +static bool gfx_ctx_emscripten_suppress_screensaver(void *data, bool enable) +{ + platform_emscripten_set_wake_lock(enable); + return true; +} + +static void gfx_ctx_emscripten_show_mouse(void *data, bool state) +{ + platform_emscripten_set_pointer_visibility(state); +} static float gfx_ctx_emscripten_translate_aspect(void *data, unsigned width, unsigned height) { return (float)width / height; } @@ -259,11 +260,11 @@ const gfx_ctx_driver_t gfx_ctx_emscripten = { gfx_ctx_emscripten_translate_aspect, NULL, /* update_title */ gfx_ctx_emscripten_check_window, - NULL, /* set_resize */ + NULL, /* set_resize: no-op */ gfx_ctx_emscripten_has_focus, gfx_ctx_emscripten_suppress_screensaver, - false, - gfx_ctx_emscripten_swap_buffers, + true, /* has_windowed */ + NULL, /* swap_buffers: no-op */ gfx_ctx_emscripten_input_driver, #ifdef HAVE_EGL egl_get_proc_address, @@ -272,11 +273,11 @@ const gfx_ctx_driver_t gfx_ctx_emscripten = { #endif gfx_ctx_emscripten_init_egl_image_buffer, gfx_ctx_emscripten_write_egl_image, - NULL, + gfx_ctx_emscripten_show_mouse, "egl_emscripten", gfx_ctx_emscripten_get_flags, gfx_ctx_emscripten_set_flags, gfx_ctx_emscripten_bind_hw_render, - NULL, - NULL + NULL, /* get_context_data */ + NULL /* make_current */ }; diff --git a/gfx/drivers_context/emscriptenwebgl_ctx.c b/gfx/drivers_context/emscriptenwebgl_ctx.c index a2e659ba27..9554f986f7 100644 --- a/gfx/drivers_context/emscriptenwebgl_ctx.c +++ b/gfx/drivers_context/emscriptenwebgl_ctx.c @@ -57,12 +57,6 @@ static void gfx_ctx_emscripten_webgl_check_window(void *data, bool *quit, *quit = false; } -/* https://github.com/emscripten-core/emscripten/issues/17816#issuecomment-1249719343 */ -static void gfx_ctx_emscripten_webgl_swap_buffers(void *data) -{ - (void)data; -} - static void gfx_ctx_emscripten_webgl_get_video_size(void *data, unsigned *width, unsigned *height) { @@ -114,7 +108,7 @@ static void *gfx_ctx_emscripten_webgl_init(void *video_driver) EmscriptenWebGLContextAttributes attrs = {0}; emscripten_webgl_init_context_attributes(&attrs); - attrs.alpha = false; + attrs.alpha = true; attrs.depth = true; attrs.stencil = true; attrs.antialias = false; @@ -152,57 +146,24 @@ error: return NULL; } -static bool gfx_ctx_emscripten_webgl_set_canvas_size(int width, int height) -{ -#ifdef NO_CANVAS_RESIZE - return false; -#endif - double dpr = platform_emscripten_get_dpr(); - EMSCRIPTEN_RESULT r = emscripten_set_element_css_size("#canvas", (double)width / dpr, (double)height / dpr); - RARCH_LOG("[EMSCRIPTEN/WebGL]: set canvas size to %d, %d\n", width, height); - - if (r != EMSCRIPTEN_RESULT_SUCCESS) - { - RARCH_ERR("[EMSCRIPTEN/WebGL]: error resizing canvas: %d\n", r); - return false; - } - return true; -} - static bool gfx_ctx_emscripten_webgl_set_video_mode(void *data, - unsigned width, unsigned height, - bool fullscreen) + unsigned width, unsigned height, bool fullscreen) { emscripten_ctx_data_t *emscripten = (emscripten_ctx_data_t*)data; if (!emscripten || !emscripten->ctx) return false; - if (width != 0 && height != 0) - { - if (!gfx_ctx_emscripten_webgl_set_canvas_size(width, height)) - return false; - } - emscripten->fb_width = width; - emscripten->fb_height = height; + platform_emscripten_set_fullscreen_state(fullscreen); + if (!fullscreen) + platform_emscripten_set_canvas_size(width, height); return true; } -bool gfx_ctx_emscripten_webgl_set_resize(void *data, unsigned width, unsigned height) -{ - emscripten_ctx_data_t *emscripten = (emscripten_ctx_data_t*)data; - if (!emscripten || !emscripten->ctx) - return false; - return gfx_ctx_emscripten_webgl_set_canvas_size(width, height); -} - static enum gfx_ctx_api gfx_ctx_emscripten_webgl_get_api(void *data) { return GFX_CTX_OPENGL_ES_API; } static bool gfx_ctx_emscripten_webgl_bind_api(void *data, - enum gfx_ctx_api api, unsigned major, unsigned minor) -{ - return true; -} + enum gfx_ctx_api api, unsigned major, unsigned minor) { return true; } static void gfx_ctx_emscripten_webgl_input_driver(void *data, const char *name, @@ -216,10 +177,19 @@ static void gfx_ctx_emscripten_webgl_input_driver(void *data, static bool gfx_ctx_emscripten_webgl_has_focus(void *data) { emscripten_ctx_data_t *emscripten = (emscripten_ctx_data_t*)data; - return emscripten && emscripten->ctx; + return emscripten && emscripten->ctx && !platform_emscripten_is_window_hidden(); } -static bool gfx_ctx_emscripten_webgl_suppress_screensaver(void *data, bool enable) { return false; } +static bool gfx_ctx_emscripten_webgl_suppress_screensaver(void *data, bool enable) +{ + platform_emscripten_set_wake_lock(enable); + return true; +} + +static void gfx_ctx_emscripten_webgl_show_mouse(void *data, bool state) +{ + platform_emscripten_set_pointer_visibility(state); +} static float gfx_ctx_emscripten_webgl_translate_aspect(void *data, unsigned width, unsigned height) { return (float)width / height; } @@ -260,16 +230,16 @@ const gfx_ctx_driver_t gfx_ctx_emscripten_webgl = { gfx_ctx_emscripten_webgl_translate_aspect, NULL, /* update_title */ gfx_ctx_emscripten_webgl_check_window, - gfx_ctx_emscripten_webgl_set_resize, + NULL, /* set_resize: no-op */ gfx_ctx_emscripten_webgl_has_focus, gfx_ctx_emscripten_webgl_suppress_screensaver, - false, - gfx_ctx_emscripten_webgl_swap_buffers, + true, /* has_windowed */ + NULL, /* swap_buffers: no-op: https://github.com/emscripten-core/emscripten/issues/17816#issuecomment-1249719343 */ gfx_ctx_emscripten_webgl_input_driver, gfx_ctx_emscripten_webgl_get_proc_address, NULL, NULL, - NULL, + gfx_ctx_emscripten_webgl_show_mouse, "webgl_emscripten", gfx_ctx_emscripten_webgl_get_flags, gfx_ctx_emscripten_webgl_set_flags, diff --git a/input/drivers/rwebinput_input.c b/input/drivers/rwebinput_input.c index 55982a056f..ce6fdd17a7 100644 --- a/input/drivers/rwebinput_input.c +++ b/input/drivers/rwebinput_input.c @@ -87,11 +87,21 @@ typedef struct rwebinput_mouse_states uint8_t buttons; } rwebinput_mouse_state_t; +typedef struct rwebinput_motion_states +{ + float x; + float y; + float z; + bool enabled; +} rwebinput_motion_state_t; + typedef struct rwebinput_input { rwebinput_mouse_state_t mouse; /* double alignment */ rwebinput_keyboard_event_queue_t keyboard; /* ptr alignment */ rwebinput_pointer_state_t pointer[MAX_TOUCH]; /* int alignment */ + rwebinput_motion_state_t accelerometer; /* float alignment */ + rwebinput_motion_state_t gyroscope; /* float alignment */ unsigned pointer_count; bool keys[RETROK_LAST]; bool pointerlock_active; @@ -406,6 +416,26 @@ static EM_BOOL rwebinput_pointerlockchange_cb(int event_type, return EM_TRUE; } +static EM_BOOL rwebinput_devicemotion_cb(int event_type, + const EmscriptenDeviceMotionEvent *device_motion_event, void *user_data) +{ + rwebinput_input_t *rwebinput = (rwebinput_input_t*)user_data; + + /* TODO: what units does mGBA want? does something need to be changed on the core side? */ + /* given in m/s^2 (inverted) */ + rwebinput->accelerometer.x = device_motion_event->accelerationIncludingGravityX / -9.8; + rwebinput->accelerometer.y = device_motion_event->accelerationIncludingGravityY / -9.8; + rwebinput->accelerometer.z = device_motion_event->accelerationIncludingGravityZ / -9.8; + /* XYZ == BetaGammaAlpha according to W3C? in my testing it is AlphaBetaGamma... */ + /* libretro wants radians/s but it is too fast in mGBA, see above comment */ + /* given in degrees/s */ + rwebinput->gyroscope.x = device_motion_event->rotationRateAlpha / 180; + rwebinput->gyroscope.y = device_motion_event->rotationRateBeta / 180; + rwebinput->gyroscope.z = device_motion_event->rotationRateGamma / 180; + + return EM_TRUE; +} + static void *rwebinput_input_init(const char *joypad_driver) { EMSCRIPTEN_RESULT r; @@ -746,6 +776,84 @@ static void rwebinput_input_free(void *data) free(data); } +static bool rwebinput_set_sensor_state(void *data, unsigned port, + enum retro_sensor_action action, unsigned rate) +{ + rwebinput_input_t *rwebinput = (rwebinput_input_t*)data; + EMSCRIPTEN_RESULT r; + bool old_state = rwebinput->accelerometer.enabled || rwebinput->gyroscope.enabled; + bool new_state; + + switch (action) + { + case RETRO_SENSOR_ACCELEROMETER_ENABLE: + rwebinput->accelerometer.enabled = true; + break; + case RETRO_SENSOR_ACCELEROMETER_DISABLE: + rwebinput->accelerometer.enabled = false; + break; + case RETRO_SENSOR_GYROSCOPE_ENABLE: + rwebinput->gyroscope.enabled = true; + break; + case RETRO_SENSOR_GYROSCOPE_DISABLE: + rwebinput->gyroscope.enabled = false; + break; + case RETRO_SENSOR_ILLUMINANCE_ENABLE: + case RETRO_SENSOR_ILLUMINANCE_DISABLE: + return false; /* not supported (browsers removed support for now) */ + default: + return false; + } + + new_state = rwebinput->accelerometer.enabled || rwebinput->gyroscope.enabled; + + if (!old_state && new_state) + { + r = emscripten_set_devicemotion_callback(rwebinput, false, rwebinput_devicemotion_cb); + if (r != EMSCRIPTEN_RESULT_SUCCESS) + { + RARCH_ERR( + "[EMSCRIPTEN/INPUT] failed to create devicemotion callback: %d\n", r); + return false; + } + } + else if (old_state && !new_state) + { + r = emscripten_set_devicemotion_callback(rwebinput, false, NULL); + if (r != EMSCRIPTEN_RESULT_SUCCESS) + { + RARCH_ERR( + "[EMSCRIPTEN/INPUT] failed to remove devicemotion callback: %d\n", r); + return false; + } + } + + return true; +} + +static float rwebinput_get_sensor_input(void *data, unsigned port, unsigned id) +{ + rwebinput_input_t *rwebinput = (rwebinput_input_t*)data; + + switch (id) + { + case RETRO_SENSOR_ACCELEROMETER_X: + return rwebinput->accelerometer.x; + case RETRO_SENSOR_ACCELEROMETER_Y: + return rwebinput->accelerometer.y; + case RETRO_SENSOR_ACCELEROMETER_Z: + return rwebinput->accelerometer.z; + case RETRO_SENSOR_GYROSCOPE_X: + return rwebinput->gyroscope.x; + case RETRO_SENSOR_GYROSCOPE_Y: + return rwebinput->gyroscope.y; + case RETRO_SENSOR_GYROSCOPE_Z: + return rwebinput->gyroscope.z; + } + + return 0.0f; +} + static void rwebinput_process_keyboard_events( rwebinput_input_t *rwebinput, rwebinput_keyboard_event_t *event) @@ -840,8 +948,8 @@ input_driver_t input_rwebinput = { rwebinput_input_poll, rwebinput_input_state, rwebinput_input_free, - NULL, - NULL, + rwebinput_set_sensor_state, + rwebinput_get_sensor_input, rwebinput_get_capabilities, "rwebinput", rwebinput_grab_mouse, diff --git a/input/drivers_joypad/rwebpad_joypad.c b/input/drivers_joypad/rwebpad_joypad.c index 09e75258cc..58bfbea023 100644 --- a/input/drivers_joypad/rwebpad_joypad.c +++ b/input/drivers_joypad/rwebpad_joypad.c @@ -30,9 +30,18 @@ #define NUM_BUTTONS 64 #define NUM_AXES 64 +struct rwebpad_joypad_rumble_state +{ + uint16_t pending_strong; + uint16_t pending_weak; + uint16_t strong; + uint16_t weak; +}; + typedef struct { struct EmscriptenGamepadEvent pads[DEFAULT_MAX_PADS]; + struct rwebpad_joypad_rumble_state rumble[DEFAULT_MAX_PADS]; bool live_pads[DEFAULT_MAX_PADS]; } rwebpad_joypad_data_t; @@ -45,6 +54,9 @@ static EM_BOOL rwebpad_gamepad_cb(int event_type, unsigned vid = 1; unsigned pid = 1; + if (gamepad_event->index >= DEFAULT_MAX_PADS) + return EM_FALSE; + switch (event_type) { case EMSCRIPTEN_EVENT_GAMEPADCONNECTED: @@ -93,12 +105,11 @@ static void *rwebpad_joypad_init(void *data) return (void*)(-1); } -static const char *rwebpad_joypad_name(unsigned pad) +static const char *rwebpad_joypad_name(unsigned port) { - if (pad >= DEFAULT_MAX_PADS || !rwebpad_joypad_data->live_pads[pad]) { + if (port >= DEFAULT_MAX_PADS || !rwebpad_joypad_data->live_pads[port]) return ""; - } - return rwebpad_joypad_data->pads[pad].id; + return rwebpad_joypad_data->pads[port].id; } static int32_t rwebpad_joypad_button(unsigned port, uint16_t joykey) @@ -116,7 +127,8 @@ static void rwebpad_joypad_get_buttons(unsigned port, input_bits_t *state) { EmscriptenGamepadEvent gamepad_state; unsigned i; - if (port >= DEFAULT_MAX_PADS || !rwebpad_joypad_data->live_pads[port]) { + if (port >= DEFAULT_MAX_PADS || !rwebpad_joypad_data->live_pads[port]) + { BIT256_CLEAR_ALL_PTR(state); return; } @@ -151,9 +163,8 @@ static int16_t rwebpad_joypad_axis_state( static int16_t rwebpad_joypad_axis(unsigned port, uint32_t joyaxis) { - if (port >= DEFAULT_MAX_PADS || !rwebpad_joypad_data->live_pads[port]) { + if (port >= DEFAULT_MAX_PADS || !rwebpad_joypad_data->live_pads[port]) return 0; - } return rwebpad_joypad_axis_state(&rwebpad_joypad_data->pads[port], port, joyaxis); } @@ -192,14 +203,41 @@ static int16_t rwebpad_joypad_state( return ret; } +static void rwebpad_joypad_update_rumble(unsigned port) +{ + bool rumble_old_state = rwebpad_joypad_data->rumble[port].strong || rwebpad_joypad_data->rumble[port].weak; + bool rumble_new_state = rwebpad_joypad_data->rumble[port].pending_strong || rwebpad_joypad_data->rumble[port].pending_weak; + rwebpad_joypad_data->rumble[port].strong = rwebpad_joypad_data->rumble[port].pending_strong; + rwebpad_joypad_data->rumble[port].weak = rwebpad_joypad_data->rumble[port].pending_weak; + + if (rumble_new_state) + { + EM_ASM({ + try { + JSEvents?.lastGamepadState?.[$0]?.vibrationActuator?.playEffect?.("dual-rumble", {startDelay: 0, duration: 200, strongMagnitude: $1 / 65536, weakMagnitude: $2 / 65536}); + } catch (e) {} + }, port, rwebpad_joypad_data->rumble[port].strong, rwebpad_joypad_data->rumble[port].weak); + } + else if (rumble_old_state && !rumble_new_state) + { + EM_ASM({ + try { + JSEvents?.lastGamepadState?.[$0]?.vibrationActuator?.reset?.(); + } catch (e) {} + }, port); + } +} + static void rwebpad_joypad_do_poll(void *data) { - int i; + unsigned port; emscripten_sample_gamepad_data(); - for (i = 0; i < DEFAULT_MAX_PADS; i++) + for (port = 0; port < DEFAULT_MAX_PADS; port++) { - if (rwebpad_joypad_data->live_pads[i]) - emscripten_get_gamepad_status(i, &rwebpad_joypad_data->pads[i]); + if (!rwebpad_joypad_data->live_pads[port]) + continue; + emscripten_get_gamepad_status(port, &rwebpad_joypad_data->pads[port]); + rwebpad_joypad_update_rumble(port); } } @@ -208,9 +246,29 @@ static void rwebpad_joypad_poll(void) platform_emscripten_run_on_browser_thread_sync(rwebpad_joypad_do_poll, NULL); } -static bool rwebpad_joypad_query_pad(unsigned pad) +static bool rwebpad_joypad_query_pad(unsigned port) { - return rwebpad_joypad_data->live_pads[pad]; + return rwebpad_joypad_data->live_pads[port]; +} + +static bool rwebpad_joypad_set_rumble(unsigned port, enum retro_rumble_effect effect, uint16_t strength) +{ + if (port >= DEFAULT_MAX_PADS || !rwebpad_joypad_data->live_pads[port]) + return false; + + switch (effect) + { + case RETRO_RUMBLE_STRONG: + rwebpad_joypad_data->rumble[port].pending_strong = strength; + break; + case RETRO_RUMBLE_WEAK: + rwebpad_joypad_data->rumble[port].pending_weak = strength; + break; + default: + return false; + } + + return true; } static void rwebpad_joypad_destroy(void) { } @@ -224,7 +282,7 @@ input_device_driver_t rwebpad_joypad = { rwebpad_joypad_get_buttons, rwebpad_joypad_axis, rwebpad_joypad_poll, - NULL, /* set_rumble */ + rwebpad_joypad_set_rumble, NULL, /* set_rumble_gain */ NULL, /* set_sensor_state */ NULL, /* get_sensor_input */ diff --git a/input/input_driver.h b/input/input_driver.h index 8835a96505..14f6f57a0d 100644 --- a/input/input_driver.h +++ b/input/input_driver.h @@ -96,6 +96,8 @@ #define DEFAULT_MAX_PADS 4 #elif defined(DINGUX) #define DEFAULT_MAX_PADS 2 +#elif defined(EMSCRIPTEN) +#define DEFAULT_MAX_PADS 4 #else #define DEFAULT_MAX_PADS 16 #endif /* defined(ANDROID) */ diff --git a/libretro-common/include/retro_timers.h b/libretro-common/include/retro_timers.h index 4882b483ea..2ff7d76f3d 100644 --- a/libretro-common/include/retro_timers.h +++ b/libretro-common/include/retro_timers.h @@ -39,9 +39,6 @@ #include #elif defined(_3DS) #include <3ds.h> -#elif defined(EMSCRIPTEN) -#include -#include #else #include #endif @@ -101,18 +98,15 @@ static int nanosleepDOS(const struct timespec *rqtp, struct timespec *rmtp) #define retro_sleep(msec) (usleep(1000 * (msec))) #elif defined(WIIU) #define retro_sleep(msec) (OSSleepTicks(ms_to_ticks((msec)))) -#elif defined(EMSCRIPTEN) && defined(EMSCRIPTEN_ASYNCIFY) && !defined(HAVE_THREADS) -#define retro_sleep(msec) (emscripten_sleep(msec)) -#elif defined(EMSCRIPTEN) && defined(EMSCRIPTEN_ASYNCIFY) -static INLINE void retro_sleep(unsigned msec) -{ - if (emscripten_is_main_browser_thread()) - emscripten_sleep(msec); - else - emscripten_thread_sleep(msec); +#elif defined(__EMSCRIPTEN__) +/* defined in frontend */ +#ifdef __cplusplus +extern "C" { +#endif +void retro_sleep(unsigned msec); +#ifdef __cplusplus } -#elif defined(EMSCRIPTEN) -#define retro_sleep(msec) (emscripten_thread_sleep(msec)) +#endif #else static INLINE void retro_sleep(unsigned msec) { diff --git a/pkg/emscripten/libretro-thread/index.html b/pkg/emscripten/libretro-thread/index.html index 211adf914a..07856601df 100644 --- a/pkg/emscripten/libretro-thread/index.html +++ b/pkg/emscripten/libretro-thread/index.html @@ -194,7 +194,7 @@
- +
diff --git a/pkg/emscripten/libretro-thread/libretro.css b/pkg/emscripten/libretro-thread/libretro.css index cf08d09936..385f4277d0 100644 --- a/pkg/emscripten/libretro-thread/libretro.css +++ b/pkg/emscripten/libretro-thread/libretro.css @@ -339,6 +339,9 @@ label { left: 50%; top: 50%; transform: translate(-50%, -50%); + left: round(50%, 1px); + top: round(50%, 1px); + transform: translate(round(-50%, 1px), round(-50%, 1px)); max-height: calc(100vh - 40px); max-width: calc(100vw - 40px); width: 750px; @@ -388,6 +391,19 @@ label { margin-top: 0px; } +/* debug */ + +.retroarchWebcamVideo, .retroarchWebcamCanvas { + position: absolute; + left: 0px; + top: 0px; + z-index: 5000; +} + +.retroarchWebcamCanvas { + top: 112px; +} + /* mobile */ @media only screen and (max-width: 720px) { diff --git a/pkg/emscripten/libretro-thread/libretro.js b/pkg/emscripten/libretro-thread/libretro.js index 324c4cc44a..7ca538b7ec 100644 --- a/pkg/emscripten/libretro-thread/libretro.js +++ b/pkg/emscripten/libretro-thread/libretro.js @@ -170,6 +170,10 @@ var Module = { module.ENV["OPFS_MOUNT"] = "/home/web_user"; } ], + locateFile: function(path, prefix) { + if (path.endsWith(".js")) return typeof this.mainScriptUrlOrBlob == "string" ? this.mainScriptUrlOrBlob : URL.createObjectURL(this.mainScriptUrlOrBlob); + return path; + }, onRuntimeInitialized: function() { appInitialized(); }, @@ -313,19 +317,15 @@ function startRetroArch() { btnMenu.classList.remove("disabled"); btnMenu.addEventListener("click", function() { - Module._cmd_toggle_menu(); + Module.retroArchSend("MENU_TOGGLE"); }); btnFullscreen.classList.remove("disabled"); btnFullscreen.addEventListener("click", function() { - Module.requestFullscreen(false); + Module.retroArchSend("FULLSCREEN_TOGGLE"); }); - // ensure the canvas is focused so that keyboard events work - Module.canvas.focus(); - Module.canvas.addEventListener("pointerdown", function() { - Module.canvas.focus(); - }, false); + // refocus the canvas so that keyboard events work menuBar.addEventListener("pointerdown", function() { setTimeout(function() { Module.canvas.focus(); diff --git a/pkg/emscripten/libretro/index.html b/pkg/emscripten/libretro/index.html index 31cd1bda9b..07e6803a1d 100644 --- a/pkg/emscripten/libretro/index.html +++ b/pkg/emscripten/libretro/index.html @@ -188,7 +188,7 @@ Show Top Navigation - + RetroArch Logo diff --git a/pkg/emscripten/libretro/libretro.js b/pkg/emscripten/libretro/libretro.js index d4dcb946fb..aa252ab5e0 100644 --- a/pkg/emscripten/libretro/libretro.js +++ b/pkg/emscripten/libretro/libretro.js @@ -187,18 +187,14 @@ function startRetroArch() { selectFiles(e.target.files); }); $('#btnMenu').removeClass("disabled").removeAttr("disabled").click(function() { - Module._cmd_toggle_menu(); + Module.retroArchSend("MENU_TOGGLE"); Module.canvas.focus(); }); $('#btnFullscreen').removeClass("disabled").removeAttr("disabled").click(function() { - Module.requestFullscreen(false); + Module.retroArchSend("FULLSCREEN_TOGGLE"); Module.canvas.focus(); }); - Module.canvas.focus(); - Module.canvas.addEventListener("pointerdown", function() { - Module.canvas.focus(); - }, false); Module.callMain(Module.arguments); } diff --git a/retroarch.c b/retroarch.c index e23408e7d5..65f455c8fd 100644 --- a/retroarch.c +++ b/retroarch.c @@ -6029,7 +6029,11 @@ void emscripten_mainloop(void) bool runloop_is_slowmotion = (runloop_flags & RUNLOOP_FLAG_SLOWMOTION) ? true : false; bool runloop_is_paused = (runloop_flags & RUNLOOP_FLAG_PAUSED) ? true : false; - /* firefox especially seems to bug without this */ + /* Prevents the program from running in any of the following conditions: + * 1. requestAnimationFrame is being used and the window is not visible. + * Firefox likes to call requestAnimationFrame at 1 FPS when the window isn't focused, + * we want to avoid this. + * 2. The GL context is lost and hasn't been recovered yet. */ if (platform_emscripten_should_drop_iter()) return;