Emscripten improvements pt. 4

This commit is contained in:
BinBashBanana 2025-05-08 11:01:43 -07:00
parent 7cffc6bb60
commit 2d3c18e48d
23 changed files with 851 additions and 400 deletions

View File

@ -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

View File

@ -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 $@ \<obj\> $(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 $@ \<obj\> $(libretro_new) $(LIBS) $(LDFLAGS)"),)
$(Q)$(LD) -o $@ $(RARCH_OBJ) $(libretro_new) $(LIBS) $(LDFLAGS)
endif
$(OBJDIR)/%.o: %.c
@mkdir -p $(dir $@)

View File

@ -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;

View File

@ -14,6 +14,7 @@
* If not, see <http://www.gnu.org/licenses/>.
*/
#include <stdlib.h>
#include <stdint.h>
#include <boolean.h>
@ -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)

View File

@ -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

View File

@ -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);

View File

@ -168,4 +168,4 @@ var LibraryRWebAudio = {
};
autoAddDeps(LibraryRWebAudio, '$RA');
mergeInto(LibraryManager.library, LibraryRWebAudio);
addToLibrary(LibraryRWebAudio);

View File

@ -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);

View File

@ -1,134 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>RetroArch Web Player</title>
<script type="text/javascript" src="browserfs.js"></script>
<style>
.emscripten { padding-right: 0; margin-left: auto; margin-right: auto; display: block; }
textarea.emscripten { border: 0px; font-family: 'Share Tech Mono'; font-size: 12px; width: 100%; overflow:hide; resize:none; color:black; }
div.emscripten, h1 { text-align: left; }
div.canvas_border { background-color:gray; width:800px; height:600px; margin-left: auto; margin-right: auto; }
canvas.emscripten { border: 0px none; }
</style>
</head>
<body>
<hr/>
<div class="emscripten_border" id="canvas_div" style="display: none">
<canvas class="emscripten" id="canvas" tabindex="1" oncontextmenu="event.preventDefault()"></canvas>
</div>
<div class="emscripten emscripten_border" id="openrom">
<button id="btnLoad" onclick="document.getElementById('rom').click()">Upload Content</button>
<input style="display: none" type="file" id="rom" name="upload" onclick="document.getElementById('btnLoad').click();" onchange="runEmulator(event.target.files);" multiple />
<button id="btnStart" onclick="startRetroArch()">Start RetroArch</button>
</div>
<hr/>
<div class="emscripten">
<input type="checkbox" id="resize"><label for="resize">Resize canvas</label>
<input type="checkbox" id="pointerLock" checked><label for="pointerLock">Lock/hide mouse pointer</label>
<input type="button" value="Fullscreen" onclick="Module.requestFullScreen(document.getElementById('pointerLock').checked, document.getElementById('resize').checked)"><br>
<input type="checkbox" id="vsync"><label for="vsync" id="vsync-label">Enable V-sync (can only be done before loading game)</label><br>
<input type="checkbox" id="sdl2"><label for="sdl2" id="sdl2-label">Enable SDL2</label><br>
<input type="textbox" id="latency" size="3" maxlength="3" value="96"> <label for="latency" id="latency-label">Audio latency (ms) (increase if you hear pops at fullspeed, can only be done before loading game)</label>
</div>
<div class="emscripten" id="controls">
Controls:<br>
<br>
A button (OK in menu): X<br>
B button (Back in menu): Z<br>
X Button: S<br>
Y Button: A<br>
L Button: Q<br>
R Button: W<br>
D-pad: Arrow Keys<br>
Start Button: Enter<br>
Select Button: Shift<br>
Toggle Menu: F1<br>
Fast forward: Spacebar (toggle)<br>
Slow motion: E (hold)</br>
Save state: F2<br>
Load state: F4
</div>
</body>
</html>
<script type='text/javascript'>
var count = 0;
function runEmulator(files)
{
count = files.length;
document.getElementById("openrom").innerHTML = '';
document.getElementById("openrom").style.display = 'none';
for (var i = 0; i < files.length; i++)
{
filereader = new FileReader();
filereader.file_name = files[i].name;
filereader.onload = function(){uploadData(this.result, this.file_name)};
filereader.readAsArrayBuffer(files[i]);
}
}
function startRetroArch()
{
Module.FS_createFolder('/', 'etc', true, true);
Module.FS_createFolder('/', 'config', true, true);
Module.FS_createFolder('/', 'content', true, true);
Module.FS_createFolder('/', 'savefiles', true, true);
Module.FS_createFolder('/', 'savestates', true, true);
var config = 'input_player1_select = shift\n';
var latency = parseInt(document.getElementById('latency').value, 10);
if (isNaN(latency)) latency = 96;
config += 'audio_latency = ' + latency + '\n'
if (document.getElementById('vsync').checked)
config += 'video_vsync = true\n';
else
config += 'video_vsync = false\n';
Module.FS_createDataFile('/etc', 'retroarch.cfg', config, true, true);
document.getElementById('canvas_div').style.display = 'block';
document.getElementById('vsync').disabled = true;
document.getElementById('vsync-label').style.color = 'gray';
document.getElementById('latency').disabled = true;
document.getElementById('latency-label').style.color = 'gray';
Module['callMain'](Module['arguments']);
}
function uploadData(data, name)
{
var dataView = new Uint8Array(data);
Module.FS_createDataFile('/', name, dataView, true, false);
}
var Module =
{
noInitialRun: true,
arguments: ["-v", "--menu"],
preRun: [],
postRun: [],
print: (function()
{
return function(text)
{
text = Array.prototype.slice.call(arguments).join(' ');
console.log(text);
};
})(),
printErr: function(text)
{
var errArgs = Array.prototype.slice.call(arguments).join(' ');
console.error(errArgs);
},
canvas: document.getElementById('canvas'),
totalDependencies: 0,
monitorRunDependencies: function(left)
{
this.totalDependencies = Math.max(this.totalDependencies, left);
}
};
</script>
<script type="text/javascript" src="browserfs.js"></script>
<script type="text/javascript" src="gambatte.js"></script>

View File

@ -17,6 +17,7 @@
#include <emscripten/emscripten.h>
#include <emscripten/html5.h>
#include <emscripten/threading.h>
#include <string.h>
#include <malloc.h>
#include <stdio.h>
@ -60,7 +61,6 @@
#ifdef PROXY_TO_PTHREAD
#include <emscripten/wasm_worker.h>
#include <emscripten/threading.h>
#include <emscripten/proxying.h>
#include <emscripten/atomic.h>
#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

View File

@ -20,6 +20,23 @@
#include <boolean.h>
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

View File

@ -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 */
};

View File

@ -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,

View File

@ -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,

View File

@ -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 */

View File

@ -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) */

View File

@ -39,9 +39,6 @@
#include <psp2/kernel/threadmgr.h>
#elif defined(_3DS)
#include <3ds.h>
#elif defined(EMSCRIPTEN)
#include <emscripten/emscripten.h>
#include <emscripten/threading.h>
#else
#include <time.h>
#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)
{

View File

@ -194,7 +194,7 @@
</div>
</div>
<div class="webplayer-container">
<canvas class="webplayer" id="canvas" tabindex="1"></canvas>
<canvas class="webplayer" id="canvas"></canvas>
<div id="webplayer-preview"></div>
</div>
<script src="libretro.js"></script>

View File

@ -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) {

View File

@ -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();

View File

@ -188,7 +188,7 @@
<span class="fa fa-chevron-down" id="icnShowMenu"></span> <span class="sr-only">Show Top Navigation</span>
</button>
</div>
<canvas class="webplayer" id="canvas" tabindex="1" oncontextmenu="event.preventDefault()" style="display: none"></canvas>
<canvas class="webplayer" id="canvas" style="display: none"></canvas>
<img class="webplayer-preview img-fluid" src="media/canvas.png" width="960" height="720" alt="RetroArch Logo">
</div>
</div>

View File

@ -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);
}

View File

@ -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;