mirror of
https://github.com/libretro/RetroArch
synced 2025-03-14 10:21:46 +00:00
Threaded emscripten improvements
This commit is contained in:
parent
a6316465a3
commit
f6eb1ed5c5
@ -13,8 +13,9 @@ EOPTS = $(addprefix -s $(EMPTY), $(EOPT)) # Add '-s ' to each option
|
||||
|
||||
OS = Emscripten
|
||||
OBJ :=
|
||||
DEFINES := -DRARCH_INTERNAL -DHAVE_MAIN
|
||||
DEFINES += -DHAVE_FILTERS_BUILTIN
|
||||
DEFINES := -DRARCH_INTERNAL -DHAVE_MAIN -DEMSCRIPTEN -DNO_CANVAS_RESIZE
|
||||
DEFINES += -DHAVE_FILTERS_BUILTIN -DHAVE_ONLINE_UPDATER -DHAVE_UPDATE_ASSETS -DHAVE_UPDATE_CORE_INFO
|
||||
|
||||
HAVE_PATCH = 1
|
||||
HAVE_DSP_FILTER = 1
|
||||
HAVE_VIDEO_FILTER = 1
|
||||
@ -29,7 +30,7 @@ HAVE_SCREENSHOTS = 1
|
||||
HAVE_REWIND = 1
|
||||
HAVE_AUDIOMIXER = 1
|
||||
HAVE_CC_RESAMPLER = 1
|
||||
HAVE_EGL ?= 1
|
||||
HAVE_EGL ?= 0
|
||||
HAVE_OPENGLES = 1
|
||||
HAVE_RJPEG = 0
|
||||
HAVE_RPNG = 1
|
||||
@ -54,13 +55,8 @@ HAVE_7ZIP = 1
|
||||
HAVE_BSV_MOVIE = 1
|
||||
HAVE_AL = 1
|
||||
HAVE_CHD ?= 0
|
||||
HAVE_WASMFS ?= 0
|
||||
PROXY_TO_PTHREAD ?= 0
|
||||
HAVE_NETPLAYDISCOVERY ?= 0
|
||||
|
||||
DEFINES += -DHAVE_NETWORKING -DHAVE_ONLINE_UPDATER -DHAVE_UPDATE_ASSETS -DHAVE_COMPRESSION
|
||||
DEFINES += -DHAVE_UPDATE_CORE_INFO
|
||||
|
||||
# WARNING -- READ BEFORE ENABLING
|
||||
# The rwebaudio driver is known to have several audio bugs, such as
|
||||
# minor crackling, or the entire page freezing/crashing.
|
||||
@ -78,9 +74,13 @@ FS_DEBUG = 0
|
||||
HAVE_OPENGLES ?= 1
|
||||
HAVE_OPENGLES3 ?= 0
|
||||
|
||||
ASYNC ?= 1
|
||||
HAVE_WASMFS ?= 0
|
||||
PROXY_TO_PTHREAD ?= 0
|
||||
|
||||
ASYNC ?= 0
|
||||
LTO ?= 0
|
||||
PTHREAD ?= 0
|
||||
PTHREAD_POOL_SIZE ?= 4
|
||||
|
||||
STACK_SIZE ?= 4194304
|
||||
INITIAL_HEAP ?= 134217728
|
||||
@ -102,55 +102,57 @@ 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,\
|
||||
_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
|
||||
_cmd_cheat_get_size,_cmd_cheat_apply_cheats,_update_canvas_dimensions,_update_window_hidden,_update_power_state,_update_memory_usage,\
|
||||
EmscriptenSendCommand,EmscriptenReceiveCommandReply
|
||||
|
||||
EXPORTS := callMain,FS,PATH,ERRNO_CODES,ENV,stringToNewUTF8,UTF8ToString,Browser,GL,EmscriptenSendCommand,EmscriptenReceiveCommandReply
|
||||
EXPORTS := callMain,FS,PATH,ERRNO_CODES,ENV,stringToNewUTF8,UTF8ToString,Browser,EmscriptenSendCommand,EmscriptenReceiveCommandReply
|
||||
|
||||
LIBS := -s USE_ZLIB=1 -lbrowser.js
|
||||
LIBS := -s USE_ZLIB=1
|
||||
|
||||
ifeq ($(HAVE_WASMFS), 1)
|
||||
DEFINES += -DHAVE_WASMFS=1
|
||||
LIBS += -sWASMFS -sFORCE_FILESYSTEM=1 -lfetchfs.js -lopfs.js
|
||||
EXPORTS := $(EXPORTS),FETCHFS,OPFS
|
||||
ifeq ($(PTHREAD),0)
|
||||
$(error ERROR: WASMFS requires threading support)
|
||||
LIBS += -s WASMFS -s FORCE_FILESYSTEM=1 -lfetchfs.js -lopfs.js
|
||||
DEFINES += -DHAVE_WASMFS
|
||||
ifeq ($(PROXY_TO_PTHREAD), 0)
|
||||
$(error ERROR: WASMFS requires PROXY_TO_PTHREAD)
|
||||
endif
|
||||
endif
|
||||
|
||||
ifeq ($(PROXY_TO_PTHREAD),1)
|
||||
LIBS += -sENVIRONMENT=worker,web
|
||||
LIBS += -sPROXY_TO_PTHREAD -sOFFSCREENCANVAS_SUPPORT
|
||||
DEFINES += -DUSE_OFFSCREENCANVAS=1 -DPROXY_TO_PTHREAD=1
|
||||
else
|
||||
# note: real PROXY_TO_PTHREAD is not used here; we do the pthread management ourselves
|
||||
ifeq ($(PROXY_TO_PTHREAD), 1)
|
||||
LIBS += -s OFFSCREENCANVAS_SUPPORT
|
||||
DEFINES += -DPROXY_TO_PTHREAD -DEMSCRIPTEN_STACK_SIZE=$(STACK_SIZE)
|
||||
override PTHREAD = 1
|
||||
override STACK_SIZE = 4194304
|
||||
else ifeq ($(HAVE_AL), 1)
|
||||
override ASYNC = 1
|
||||
endif
|
||||
|
||||
|
||||
ifeq ($(HAVE_SDL2), 1)
|
||||
LIBS += -s USE_SDL=2
|
||||
DEFINES += -DHAVE_SDL2
|
||||
endif
|
||||
|
||||
|
||||
LDFLAGS := -L. --no-heap-copy -s STACK_SIZE=$(STACK_SIZE) -s INITIAL_MEMORY=$(INITIAL_HEAP) \
|
||||
-s EXPORTED_RUNTIME_METHODS=$(EXPORTS) \
|
||||
-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 \
|
||||
--extern-pre-js emscripten/pre.js \
|
||||
--js-library emscripten/library_rwebcam.js \
|
||||
-gsource-map -g2 \
|
||||
--js-library emscripten/library_platform_emscripten.js
|
||||
|
||||
ifeq ($(HAVE_OPENGLES), 1)
|
||||
ifeq ($(HAVE_OPENGLES3), 1)
|
||||
LDFLAGS += -s FULL_ES3=1 -s MIN_WEBGL_VERSION=2 -s MAX_WEBGL_VERSION=2 -lGL
|
||||
LDFLAGS += -s FULL_ES3=1 -s MIN_WEBGL_VERSION=2 -s MAX_WEBGL_VERSION=2
|
||||
else
|
||||
LDFLAGS += -s FULL_ES2=1 -s MIN_WEBGL_VERSION=1 -s MAX_WEBGL_VERSION=2 -lGL
|
||||
LDFLAGS += -s FULL_ES2=1 -s MIN_WEBGL_VERSION=1 -s MAX_WEBGL_VERSION=2
|
||||
endif
|
||||
endif
|
||||
|
||||
ifeq ($(GL_DEBUG), 1)
|
||||
LDFLAGS += -s GL_ASSERTIONS=1 -s GL_DEBUG=1 -DHAVE_GL_DEBUG_ES=1
|
||||
LDFLAGS += -s GL_ASSERTIONS=1 -s GL_DEBUG=1
|
||||
DEFINES += -DHAVE_GL_DEBUG_ES=1
|
||||
endif
|
||||
|
||||
ifeq ($(FS_DEBUG), 1)
|
||||
@ -167,20 +169,19 @@ ifeq ($(HAVE_AL), 1)
|
||||
DEFINES += -DHAVE_AL
|
||||
endif
|
||||
|
||||
ifneq ($(PTHREAD), 0)
|
||||
LDFLAGS += -s WASM_MEM_MAX=1073741824 -pthread -s PTHREAD_POOL_SIZE=$(PTHREAD)
|
||||
ifeq ($(PTHREAD), 1)
|
||||
LDFLAGS += -pthread -s PTHREAD_POOL_SIZE=$(PTHREAD_POOL_SIZE)
|
||||
CFLAGS += -pthread -s SHARED_MEMORY
|
||||
HAVE_THREADS=1
|
||||
HAVE_THREADS = 1
|
||||
else
|
||||
HAVE_THREADS=0
|
||||
HAVE_THREADS = 0
|
||||
endif
|
||||
|
||||
|
||||
ifeq ($(ASYNC), 1)
|
||||
DEFINES += -DEMSCRIPTEN_ASYNCIFY
|
||||
LDFLAGS += -s ASYNCIFY=$(ASYNC) -s ASYNCIFY_STACK_SIZE=8192
|
||||
LDFLAGS += -s ASYNCIFY=1 -s ASYNCIFY_STACK_SIZE=8192
|
||||
ifeq ($(DEBUG), 1)
|
||||
LDFLAGS += -s ASYNCIFY_DEBUG=1 # -s ASYNCIFY_ADVISE
|
||||
# LDFLAGS += -s ASYNCIFY_DEBUG=1 # -s ASYNCIFY_ADVISE
|
||||
endif
|
||||
endif
|
||||
|
||||
|
@ -213,14 +213,16 @@ for f in `ls -v *_${platform}.${EXT}`; do
|
||||
big_stack="BIG_STACK=1"
|
||||
fi
|
||||
if [ $PLATFORM = "emscripten" ]; then
|
||||
async=0
|
||||
pthread=${pthread:-0}
|
||||
gles3=0
|
||||
async=${ASYNC:-0}
|
||||
pthread=${PTHREAD:-0}
|
||||
proxy_to_pthread=${PROXY_TO_PTHREAD:-0}
|
||||
gles3=${HAVE_OPENGLES3:-0}
|
||||
stack_mem=4194304
|
||||
heap_mem=134217728
|
||||
if [ $name = "mupen64plus_next" ] ; then
|
||||
gles3=1
|
||||
async=1
|
||||
#async=1
|
||||
#proxy_to_pthread=0
|
||||
stack_mem=134217728
|
||||
heap_mem=268435456
|
||||
elif [ $name = "parallel_n64" ] ; then
|
||||
@ -248,6 +250,7 @@ for f in `ls -v *_${platform}.${EXT}`; do
|
||||
if [ $PLATFORM = "emscripten" ]; then
|
||||
echo ASYNC: $async
|
||||
echo PTHREAD: $pthread
|
||||
echo PROXY_TO_PTHREAD: $proxy_to_pthread
|
||||
echo GLES3: $gles3
|
||||
echo STACK_MEMORY: $stack_mem
|
||||
echo HEAP_MEMORY: $heap_mem
|
||||
@ -270,8 +273,8 @@ for f in `ls -v *_${platform}.${EXT}`; do
|
||||
if [ $MAKEFILE_GRIFFIN = "yes" ]; then
|
||||
make -C ../ -f Makefile.griffin $OPTS platform=${platform} $whole_archive $big_stack -j3 || exit 1
|
||||
elif [ $PLATFORM = "emscripten" ]; then
|
||||
echo "BUILD COMMAND: make -C ../ -f Makefile.emscripten PTHREAD=$pthread ASYNC=$async LTO=$lto HAVE_OPENGLES3=$gles3 STACK_SIZE=$stack_mem INITIAL_HEAP=$heap_mem -j7 LIBRETRO=${name} TARGET=${name}_libretro.js"
|
||||
make -C ../ -f Makefile.emscripten $OPTS PTHREAD=$pthread ASYNC=$async LTO=$lto HAVE_OPENGLES3=$gles3 STACK_SIZE=$stack_mem INITIAL_HEAP=$heap_mem -j7 LIBRETRO=${name} TARGET=${name}_libretro.js || exit 1
|
||||
echo "BUILD COMMAND: make -C ../ -f Makefile.emscripten $OPTS LTO=$lto ASYNC=$async PTHREAD=$pthread PROXY_TO_PTHREAD=$proxy_to_pthread HAVE_OPENGLES3=$gles3 STACK_SIZE=$stack_mem INITIAL_HEAP=$heap_mem -j7 LIBRETRO=${name} TARGET=${name}_libretro.js"
|
||||
make -C ../ -f Makefile.emscripten $OPTS LTO=$lto ASYNC=$async PTHREAD=$pthread PROXY_TO_PTHREAD=$proxy_to_pthread HAVE_OPENGLES3=$gles3 STACK_SIZE=$stack_mem INITIAL_HEAP=$heap_mem -j7 LIBRETRO=${name} TARGET=${name}_libretro.js || exit 1
|
||||
elif [ $PLATFORM = "unix" ]; then
|
||||
make -C ../ -f Makefile LINK=g++ $whole_archive $big_stack -j3 || exit 1
|
||||
elif [ $PLATFORM = "ctr" ]; then
|
||||
@ -338,7 +341,7 @@ for f in `ls -v *_${platform}.${EXT}`; do
|
||||
mkdir -p ../pkg/emscripten/
|
||||
mv -f ../${name}_libretro.js ../pkg/emscripten/${name}_libretro.js
|
||||
mv -f ../${name}_libretro.wasm ../pkg/emscripten/${name}_libretro.wasm
|
||||
if [ $pthread != 0 ] ; then
|
||||
if [ -f ../${name}_libretro.worker.js ] ; then
|
||||
mv -f ../${name}_libretro.worker.js ../pkg/emscripten/${name}_libretro.worker.js
|
||||
fi
|
||||
if [ -f ../${name}_libretro.wasm.map ] ; then
|
||||
|
@ -2,19 +2,54 @@
|
||||
|
||||
var LibraryPlatformEmscripten = {
|
||||
$RPE: {
|
||||
powerState: {
|
||||
supported: false,
|
||||
dischargeTime: 0,
|
||||
level: 0,
|
||||
charging: false
|
||||
},
|
||||
powerStateChange: function(e) {
|
||||
RPE.powerState.dischargeTime = Number.isFinite(e.target.dischargingTime) ? e.target.dischargingTime : 0x7FFFFFFF;
|
||||
RPE.powerState.level = e.target.level;
|
||||
RPE.powerState.charging = e.target.charging;
|
||||
Module._update_power_state(true, Number.isFinite(e.target.dischargingTime) ? e.target.dischargingTime : 0x7FFFFFFF, e.target.level, e.target.charging);
|
||||
},
|
||||
command_queue:[],
|
||||
command_reply_queue:[],
|
||||
|
||||
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
|
||||
Module._update_memory_usage(Number(used & 0xFFFFFFFFn), Number(used >> 32n), Number(limit & 0xFFFFFFFFn), Number(limit >> 32n));
|
||||
setTimeout(RPE.updateMemoryUsage, 5000);
|
||||
},
|
||||
command_queue: [],
|
||||
command_reply_queue: []
|
||||
},
|
||||
|
||||
PlatformEmscriptenWatchCanvasSizeAndDpr: function(dpr) {
|
||||
if (RPE.observer) {
|
||||
RPE.observer.unobserve(Module.canvas);
|
||||
RPE.observer.observe(Module.canvas);
|
||||
return;
|
||||
}
|
||||
RPE.observer = new ResizeObserver(function(e) {
|
||||
var width, height;
|
||||
var entry = e.find(i => i.target == Module.canvas);
|
||||
if (!entry) return;
|
||||
if (entry.devicePixelContentBoxSize) {
|
||||
width = entry.devicePixelContentBoxSize[0].inlineSize;
|
||||
height = entry.devicePixelContentBoxSize[0].blockSize;
|
||||
} else {
|
||||
width = Math.round(entry.contentRect.width * window.devicePixelRatio);
|
||||
height = Math.round(entry.contentRect.height * window.devicePixelRatio);
|
||||
}
|
||||
// doubles are too big to pass as an argument to exported functions
|
||||
{{{ makeSetValue("dpr", "0", "window.devicePixelRatio", "double") }}};
|
||||
Module._update_canvas_dimensions(width, height, dpr);
|
||||
});
|
||||
RPE.observer.observe(Module.canvas);
|
||||
window.addEventListener("resize", function() {
|
||||
RPE.observer.unobserve(Module.canvas);
|
||||
RPE.observer.observe(Module.canvas);
|
||||
}, false);
|
||||
},
|
||||
|
||||
PlatformEmscriptenWatchWindowVisibility: function() {
|
||||
document.addEventListener("visibilitychange", function() {
|
||||
Module._update_window_hidden(document.visibilityState == "hidden");
|
||||
}, false);
|
||||
},
|
||||
|
||||
PlatformEmscriptenPowerStateInit: function() {
|
||||
@ -23,41 +58,20 @@ var LibraryPlatformEmscripten = {
|
||||
battery.addEventListener("chargingchange", RPE.powerStateChange);
|
||||
battery.addEventListener("levelchange", RPE.powerStateChange);
|
||||
RPE.powerStateChange({target: battery});
|
||||
RPE.powerState.supported = true;
|
||||
});
|
||||
},
|
||||
|
||||
PlatformEmscriptenPowerStateGetSupported: function() {
|
||||
return RPE.powerState.supported;
|
||||
PlatformEmscriptenMemoryUsageInit: function() {
|
||||
if (!performance.memory) return;
|
||||
RPE.updateMemoryUsage();
|
||||
},
|
||||
|
||||
PlatformEmscriptenPowerStateGetDischargeTime: function() {
|
||||
return RPE.powerState.dischargeTime;
|
||||
},
|
||||
|
||||
PlatformEmscriptenPowerStateGetLevel: function() {
|
||||
return RPE.powerState.level;
|
||||
},
|
||||
|
||||
PlatformEmscriptenPowerStateGetCharging: function() {
|
||||
return RPE.powerState.charging;
|
||||
},
|
||||
|
||||
PlatformEmscriptenGetTotalMem: function() {
|
||||
if (!performance.memory) return 0;
|
||||
return performance.memory.jsHeapSizeLimit || 0;
|
||||
},
|
||||
|
||||
PlatformEmscriptenGetFreeMem: function() {
|
||||
if (!performance.memory) return 0;
|
||||
return (performance.memory.jsHeapSizeLimit || 0) - (performance.memory.usedJSHeapSize || 0);
|
||||
},
|
||||
|
||||
$EmscriptenSendCommand__deps:["PlatformEmscriptenCommandRaiseFlag"],
|
||||
$EmscriptenSendCommand__deps: ["PlatformEmscriptenCommandRaiseFlag"],
|
||||
$EmscriptenSendCommand: function(str) {
|
||||
RPE.command_queue.push(str);
|
||||
_PlatformEmscriptenCommandRaiseFlag();
|
||||
},
|
||||
|
||||
$EmscriptenReceiveCommandReply: function() {
|
||||
return RPE.command_reply_queue.shift();
|
||||
}
|
||||
|
@ -17,10 +17,9 @@
|
||||
|
||||
#include <emscripten/emscripten.h>
|
||||
#include <emscripten/html5.h>
|
||||
#if HAVE_WASMFS
|
||||
#include <emscripten/wasmfs.h>
|
||||
#endif
|
||||
#include <string.h>
|
||||
#include <malloc.h>
|
||||
#include <stdio.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
@ -55,69 +54,74 @@
|
||||
#include "../../cheat_manager.h"
|
||||
#include "../../audio/audio_driver.h"
|
||||
|
||||
void emscripten_mainloop(void);
|
||||
void PlatformEmscriptenWatchCanvasSize(void) {
|
||||
MAIN_THREAD_ASYNC_EM_ASM(
|
||||
RPE.observer = new ResizeObserver(function(_e) {
|
||||
var container = Module.canvas.parentElement;
|
||||
var width = container.offsetWidth;
|
||||
var height = container.offsetHeight;
|
||||
var w = Module.canvas.width;
|
||||
var h = Module.canvas.height;
|
||||
if (w == 0 || h == 0 || width == 0 || height == 0) { return; }
|
||||
/* Module.print("Setting real canvas size: " + width + " x " + height); */
|
||||
var new_w = `${width}px`;
|
||||
var new_h = `${height}px`;
|
||||
if (Module.canvas.style.width != new_w || Module.canvas.style.height != new_h) {
|
||||
Module.canvas.style.width = new_w;
|
||||
Module.canvas.style.height = new_h;
|
||||
}
|
||||
if (!Module.canvas.controlTransferredOffscreen) {
|
||||
Module.Browser.setCanvasSize(width, height);
|
||||
}
|
||||
});
|
||||
RPE.observer.observe(Module.canvas.parentElement);
|
||||
window.addEventListener("resize", function(e) {
|
||||
RPE.observer.unobserve(Module.canvas.parentElement);
|
||||
RPE.observer.observe(Module.canvas.parentElement);
|
||||
}, false);
|
||||
);
|
||||
}
|
||||
void PlatformEmscriptenPowerStateInit(void);
|
||||
bool PlatformEmscriptenPowerStateGetSupported(void);
|
||||
int PlatformEmscriptenPowerStateGetDischargeTime(void);
|
||||
float PlatformEmscriptenPowerStateGetLevel(void);
|
||||
bool PlatformEmscriptenPowerStateGetCharging(void);
|
||||
uint64_t PlatformEmscriptenGetTotalMem(void);
|
||||
uint64_t PlatformEmscriptenGetFreeMem(void);
|
||||
#ifdef HAVE_WASMFS
|
||||
#include <emscripten/wasmfs.h>
|
||||
#endif
|
||||
|
||||
#ifdef PROXY_TO_PTHREAD
|
||||
#include <emscripten/threading.h>
|
||||
#include <emscripten/proxying.h>
|
||||
#include <emscripten/atomic.h>
|
||||
#define PLATFORM_SETVAL(type, addr, val) emscripten_atomic_store_##type(addr, val)
|
||||
#else
|
||||
#define PLATFORM_SETVAL(type, addr, val) *addr = val
|
||||
#endif
|
||||
|
||||
void emscripten_mainloop(void);
|
||||
void PlatformEmscriptenWatchCanvasSizeAndDpr(double *dpr);
|
||||
void PlatformEmscriptenWatchWindowVisibility(void);
|
||||
void PlatformEmscriptenPowerStateInit(void);
|
||||
void PlatformEmscriptenMemoryUsageInit(void);
|
||||
|
||||
void PlatformEmscriptenCommandReply(const char *msg, size_t len) {
|
||||
MAIN_THREAD_EM_ASM({
|
||||
var message = UTF8ToString($0,$1);
|
||||
RPE.command_reply_queue.push(message);
|
||||
}, msg, len);
|
||||
}
|
||||
static bool command_flag = false;
|
||||
size_t PlatformEmscriptenCommandRead(char **into, size_t max_len) {
|
||||
if(!command_flag) { return 0; }
|
||||
|
||||
void PlatformEmscriptenCommandReply(const char *msg, size_t len)
|
||||
{
|
||||
MAIN_THREAD_EM_ASM({
|
||||
var message = UTF8ToString($0, $1);
|
||||
RPE.command_reply_queue.push(message);
|
||||
}, msg, len);
|
||||
}
|
||||
|
||||
size_t PlatformEmscriptenCommandRead(char **into, size_t max_len)
|
||||
{
|
||||
if (!command_flag) { return 0; }
|
||||
return MAIN_THREAD_EM_ASM_INT({
|
||||
var next_command = RPE.command_queue.shift();
|
||||
var length = lengthBytesUTF8(next_command);
|
||||
if(length > $2) {
|
||||
console.error("[CMD] Command too long, skipping",next_command);
|
||||
if (length > $2) {
|
||||
console.error("[CMD] Command too long, skipping", next_command);
|
||||
return 0;
|
||||
}
|
||||
stringToUTF8(next_command, $1, $2);
|
||||
if(RPE.command_queue.length == 0) {
|
||||
if (RPE.command_queue.length == 0) {
|
||||
setValue($0, 0, 'i8');
|
||||
}
|
||||
return length;
|
||||
}, &command_flag, into, max_len);
|
||||
}
|
||||
void PlatformEmscriptenCommandRaiseFlag() {
|
||||
command_flag = true;
|
||||
|
||||
void PlatformEmscriptenCommandRaiseFlag()
|
||||
{
|
||||
command_flag = true;
|
||||
}
|
||||
|
||||
typedef struct
|
||||
{
|
||||
uint64_t memory_used;
|
||||
uint64_t memory_limit;
|
||||
double device_pixel_ratio;
|
||||
int canvas_width;
|
||||
int canvas_height;
|
||||
int power_state_discharge_time;
|
||||
float power_state_level;
|
||||
volatile bool power_state_charging;
|
||||
volatile bool power_state_supported;
|
||||
volatile bool window_hidden;
|
||||
} emscripten_platform_data_t;
|
||||
|
||||
static emscripten_platform_data_t *emscripten_platform_data = NULL;
|
||||
|
||||
/* begin exported functions */
|
||||
|
||||
/* saves and states */
|
||||
@ -246,50 +250,153 @@ void cmd_cheat_apply_cheats(void)
|
||||
config_get_ptr()->bools.notification_show_cheats_applied);
|
||||
}
|
||||
|
||||
/* end exported functions */
|
||||
/* javascript callbacks */
|
||||
|
||||
void update_canvas_dimensions(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);
|
||||
if (!emscripten_platform_data)
|
||||
return;
|
||||
PLATFORM_SETVAL(u32, &emscripten_platform_data->canvas_width, width);
|
||||
PLATFORM_SETVAL(u32, &emscripten_platform_data->canvas_height, height);
|
||||
PLATFORM_SETVAL(f64, &emscripten_platform_data->device_pixel_ratio, *dpr);
|
||||
}
|
||||
|
||||
void update_window_hidden(bool hidden)
|
||||
{
|
||||
if (!emscripten_platform_data)
|
||||
return;
|
||||
emscripten_platform_data->window_hidden = hidden;
|
||||
}
|
||||
|
||||
void update_power_state(bool supported, int discharge_time, float level, bool charging)
|
||||
{
|
||||
if (!emscripten_platform_data)
|
||||
return;
|
||||
emscripten_platform_data->power_state_supported = supported;
|
||||
emscripten_platform_data->power_state_charging = charging;
|
||||
PLATFORM_SETVAL(u32, &emscripten_platform_data->power_state_discharge_time, discharge_time);
|
||||
PLATFORM_SETVAL(f32, &emscripten_platform_data->power_state_level, level);
|
||||
}
|
||||
|
||||
void update_memory_usage(uint32_t used1, uint32_t used2, uint32_t limit1, uint32_t limit2)
|
||||
{
|
||||
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 specific c helpers */
|
||||
|
||||
void platform_emscripten_get_canvas_size(int *width, int *height)
|
||||
{
|
||||
if (!emscripten_platform_data ||
|
||||
(emscripten_platform_data->canvas_width == 0 && emscripten_platform_data->canvas_height == 0))
|
||||
{
|
||||
*width = 800;
|
||||
*height = 600;
|
||||
RARCH_ERR("[EMSCRIPTEN]: Could not get screen dimensions!\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
*width = emscripten_platform_data->canvas_width;
|
||||
*height = emscripten_platform_data->canvas_height;
|
||||
}
|
||||
}
|
||||
|
||||
double platform_emscripten_get_dpr(void)
|
||||
{
|
||||
return emscripten_platform_data->device_pixel_ratio;
|
||||
}
|
||||
|
||||
bool platform_emscripten_is_window_hidden(void)
|
||||
{
|
||||
return emscripten_platform_data->window_hidden;
|
||||
}
|
||||
|
||||
void platform_emscripten_run_on_browser_thread_sync(void (*func)(void*), void* arg)
|
||||
{
|
||||
#ifdef PROXY_TO_PTHREAD
|
||||
emscripten_proxy_sync(emscripten_proxy_get_system_queue(), emscripten_main_runtime_thread_id(), func, arg);
|
||||
#else
|
||||
func(arg);
|
||||
#endif
|
||||
}
|
||||
|
||||
void platform_emscripten_run_on_browser_thread_async(void (*func)(void*), void* arg)
|
||||
{
|
||||
#ifdef PROXY_TO_PTHREAD
|
||||
emscripten_proxy_async(emscripten_proxy_get_system_queue(), emscripten_main_runtime_thread_id(), func, arg);
|
||||
#else
|
||||
// for now, not async
|
||||
func(arg);
|
||||
#endif
|
||||
}
|
||||
|
||||
/* frontend driver impl */
|
||||
|
||||
static void frontend_emscripten_get_env(int *argc, char *argv[],
|
||||
void *args, void *params_data)
|
||||
{
|
||||
char base_path[PATH_MAX];
|
||||
char user_path[PATH_MAX];
|
||||
const char *home = getenv("HOME");
|
||||
char bundle_path[PATH_MAX];
|
||||
const char *home = getenv("HOME");
|
||||
|
||||
if (home)
|
||||
{
|
||||
size_t _len = strlcpy(base_path, home, sizeof(base_path));
|
||||
strlcpy(base_path + _len, "/retroarch", sizeof(base_path) - _len);
|
||||
#ifndef HAVE_WASMFS
|
||||
/* can be removed when the new web player replaces the old one */
|
||||
_len = strlcpy(user_path, home, sizeof(user_path));
|
||||
strlcpy(user_path + _len, "/retroarch/userdata", sizeof(user_path) - _len);
|
||||
_len = strlcpy(bundle_path, home, sizeof(bundle_path));
|
||||
strlcpy(bundle_path + _len, "/retroarch/bundle", sizeof(bundle_path) - _len);
|
||||
#else
|
||||
_len = strlcpy(user_path, home, sizeof(user_path));
|
||||
strlcpy(user_path + _len, "/retroarch", sizeof(user_path) - _len);
|
||||
_len = strlcpy(bundle_path, home, sizeof(bundle_path));
|
||||
strlcpy(bundle_path + _len, "/retroarch", sizeof(bundle_path) - _len);
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
strlcpy(base_path, "retroarch", sizeof(base_path));
|
||||
#ifndef HAVE_WASMFS
|
||||
/* can be removed when the new web player replaces the old one */
|
||||
strlcpy(user_path, "retroarch/userdata", sizeof(user_path));
|
||||
strlcpy(bundle_path, "retroarch/bundle", sizeof(bundle_path));
|
||||
#else
|
||||
strlcpy(user_path, "retroarch", sizeof(user_path));
|
||||
strlcpy(bundle_path, "retroarch", sizeof(bundle_path));
|
||||
#endif
|
||||
}
|
||||
|
||||
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_CORE], base_path,
|
||||
"cores", sizeof(g_defaults.dirs[DEFAULT_DIR_CORE]));
|
||||
|
||||
/* bundle data */
|
||||
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_ASSETS], base_path,
|
||||
"bundle/assets", sizeof(g_defaults.dirs[DEFAULT_DIR_ASSETS]));
|
||||
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_AUTOCONFIG], base_path,
|
||||
"bundle/autoconfig", sizeof(g_defaults.dirs[DEFAULT_DIR_AUTOCONFIG]));
|
||||
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_DATABASE], base_path,
|
||||
"bundle/database/rdb", sizeof(g_defaults.dirs[DEFAULT_DIR_DATABASE]));
|
||||
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_CORE_INFO], base_path,
|
||||
"bundle/info", sizeof(g_defaults.dirs[DEFAULT_DIR_CORE_INFO]));
|
||||
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_OVERLAY], base_path,
|
||||
"bundle/overlays", sizeof(g_defaults.dirs[DEFAULT_DIR_OVERLAY]));
|
||||
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_OSK_OVERLAY], base_path,
|
||||
"bundle/overlays/keyboards", sizeof(g_defaults.dirs[DEFAULT_DIR_OSK_OVERLAY]));
|
||||
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_SHADER], base_path,
|
||||
"bundle/shaders", sizeof(g_defaults.dirs[DEFAULT_DIR_SHADER]));
|
||||
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_AUDIO_FILTER], base_path,
|
||||
"bundle/filters/audio", sizeof(g_defaults.dirs[DEFAULT_DIR_AUDIO_FILTER]));
|
||||
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_VIDEO_FILTER], base_path,
|
||||
"bundle/filters/video", sizeof(g_defaults.dirs[DEFAULT_DIR_VIDEO_FILTER]));
|
||||
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_ASSETS], bundle_path,
|
||||
"assets", sizeof(g_defaults.dirs[DEFAULT_DIR_ASSETS]));
|
||||
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_AUTOCONFIG], bundle_path,
|
||||
"autoconfig", sizeof(g_defaults.dirs[DEFAULT_DIR_AUTOCONFIG]));
|
||||
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_DATABASE], bundle_path,
|
||||
"database/rdb", sizeof(g_defaults.dirs[DEFAULT_DIR_DATABASE]));
|
||||
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_CORE_INFO], bundle_path,
|
||||
"info", sizeof(g_defaults.dirs[DEFAULT_DIR_CORE_INFO]));
|
||||
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_OVERLAY], bundle_path,
|
||||
"overlays", sizeof(g_defaults.dirs[DEFAULT_DIR_OVERLAY]));
|
||||
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_OSK_OVERLAY], bundle_path,
|
||||
"overlays/keyboards", sizeof(g_defaults.dirs[DEFAULT_DIR_OSK_OVERLAY]));
|
||||
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_SHADER], bundle_path,
|
||||
"shaders", sizeof(g_defaults.dirs[DEFAULT_DIR_SHADER]));
|
||||
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_AUDIO_FILTER], bundle_path,
|
||||
"filters/audio", sizeof(g_defaults.dirs[DEFAULT_DIR_AUDIO_FILTER]));
|
||||
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_VIDEO_FILTER], bundle_path,
|
||||
"filters/video", sizeof(g_defaults.dirs[DEFAULT_DIR_VIDEO_FILTER]));
|
||||
|
||||
/* user data dirs */
|
||||
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_CHEATS], user_path,
|
||||
@ -299,7 +406,7 @@ static void frontend_emscripten_get_env(int *argc, char *argv[],
|
||||
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_MENU_CONTENT], user_path,
|
||||
"content", sizeof(g_defaults.dirs[DEFAULT_DIR_MENU_CONTENT]));
|
||||
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_CORE_ASSETS], user_path,
|
||||
"content/downloads", sizeof(g_defaults.dirs[DEFAULT_DIR_CORE_ASSETS]));
|
||||
"downloads", sizeof(g_defaults.dirs[DEFAULT_DIR_CORE_ASSETS]));
|
||||
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_PLAYLIST], user_path,
|
||||
"playlists", sizeof(g_defaults.dirs[DEFAULT_DIR_PLAYLIST]));
|
||||
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_REMAP], g_defaults.dirs[DEFAULT_DIR_MENU_CONFIG],
|
||||
@ -330,41 +437,82 @@ static void frontend_emscripten_get_env(int *argc, char *argv[],
|
||||
#endif
|
||||
}
|
||||
|
||||
typedef struct args {
|
||||
int argc;
|
||||
char **argv;
|
||||
} args_t;
|
||||
static bool retro_started = false;
|
||||
static bool filesystem_ready = false;
|
||||
static enum frontend_powerstate frontend_emscripten_get_powerstate(int *seconds, int *percent)
|
||||
{
|
||||
enum frontend_powerstate ret = FRONTEND_POWERSTATE_NONE;
|
||||
|
||||
#if HAVE_WASMFS
|
||||
void PlatformEmscriptenMountFilesystems(void *info) {
|
||||
char *opfs_mount = getenv("OPFS");
|
||||
if (!emscripten_platform_data || !emscripten_platform_data->power_state_supported)
|
||||
return ret;
|
||||
|
||||
if (!emscripten_platform_data->power_state_charging)
|
||||
ret = FRONTEND_POWERSTATE_ON_POWER_SOURCE;
|
||||
else if (emscripten_platform_data->power_state_level == 1)
|
||||
ret = FRONTEND_POWERSTATE_CHARGED;
|
||||
else
|
||||
ret = FRONTEND_POWERSTATE_CHARGING;
|
||||
|
||||
*seconds = emscripten_platform_data->power_state_discharge_time;
|
||||
*percent = (int)(emscripten_platform_data->power_state_level * 100);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static uint64_t frontend_emscripten_get_total_mem(void)
|
||||
{
|
||||
if (!emscripten_platform_data)
|
||||
return 0;
|
||||
return emscripten_platform_data->memory_limit;
|
||||
}
|
||||
|
||||
static uint64_t frontend_emscripten_get_free_mem(void)
|
||||
{
|
||||
if (!emscripten_platform_data)
|
||||
return 0;
|
||||
#ifndef PROXY_TO_PTHREAD
|
||||
uint64_t used = emscripten_platform_data->memory_used;
|
||||
#else
|
||||
uint64_t used = mallinfo().uordblks;
|
||||
#endif
|
||||
return (emscripten_platform_data->memory_limit - used);
|
||||
}
|
||||
|
||||
/* program entry and startup */
|
||||
|
||||
#ifdef HAVE_WASMFS
|
||||
void platform_emscripten_mount_filesystems(void)
|
||||
{
|
||||
char *opfs_mount = getenv("OPFS_MOUNT");
|
||||
char *fetch_manifest = getenv("FETCH_MANIFEST");
|
||||
if(opfs_mount) {
|
||||
if (opfs_mount)
|
||||
{
|
||||
int res;
|
||||
printf("[OPFS] Mount OPFS at %s\n", opfs_mount);
|
||||
backend_t opfs = wasmfs_create_opfs_backend();
|
||||
{
|
||||
char *parent = strdup(opfs_mount);
|
||||
path_parent_dir(parent, strlen(parent));
|
||||
if(!path_mkdir(parent)) {
|
||||
printf("mkdir error %d\n",errno);
|
||||
if (!path_mkdir(parent))
|
||||
{
|
||||
printf("mkdir error %d\n", errno);
|
||||
abort();
|
||||
}
|
||||
free(parent);
|
||||
}
|
||||
res = wasmfs_create_directory(opfs_mount, 0777, opfs);
|
||||
if(res) {
|
||||
printf("[OPFS] error result %d\n",res);
|
||||
if(errno) {
|
||||
printf("[OPFS] errno %d\n",errno);
|
||||
if (res)
|
||||
{
|
||||
printf("[OPFS] error result %d\n", res);
|
||||
if (errno)
|
||||
{
|
||||
printf("[OPFS] errno %d\n", errno);
|
||||
abort();
|
||||
}
|
||||
abort();
|
||||
}
|
||||
}
|
||||
if(fetch_manifest) {
|
||||
#if false
|
||||
if (fetch_manifest)
|
||||
{
|
||||
/* fetch_manifest should be a path to a manifest file.
|
||||
manifest files have this format:
|
||||
|
||||
@ -376,20 +524,23 @@ void PlatformEmscriptenMountFilesystems(void *info) {
|
||||
Where URL may not contain spaces, but PATH may.
|
||||
*/
|
||||
int max_line_len = 1024;
|
||||
printf("[FetchFS] read fetch manifest from %s\n",fetch_manifest);
|
||||
printf("[FetchFS] read fetch manifest from %s\n", fetch_manifest);
|
||||
FILE *file = fopen(fetch_manifest, "r");
|
||||
if(!file) {
|
||||
if (!file)
|
||||
{
|
||||
printf("[FetchFS] missing manifest file\n");
|
||||
abort();
|
||||
}
|
||||
char *line = calloc(sizeof(char), max_line_len);
|
||||
size_t len = max_line_len;
|
||||
while (getline(&line, &len, file) != -1) {
|
||||
while (getline(&line, &len, file) != -1)
|
||||
{
|
||||
char *path = strstr(line, " ");
|
||||
backend_t fetch;
|
||||
int fd;
|
||||
if(len <= 2 || !path) {
|
||||
printf("[FetchFS] Manifest file has invalid line %s\n",line);
|
||||
if (len <= 2 || !path)
|
||||
{
|
||||
printf("[FetchFS] Manifest file has invalid line %s\n", line);
|
||||
continue;
|
||||
}
|
||||
*path = '\0';
|
||||
@ -399,19 +550,22 @@ void PlatformEmscriptenMountFilesystems(void *info) {
|
||||
{
|
||||
char *parent = strdup(path);
|
||||
path_parent_dir(parent, strlen(parent));
|
||||
if(!path_mkdir(parent)) {
|
||||
printf("[FetchFS] mkdir error %d\n",errno);
|
||||
if (!path_mkdir(parent))
|
||||
{
|
||||
printf("[FetchFS] mkdir error %d\n", errno);
|
||||
abort();
|
||||
}
|
||||
free(parent);
|
||||
}
|
||||
fetch = wasmfs_create_fetch_backend(line, 16*1024*1024);
|
||||
if(!fetch) {
|
||||
if (!fetch)
|
||||
{
|
||||
printf("[FetchFS] couldn't create fetch backend\n");
|
||||
abort();
|
||||
}
|
||||
fd = wasmfs_create_file(path, 0777, fetch);
|
||||
if(!fd) {
|
||||
if (!fd)
|
||||
{
|
||||
printf("[FetchFS] couldn't create fetch file\n");
|
||||
abort();
|
||||
}
|
||||
@ -421,91 +575,62 @@ void PlatformEmscriptenMountFilesystems(void *info) {
|
||||
fclose(file);
|
||||
free(line);
|
||||
}
|
||||
filesystem_ready = true;
|
||||
#if !PROXY_TO_PTHREAD
|
||||
while (!retro_started) {
|
||||
retro_sleep(1);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#endif /* HAVE_WASMFS */
|
||||
|
||||
static enum frontend_powerstate frontend_emscripten_get_powerstate(int *seconds, int *percent)
|
||||
static int thread_main(int argc, char *argv[])
|
||||
{
|
||||
enum frontend_powerstate ret = FRONTEND_POWERSTATE_NONE;
|
||||
#ifdef HAVE_WASMFS
|
||||
platform_emscripten_mount_filesystems();
|
||||
#endif
|
||||
|
||||
if (!PlatformEmscriptenPowerStateGetSupported())
|
||||
return ret;
|
||||
emscripten_set_main_loop(emscripten_mainloop, 0, 0);
|
||||
emscripten_set_main_loop_timing(EM_TIMING_RAF, 1);
|
||||
rarch_main(argc, argv, NULL);
|
||||
|
||||
if (!PlatformEmscriptenPowerStateGetCharging())
|
||||
ret = FRONTEND_POWERSTATE_ON_POWER_SOURCE;
|
||||
else if (PlatformEmscriptenPowerStateGetLevel() == 1)
|
||||
ret = FRONTEND_POWERSTATE_CHARGED;
|
||||
else
|
||||
ret = FRONTEND_POWERSTATE_CHARGING;
|
||||
|
||||
*seconds = PlatformEmscriptenPowerStateGetDischargeTime();
|
||||
*percent = (int)(PlatformEmscriptenPowerStateGetLevel() * 100);
|
||||
|
||||
return ret;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static uint64_t frontend_emscripten_get_total_mem(void)
|
||||
#ifdef PROXY_TO_PTHREAD
|
||||
|
||||
static int _main_argc;
|
||||
static char** _main_argv;
|
||||
|
||||
static void *main_pthread(void* arg)
|
||||
{
|
||||
return PlatformEmscriptenGetTotalMem();
|
||||
}
|
||||
|
||||
static uint64_t frontend_emscripten_get_free_mem(void)
|
||||
{
|
||||
return PlatformEmscriptenGetFreeMem();
|
||||
}
|
||||
|
||||
void emscripten_bootup_mainloop(void *argptr) {
|
||||
if(retro_started) {
|
||||
/* A stale extra call to bootup_mainloop for some reason */
|
||||
RARCH_ERR("[Emscripten] unexpected second call to bootup_mainloop after rarch_main called\n");
|
||||
return;
|
||||
}
|
||||
if(filesystem_ready) {
|
||||
args_t *args = (args_t*)argptr;
|
||||
emscripten_cancel_main_loop();
|
||||
emscripten_set_main_loop(emscripten_mainloop, 0, 0);
|
||||
emscripten_set_main_loop_timing(EM_TIMING_RAF, 1);
|
||||
rarch_main(args->argc, args->argv, NULL);
|
||||
retro_started = true;
|
||||
free(args);
|
||||
}
|
||||
emscripten_set_thread_name(pthread_self(), "Application main thread");
|
||||
thread_main(_main_argc, _main_argv);
|
||||
return NULL;
|
||||
}
|
||||
#endif
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
args_t *args = calloc(sizeof(args_t), 1);
|
||||
args->argc = argc;
|
||||
args->argv = argv;
|
||||
|
||||
PlatformEmscriptenWatchCanvasSize();
|
||||
int ret = 0;
|
||||
// this never gets freed
|
||||
emscripten_platform_data = (emscripten_platform_data_t *)calloc(1, sizeof(emscripten_platform_data_t));
|
||||
|
||||
PlatformEmscriptenWatchCanvasSizeAndDpr(malloc(sizeof(double)));
|
||||
PlatformEmscriptenWatchWindowVisibility();
|
||||
PlatformEmscriptenPowerStateInit();
|
||||
PlatformEmscriptenMemoryUsageInit();
|
||||
|
||||
emscripten_set_canvas_element_size("#canvas", 800, 600);
|
||||
emscripten_set_element_css_size("#canvas", 800.0, 600.0);
|
||||
#if HAVE_WASMFS
|
||||
#if PROXY_TO_PTHREAD
|
||||
{
|
||||
PlatformEmscriptenMountFilesystems(NULL);
|
||||
}
|
||||
#else /* !PROXY_TO_PTHREAD */
|
||||
{
|
||||
sthread_t *thread = sthread_create(PlatformEmscriptenMountFilesystems, NULL);
|
||||
sthread_detach(thread);
|
||||
}
|
||||
#endif /* PROXY_TO_PTHREAD */
|
||||
#else /* !HAVE_WASMFS */
|
||||
filesystem_ready = true;
|
||||
#endif /* HAVE_WASMFS */
|
||||
emscripten_set_main_loop_arg(emscripten_bootup_mainloop, (void *)args, 0, 0);
|
||||
emscripten_set_main_loop_timing(EM_TIMING_RAF, 1);
|
||||
|
||||
return 0;
|
||||
#ifdef PROXY_TO_PTHREAD
|
||||
_main_argc = argc;
|
||||
_main_argv = argv;
|
||||
pthread_attr_t attr;
|
||||
pthread_t thread;
|
||||
pthread_attr_init(&attr);
|
||||
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
|
||||
pthread_attr_setstacksize(&attr, EMSCRIPTEN_STACK_SIZE);
|
||||
emscripten_pthread_attr_settransferredcanvases(&attr, (const char*)-1);
|
||||
ret = pthread_create(&thread, &attr, main_pthread, NULL);
|
||||
pthread_attr_destroy(&attr);
|
||||
#else
|
||||
ret = thread_main(argc, argv);
|
||||
#endif
|
||||
return ret;
|
||||
}
|
||||
|
||||
frontend_ctx_driver_t frontend_ctx_emscripten = {
|
||||
|
@ -33,6 +33,8 @@
|
||||
#include "../common/egl_common.h"
|
||||
#endif
|
||||
|
||||
void platform_emscripten_get_canvas_size(int *width, int *height);
|
||||
|
||||
typedef struct
|
||||
{
|
||||
#ifdef HAVE_EGL
|
||||
@ -50,18 +52,6 @@ static void gfx_ctx_emscripten_swap_interval(void *data, int interval)
|
||||
emscripten_set_main_loop_timing(EM_TIMING_RAF, interval);
|
||||
}
|
||||
|
||||
static void gfx_ctx_emscripten_get_canvas_size(int *width, int *height)
|
||||
{
|
||||
EMSCRIPTEN_RESULT r = emscripten_get_canvas_element_size("#canvas", width, height);
|
||||
|
||||
if (r != EMSCRIPTEN_RESULT_SUCCESS)
|
||||
{
|
||||
*width = 800;
|
||||
*height = 600;
|
||||
RARCH_ERR("[EMSCRIPTEN/EGL]: Could not get screen dimensions: %d\n",r);
|
||||
}
|
||||
}
|
||||
|
||||
static void gfx_ctx_emscripten_check_window(void *data, bool *quit,
|
||||
bool *resize, unsigned *width, unsigned *height)
|
||||
{
|
||||
@ -69,7 +59,8 @@ static void gfx_ctx_emscripten_check_window(void *data, bool *quit,
|
||||
int input_height;
|
||||
emscripten_ctx_data_t *emscripten = (emscripten_ctx_data_t*)data;
|
||||
|
||||
gfx_ctx_emscripten_get_canvas_size(&input_width, &input_height);
|
||||
platform_emscripten_get_canvas_size(&input_width, &input_height);
|
||||
|
||||
*resize = (emscripten->fb_width != input_width || emscripten->fb_height != input_height);
|
||||
*width = emscripten->fb_width = (unsigned)input_width;
|
||||
*height = emscripten->fb_height = (unsigned)input_height;
|
||||
@ -93,10 +84,9 @@ static void gfx_ctx_emscripten_get_video_size(void *data,
|
||||
|
||||
if (!emscripten)
|
||||
return;
|
||||
int w, h;
|
||||
gfx_ctx_emscripten_get_canvas_size(&w, &h);
|
||||
*width = w;
|
||||
*height = h;
|
||||
|
||||
*width = emscripten->fb_width;
|
||||
*height = emscripten->fb_height;
|
||||
}
|
||||
|
||||
static bool gfx_ctx_emscripten_get_metrics(void *data,
|
||||
|
@ -29,6 +29,9 @@
|
||||
#include "../../retroarch.h"
|
||||
#include "../../verbosity.h"
|
||||
|
||||
void platform_emscripten_get_canvas_size(int *width, int *height);
|
||||
double platform_emscripten_get_dpr(void);
|
||||
|
||||
typedef struct
|
||||
{
|
||||
EMSCRIPTEN_WEBGL_CONTEXT_HANDLE ctx;
|
||||
@ -44,179 +47,40 @@ static void gfx_ctx_emscripten_webgl_swap_interval(void *data, int interval)
|
||||
emscripten_set_main_loop_timing(EM_TIMING_RAF, interval);
|
||||
}
|
||||
|
||||
static void gfx_ctx_emscripten_webgl_get_canvas_size(int *width, int *height)
|
||||
{
|
||||
EmscriptenFullscreenChangeEvent fullscreen_status;
|
||||
bool is_fullscreen = false;
|
||||
EMSCRIPTEN_RESULT r = emscripten_get_fullscreen_status(&fullscreen_status);
|
||||
|
||||
if (r == EMSCRIPTEN_RESULT_SUCCESS)
|
||||
{
|
||||
if (fullscreen_status.isFullscreen)
|
||||
{
|
||||
is_fullscreen = true;
|
||||
*width = fullscreen_status.screenWidth;
|
||||
*height = fullscreen_status.screenHeight;
|
||||
}
|
||||
}
|
||||
if (!is_fullscreen)
|
||||
{
|
||||
double w, h;
|
||||
r = emscripten_get_element_css_size("#canvas", &w, &h);
|
||||
*width = (int)w;
|
||||
*height = (int)h;
|
||||
|
||||
if (r != EMSCRIPTEN_RESULT_SUCCESS)
|
||||
{
|
||||
*width = 800;
|
||||
*height = 600;
|
||||
RARCH_ERR("[EMSCRIPTEN/WebGL]: Could not get screen dimensions: %d\n",r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void gfx_ctx_emscripten_webgl_check_window(void *data, bool *quit,
|
||||
bool *resize, unsigned *width, unsigned *height)
|
||||
{
|
||||
int input_width=0;
|
||||
int input_height=0;
|
||||
int input_width;
|
||||
int input_height;
|
||||
emscripten_ctx_data_t *emscripten = (emscripten_ctx_data_t*)data;
|
||||
|
||||
gfx_ctx_emscripten_webgl_get_canvas_size(&input_width, &input_height);
|
||||
*width = (unsigned)input_width;
|
||||
*height = (unsigned)input_height;
|
||||
*resize = (*width != emscripten->fb_width || *height != emscripten->fb_height);
|
||||
emscripten->fb_width = *width;
|
||||
emscripten->fb_height = *height;
|
||||
*quit = false;
|
||||
platform_emscripten_get_canvas_size(&input_width, &input_height);
|
||||
|
||||
*resize = (emscripten->fb_width != input_width || emscripten->fb_height != input_height);
|
||||
*width = emscripten->fb_width = (unsigned)input_width;
|
||||
*height = emscripten->fb_height = (unsigned)input_height;
|
||||
*quit = false;
|
||||
}
|
||||
|
||||
static void gfx_ctx_emscripten_webgl_swap_buffers(void *data)
|
||||
{
|
||||
#ifdef USE_OFFSCREENCANVAS
|
||||
#ifdef PROXY_TO_PTHREAD
|
||||
emscripten_webgl_commit_frame();
|
||||
#else
|
||||
(void)data;
|
||||
#endif
|
||||
}
|
||||
|
||||
static void gfx_ctx_emscripten_webgl_get_video_size(void *data,
|
||||
unsigned *width, unsigned *height)
|
||||
{
|
||||
emscripten_ctx_data_t *emscripten = (emscripten_ctx_data_t*)data;
|
||||
int s_width, s_height;
|
||||
if (!emscripten)
|
||||
return;
|
||||
gfx_ctx_emscripten_webgl_get_canvas_size(&s_width, &s_height);
|
||||
*width = (unsigned)s_width;
|
||||
*height = (unsigned)s_height;
|
||||
}
|
||||
|
||||
static void gfx_ctx_emscripten_webgl_destroy(void *data)
|
||||
{
|
||||
emscripten_ctx_data_t *emscripten = (emscripten_ctx_data_t*)data;
|
||||
|
||||
if (!emscripten)
|
||||
return;
|
||||
|
||||
emscripten_webgl_destroy_context(emscripten->ctx);
|
||||
|
||||
free(data);
|
||||
}
|
||||
|
||||
static void *gfx_ctx_emscripten_webgl_init(void *video_driver)
|
||||
{
|
||||
int width, height;
|
||||
emscripten_ctx_data_t *emscripten = (emscripten_ctx_data_t*)
|
||||
calloc(1, sizeof(*emscripten));
|
||||
|
||||
EmscriptenWebGLContextAttributes attrs={0};
|
||||
emscripten_webgl_init_context_attributes(&attrs);
|
||||
attrs.alpha = false;
|
||||
attrs.depth = true;
|
||||
attrs.stencil = true;
|
||||
attrs.antialias = false;
|
||||
attrs.powerPreference = EM_WEBGL_POWER_PREFERENCE_HIGH_PERFORMANCE;
|
||||
attrs.majorVersion = 2;
|
||||
attrs.minorVersion = 0;
|
||||
attrs.enableExtensionsByDefault = true;
|
||||
#ifdef USE_OFFSCREENCANVAS
|
||||
attrs.explicitSwapControl = true;
|
||||
#else
|
||||
attrs.explicitSwapControl = false;
|
||||
#endif
|
||||
attrs.renderViaOffscreenBackBuffer = false;
|
||||
attrs.proxyContextToMainThread = EMSCRIPTEN_WEBGL_CONTEXT_PROXY_DISALLOW;
|
||||
|
||||
if (!emscripten)
|
||||
return NULL;
|
||||
|
||||
emscripten->ctx = emscripten_webgl_create_context("#canvas", &attrs);
|
||||
if(!emscripten->ctx) {
|
||||
RARCH_ERR("[EMSCRIPTEN/WEBGL]: Failed to initialize webgl\n");
|
||||
goto error;
|
||||
}
|
||||
emscripten_webgl_get_drawing_buffer_size(emscripten->ctx, &width, &height);
|
||||
emscripten_webgl_make_context_current(emscripten->ctx);
|
||||
emscripten->fb_width = (unsigned)width;
|
||||
emscripten->fb_height = (unsigned)height;
|
||||
|
||||
return emscripten;
|
||||
|
||||
error:
|
||||
gfx_ctx_emscripten_webgl_destroy(video_driver);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static bool gfx_ctx_emscripten_webgl_set_video_mode(void *data,
|
||||
unsigned width, unsigned height,
|
||||
bool fullscreen)
|
||||
{
|
||||
emscripten_ctx_data_t *emscripten = (emscripten_ctx_data_t*)data;
|
||||
EMSCRIPTEN_RESULT r;
|
||||
if(!emscripten || !emscripten->ctx) return false;
|
||||
|
||||
if (width != 0 && height != 0) {
|
||||
r = emscripten_set_canvas_element_size("#canvas",
|
||||
(int)width, (int)height);
|
||||
|
||||
if (r != EMSCRIPTEN_RESULT_SUCCESS) {
|
||||
RARCH_ERR("[EMSCRIPTEN/WebGL]: error resizing canvas: %d\n", r);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
emscripten->fb_width = width;
|
||||
emscripten->fb_height = 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;
|
||||
EMSCRIPTEN_RESULT r;
|
||||
if(!emscripten || !emscripten->ctx) return false;
|
||||
r = emscripten_set_canvas_element_size("#canvas",
|
||||
(int)width, (int)height);
|
||||
if (r != EMSCRIPTEN_RESULT_SUCCESS) {
|
||||
RARCH_ERR("[EMSCRIPTEN/WebGL]: error resizing canvas: %d\n", r);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
static void gfx_ctx_emscripten_webgl_input_driver(void *data,
|
||||
const char *name,
|
||||
input_driver_t **input, void **input_data)
|
||||
{
|
||||
void *rwebinput = input_driver_init_wrap(&input_rwebinput, name);
|
||||
*input = rwebinput ? &input_rwebinput : NULL;
|
||||
*input_data = rwebinput;
|
||||
*width = emscripten->fb_width;
|
||||
*height = emscripten->fb_height;
|
||||
}
|
||||
|
||||
static bool gfx_ctx_emscripten_webgl_get_metrics(void *data,
|
||||
@ -238,7 +102,131 @@ static bool gfx_ctx_emscripten_webgl_get_metrics(void *data,
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool gfx_ctx_emscripten_webgl_has_focus(void *data) {
|
||||
static void gfx_ctx_emscripten_webgl_destroy(void *data)
|
||||
{
|
||||
emscripten_ctx_data_t *emscripten = (emscripten_ctx_data_t*)data;
|
||||
|
||||
if (!emscripten)
|
||||
return;
|
||||
|
||||
emscripten_webgl_destroy_context(emscripten->ctx);
|
||||
|
||||
free(data);
|
||||
}
|
||||
|
||||
static void *gfx_ctx_emscripten_webgl_init(void *video_driver)
|
||||
{
|
||||
int width, height;
|
||||
emscripten_ctx_data_t *emscripten = (emscripten_ctx_data_t*)
|
||||
calloc(1, sizeof(*emscripten));
|
||||
|
||||
EmscriptenWebGLContextAttributes attrs = {0};
|
||||
emscripten_webgl_init_context_attributes(&attrs);
|
||||
attrs.alpha = false;
|
||||
attrs.depth = true;
|
||||
attrs.stencil = true;
|
||||
attrs.antialias = false;
|
||||
attrs.powerPreference = EM_WEBGL_POWER_PREFERENCE_HIGH_PERFORMANCE;
|
||||
#ifdef HAVE_OPENGLES3
|
||||
attrs.majorVersion = 2;
|
||||
#else
|
||||
attrs.majorVersion = 1;
|
||||
#endif
|
||||
attrs.minorVersion = 0;
|
||||
attrs.enableExtensionsByDefault = true;
|
||||
#ifdef PROXY_TO_PTHREAD
|
||||
attrs.explicitSwapControl = true;
|
||||
#else
|
||||
attrs.explicitSwapControl = false;
|
||||
#endif
|
||||
attrs.renderViaOffscreenBackBuffer = false;
|
||||
attrs.proxyContextToMainThread = EMSCRIPTEN_WEBGL_CONTEXT_PROXY_DISALLOW;
|
||||
|
||||
if (!emscripten)
|
||||
return NULL;
|
||||
|
||||
emscripten->ctx = emscripten_webgl_create_context("#canvas", &attrs);
|
||||
if (!emscripten->ctx)
|
||||
{
|
||||
RARCH_ERR("[EMSCRIPTEN/WebGL]: Failed to initialize webgl\n");
|
||||
goto error;
|
||||
}
|
||||
emscripten_webgl_get_drawing_buffer_size(emscripten->ctx, &width, &height);
|
||||
emscripten_webgl_make_context_current(emscripten->ctx);
|
||||
emscripten->fb_width = (unsigned)width;
|
||||
emscripten->fb_height = (unsigned)height;
|
||||
RARCH_LOG("[EMSCRIPTEN/WebGL]: Dimensions: %ux%u\n", emscripten->fb_width, emscripten->fb_height);
|
||||
|
||||
return emscripten;
|
||||
|
||||
error:
|
||||
gfx_ctx_emscripten_webgl_destroy(video_driver);
|
||||
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)
|
||||
{
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
static void gfx_ctx_emscripten_webgl_input_driver(void *data,
|
||||
const char *name,
|
||||
input_driver_t **input, void **input_data)
|
||||
{
|
||||
void *rwebinput = input_driver_init_wrap(&input_rwebinput, name);
|
||||
*input = rwebinput ? &input_rwebinput : NULL;
|
||||
*input_data = rwebinput;
|
||||
}
|
||||
|
||||
static bool gfx_ctx_emscripten_webgl_has_focus(void *data)
|
||||
{
|
||||
emscripten_ctx_data_t *emscripten = (emscripten_ctx_data_t*)data;
|
||||
return emscripten && emscripten->ctx;
|
||||
}
|
||||
@ -279,7 +267,7 @@ 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, /* set_resize */
|
||||
gfx_ctx_emscripten_webgl_set_resize,
|
||||
gfx_ctx_emscripten_webgl_has_focus,
|
||||
gfx_ctx_emscripten_webgl_suppress_screensaver,
|
||||
false,
|
||||
@ -294,5 +282,5 @@ const gfx_ctx_driver_t gfx_ctx_emscripten_webgl = {
|
||||
gfx_ctx_emscripten_webgl_set_flags,
|
||||
gfx_ctx_emscripten_webgl_bind_hw_render,
|
||||
NULL, /* get_context_data */
|
||||
NULL /* make_current */
|
||||
NULL /* make_current */
|
||||
};
|
||||
|
@ -45,6 +45,8 @@
|
||||
|
||||
#define MAX_TOUCH 32
|
||||
|
||||
double platform_emscripten_get_dpr(void);
|
||||
|
||||
typedef struct rwebinput_key_to_code_map_entry
|
||||
{
|
||||
const char *key;
|
||||
@ -299,7 +301,7 @@ static EM_BOOL rwebinput_mouse_cb(int event_type,
|
||||
}
|
||||
else
|
||||
{
|
||||
double dpr = emscripten_get_device_pixel_ratio();
|
||||
double dpr = platform_emscripten_get_dpr();
|
||||
rwebinput->mouse.x = (int)(mouse_event->targetX * dpr);
|
||||
rwebinput->mouse.y = (int)(mouse_event->targetY * dpr);
|
||||
}
|
||||
@ -317,7 +319,7 @@ static EM_BOOL rwebinput_wheel_cb(int event_type,
|
||||
{
|
||||
rwebinput_input_t *rwebinput = (rwebinput_input_t*)user_data;
|
||||
|
||||
double dpr = emscripten_get_device_pixel_ratio();
|
||||
double dpr = platform_emscripten_get_dpr();
|
||||
rwebinput->mouse.pending_scroll_x += wheel_event->deltaX * dpr;
|
||||
rwebinput->mouse.pending_scroll_y += wheel_event->deltaY * dpr;
|
||||
|
||||
@ -341,7 +343,7 @@ static EM_BOOL rwebinput_touch_cb(int event_type,
|
||||
if (!(touch_event->touches[touch].isChanged) && rwebinput->pointer[touch].id == touch_event->touches[touch].identifier)
|
||||
continue;
|
||||
|
||||
double dpr = emscripten_get_device_pixel_ratio();
|
||||
double dpr = platform_emscripten_get_dpr();
|
||||
rwebinput->pointer[touch].x = (int)(touch_event->touches[touch].targetX * dpr);
|
||||
rwebinput->pointer[touch].y = (int)(touch_event->touches[touch].targetY * dpr);
|
||||
rwebinput->pointer[touch].id = touch_event->touches[touch].identifier;
|
||||
@ -416,8 +418,9 @@ static void *rwebinput_input_init(const char *joypad_driver)
|
||||
|
||||
rwebinput_generate_lut();
|
||||
|
||||
r = emscripten_set_keydown_callback(
|
||||
"#canvas", rwebinput, false,
|
||||
input_keymaps_init_keyboard_lut(rarch_key_map_rwebinput);
|
||||
|
||||
r = emscripten_set_keydown_callback("#canvas", rwebinput, false,
|
||||
rwebinput_keyboard_cb);
|
||||
if (r != EMSCRIPTEN_RESULT_SUCCESS)
|
||||
{
|
||||
@ -425,8 +428,7 @@ static void *rwebinput_input_init(const char *joypad_driver)
|
||||
"[EMSCRIPTEN/INPUT] failed to create keydown callback: %d\n", r);
|
||||
}
|
||||
|
||||
r = emscripten_set_keyup_callback(
|
||||
"#canvas", rwebinput, false,
|
||||
r = emscripten_set_keyup_callback("#canvas", rwebinput, false,
|
||||
rwebinput_keyboard_cb);
|
||||
if (r != EMSCRIPTEN_RESULT_SUCCESS)
|
||||
{
|
||||
@ -434,8 +436,7 @@ static void *rwebinput_input_init(const char *joypad_driver)
|
||||
"[EMSCRIPTEN/INPUT] failed to create keyup callback: %d\n", r);
|
||||
}
|
||||
|
||||
r = emscripten_set_keypress_callback(
|
||||
"#canvas", rwebinput, false,
|
||||
r = emscripten_set_keypress_callback("#canvas", rwebinput, false,
|
||||
rwebinput_keyboard_cb);
|
||||
if (r != EMSCRIPTEN_RESULT_SUCCESS)
|
||||
{
|
||||
@ -467,8 +468,7 @@ static void *rwebinput_input_init(const char *joypad_driver)
|
||||
"[EMSCRIPTEN/INPUT] failed to create mousemove callback: %d\n", r);
|
||||
}
|
||||
|
||||
r = emscripten_set_wheel_callback(
|
||||
"#canvas", rwebinput, false,
|
||||
r = emscripten_set_wheel_callback("#canvas", rwebinput, false,
|
||||
rwebinput_wheel_cb);
|
||||
if (r != EMSCRIPTEN_RESULT_SUCCESS)
|
||||
{
|
||||
@ -517,8 +517,6 @@ static void *rwebinput_input_init(const char *joypad_driver)
|
||||
"[EMSCRIPTEN/INPUT] failed to create pointerlockchange callback: %d\n", r);
|
||||
}
|
||||
|
||||
input_keymaps_init_keyboard_lut(rarch_key_map_rwebinput);
|
||||
|
||||
return rwebinput;
|
||||
}
|
||||
|
||||
|
@ -1,83 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>RetroArch Web Player</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<!-- Bootstrap core CSS -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-alpha.3/css/bootstrap.min.css" rel="stylesheet" type="text/css">
|
||||
<!-- Font Awesome -->
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.0/css/font-awesome.min.css">
|
||||
<!-- Material Design Bootstrap -->
|
||||
<link href="//cdnjs.cloudflare.com/ajax/libs/mdbootstrap/4.1.1/css/mdb.min.css" rel="stylesheet">
|
||||
<link href="libretro.css" rel="stylesheet" type="text/css">
|
||||
<link rel="shortcut icon" href="https://web.libretro.com/media/retroarch.ico" />
|
||||
</head>
|
||||
<body>
|
||||
<!--Navbar-->
|
||||
<nav class="navbar navbar-dark bg-primary">
|
||||
<div class="container">
|
||||
<!--navbar content-->
|
||||
<div class="navbar-toggleable-xs">
|
||||
<!--Links-->
|
||||
<ul class="nav navbar-nav">
|
||||
<div class="dropdown">
|
||||
<li class="nav-item dropdown">
|
||||
<button class="btn btn-primary dropdown-toggle" type="button" id="dropdownMenu1" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Core Selection</button>
|
||||
<div class="dropdown-menu dropdown-primary" aria-labelledby="dropdownMenu1" data-dropdown-in="fadeIn" data-dropdown-out="fadeOut" id="core-selector">
|
||||
<a class="dropdown-item" href="." data-core="chailove">ChaiLove</a>
|
||||
<a class="dropdown-item" href="." data-core="fceumm">FCEUmm</a>
|
||||
<a class="dropdown-item" href="." data-core="gambatte">Gambatte</a>
|
||||
<a class="dropdown-item" href="." data-core="genesis_plus_gx">Genesis Plus GX</a>
|
||||
<a class="dropdown-item" href="." data-core="lutro">Lutro</a>
|
||||
<a class="dropdown-item" href="." data-core="nestopia">Nestopia (NES)</a>
|
||||
<a class="dropdown-item" href="." data-core="snes9x">Snes9x (SNES)</a>
|
||||
<a class="dropdown-item" href="." data-core="snes9x2010">Snes9x 2010 (SNES)</a>
|
||||
<a class="dropdown-item" href="." data-core="theodore">Theodore (Thomson TO8/TO9)</a>
|
||||
<a class="dropdown-item" href="." data-core="vba_next">VBA Next (Gameboy Advance)</a>
|
||||
</div>
|
||||
<button class="btn btn-primary disabled" id="btnRun" disabled>
|
||||
<span class="fa fa-spinner fa-spin" id="icnRun"></span> Run
|
||||
</button>
|
||||
<button class="btn btn-primary disabled" id="btnAdd" disabled>
|
||||
<span class="fa fa-plus" id="icnAdd"></span> Add Content
|
||||
</button>
|
||||
<input style="display: none" type="file" id="btnRom" name="upload" multiple />
|
||||
<button class="btn btn-primary tooltip-enable" id="btnClean" title="Cleanup storage">
|
||||
<span class="fa fa-trash-o" id="icnClean"></span> <span class="sr-only">Cleanup</span>
|
||||
</button>
|
||||
<button class="btn btn-primary disabled tooltip-enable" id="btnMenu" title="Menu toggle" disabled>
|
||||
<span class="fa fa-bars" id="icnMenu"></span> <span class="sr-only">Menu</span>
|
||||
</button>
|
||||
<button class="btn btn-primary disabled tooltip-enable" id="btnFullscreen" title="Fullscreen" disabled>
|
||||
<span class="fa fa-desktop" id="icnFullscreen"></span> <span class="sr-only">Fullscreen</span>
|
||||
</button>
|
||||
</li>
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
<!--/.navbar content-->
|
||||
</div>
|
||||
</nav>
|
||||
<div class="bg-inverse webplayer-container">
|
||||
<div class="container">
|
||||
<div class="webplayer_border text-xs-center" id="canvas_div">
|
||||
<canvas class="webplayer" id="canvas" tabindex="1" oncontextmenu="event.preventDefault()" style="display: none"></canvas>
|
||||
<img class="webplayer-preview img-fluid" src="media/canvas.png" width="960" height="720" alt="RetroArch Logo">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="//code.jquery.com/jquery-3.1.0.min.js"></script>
|
||||
<script src="//rawgit.com/jeresig/jquery.hotkeys/master/jquery.hotkeys.js"></script>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/tether/1.3.4/js/tether.min.js"></script>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-alpha.3/js/bootstrap.min.js"></script>
|
||||
<script src="analytics.js"></script>
|
||||
<!--script src="//wzrd.in/standalone/browserfs@0.6.1"></script-->
|
||||
<script src="browserfs.min.js"></script>
|
||||
<script src="libretro.js"></script>
|
||||
<div align="center">
|
||||
<a href="https://www.patreon.com/libretro">
|
||||
<img src="https://patreon_public_assets.s3.amazonaws.com/sized/becomeAPatronBanner.png" alt="Become a patron" width="350" height="116"></a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -1,203 +1,202 @@
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>RetroArch Web Player</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<!-- Bootstrap core CSS -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-alpha.3/css/bootstrap.min.css" rel="stylesheet" type="text/css">
|
||||
<!-- Font Awesome -->
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.0/css/font-awesome.min.css">
|
||||
<!-- Material Design Bootstrap -->
|
||||
<link href="//cdnjs.cloudflare.com/ajax/libs/mdbootstrap/4.1.1/css/mdb.min.css" rel="stylesheet">
|
||||
<link href="libretro.css" rel="stylesheet" type="text/css">
|
||||
<link rel="shortcut icon" href="media/retroarch.ico" />
|
||||
</head>
|
||||
<body>
|
||||
<!--Navbar-->
|
||||
<nav class="navbar navbar-dark bg-primary">
|
||||
<div class="container">
|
||||
<!--navbar content-->
|
||||
<div class="navbar-toggleable-xs">
|
||||
<!--Links-->
|
||||
<ul class="nav navbar-nav">
|
||||
<div class="dropdown">
|
||||
<li class="nav-item dropdown">
|
||||
<button class="btn btn-primary dropdown-toggle" type="button" id="dropdownMenu1" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Core Selection</button>
|
||||
<div class="dropdown-menu dropdown-primary" aria-labelledby="dropdownMenu1" data-dropdown-in="fadeIn" data-dropdown-out="fadeOut" id="core-selector">
|
||||
<a class="dropdown-item" href="." data-core="2048">2048</a>
|
||||
<a class="dropdown-item" href="." data-core="anarch">Anarch</a>
|
||||
<a class="dropdown-item" href="." data-core="ardens">Arduboy (Ardens)</a>
|
||||
<a class="dropdown-item" href="." data-core="arduous">Arduboy (Arduous)</a>
|
||||
<a class="dropdown-item" href="." data-core="bk">Elektronika - BK-0010/BK-0011 (BK)</a>
|
||||
<a class="dropdown-item" href="." data-core="chailove">ChaiLove</a>
|
||||
<a class="dropdown-item" href="." data-core="craft">Minecraft (Craft)</a>
|
||||
<a class="dropdown-item" href="." data-core="DoubleCherryGB">Nintendo - Game Boy / Color (DoubleCherryGB)</a>
|
||||
<a class="dropdown-item" href="." data-core="ecwolf">Wolfenstein 3D (ECWolf)</a>
|
||||
<a class="dropdown-item" href="." data-core="fbalpha2012">Arcade (FB Alpha 2012)</a>
|
||||
<a class="dropdown-item" href="." data-core="fbalpha2012_cps1">Arcade (FB Alpha 2012 CPS1)</a>
|
||||
<a class="dropdown-item" href="." data-core="fbalpha2012_cps2">Arcade (FB Alpha 2012 CPS2)</a>
|
||||
<a class="dropdown-item" href="." data-core="fbalpha2012_neogeo">Arcade (FB Alpha 2012 NeoGeo)</a>
|
||||
<a class="dropdown-item" href="." data-core="fceumm">Nintendo - NES / Famicom (FCEUmm)</a>
|
||||
<a class="dropdown-item" href="." data-core="freechaf">Fairchild ChannelF (FreeChaF)</a>
|
||||
<a class="dropdown-item" href="." data-core="galaksija">Galaksija</a>
|
||||
<a class="dropdown-item" href="." data-core="gambatte">Nintendo - Game Boy / Color (Gambatte)</a>
|
||||
<a class="dropdown-item" href="." data-core="gme">Game Music Emu</a>
|
||||
<a class="dropdown-item" href="." data-core="gearboy">Nintendo - Game Boy / Color (GearBoy)</a>
|
||||
<a class="dropdown-item" href="." data-core="gearcoleco">Coleco - ColecoVision (GearColeco)</a>
|
||||
<a class="dropdown-item" href="." data-core="gearsystem">Sega - MS/GG/SG-1000 (GearSystem)</a>
|
||||
<a class="dropdown-item" href="." data-core="genesis_plus_gx">Sega - MS/GG/MD/CD (Genesis Plus GX)</a>
|
||||
<a class="dropdown-item" href="." data-core="genesis_plus_gx_wide">Sega - MS/GG/MD/CD (Genesis Plus GX Wide)</a>
|
||||
<a class="dropdown-item" href="." data-core="gong">Gong</a>
|
||||
<a class="dropdown-item" href="." data-core="gw">Handheld Electronic (GW)</a>
|
||||
<a class="dropdown-item" href="." data-core="handy">Atari - Lynx (Handy)</a>
|
||||
<a class="dropdown-item" href="." data-core="jaxe">CHIP-8/S-CHIP/XO-CHIP (JAXE)</a>
|
||||
<a class="dropdown-item" href="." data-core="jumpnbump">Jump 'n Bump</a>
|
||||
<a class="dropdown-item" href="." data-core="lowresnx">LowResNX</a>
|
||||
<a class="dropdown-item" href="." data-core="lutro">Lua Engine (Lutro)</a>
|
||||
<a class="dropdown-item" href="." data-core="m2000">Philips - P2000T (M2000)</a>
|
||||
<a class="dropdown-item" href="." data-core="mame2000">Arcade - MAME 2000</a>
|
||||
<a class="dropdown-item" href="." data-core="mame2003">Arcade - MAME 2003</a>
|
||||
<a class="dropdown-item" href="." data-core="mame2003_plus">Arcade - MAME 2003-Plus</a>
|
||||
<a class="dropdown-item" href="." data-core="mednafen_lynx">Atari - Lynx (Beetle Lynx)</a>
|
||||
<a class="dropdown-item" href="." data-core="mednafen_ngp">SNK - Neo Geo Pocket / Color (Beetle Neo Geo Pop)</a>
|
||||
<a class="dropdown-item" href="." data-core="mednafen_pce_fast">NEC - PC Engine / CD (Beetle PC Engine Fast)</a>
|
||||
<a class="dropdown-item" href="." data-core="mednafen_vb">Nintendo - Virtual Boy (Beetle VB)</a>
|
||||
<a class="dropdown-item" href="." data-core="mednafen_wswan">Bandai - WonderSwan/Color (Beetle WonderSwan)</a>
|
||||
<a class="dropdown-item" href="." data-core="mgba">Nintendo - Game Boy Advance (mGBA)</a>
|
||||
<a class="dropdown-item" href="." data-core="minivmac">Mac II (MiniVmac)</a>
|
||||
<a class="dropdown-item" href="." data-core="mu">Palm OS(Mu)</a>
|
||||
<a class="dropdown-item" href="." data-core="mrboom">Bomberman (Mr.Boom)</a>
|
||||
<a class="dropdown-item" href="." data-core="neocd">SNK - Neo Geo CD (NeoCD)</a>
|
||||
<a class="dropdown-item" href="." data-core="nestopia">Nintendo - NES / Famicom (Nestopia)</a>
|
||||
<a class="dropdown-item" href="." data-core="numero">Texas Instruments TI-83 (Numero)</a>
|
||||
<a class="dropdown-item" href="." data-core="nxengine">Cave Story (NX Engine)</a>
|
||||
<a class="dropdown-item" href="." data-core="o2em">Magnavox - Odyssey2 / Philips Videopac+ (O2EM)</a>
|
||||
<a class="dropdown-item" href="." data-core="opera">The 3DO Company - 3DO (Opera)</a>
|
||||
<a class="dropdown-item" href="." data-core="pcsx_rearmed">Sony - PlayStation (PCSX ReARMed)</a>
|
||||
<a class="dropdown-item" href="." data-core="picodrive">Sega - MS/GG/MD/CD/32X (PicoDrive)</a>
|
||||
<a class="dropdown-item" href="." data-core="pocketcdg">PocketCDG</a>
|
||||
<a class="dropdown-item" href="." data-core="prboom">Doom (PrBoom)</a>
|
||||
<a class="dropdown-item" href="." data-core="quasi88">NEC - PC-8000 / PC-8800 series (QUASI88)</a>
|
||||
<a class="dropdown-item" href="." data-core="quicknes">Nintendo - NES / Famicom (QuickNES)</a>
|
||||
<a class="dropdown-item" href="." data-core="retro8">PICO-8 (Retro8)</a>
|
||||
<a class="dropdown-item" href="." data-core="scummvm">ScummVM</a>
|
||||
<a class="dropdown-item" href="." data-core="snes9x2002">Nintendo - SNES / SFC (Snes9x 2002)</a>
|
||||
<a class="dropdown-item" href="." data-core="snes9x2005">Nintendo - SNES / SFC (Snes9x 2005)</a>
|
||||
<a class="dropdown-item" href="." data-core="snes9x2010">Nintendo - SNES / SFC (Snes9x 2010)</a>
|
||||
<a class="dropdown-item" href="." data-core="snes9x">Nintendo - SNES / SFC (Snes9x)</a>
|
||||
<a class="dropdown-item" href="." data-core="squirreljme">Java ME (SquirrelJME)</a>
|
||||
<a class="dropdown-item" href="." data-core="tamalibretro">Bandai - Tamagothci P1 (TamaLIBretro)</a>
|
||||
<a class="dropdown-item" href="." data-core="tgbdual">Nintendo - Game Boy / Color (TGB Dual)</a>
|
||||
<a class="dropdown-item" href="." data-core="theodore">Theodore (Thomson TO8/TO9)</a>
|
||||
<a class="dropdown-item" href="." data-core="tic80">TIC-80</a>
|
||||
<a class="dropdown-item" href="." data-core="tyrquake">Quake (TyrQuake)</a>
|
||||
<a class="dropdown-item" href="." data-core="uw8">MicroW8 (UW8)</a>
|
||||
<a class="dropdown-item" href="." data-core="uzem">Uzebox (Uzem)</a>
|
||||
<a class="dropdown-item" href="." data-core="vaporspec">Vaporspec</a>
|
||||
<a class="dropdown-item" href="." data-core="vba_next">Nintendo - Game Boy Advance (VBA Next)</a>
|
||||
<a class="dropdown-item" href="." data-core="vecx">GCE - Vectrex (Vecx)</a>
|
||||
<a class="dropdown-item" href="." data-core="vice_x64">Commodore - C64 (VICE x64, fast)</a>
|
||||
<a class="dropdown-item" href="." data-core="vice_x64sc">Commodore - C64 (VICE x64sc, accurate)</a>
|
||||
<a class="dropdown-item" href="." data-core="vice_x128">Commodore - C128 (VICE x128)</a>
|
||||
<a class="dropdown-item" href="." data-core="vice_xcbm2">Commodore - CBM-II 6x0/7x0 (VICE xcbm2)</a>
|
||||
<a class="dropdown-item" href="." data-core="vice_xcbm5x0">Commodore - CBM-II 5x0 (xcbm5x0)</a>
|
||||
<a class="dropdown-item" href="." data-core="vice_xpet">Commodore - PET (VICE xpet)</a>
|
||||
<a class="dropdown-item" href="." data-core="vice_xplus4">Commodore - PLUS/4 (VICE xplus4)</a>
|
||||
<a class="dropdown-item" href="." data-core="vice_xscpu64">Commodore - C64 SuperCPU (VICE xscpu4)</a>
|
||||
<a class="dropdown-item" href="." data-core="vice_xvic">Commodore - VIC-20 (VICE xvic)</a>
|
||||
<a class="dropdown-item" href="." data-core="virtualxt">VirtualXT</a>
|
||||
<a class="dropdown-item" href="." data-core="vitaquake2">Quake II (vitaQuake 2)</a>
|
||||
<a class="dropdown-item" href="." data-core="vitaquake2-rogue">Quake II - Ground Zero (vitaQuake2 (rogue))</a>
|
||||
<a class="dropdown-item" href="." data-core="vitaquake2-xatrix">Quake II - The Reckoning (vitaQuake2 (xatrix))</a>
|
||||
<a class="dropdown-item" href="." data-core="vitaquake2-zaero">Quake II - Zaero (vitaQuake2 (zaero))</a>
|
||||
<a class="dropdown-item" href="." data-core="wasm4">WASM4</a>
|
||||
<a class="dropdown-item" href="." data-core="x1">Sharp X1 (X Millenium)</a>
|
||||
<a class="dropdown-item" href="." data-core="xrick">Rick Dangerous (XRick)</a>
|
||||
</div>
|
||||
<button class="btn btn-primary disabled" id="btnRun" disabled>
|
||||
<span class="fa fa-spinner fa-spin" id="icnRun"></span> Run
|
||||
</button>
|
||||
<button class="btn btn-primary disabled" id="btnAdd" disabled>
|
||||
<span class="fa fa-plus" id="icnAdd"></span> Add Content
|
||||
</button>
|
||||
<input style="display: none" type="file" id="btnRom" name="upload" multiple />
|
||||
<button class="btn btn-primary tooltip-enable" id="btnClean" title="Cleanup storage">
|
||||
<span class="fa fa-trash-o" id="icnClean"></span> <span class="sr-only">Cleanup</span>
|
||||
</button>
|
||||
<button class="btn btn-primary disabled tooltip-enable" id="btnMenu" title="Menu toggle" disabled>
|
||||
<span class="fa fa-bars" id="icnMenu"></span> <span class="sr-only">Menu</span>
|
||||
</button>
|
||||
<button class="btn btn-primary disabled tooltip-enable" id="btnFullscreen" title="Fullscreen" disabled>
|
||||
<span class="fa fa-desktop" id="icnFullscreen"></span> <span class="sr-only">Fullscreen</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary tooltip-enable" data-toggle="modal" data-target="#helpModal">Help</button>
|
||||
</li>
|
||||
</div>
|
||||
</ul>
|
||||
<div class="toggleMenu">
|
||||
<button class="btn btn-primary" id="btnHideMenu" title="Toggle Menu">
|
||||
<span class="fa fa-chevron-up" id="icnHideMenu"></span> <span class="sr-only">Hide Top Navigation</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Basics steps modal for Web Libretro -->
|
||||
<div class="modal fade" id="helpModal" role="dialog" style="color:black;">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
<h1 class="modal-title">Basics</h1>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<h3><b>Load Core</b></h3>
|
||||
<p>Load your core by clicking on the first tab. Scroll down until you reach the desired Core. We will use Nestopia for now. Don't forget - Content must be compatible with the matched Core.</p>
|
||||
<ul>
|
||||
<li>Nes: <i>NESTOPIA</i></li>
|
||||
<li>Game Boy / Color: <i>Gambatte</i></li>
|
||||
</ul>
|
||||
<p>etc.</p>
|
||||
<p></p>
|
||||
<h3><b>Load Content</b></h3>
|
||||
<p>After selecting Core, click Run. After RetroArch opens, click Add Content and select your compatible ROM.</p>
|
||||
<ul>
|
||||
<li>Nestopia > <i>YourGame.nes</i></li>
|
||||
<li>Gambatte > <i>YourGame.gbc</i></li>
|
||||
</ul>
|
||||
<p>etc.</p>
|
||||
<p></p>
|
||||
<h3><b><span class="fa fa-trash-o"></span> Cleanup Storage</b></h3>
|
||||
<p>The trashcan erases your existing configuration and presets. If the Web Player doesn't start, you should click the trashcan and refresh the cache in your browser (usually F5 or Shift+F5).</p>
|
||||
<p></p>
|
||||
<h3><b><span class="fa fa-bars"></span> Quick Menu</b></h3>
|
||||
<p>If you click on the three line icons, the Quick Menu will open here as in RetroArch.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--/.navbar content-->
|
||||
</nav>
|
||||
<div class="bg-inverse webplayer-container">
|
||||
<div class="webplayer_border text-xs-center" id="canvas_div">
|
||||
<div class="showMenu">
|
||||
<button type="button" class="btn btn-link">
|
||||
<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>
|
||||
<img class="webplayer-preview img-fluid" src="media/canvas.png" width="960" height="720" alt="RetroArch Logo">
|
||||
</div>
|
||||
</div>
|
||||
<script crossorigin="anonymous" src="//code.jquery.com/jquery-3.1.0.min.js"></script>
|
||||
<script crossorigin="anonymous" src="//rawgit.com/jeresig/jquery.hotkeys/master/jquery.hotkeys.js"></script>
|
||||
<script crossorigin="anonymous" src="//cdnjs.cloudflare.com/ajax/libs/tether/1.3.4/js/tether.min.js"></script>
|
||||
<script crossorigin="anonymous" src="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-alpha.3/js/bootstrap.min.js"></script>
|
||||
<script src="analytics.js"></script>
|
||||
<script src="zip-no-worker.min.js"></script>
|
||||
<script src="libretro.js"></script>
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>RetroArch Web Player</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="libretro.css" rel="stylesheet" type="text/css">
|
||||
<link rel="shortcut icon" href="media/retroarch.ico">
|
||||
<!-- Font Awesome -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@5.15.4/css/all.min.css">
|
||||
<!-- Google Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
</head>
|
||||
<body>
|
||||
<!--Navbar-->
|
||||
<div id="navbar">
|
||||
<input type="checkbox" id="menuhider">
|
||||
<label for="menuhider" class="menuhiderlabel" aria-label="Hide/Show Top Navigation" id="btnHideMenu" title="Hide/Show Top Navigation">
|
||||
<span class="fa fa-chevron-up"></span>
|
||||
</label>
|
||||
<ul id="menu">
|
||||
<li class="dropdown-parent">
|
||||
<input type="checkbox" id="dropdown-box">
|
||||
<label for="dropdown-box">
|
||||
<span id="current-core">Core Selection</span>
|
||||
<span class="fa fa-caret-down"></span>
|
||||
</label>
|
||||
<div class="dropdown-child ozone-list" id="core-selector">
|
||||
<a href="." data-core="2048">2048</a>
|
||||
<a href="." data-core="anarch">Anarch</a>
|
||||
<a href="." data-core="ardens">Arduboy (Ardens)</a>
|
||||
<a href="." data-core="arduous">Arduboy (Arduous)</a>
|
||||
<a href="." data-core="bk">Elektronika - BK-0010/BK-0011 (BK)</a>
|
||||
<a href="." data-core="chailove">ChaiLove</a>
|
||||
<a href="." data-core="craft">Minecraft (Craft)</a>
|
||||
<a href="." data-core="DoubleCherryGB">Nintendo - Game Boy / Color (DoubleCherryGB)</a>
|
||||
<a href="." data-core="ecwolf">Wolfenstein 3D (ECWolf)</a>
|
||||
<a href="." data-core="fbalpha2012">Arcade (FB Alpha 2012)</a>
|
||||
<a href="." data-core="fbalpha2012_cps1">Arcade (FB Alpha 2012 CPS1)</a>
|
||||
<a href="." data-core="fbalpha2012_cps2">Arcade (FB Alpha 2012 CPS2)</a>
|
||||
<a href="." data-core="fbalpha2012_neogeo">Arcade (FB Alpha 2012 NeoGeo)</a>
|
||||
<a href="." data-core="fceumm">Nintendo - NES / Famicom (FCEUmm)</a>
|
||||
<a href="." data-core="freechaf">Fairchild ChannelF (FreeChaF)</a>
|
||||
<a href="." data-core="galaksija">Galaksija</a>
|
||||
<a href="." data-core="gambatte">Nintendo - Game Boy / Color (Gambatte)</a>
|
||||
<a href="." data-core="gme">Game Music Emu</a>
|
||||
<a href="." data-core="gearboy">Nintendo - Game Boy / Color (GearBoy)</a>
|
||||
<a href="." data-core="gearcoleco">Coleco - ColecoVision (GearColeco)</a>
|
||||
<a href="." data-core="gearsystem">Sega - MS/GG/SG-1000 (GearSystem)</a>
|
||||
<a href="." data-core="genesis_plus_gx">Sega - MS/GG/MD/CD (Genesis Plus GX)</a>
|
||||
<a href="." data-core="genesis_plus_gx_wide">Sega - MS/GG/MD/CD (Genesis Plus GX Wide)</a>
|
||||
<a href="." data-core="gong">Gong</a>
|
||||
<a href="." data-core="gw">Handheld Electronic (GW)</a>
|
||||
<a href="." data-core="handy">Atari - Lynx (Handy)</a>
|
||||
<a href="." data-core="jaxe">CHIP-8/S-CHIP/XO-CHIP (JAXE)</a>
|
||||
<a href="." data-core="jumpnbump">Jump 'n Bump</a>
|
||||
<a href="." data-core="lowresnx">LowResNX</a>
|
||||
<a href="." data-core="lutro">Lua Engine (Lutro)</a>
|
||||
<a href="." data-core="m2000">Philips - P2000T (M2000)</a>
|
||||
<a href="." data-core="mame2000">Arcade - MAME 2000</a>
|
||||
<a href="." data-core="mame2003">Arcade - MAME 2003</a>
|
||||
<a href="." data-core="mame2003_plus">Arcade - MAME 2003-Plus</a>
|
||||
<a href="." data-core="mednafen_lynx">Atari - Lynx (Beetle Lynx)</a>
|
||||
<a href="." data-core="mednafen_ngp">SNK - Neo Geo Pocket / Color (Beetle Neo Geo Pop)</a>
|
||||
<a href="." data-core="mednafen_pce_fast">NEC - PC Engine / CD (Beetle PC Engine Fast)</a>
|
||||
<a href="." data-core="mednafen_vb">Nintendo - Virtual Boy (Beetle VB)</a>
|
||||
<a href="." data-core="mednafen_wswan">Bandai - WonderSwan/Color (Beetle WonderSwan)</a>
|
||||
<a href="." data-core="mgba">Nintendo - Game Boy Advance (mGBA)</a>
|
||||
<a href="." data-core="minivmac">Mac II (MiniVmac)</a>
|
||||
<a href="." data-core="mu">Palm OS(Mu)</a>
|
||||
<a href="." data-core="mrboom">Bomberman (Mr.Boom)</a>
|
||||
<a href="." data-core="neocd">SNK - Neo Geo CD (NeoCD)</a>
|
||||
<a href="." data-core="nestopia">Nintendo - NES / Famicom (Nestopia)</a>
|
||||
<a href="." data-core="numero">Texas Instruments TI-83 (Numero)</a>
|
||||
<a href="." data-core="nxengine">Cave Story (NX Engine)</a>
|
||||
<a href="." data-core="o2em">Magnavox - Odyssey2 / Philips Videopac+ (O2EM)</a>
|
||||
<a href="." data-core="opera">The 3DO Company - 3DO (Opera)</a>
|
||||
<a href="." data-core="pcsx_rearmed">Sony - PlayStation (PCSX ReARMed)</a>
|
||||
<a href="." data-core="picodrive">Sega - MS/GG/MD/CD/32X (PicoDrive)</a>
|
||||
<a href="." data-core="pocketcdg">PocketCDG</a>
|
||||
<a href="." data-core="prboom">Doom (PrBoom)</a>
|
||||
<a href="." data-core="quasi88">NEC - PC-8000 / PC-8800 series (QUASI88)</a>
|
||||
<a href="." data-core="quicknes">Nintendo - NES / Famicom (QuickNES)</a>
|
||||
<a href="." data-core="retro8">PICO-8 (Retro8)</a>
|
||||
<a href="." data-core="scummvm">ScummVM</a>
|
||||
<a href="." data-core="snes9x2002">Nintendo - SNES / SFC (Snes9x 2002)</a>
|
||||
<a href="." data-core="snes9x2005">Nintendo - SNES / SFC (Snes9x 2005)</a>
|
||||
<a href="." data-core="snes9x2010">Nintendo - SNES / SFC (Snes9x 2010)</a>
|
||||
<a href="." data-core="snes9x">Nintendo - SNES / SFC (Snes9x)</a>
|
||||
<a href="." data-core="squirreljme">Java ME (SquirrelJME)</a>
|
||||
<a href="." data-core="tamalibretro">Bandai - Tamagothci P1 (TamaLIBretro)</a>
|
||||
<a href="." data-core="tgbdual">Nintendo - Game Boy / Color (TGB Dual)</a>
|
||||
<a href="." data-core="theodore">Theodore (Thomson TO8/TO9)</a>
|
||||
<a href="." data-core="tic80">TIC-80</a>
|
||||
<a href="." data-core="tyrquake">Quake (TyrQuake)</a>
|
||||
<a href="." data-core="uw8">MicroW8 (UW8)</a>
|
||||
<a href="." data-core="uzem">Uzebox (Uzem)</a>
|
||||
<a href="." data-core="vaporspec">Vaporspec</a>
|
||||
<a href="." data-core="vba_next">Nintendo - Game Boy Advance (VBA Next)</a>
|
||||
<a href="." data-core="vecx">GCE - Vectrex (Vecx)</a>
|
||||
<a href="." data-core="vice_x64">Commodore - C64 (VICE x64, fast)</a>
|
||||
<a href="." data-core="vice_x64sc">Commodore - C64 (VICE x64sc, accurate)</a>
|
||||
<a href="." data-core="vice_x128">Commodore - C128 (VICE x128)</a>
|
||||
<a href="." data-core="vice_xcbm2">Commodore - CBM-II 6x0/7x0 (VICE xcbm2)</a>
|
||||
<a href="." data-core="vice_xcbm5x0">Commodore - CBM-II 5x0 (xcbm5x0)</a>
|
||||
<a href="." data-core="vice_xpet">Commodore - PET (VICE xpet)</a>
|
||||
<a href="." data-core="vice_xplus4">Commodore - PLUS/4 (VICE xplus4)</a>
|
||||
<a href="." data-core="vice_xscpu64">Commodore - C64 SuperCPU (VICE xscpu4)</a>
|
||||
<a href="." data-core="vice_xvic">Commodore - VIC-20 (VICE xvic)</a>
|
||||
<a href="." data-core="virtualxt">VirtualXT</a>
|
||||
<a href="." data-core="vitaquake2">Quake II (vitaQuake 2)</a>
|
||||
<a href="." data-core="vitaquake2-rogue">Quake II - Ground Zero (vitaQuake2 (rogue))</a>
|
||||
<a href="." data-core="vitaquake2-xatrix">Quake II - The Reckoning (vitaQuake2 (xatrix))</a>
|
||||
<a href="." data-core="vitaquake2-zaero">Quake II - Zaero (vitaQuake2 (zaero))</a>
|
||||
<a href="." data-core="wasm4">WASM4</a>
|
||||
<a href="." data-core="x1">Sharp X1 (X Millenium)</a>
|
||||
<a href="." data-core="xrick">Rick Dangerous (XRick)</a>
|
||||
</div>
|
||||
</li>
|
||||
<li id="btnRun" class="disabled">
|
||||
<span class="fa fa-spinner fa-spin" id="icnRun"></span> Run
|
||||
</li>
|
||||
<li id="btnAdd" class="disabled">
|
||||
<span class="fa fa-plus" id="icnAdd"></span> Add Content
|
||||
</li>
|
||||
<li id="btnFiles" title="Manage files" aria-label="Manage files">
|
||||
<span class="fa fa-file"></span>
|
||||
</li>
|
||||
<li id="btnMenu" title="Menu toggle" aria-label="Menu" class="disabled">
|
||||
<span class="fa fa-bars"></span>
|
||||
</li>
|
||||
<li id="btnFullscreen" title="Fullscreen" aria-label="Fullscreen" class="disabled">
|
||||
<span class="fa fa-expand"></span>
|
||||
</li>
|
||||
<li id="btnHelp">Help</li>
|
||||
</ul>
|
||||
<div class="progressContainer">
|
||||
<div class="progressBar" id="progressBarMain"></div>
|
||||
<div class="progressText" id="progressTextMain"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="modals">
|
||||
<div id="modal-window">
|
||||
<div class="modal-header">
|
||||
<h2 id="modal-title">Sample title</h2>
|
||||
<div id="modal-close"><span class="fa fa-times"></span></div>
|
||||
<div class="progressContainer">
|
||||
<div class="progressBar" id="progressBarModal"></div>
|
||||
<div class="progressText" id="progressTextModal"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Basic steps modal for Web Libretro -->
|
||||
<div class="modal-body" role="dialog" id="helpModal">
|
||||
<h3><b>Load Core</b></h3>
|
||||
<p>Load your core by clicking on the first tab. Scroll down until you reach the desired Core. We will use Nestopia for now. Don't forget - Content must be compatible with the matched Core.</p>
|
||||
<ul>
|
||||
<li>NES: <i>Nestopia</i></li>
|
||||
<li>Game Boy / Color: <i>Gambatte</i></li>
|
||||
</ul>
|
||||
<p>etc.</p>
|
||||
<p></p>
|
||||
<h3><b>Load Content</b></h3>
|
||||
<p>After selecting Core, click Run. After RetroArch opens, click Add Content and select your compatible ROM.</p>
|
||||
<ul>
|
||||
<li>Nestopia > <i>YourGame.nes</i></li>
|
||||
<li>Gambatte > <i>YourGame.gb(c)</i></li>
|
||||
</ul>
|
||||
<p>etc.</p>
|
||||
<p></p>
|
||||
<h3><b><span class="fa fa-file"></span> File Management</b></h3>
|
||||
<p>Download/upload/erase save data</p>
|
||||
<p>If the Web Player doesn't start, you should click "Delete all" and refresh the cache in your browser (usually F5 or Shift+F5).</p>
|
||||
<p>If this happens, please also report an issue on <a target="_blank" href="https://github.com/libretro/RetroArch/issues">GitHub</a> with logs from the browser's developer tools console.</p>
|
||||
<p></p>
|
||||
<h3><b><span class="fa fa-bars"></span> Quick Menu</b></h3>
|
||||
<p>If you click on the three line icons, the Quick Menu will open in RetroArch.</p>
|
||||
</div>
|
||||
<!-- File management -->
|
||||
<div class="modal-body" role="dialog" id="filesModal">
|
||||
<div class="ozone-list" id="fileManagerPanel">
|
||||
<span data-action="upload_saves">Upload saves</span>
|
||||
<span data-action="upload_states">Upload states</span>
|
||||
<span data-action="upload_system">Upload system files</span>
|
||||
<span data-action="download_sss">Download saves/states/screenshots</span>
|
||||
<span data-action="download_all">Download all (slow)</span>
|
||||
<span data-action="delete_sss" class="danger">Delete saves/states/screenshots</span>
|
||||
<span data-action="delete_content" class="danger">Delete content</span>
|
||||
<span data-action="delete_config" class="danger">Delete config</span>
|
||||
<span data-action="delete_assets" class="danger">Delete assets</span>
|
||||
<span data-action="delete_all" class="danger">Delete all</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="webplayer-container">
|
||||
<canvas class="webplayer" id="canvas" tabindex="1"></canvas>
|
||||
<div id="webplayer-preview"></div>
|
||||
</div>
|
||||
<script src="libretro.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
15
pkg/emscripten/libretro-thread/jsdeps/browserfs.min.js
vendored
Normal file
15
pkg/emscripten/libretro-thread/jsdeps/browserfs.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
pkg/emscripten/libretro-thread/jsdeps/zip-full.min.js
vendored
Normal file
1
pkg/emscripten/libretro-thread/jsdeps/zip-full.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -4,94 +4,103 @@
|
||||
* This provides the basic styling for the RetroArch web player.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Make sure the background of the player is black.
|
||||
* Also make sure line height is 0 so there's no extra space on the bottom.
|
||||
*/
|
||||
.webplayer-container {
|
||||
background-color: black;
|
||||
line-height: 0;
|
||||
}
|
||||
@import url("https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,400..700&display=swap");
|
||||
|
||||
/**
|
||||
* Webplayer Preview when not loaded.
|
||||
*/
|
||||
.webplayer-preview {
|
||||
margin: 0 auto;
|
||||
cursor: wait;
|
||||
opacity: 0.2;
|
||||
transition: all 0.8s;
|
||||
-webkit-animation: loading 0.8s ease-in-out infinite alternate;
|
||||
-moz-animation: loading 0.8s ease-in-out infinite alternate;
|
||||
animation: loading 0.8s ease-in-out infinite alternate;
|
||||
}
|
||||
.webplayer-preview.loaded {
|
||||
cursor: pointer;
|
||||
opacity: 1;
|
||||
-webkit-animation: loaded 0.8s ease-in-out;
|
||||
-moz-animation: loaded 0.8s ease-in-out;
|
||||
animation: loaded 0.8s ease-in-out;
|
||||
}
|
||||
@keyframes loaded {
|
||||
from {
|
||||
opacity: 0.2;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@-moz-keyframes loaded {
|
||||
from {
|
||||
opacity: 0.2;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes loaded {
|
||||
from {
|
||||
opacity: 0.2;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes loading{
|
||||
from {
|
||||
opacity: 0.2;
|
||||
}
|
||||
to {
|
||||
opacity: 0.35;
|
||||
}
|
||||
}
|
||||
@-moz-keyframes loading{
|
||||
from {
|
||||
opacity: 0.2;
|
||||
}
|
||||
to {
|
||||
opacity: 0.35;
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes loading {
|
||||
from {
|
||||
opacity: 0.2;
|
||||
}
|
||||
to {
|
||||
opacity: 0.35;
|
||||
}
|
||||
from {
|
||||
opacity: 0.2;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
canvas.webplayer {
|
||||
border: none;
|
||||
outline: none;
|
||||
width: 800px;
|
||||
height: 600px;
|
||||
@keyframes loading {
|
||||
from {
|
||||
opacity: 0.2;
|
||||
}
|
||||
to {
|
||||
opacity: 0.35;
|
||||
}
|
||||
}
|
||||
|
||||
#canvas_div {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
/* real ozone */
|
||||
@keyframes hover {
|
||||
from {
|
||||
box-shadow: 0px 0px 1px 1px #198AC6, inset 0px 0px 1px 1px #198AC6;
|
||||
}
|
||||
to {
|
||||
box-shadow: 0px 0px 1px 1px #89F1F2, inset 0px 0px 1px 1px #89F1F2;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes hover-danger {
|
||||
from {
|
||||
box-shadow: 0px 0px 1px 1px #C7261A, inset 0px 0px 1px 1px #C7261A;
|
||||
}
|
||||
to {
|
||||
box-shadow: 0px 0px 1px 1px #F75C4A, inset 0px 0px 1px 1px #F75C4A;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0px;
|
||||
background-color: black;
|
||||
font-family: "Inter", sans-serif;
|
||||
font-variant-ligatures: no-contextual;
|
||||
scrollbar-color: #505050 #2e2e2e;
|
||||
|
||||
--menuheight: 65px;
|
||||
--menumarginy: 14px;
|
||||
--menumarginx: 7px;
|
||||
--menupaddingy: 8px;
|
||||
--menupaddingx: 16px;
|
||||
--submarginy: 0px;
|
||||
--submarginx: 0px;
|
||||
--subpaddingy: 5px;
|
||||
--subpaddingx: 8px;
|
||||
--rounding: 1px;
|
||||
--barcolor: #2e2e2e;
|
||||
--subcolor: #282828;
|
||||
--barbuttoncolor: var(--barcolor);
|
||||
--barbuttonhovercolor: #212227;
|
||||
--barbuttonoutline: 1px;
|
||||
--barbuttonoutlinecolor: #51514f;
|
||||
--barfontcolor: #ffffff;
|
||||
--bardisabledfontcolor: #b6b6b6;
|
||||
|
||||
/* do not modify */
|
||||
--actualmenuheight: var(--menuheight);
|
||||
--menubuttonbordery: calc(var(--menuheight) - calc(var(--menumarginy) * 2));
|
||||
--menubuttoncontenty: calc(var(--menubuttonbordery) - calc(var(--menupaddingy) * 2));
|
||||
}
|
||||
|
||||
canvas.webplayer, #webplayer-preview {
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: var(--actualmenuheight);
|
||||
width: 100vw;
|
||||
height: calc(100vh - var(--actualmenuheight));
|
||||
outline: none;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
#webplayer-preview {
|
||||
background-image: url(media/canvas.png);
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
cursor: wait;
|
||||
opacity: 0.2;
|
||||
transition: opacity 0.8s;
|
||||
animation: loading 0.8s ease-in-out infinite alternate;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
#webplayer-preview.loaded {
|
||||
cursor: pointer;
|
||||
opacity: 1;
|
||||
animation: loaded 0.8s ease-in-out;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -99,38 +108,310 @@ canvas.webplayer {
|
||||
* Foiled again!
|
||||
*/
|
||||
:fullscreen canvas.webplayer {
|
||||
min-width: 100vw;
|
||||
max-width: 100vw;
|
||||
min-height: 100vh;
|
||||
max-height: 100vh;
|
||||
top: 0px;
|
||||
min-width: 100vw;
|
||||
max-width: 100vw;
|
||||
min-height: 100vh;
|
||||
max-height: 100vh;
|
||||
}
|
||||
|
||||
textarea {
|
||||
font-family: monospace;
|
||||
font-size: 0.7em;
|
||||
height: 95%;
|
||||
width: 95%;
|
||||
border-style: none;
|
||||
border-color: transparent;
|
||||
overflow: auto;
|
||||
resize: none;
|
||||
a {
|
||||
color: #3ec3f0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle Top Navigation
|
||||
*/
|
||||
.toggleMenu {
|
||||
float: right;
|
||||
}
|
||||
.showMenu {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
#icnShowMenu {
|
||||
color: #565656 !important;
|
||||
/* menu bar */
|
||||
|
||||
#navbar {
|
||||
position: fixed;
|
||||
width: 100vw;
|
||||
height: var(--actualmenuheight);
|
||||
background-color: var(--barcolor);
|
||||
color: var(--barfontcolor);
|
||||
z-index: 20;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
box-shadow: none;
|
||||
#menu {
|
||||
float: left;
|
||||
list-style: none;
|
||||
margin: 0px;
|
||||
width: max-content;
|
||||
height: 100%;
|
||||
padding: 0px var(--menumarginx);
|
||||
}
|
||||
|
||||
.progressContainer {
|
||||
width: 100%;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.progressBar {
|
||||
width: 100%;
|
||||
height: 0px;
|
||||
--progressbarpercent: 0%;
|
||||
--progressbarcolor: #1fb01a;
|
||||
}
|
||||
|
||||
.progressBar::after {
|
||||
content: "";
|
||||
display: block;
|
||||
width: var(--progressbarpercent);
|
||||
height: 100%;
|
||||
transition: width 0.2s ease-out;
|
||||
background-color: var(--progressbarcolor);
|
||||
}
|
||||
|
||||
.progressText {
|
||||
padding: 0px 4px;
|
||||
}
|
||||
|
||||
#menu li {
|
||||
white-space: nowrap;
|
||||
max-width: 500px;
|
||||
position: relative;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#menu>li {
|
||||
margin: var(--menumarginy) var(--menumarginx);
|
||||
display: inline-block;
|
||||
float: left;
|
||||
height: var(--menubuttoncontenty);
|
||||
line-height: var(--menubuttoncontenty);
|
||||
border-radius: var(--rounding);
|
||||
background-color: var(--barbuttoncolor);
|
||||
outline: var(--barbuttonoutline) solid var(--barbuttonoutlinecolor);
|
||||
font-size: 12pt;
|
||||
}
|
||||
|
||||
.dropdown-parent {
|
||||
height: var(--menubuttonbordery) !important;
|
||||
}
|
||||
|
||||
.dropdown-parent>label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#menu>li:not(.dropdown-parent),
|
||||
.dropdown-parent>label {
|
||||
padding: var(--menupaddingy) var(--menupaddingx);
|
||||
}
|
||||
|
||||
label {
|
||||
cursor: inherit;
|
||||
}
|
||||
|
||||
.ozone-list {
|
||||
background-color: var(--subcolor);
|
||||
width: max-content;
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
.dropdown-child {
|
||||
position: absolute;
|
||||
overflow: hidden auto;
|
||||
max-height: calc(100vh - var(--actualmenuheight));
|
||||
display: none;
|
||||
top: calc(100% + 1px);
|
||||
left: 0px;
|
||||
border-radius: var(--rounding);
|
||||
box-sizing: border-box;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#fileManagerPanel {
|
||||
margin: auto;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.ozone-list>* {
|
||||
display: block;
|
||||
border-radius: var(--rounding);
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
margin: var(--submarginy) var(--submarginx);
|
||||
padding: 8px 12px;
|
||||
line-height: initial;
|
||||
font-size: 10pt;
|
||||
outline: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.dropdown-child>* {
|
||||
padding: var(--subpaddingy) var(--subpaddingx);
|
||||
}
|
||||
|
||||
.ozone-list>*:not(:last-child) {
|
||||
box-shadow: 0px 2px 0px -1px var(--barbuttonoutlinecolor);
|
||||
}
|
||||
|
||||
#menu>li:not(.disabled),
|
||||
.ozone-list>*:not(.disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#menu .disabled,
|
||||
.ozone-list .disabled {
|
||||
cursor: not-allowed !important;
|
||||
color: var(--bardisabledfontcolor);
|
||||
}
|
||||
|
||||
#menu>li:not(.disabled):not(.dropdown-parent:has(.dropdown-child:hover)):hover,
|
||||
.ozone-list>*:hover,
|
||||
.ozone-list>*:focus,
|
||||
#menuhider:not(:checked) ~ .menuhiderlabel:hover {
|
||||
background-color: var(--barbuttonhovercolor);
|
||||
outline: none;
|
||||
animation: hover 0.5s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
.ozone-list>.danger:hover,
|
||||
.ozone-list>.danger:focus {
|
||||
animation: hover-danger 0.5s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
/* hide/show the menu */
|
||||
|
||||
.menuhiderlabel {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#menuhider:not(:checked) ~ .menuhiderlabel {
|
||||
margin: var(--menumarginy) 0px;
|
||||
margin-right: calc(var(--menumarginx) * 2);
|
||||
width: var(--menubuttonbordery) !important;
|
||||
height: var(--menubuttonbordery) !important;
|
||||
border-radius: var(--rounding);
|
||||
background-color: var(--barbuttoncolor);
|
||||
outline: var(--barbuttonoutline) solid var(--barbuttonoutlinecolor);
|
||||
}
|
||||
|
||||
.menuhiderlabel span {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
#menuhider:checked ~ .menuhiderlabel span {
|
||||
transform: translate(-50%, -50%) scaleY(-1);
|
||||
color: #dfdfdf !important;
|
||||
}
|
||||
|
||||
#menuhider:checked ~ .menuhiderlabel:hover {
|
||||
background-color: rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
|
||||
.hide,
|
||||
#dropdown-box,
|
||||
#menuhider,
|
||||
#menuhider:checked ~ #menu {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#dropdown-box:checked ~ .dropdown-child {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* modals */
|
||||
|
||||
#modals {
|
||||
display: none;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
backdrop-filter: blur(6px);
|
||||
position: fixed;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 30;
|
||||
}
|
||||
|
||||
#modal-window {
|
||||
background-color: var(--barcolor);
|
||||
color: var(--barfontcolor);
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
max-height: calc(100vh - 40px);
|
||||
max-width: calc(100vw - 40px);
|
||||
width: 750px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
height: 50px;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.modal-header .progressContainer {
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
}
|
||||
|
||||
.modal-header .progressText {
|
||||
white-space: nowrap;
|
||||
font-size: 10pt;
|
||||
}
|
||||
|
||||
#modal-title {
|
||||
display: inline-block;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
#modal-close {
|
||||
float: right;
|
||||
width: 30px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
font-size: 24px;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
display: none;
|
||||
width: calc(100% - 40px);
|
||||
max-height: calc(100vh - 135px);
|
||||
padding: 20px;
|
||||
padding-top: 0px;
|
||||
margin-top: 25px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.modal-body>*:first-child {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
/* mobile */
|
||||
|
||||
@media only screen and (max-width: 720px) {
|
||||
body {
|
||||
--subpaddingy: 8px;
|
||||
}
|
||||
|
||||
#menu {
|
||||
width: calc(100% - calc(var(--menumarginx) * 2));
|
||||
height: max-content;
|
||||
padding-bottom: var(--menumarginy);
|
||||
background-color: var(--barcolor);
|
||||
}
|
||||
|
||||
#menu>li {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.dropdown-child {
|
||||
max-width: calc(100vw - calc(var(--menumarginx) * 4));
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.dropdown-child a {
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
@ -4,237 +4,416 @@
|
||||
* This provides the basic JavaScript for the RetroArch web player.
|
||||
*/
|
||||
|
||||
var filesystem_ready = false;
|
||||
var retroarch_ready = false;
|
||||
var setImmediate;
|
||||
const canvas = document.getElementById("canvas");
|
||||
const webplayerPreview = document.getElementById("webplayer-preview");
|
||||
const menuBar = document.getElementById("navbar");
|
||||
const menuHider = document.getElementById("menuhider");
|
||||
const coreSelector = document.getElementById("core-selector");
|
||||
const coreSelectorCurrent = document.getElementById("current-core");
|
||||
const dropdownBox = document.getElementById("dropdown-box");
|
||||
const btnFiles = document.getElementById("btnFiles");
|
||||
const btnRun = document.getElementById("btnRun");
|
||||
const btnMenu = document.getElementById("btnMenu");
|
||||
const btnFullscreen = document.getElementById("btnFullscreen");
|
||||
const btnHelp = document.getElementById("btnHelp");
|
||||
const btnAdd = document.getElementById("btnAdd");
|
||||
const icnRun = document.getElementById("icnRun");
|
||||
const icnAdd = document.getElementById("icnAdd");
|
||||
const modalContainer = document.getElementById("modals");
|
||||
const modalWindow = document.getElementById("modal-window");
|
||||
const modalTitle = document.getElementById("modal-title");
|
||||
const modalClose = document.getElementById("modal-close");
|
||||
const fileManagerPanel = document.getElementById("fileManagerPanel");
|
||||
|
||||
var Module = {
|
||||
noInitialRun: true,
|
||||
arguments: ["-v", "--menu"],
|
||||
noImageDecoding: true,
|
||||
noAudioDecoding: true,
|
||||
|
||||
retroArchSend: function(msg) {
|
||||
this.EmscriptenSendCommand(msg);
|
||||
},
|
||||
retroArchRecv: function() {
|
||||
return this.EmscriptenReceiveCommandReply();
|
||||
},
|
||||
preRun: [
|
||||
function(module) {
|
||||
Module.ENV['OPFS'] = "/home/web_user/retroarch";
|
||||
}
|
||||
],
|
||||
postRun: [],
|
||||
onRuntimeInitialized: function() {
|
||||
retroarch_ready = true;
|
||||
appInitialized();
|
||||
},
|
||||
print: function(text) {
|
||||
console.log("stdout:", text);
|
||||
},
|
||||
printErr: function(text) {
|
||||
console.log("stderr:", text);
|
||||
},
|
||||
canvas: document.getElementById("canvas"),
|
||||
totalDependencies: 0,
|
||||
monitorRunDependencies: function(left) {
|
||||
this.totalDependencies = Math.max(this.totalDependencies, left);
|
||||
}
|
||||
const progressTrackers = {
|
||||
"main": {bar: document.getElementById("progressBarMain"), text: document.getElementById("progressTextMain")},
|
||||
"modal": {bar: document.getElementById("progressBarModal"), text: document.getElementById("progressTextModal")}
|
||||
};
|
||||
|
||||
const modals = {
|
||||
"help": {title: "Basics", width: "750px", element: document.getElementById("helpModal")},
|
||||
"files": {title: "File Management", width: "400px", element: document.getElementById("filesModal")}
|
||||
};
|
||||
|
||||
async function cleanupStorage()
|
||||
{
|
||||
localStorage.clear();
|
||||
const root = await navigator.storage.getDirectory();
|
||||
for await (const handle of root.values()) {
|
||||
await root.removeEntry(handle.name, {recursive: true});
|
||||
}
|
||||
document.getElementById("btnClean").disabled = true;
|
||||
// Attempt to disable some default browser keys.
|
||||
const disableKeys = {
|
||||
9: "tab",
|
||||
13: "enter",
|
||||
16: "shift",
|
||||
18: "alt",
|
||||
27: "esc",
|
||||
33: "rePag",
|
||||
34: "avPag",
|
||||
35: "end",
|
||||
36: "home",
|
||||
37: "left",
|
||||
38: "up",
|
||||
39: "right",
|
||||
40: "down",
|
||||
112: "F1",
|
||||
113: "F2",
|
||||
114: "F3",
|
||||
115: "F4",
|
||||
116: "F5",
|
||||
117: "F6",
|
||||
118: "F7",
|
||||
119: "F8",
|
||||
120: "F9",
|
||||
121: "F10",
|
||||
122: "F11",
|
||||
123: "F12"
|
||||
};
|
||||
|
||||
let fsLoadPromise;
|
||||
|
||||
// all methods provided by the worker that we may require
|
||||
const workerHandlers = {FS: ["init", "writeFile", "readFile", "mkdirTree", "readdir", "readdirTree", "rm", "stat"], helper: ["loadFS", "zipDirs"]};
|
||||
|
||||
const worker = new Worker("libretro.worker.js");
|
||||
let workerMessageQueue = [];
|
||||
worker.onmessage = (msg) => {
|
||||
switch (msg.data?.type) {
|
||||
case "noReturn":
|
||||
window[msg.data?.func]?.apply(null, msg.data?.args);
|
||||
break;
|
||||
case "ret":
|
||||
const ind = workerMessageQueue.findIndex(i => msg.data?.id in i);
|
||||
if (ind < 0) break;
|
||||
const promise = workerMessageQueue.splice(ind, 1)[0][msg.data.id];
|
||||
if (msg.data.err) {
|
||||
promise.reject(msg.data?.ret);
|
||||
} else {
|
||||
promise.resolve(msg.data?.ret);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function appInitialized()
|
||||
{
|
||||
/* Need to wait for the wasm runtime to load before enabling the Run button. */
|
||||
if (retroarch_ready && filesystem_ready)
|
||||
{
|
||||
preLoadingComplete();
|
||||
}
|
||||
}
|
||||
|
||||
function preLoadingComplete() {
|
||||
$('#icnRun').removeClass('fa-spinner').removeClass('fa-spin');
|
||||
$('#icnRun').addClass('fa-play');
|
||||
// Make the Preview image clickable to start RetroArch.
|
||||
$('.webplayer-preview').addClass('loaded').click(function() {
|
||||
startRetroArch();
|
||||
return false;
|
||||
});
|
||||
$('#btnRun').removeClass('disabled').removeAttr("disabled").click(function() {
|
||||
startRetroArch();
|
||||
return false;
|
||||
});
|
||||
function handleWorkerFunc(handler, method, args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const id = "" + Math.random();
|
||||
workerMessageQueue.push({[id]: {resolve: resolve, reject: reject}});
|
||||
worker.postMessage({id: id, handler: handler, method: method, args: args});
|
||||
});
|
||||
}
|
||||
|
||||
// Retrieve the value of the given GET parameter.
|
||||
function getParam(name) {
|
||||
var results = new RegExp('[?&]' + name + '=([^&#]*)').exec(window.location.href);
|
||||
if (results) {
|
||||
return results[1] || null;
|
||||
}
|
||||
// add the global functions from workerHandlers
|
||||
// this makes the methods here appear identical to the implementation in the worker
|
||||
for (let [handler, methods] of Object.entries(workerHandlers)) {
|
||||
let methodHandlers = {};
|
||||
for (let method of methods) {
|
||||
methodHandlers[method] = async function() {
|
||||
return await handleWorkerFunc(handler, method, Array.from(arguments));
|
||||
}
|
||||
}
|
||||
window[handler] = methodHandlers;
|
||||
}
|
||||
|
||||
// console.log alias for worker to use
|
||||
function debugLog() {
|
||||
console.log.apply(null, Array.from(arguments));
|
||||
}
|
||||
|
||||
// n is the name of the bar ("main" or "modal")
|
||||
// progress in range [0, 1]
|
||||
function setProgress(n, progress) {
|
||||
const progressBar = progressTrackers[n]?.bar;
|
||||
if (!progressBar) return;
|
||||
if (isNaN(progress)) progress = 0;
|
||||
progressBar.style.height = progress ? "4px" : "0px";
|
||||
progressBar.style.setProperty("--progressbarpercent", (progress * 100) + "%");
|
||||
}
|
||||
|
||||
function setProgressColor(n, color) {
|
||||
const progressBar = progressTrackers[n]?.bar;
|
||||
if (!progressBar) return;
|
||||
progressBar.style.setProperty("--progressbarcolor", color || "#1fb01a");
|
||||
}
|
||||
|
||||
function setProgressText(n, text) {
|
||||
const progressText = progressTrackers[n]?.text;
|
||||
if (!progressText) return;
|
||||
progressText.textContent = text ?? "";
|
||||
}
|
||||
|
||||
// "help" or "files"
|
||||
function openModal(which) {
|
||||
if (which in modals) {
|
||||
for (const modal of Object.values(modals)) {
|
||||
modal.element.style.display = "none";
|
||||
}
|
||||
modalTitle.textContent = modals[which].title ?? "";
|
||||
modalWindow.style.width = modals[which].width ?? "750px";
|
||||
modals[which].element.style.display = "block";
|
||||
modalContainer.style.display = "block";
|
||||
}
|
||||
}
|
||||
|
||||
modalClose.addEventListener("click", function() {
|
||||
modalContainer.style.display = "none";
|
||||
});
|
||||
|
||||
var Module = {
|
||||
noInitialRun: true,
|
||||
arguments: ["-v", "--menu"],
|
||||
noImageDecoding: true,
|
||||
noAudioDecoding: true,
|
||||
|
||||
retroArchSend: function(msg) {
|
||||
this.EmscriptenSendCommand(msg);
|
||||
},
|
||||
retroArchRecv: function() {
|
||||
return this.EmscriptenReceiveCommandReply();
|
||||
},
|
||||
preRun: [
|
||||
function(module) {
|
||||
module.ENV["OPFS_MOUNT"] = "/home/web_user";
|
||||
}
|
||||
],
|
||||
onRuntimeInitialized: function() {
|
||||
appInitialized();
|
||||
},
|
||||
print: function(text) {
|
||||
console.log("stdout:", text);
|
||||
},
|
||||
printErr: function(text) {
|
||||
console.log("stderr:", text);
|
||||
},
|
||||
canvas: canvas,
|
||||
totalDependencies: 0,
|
||||
monitorRunDependencies: function(left) {
|
||||
this.totalDependencies = Math.max(this.totalDependencies, left);
|
||||
}
|
||||
};
|
||||
|
||||
// read File object to an ArrayBuffer
|
||||
function readFile(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let reader = new FileReader();
|
||||
reader.onload = function() {
|
||||
resolve(this.result);
|
||||
}
|
||||
reader.onerror = function(e) {
|
||||
reject(e);
|
||||
}
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
}
|
||||
|
||||
// accept (optional) can be used to specify file extensions (string, comma delimited)
|
||||
// returns an array of {path: String, data: ArrayBuffer}
|
||||
function uploadFiles(accept) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.setAttribute("multiple", "");
|
||||
if (accept) input.accept = accept;
|
||||
input.onchange = async function() {
|
||||
let files = [];
|
||||
for (const file of this.files) {
|
||||
files.push({path: file.name, data: await readFile(file)});
|
||||
}
|
||||
resolve(files);
|
||||
}
|
||||
input.oncancel = function() {
|
||||
resolve([]);
|
||||
}
|
||||
input.click();
|
||||
});
|
||||
}
|
||||
|
||||
// prompt user to upload file(s) to a dir in OPFS, e.g. "/retroarch/content"
|
||||
async function uploadFilesToDir(dir, accept) {
|
||||
const files = await uploadFiles(accept);
|
||||
for (const file of files) {
|
||||
await FS.writeFile(dir + "/" + file.path, new Uint8Array(file.data));
|
||||
console.log("file upload complete: " + file.path);
|
||||
}
|
||||
}
|
||||
|
||||
// download data (ArrayBuffer/DataView) with file name and optional mime type
|
||||
function downloadFile(data, name, mime) {
|
||||
let a = document.createElement("a");
|
||||
a.download = name;
|
||||
a.href = URL.createObjectURL(new Blob([data], {type: mime || "application/octet-stream"}));
|
||||
a.click();
|
||||
window.setTimeout(function() {
|
||||
URL.revokeObjectURL(a.href);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// click handler for the file manager modal
|
||||
async function fileManagerEvent(target) {
|
||||
const action = target?.dataset?.action;
|
||||
if (!action) return;
|
||||
target.classList.add("disabled");
|
||||
let data;
|
||||
switch (action) {
|
||||
case "upload_saves":
|
||||
await uploadFilesToDir("/retroarch/saves");
|
||||
break;
|
||||
case "upload_states":
|
||||
await uploadFilesToDir("/retroarch/states");
|
||||
break;
|
||||
case "upload_system":
|
||||
await uploadFilesToDir("/retroarch/system");
|
||||
break;
|
||||
case "download_sss":
|
||||
data = await helper.zipDirs("/retroarch/saves", "/retroarch/states", "/retroarch/screenshots");
|
||||
downloadFile(data, "saves_states_screenshots.zip", "application/zip");
|
||||
break;
|
||||
case "download_all":
|
||||
data = await helper.zipDirs("/retroarch/saves", "/retroarch/states", "/retroarch/screenshots", "/retroarch/content");
|
||||
downloadFile(data, "all.zip", "application/zip");
|
||||
break;
|
||||
case "delete_sss":
|
||||
await FS.rm("/retroarch/saves", "/retroarch/states", "/retroarch/screenshots");
|
||||
break;
|
||||
case "delete_content":
|
||||
await FS.rm("/retroarch/content");
|
||||
break;
|
||||
case "delete_config":
|
||||
await FS.rm("/.config/retroarch");
|
||||
break;
|
||||
case "delete_assets":
|
||||
await FS.rm("/retroarch/.bundle-timestamp", "/retroarch/assets", "/retroarch/autoconfig",
|
||||
"/retroarch/database", "/retroarch/filters", "/retroarch/info", "/retroarch/overlays", "/retroarch/shaders");
|
||||
break;
|
||||
case "delete_all":
|
||||
await FS.rm("/retroarch", "/.config/retroarch");
|
||||
break;
|
||||
}
|
||||
target.classList.remove("disabled");
|
||||
}
|
||||
|
||||
function appIsSmallScreen() {
|
||||
return window.matchMedia("(max-width: 720px)").matches;
|
||||
}
|
||||
|
||||
// used for the menu hider
|
||||
function adjustMenuHeight() {
|
||||
const actualMenuHeight = menuHider.checked ? 0 : 65;
|
||||
document.body.style.setProperty("--actualmenuheight", actualMenuHeight + "px", "important")
|
||||
}
|
||||
|
||||
function startRetroArch() {
|
||||
$('.webplayer').show();
|
||||
$('.webplayer-preview').hide();
|
||||
document.getElementById("btnRun").disabled = true;
|
||||
// show the "changes you made may not be saved" warning
|
||||
window.addEventListener("beforeunload", function(e) { e.preventDefault(); });
|
||||
|
||||
$('#btnAdd').removeClass("disabled").removeAttr("disabled").click(function() {
|
||||
$('#btnRom').click();
|
||||
});
|
||||
$('#btnRom').removeAttr("disabled").change(function(e) {
|
||||
selectFiles(e.target.files);
|
||||
});
|
||||
$('#btnMenu').removeClass("disabled").removeAttr("disabled").click(function() {
|
||||
Module._cmd_toggle_menu();
|
||||
Module.canvas.focus();
|
||||
});
|
||||
$('#btnFullscreen').removeClass("disabled").removeAttr("disabled").click(function() {
|
||||
Module.requestFullscreen(false);
|
||||
Module.canvas.focus();
|
||||
});
|
||||
Module.canvas.focus();
|
||||
Module.canvas.addEventListener("pointerdown", function() {
|
||||
Module.canvas.focus();
|
||||
}, false);
|
||||
Module.callMain(Module.arguments);
|
||||
window.addEventListener("keydown", function(e) {
|
||||
if (disableKeys[e.which]) e.preventDefault();
|
||||
});
|
||||
|
||||
webplayerPreview.classList.add("hide");
|
||||
btnRun.classList.add("hide");
|
||||
|
||||
btnMenu.classList.remove("disabled");
|
||||
btnMenu.addEventListener("click", function() {
|
||||
Module._cmd_toggle_menu();
|
||||
});
|
||||
|
||||
btnFullscreen.classList.remove("disabled");
|
||||
btnFullscreen.addEventListener("click", function() {
|
||||
Module.requestFullscreen(false);
|
||||
});
|
||||
|
||||
// ensure the canvas is focused so that keyboard events work
|
||||
Module.canvas.focus();
|
||||
Module.canvas.addEventListener("pointerdown", function() {
|
||||
Module.canvas.focus();
|
||||
}, false);
|
||||
menuBar.addEventListener("pointerdown", function() {
|
||||
setTimeout(function() {
|
||||
Module.canvas.focus();
|
||||
}, 0);
|
||||
}, false);
|
||||
|
||||
Module.callMain(Module.arguments);
|
||||
}
|
||||
|
||||
function selectFiles(files) {
|
||||
$('#btnAdd').addClass('disabled');
|
||||
$('#icnAdd').removeClass('fa-plus');
|
||||
$('#icnAdd').addClass('fa-spinner spinning');
|
||||
var count = files.length;
|
||||
|
||||
for (var i = 0; i < count; i++) {
|
||||
filereader = new FileReader();
|
||||
filereader.file_name = files[i].name;
|
||||
filereader.readAsArrayBuffer(files[i]);
|
||||
filereader.onload = function() {
|
||||
uploadData(this.result, this.file_name)
|
||||
};
|
||||
filereader.onloadend = function(evt) {
|
||||
console.log("WEBPLAYER: file: " + this.file_name + " upload complete");
|
||||
if (evt.target.readyState == FileReader.DONE) {
|
||||
$('#btnAdd').removeClass('disabled');
|
||||
$('#icnAdd').removeClass('fa-spinner spinning');
|
||||
$('#icnAdd').addClass('fa-plus');
|
||||
}
|
||||
}
|
||||
}
|
||||
// called when the emscripten module has loaded
|
||||
async function appInitialized() {
|
||||
console.log("WASM runtime initialized");
|
||||
await fsLoadPromise;
|
||||
console.log("FS initialized");
|
||||
setProgress("main");
|
||||
setProgressText("main");
|
||||
icnRun.classList.remove("fa-spinner", "fa-spin");
|
||||
icnRun.classList.add("fa-play");
|
||||
// Make the Preview image clickable to start RetroArch.
|
||||
webplayerPreview.classList.add("loaded");
|
||||
webplayerPreview.addEventListener("click", function() {
|
||||
startRetroArch();
|
||||
});
|
||||
btnRun.classList.remove("disabled");
|
||||
btnRun.addEventListener("click", function() {
|
||||
startRetroArch();
|
||||
});
|
||||
}
|
||||
|
||||
function uploadData(data, name) {
|
||||
setupWorker.postMessage({command:"upload_file", name:name, data:data}, {transfer:[data]});
|
||||
}
|
||||
|
||||
function switchCore(corename) {
|
||||
localStorage.setItem("core", corename);
|
||||
}
|
||||
|
||||
function switchStorage(backend) {
|
||||
if (backend != localStorage.getItem("backend")) {
|
||||
localStorage.setItem("backend", backend);
|
||||
location.reload();
|
||||
}
|
||||
function loadCore(core) {
|
||||
// Make the core the selected core in the UI.
|
||||
const coreTitle = document.querySelector('#core-selector a[data-core="' + core + '"]')?.textContent;
|
||||
if (coreTitle) coreSelectorCurrent.textContent = coreTitle;
|
||||
const fileExt = (core == "retroarch") ? ".js" : "_libretro.js";
|
||||
import("./" + core + fileExt).then(script => {
|
||||
script.default(Module).then(mod => {
|
||||
Module = mod;
|
||||
}).catch(err => { console.error("Couldn't instantiate module", err); throw err; });
|
||||
}).catch(err => { console.error("Couldn't load script", err); throw err; });
|
||||
}
|
||||
|
||||
// When the browser has loaded everything.
|
||||
$(function() {
|
||||
// Enable data clear
|
||||
$('#btnClean').click(function() {
|
||||
cleanupStorage();
|
||||
});
|
||||
document.addEventListener("DOMContentLoaded", async function() {
|
||||
// watch the menu toggle checkbox
|
||||
menuHider.addEventListener("change", adjustMenuHeight);
|
||||
if (appIsSmallScreen()) menuHider.checked = true;
|
||||
adjustMenuHeight();
|
||||
|
||||
// Enable all available ToolTips.
|
||||
$('.tooltip-enable').tooltip({
|
||||
placement: 'right'
|
||||
});
|
||||
// make it easier to exit the core selector drop-down menu
|
||||
document.addEventListener("click", function(e) {
|
||||
if (!coreSelector.parentElement.contains(e.target)) dropdownBox.checked = false;
|
||||
});
|
||||
|
||||
// Allow hiding the top menu.
|
||||
$('.showMenu').hide();
|
||||
$('#btnHideMenu, .showMenu').click(function() {
|
||||
$('nav').slideToggle('slow');
|
||||
$('.showMenu').toggle('slow');
|
||||
});
|
||||
// disable default right click action
|
||||
canvas.addEventListener("contextmenu", function(e) {
|
||||
e.preventDefault();
|
||||
}, false);
|
||||
|
||||
// Attempt to disable some default browser keys.
|
||||
var keys = {
|
||||
9: "tab",
|
||||
13: "enter",
|
||||
16: "shift",
|
||||
18: "alt",
|
||||
27: "esc",
|
||||
33: "rePag",
|
||||
34: "avPag",
|
||||
35: "end",
|
||||
36: "home",
|
||||
37: "left",
|
||||
38: "up",
|
||||
39: "right",
|
||||
40: "down",
|
||||
112: "F1",
|
||||
113: "F2",
|
||||
114: "F3",
|
||||
115: "F4",
|
||||
116: "F5",
|
||||
117: "F6",
|
||||
118: "F7",
|
||||
119: "F8",
|
||||
120: "F9",
|
||||
121: "F10",
|
||||
122: "F11",
|
||||
123: "F12"
|
||||
};
|
||||
window.addEventListener('keydown', function(e) {
|
||||
if (keys[e.which]) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
// init the OPFS
|
||||
await FS.init();
|
||||
fsLoadPromise = helper.loadFS();
|
||||
|
||||
// Switch the core when selecting one.
|
||||
$('#core-selector a').click(function() {
|
||||
var coreChoice = $(this).data('core');
|
||||
switchCore(coreChoice);
|
||||
});
|
||||
// Find which core to load.
|
||||
var core = localStorage.getItem("core", core);
|
||||
if (!core) {
|
||||
core = 'gambatte';
|
||||
}
|
||||
loadCore(core);
|
||||
btnFiles.addEventListener("click", function() {
|
||||
openModal("files");
|
||||
});
|
||||
|
||||
btnHelp.addEventListener("click", function() {
|
||||
openModal("help");
|
||||
});
|
||||
|
||||
btnAdd.classList.remove("disabled");
|
||||
btnAdd.addEventListener("click", async function() {
|
||||
btnAdd.classList.add("disabled");
|
||||
icnAdd.classList.remove("fa-plus");
|
||||
icnAdd.classList.add("fa-spinner", "fa-spin");
|
||||
await uploadFilesToDir("/retroarch/content");
|
||||
btnAdd.classList.remove("disabled");
|
||||
icnAdd.classList.remove("fa-spinner", "fa-spin");
|
||||
icnAdd.classList.add("fa-plus");
|
||||
});
|
||||
|
||||
fileManagerPanel.addEventListener("click", function(e) {
|
||||
fileManagerEvent(e.target);
|
||||
});
|
||||
|
||||
// Switch the core when selecting one.
|
||||
coreSelector.addEventListener("click", function(e) {
|
||||
const coreChoice = e.target.dataset?.core;
|
||||
if (coreChoice) localStorage.setItem("core", coreChoice);
|
||||
});
|
||||
|
||||
// Find which core to load.
|
||||
const core = localStorage.getItem("core") || "gambatte";
|
||||
loadCore(core);
|
||||
});
|
||||
|
||||
function loadCore(core) {
|
||||
// Make the core the selected core in the UI.
|
||||
var coreTitle = $('#core-selector a[data-core="' + core + '"]').addClass('active').text();
|
||||
$('#dropdownMenu1').text(coreTitle);
|
||||
import("./"+core+"_libretro.js").then(script => {
|
||||
script.default(Module).then(mod => {
|
||||
Module = mod;
|
||||
}).catch(err => { console.error("Couldn't instantiate module",err); throw err; });
|
||||
}).catch(err => { console.error("Couldn't import module"); throw err; });
|
||||
}
|
||||
|
||||
const setupWorker = new Worker("libretro.worker.js");
|
||||
setupWorker.onmessage = (msg) => {
|
||||
if(msg.data.command == "loaded_bundle") {
|
||||
filesystem_ready = true;
|
||||
localStorage.setItem("asset_time", msg.data.time);
|
||||
appInitialized();
|
||||
} else if(msg.data.command == "uploaded_file") {
|
||||
// console.log("finished upload of",msg.data.name);
|
||||
}
|
||||
}
|
||||
setupWorker.postMessage({command:"load_bundle",time:localStorage.getItem("asset_time") ?? ""});
|
||||
|
@ -1,66 +1,400 @@
|
||||
importScripts("zip-no-worker.min.js");
|
||||
let root, BFS, BFSDB;
|
||||
let bundleCounter = 0;
|
||||
let loadedScripts = [];
|
||||
const FS = {};
|
||||
const helper = {};
|
||||
|
||||
async function writeFile(path, data) {
|
||||
const root = await navigator.storage.getDirectory();
|
||||
const dir_end = path.lastIndexOf("/");
|
||||
const parent = path.substr(0, dir_end);
|
||||
const child = path.substr(dir_end+1);
|
||||
const parent_dir = await mkdirTree(parent);
|
||||
const file = await parent_dir.getFileHandle(child,{create:true});
|
||||
const stream = await file.createSyncAccessHandle();
|
||||
const written = stream.write(data);
|
||||
stream.close();
|
||||
// this is huge and takes between 2 and 3 minutes to unzip. (10 minutes for firefox?)
|
||||
// luckily it only needs to be done once.
|
||||
const bundlePath = ["assets/frontend/bundle.zip.aa",
|
||||
"assets/frontend/bundle.zip.ab",
|
||||
"assets/frontend/bundle.zip.ac",
|
||||
"assets/frontend/bundle.zip.ad",
|
||||
"assets/frontend/bundle.zip.ae"];
|
||||
// ["assets/frontend/bundle-minimal.zip"]
|
||||
const removeLeadingZipDirs = 1;
|
||||
|
||||
// list of directories to migrate. previously these were mounted in the "userdata" directory. retroarch.cfg is ignored intentionally.
|
||||
const dirsToMigrate = ["cheats", "config", "content", "logs", "playlists", "saves", "screenshots", "states", "system", "thumbnails"];
|
||||
|
||||
/* no return functions that run on the browser thread */
|
||||
|
||||
const noReturnProxyFunctions = ["debugLog", "setProgress", "setProgressColor", "setProgressText"]
|
||||
|
||||
function handleNoReturnProxyFunction(func, args) {
|
||||
postMessage({type: "noReturn", func: func, args: args});
|
||||
}
|
||||
|
||||
async function mkdirTree(path) {
|
||||
const root = await navigator.storage.getDirectory();
|
||||
const parts = path.split("/");
|
||||
let here = root;
|
||||
for (const part of parts) {
|
||||
if (part == "") { continue; }
|
||||
here = await here.getDirectoryHandle(part, {create:true});
|
||||
}
|
||||
return here;
|
||||
// add global functions
|
||||
for (const func of noReturnProxyFunctions) {
|
||||
self[func] = function() {
|
||||
handleNoReturnProxyFunction(func, Array.from(arguments));
|
||||
}
|
||||
}
|
||||
|
||||
/* misc */
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise(function(resolve) {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
|
||||
// for lazy-loading of scripts, doesn't load if it's already loaded
|
||||
function loadScripts() {
|
||||
for (const path of arguments) {
|
||||
if (loadedScripts.includes(path)) continue;
|
||||
importScripts(path);
|
||||
loadedScripts.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
/* OPFS misc */
|
||||
|
||||
async function getDirHandle(path, create) {
|
||||
const parts = path.split("/");
|
||||
let here = root;
|
||||
for (const part of parts) {
|
||||
if (part == "") continue;
|
||||
try {
|
||||
here = await here.getDirectoryHandle(part, {create: !!create});
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
return here;
|
||||
}
|
||||
|
||||
/* OPFS impl */
|
||||
|
||||
FS.init = async function() {
|
||||
root = await navigator.storage.getDirectory();
|
||||
}
|
||||
|
||||
FS.writeFile = async function(path, data) {
|
||||
const dir_end = path.lastIndexOf("/");
|
||||
const parent = path.substr(0, dir_end);
|
||||
const child = path.substr(dir_end + 1);
|
||||
const parent_dir = await getDirHandle(parent, true);
|
||||
const file = await parent_dir.getFileHandle(child, {create: true});
|
||||
const handle = await file.createSyncAccessHandle({mode: "readwrite"});
|
||||
handle.write(data);
|
||||
// todo: should we call handle.flush() here?
|
||||
handle.close();
|
||||
}
|
||||
|
||||
FS.readFile = async function(path) {
|
||||
const dir_end = path.lastIndexOf("/");
|
||||
const parent = path.substr(0, dir_end);
|
||||
const child = path.substr(dir_end + 1);
|
||||
const parent_dir = await getDirHandle(parent);
|
||||
if (!parent_dir) throw "directory doesn't exist";
|
||||
const file = await parent_dir.getFileHandle(child);
|
||||
const handle = await file.createSyncAccessHandle({mode: "read-only"});
|
||||
let data = new Uint8Array(new ArrayBuffer(handle.getSize()));
|
||||
handle.read(data);
|
||||
handle.close();
|
||||
return data;
|
||||
}
|
||||
|
||||
// unlimited arguments
|
||||
FS.mkdirTree = async function() {
|
||||
for (const path of arguments) {
|
||||
await getDirHandle(path, true);
|
||||
}
|
||||
}
|
||||
|
||||
FS.readdir = async function(path) {
|
||||
let items = [];
|
||||
const dir = await getDirHandle(path);
|
||||
if (!dir) return;
|
||||
for await (const entry of dir.keys()) {
|
||||
items.push(entry);
|
||||
}
|
||||
items.reverse();
|
||||
return items;
|
||||
}
|
||||
|
||||
FS.readdirTree = async function(path, maxDepth) {
|
||||
let items = [];
|
||||
if (isNaN(maxDepth)) maxDepth = 10;
|
||||
const dir = await getDirHandle(path);
|
||||
if (!dir) return;
|
||||
if (!path.endsWith("/")) path += "/";
|
||||
for await (const handle of dir.values()) {
|
||||
if (handle.kind == "file") {
|
||||
items.push(path + handle.name);
|
||||
} else if (handle.kind == "directory" && maxDepth > 0) {
|
||||
items.push.apply(items, await FS.readdirTree(path + handle.name, maxDepth - 1));
|
||||
}
|
||||
}
|
||||
items.reverse();
|
||||
return items;
|
||||
}
|
||||
|
||||
// unlimited arguments
|
||||
FS.rm = async function() {
|
||||
for (const path of arguments) {
|
||||
const dir_end = path.lastIndexOf("/");
|
||||
const parent = path.substr(0, dir_end);
|
||||
const child = path.substr(dir_end + 1);
|
||||
const parent_dir = await getDirHandle(parent);
|
||||
if (!parent_dir) continue;
|
||||
await parent_dir.removeEntry(child, {recursive: true});
|
||||
}
|
||||
}
|
||||
|
||||
FS.stat = async function(path) {
|
||||
const dir_end = path.lastIndexOf("/");
|
||||
const parent = path.substr(0, dir_end);
|
||||
if (!parent) return "directory";
|
||||
const child = path.substr(dir_end + 1);
|
||||
const parent_dir = await getDirHandle(parent);
|
||||
if (!parent_dir) return;
|
||||
for await (const handle of parent_dir.values()) {
|
||||
if (handle.name == child) return handle.kind;
|
||||
}
|
||||
}
|
||||
|
||||
/* data migration */
|
||||
|
||||
function idbExists(name) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = indexedDB.open(name);
|
||||
request.onupgradeneeded = function(e) {
|
||||
e.target.transaction.abort();
|
||||
resolve(false);
|
||||
}
|
||||
request.onsuccess = function(e) {
|
||||
e.target.result.close();
|
||||
resolve(true);
|
||||
}
|
||||
request.onerror = function(e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function deleteIdb(name) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = indexedDB.deleteDatabase(name);
|
||||
request.onsuccess = function() {
|
||||
resolve();
|
||||
}
|
||||
request.onerror = function(e) {
|
||||
reject("Error deleting IndexedDB!");
|
||||
}
|
||||
request.onblocked = function(e) {
|
||||
reject("Request to delete IndexedDB was blocked!");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initBfsIdbfs(name) {
|
||||
return new Promise((resolve, reject) => {
|
||||
loadScripts("jsdeps/browserfs.min.js");
|
||||
BrowserFS.getFileSystem({fs: "IndexedDB", options: {storeName: name}}, function(err, rv) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
BrowserFS.initialize(rv);
|
||||
BFSDB = rv.store.db;
|
||||
BFS = BrowserFS.BFSRequire("fs");
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// calls BFS.<method> with arguments..., returns a promise
|
||||
function bfsAsyncCall(method) {
|
||||
return new Promise((resolve, reject) => {
|
||||
BFS[method].apply(null, Array.from(arguments).slice(1).concat(function(err, rv) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(rv);
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
async function migrateFiles(files) {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const path = files[i];
|
||||
debugLog(" Migrating " + path);
|
||||
setProgressText("main", "Migrating file: " + path.substr(1));
|
||||
setProgress("main", (i + 1) / files.length);
|
||||
const data = await bfsAsyncCall("readFile", path);
|
||||
await FS.writeFile("/retroarch" + path, data);
|
||||
}
|
||||
setProgress("main", 0);
|
||||
setProgressText("main");
|
||||
}
|
||||
|
||||
// this is really finicky (thanks browserfs), probably don't touch this
|
||||
async function indexMigrateTree(dir, maxDepth) {
|
||||
let toMigrate = [];
|
||||
if (isNaN(maxDepth)) maxDepth = 10;
|
||||
const children = await bfsAsyncCall("readdir", dir);
|
||||
if (!dir.endsWith("/")) dir += "/";
|
||||
for (const child of children) {
|
||||
const info = await bfsAsyncCall("lstat", dir + child);
|
||||
if (info.isSymbolicLink()) continue;
|
||||
if (info.isFile() && dir != "/") {
|
||||
toMigrate.push(dir + child);
|
||||
} else if (info.isDirectory() && maxDepth > 0 && (dir != "/" || dirsToMigrate.includes(child))) {
|
||||
toMigrate.push.apply(toMigrate, await indexMigrateTree(dir + child, maxDepth - 1));
|
||||
}
|
||||
}
|
||||
return toMigrate;
|
||||
}
|
||||
|
||||
// look for and migrate any data to the OPFS from the old BrowserFS in IndexedDB
|
||||
async function tryMigrateFromIdbfs() {
|
||||
if (await FS.stat("/retroarch/.migration-finished") == "file" || !(await idbExists("RetroArch"))) return;
|
||||
debugLog("Migrating data from BrowserFS IndexedDB");
|
||||
await initBfsIdbfs("RetroArch");
|
||||
const files = await indexMigrateTree("/", 5);
|
||||
await migrateFiles(files);
|
||||
await FS.writeFile("/retroarch/.migration-finished", new Uint8Array());
|
||||
BFSDB.close();
|
||||
await sleep(100); // above method might need extra time, and indexedDB.deleteDatabase only gives us one shot
|
||||
try {
|
||||
await deleteIdb("RetroArch");
|
||||
} catch (e) {
|
||||
debugLog("Warning: failed to delete old IndexedDB, probably doesn't matter.", e);
|
||||
}
|
||||
debugLog("Finished data migration! " + files.length + " files migrated successfully.");
|
||||
}
|
||||
|
||||
/* bundle loading */
|
||||
|
||||
function incBundleCounter() {
|
||||
setProgress("main", ++bundleCounter / bundlePath.length);
|
||||
}
|
||||
|
||||
async function setupZipFS(zipBuf) {
|
||||
const root = await navigator.storage.getDirectory();
|
||||
const zipReader = new zip.ZipReader(new zip.Uint8ArrayReader(zipBuf), {useWebWorkers:false});
|
||||
const entries = await zipReader.getEntries();
|
||||
for(const file of entries) {
|
||||
if (file.getData && !file.directory) {
|
||||
const writer = new zip.Uint8ArrayWriter();
|
||||
const data = await file.getData(writer);
|
||||
await writeFile(file.filename, data);
|
||||
} else if (file.directory) {
|
||||
await mkdirTree(file.filename);
|
||||
}
|
||||
}
|
||||
await zipReader.close();
|
||||
loadScripts("jsdeps/zip-full.min.js");
|
||||
const mount = "/retroarch/";
|
||||
const zipReader = new zip.ZipReader(new zip.Uint8ArrayReader(zipBuf), {useWebWorkers: false});
|
||||
const entries = await zipReader.getEntries();
|
||||
setProgressText("main", "Extracting bundle... This only happens on the first visit or when the bundle is updated");
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
const file = entries[i];
|
||||
if (file.getData && !file.directory) {
|
||||
setProgress("main", (i + 1) / entries.length);
|
||||
const path = mount + file.filename.split("/").slice(removeLeadingZipDirs).join("/");
|
||||
const data = await file.getData(new zip.Uint8ArrayWriter());
|
||||
await FS.writeFile(path, data);
|
||||
}
|
||||
}
|
||||
await zipReader.close();
|
||||
setProgress("main", 0);
|
||||
setProgressText("main");
|
||||
}
|
||||
|
||||
onmessage = async (msg) => {
|
||||
if(msg.data.command == "load_bundle") {
|
||||
let old_timestamp = msg.data.time;
|
||||
try {
|
||||
const root = await navigator.storage.getDirectory();
|
||||
const _bundle = await root.getDirectoryHandle("bundle");
|
||||
} catch (_e) {
|
||||
old_timestamp = "";
|
||||
}
|
||||
let resp = await fetch("assets/frontend/bundle-minimal.zip", {
|
||||
headers: {
|
||||
"If-Modified-Since": old_timestamp
|
||||
}
|
||||
});
|
||||
if (resp.status == 200) {
|
||||
await setupZipFS(new Uint8Array(await resp.arrayBuffer()));
|
||||
} else {
|
||||
await resp.text();
|
||||
}
|
||||
postMessage({command:"loaded_bundle", time:resp.headers.get("last-modified")});
|
||||
} else if(msg.data.command == "upload_file") {
|
||||
await writeFile("userdata/content/"+msg.data.name, new Uint8Array(msg.data.data));
|
||||
postMessage({command:"uploaded_file",name:msg.data.name});
|
||||
}
|
||||
async function tryLoadBundle() {
|
||||
let outBuf;
|
||||
const timestampFile = "/retroarch/.bundle-timestamp";
|
||||
let timestamp = "";
|
||||
if (await FS.stat(timestampFile) == "file")
|
||||
timestamp = new TextDecoder().decode(await FS.readFile(timestampFile));
|
||||
|
||||
// debuggers beware: the network tab's "Disable cache" option disables If-Modified-Since too
|
||||
let resp = await fetch(bundlePath[0], {headers: {"If-Modified-Since": timestamp}});
|
||||
if (resp.status == 200) {
|
||||
debugLog("Got new bundle");
|
||||
timestamp = resp.headers.get("last-modified");
|
||||
if (bundlePath.length > 1) {
|
||||
// split bundle
|
||||
let firstBuffer = await resp.arrayBuffer();
|
||||
setProgressColor("main", "#0275d8");
|
||||
setProgressText("main", "Fetching bundle... This only happens on the first visit or when the bundle is updated");
|
||||
incBundleCounter();
|
||||
// 256 MB max bundle size
|
||||
let buffer = new ArrayBuffer(256 * 1024 * 1024);
|
||||
let bufferView = new Uint8Array(buffer);
|
||||
bufferView.set(new Uint8Array(firstBuffer), 0);
|
||||
let idx = firstBuffer.byteLength;
|
||||
let buffers = await Promise.all(bundlePath.slice(1).map(i => fetch(i).then(r => { incBundleCounter(); return r.arrayBuffer(); })));
|
||||
for (let buf of buffers) {
|
||||
if (idx + buf.byteLength > buffer.maxByteLength) {
|
||||
throw "error: bundle zip is too large";
|
||||
}
|
||||
bufferView.set(new Uint8Array(buf), idx);
|
||||
idx += buf.byteLength;
|
||||
}
|
||||
setProgress("main", 0);
|
||||
setProgressColor("main");
|
||||
setProgressText("main");
|
||||
outBuf = new Uint8Array(buffer, 0, idx);
|
||||
} else {
|
||||
// single-file bundle
|
||||
outBuf = new Uint8Array(await resp.arrayBuffer());
|
||||
}
|
||||
debugLog("Unzipping...");
|
||||
let oldTime = performance.now();
|
||||
await setupZipFS(outBuf);
|
||||
await FS.writeFile(timestampFile, new TextEncoder().encode(timestamp));
|
||||
debugLog("Finished bundle load in " + Math.round((performance.now() - oldTime) / 1000) + " seconds");
|
||||
} else {
|
||||
debugLog("No new bundle exists");
|
||||
}
|
||||
}
|
||||
|
||||
/* helper functions */
|
||||
|
||||
helper.loadFS = async function() {
|
||||
await tryMigrateFromIdbfs();
|
||||
await tryLoadBundle();
|
||||
}
|
||||
|
||||
// zip directories... and return Uint8Array with zip file data
|
||||
helper.zipDirs = async function() {
|
||||
let toZip = [];
|
||||
for (const path of arguments) {
|
||||
const files = await FS.readdirTree(path);
|
||||
if (files) toZip.push.apply(toZip, files);
|
||||
}
|
||||
if (toZip.length == 0) return;
|
||||
|
||||
loadScripts("jsdeps/zip-full.min.js");
|
||||
const u8aWriter = new zip.Uint8ArrayWriter("application/zip");
|
||||
// using workers is faster for deflating, hmm...
|
||||
const writer = new zip.ZipWriter(u8aWriter, {useWebWorkers: true});
|
||||
for (let i = 0; i < toZip.length; i++) {
|
||||
const path = toZip[i];
|
||||
setProgress("modal", (i + 1) / toZip.length);
|
||||
setProgressText("modal", "Deflating: " + path.substr(1));
|
||||
try {
|
||||
const data = await FS.readFile(path);
|
||||
await writer.add(path.substr(1), new zip.Uint8ArrayReader(data), {level: 1});
|
||||
} catch (e) {
|
||||
debugLog("error while preparing zip", e);
|
||||
}
|
||||
}
|
||||
await writer.close();
|
||||
const zipped = await u8aWriter.getData();
|
||||
setProgress("modal", 0);
|
||||
setProgressText("modal");
|
||||
return zipped;
|
||||
}
|
||||
|
||||
/* handle messages from main thread */
|
||||
|
||||
const handlers = {FS: FS, helper: helper};
|
||||
|
||||
onmessage = async function(msg) {
|
||||
if (msg.data?.handler in handlers && msg.data?.method in handlers[msg.data.handler]) {
|
||||
let ret;
|
||||
let err = false;
|
||||
try {
|
||||
ret = await handlers[msg.data.handler][msg.data.method].apply(null, msg.data?.args);
|
||||
} catch (e) {
|
||||
ret = e;
|
||||
err = true;
|
||||
}
|
||||
postMessage({type: "ret", id: msg.data?.id, ret: ret, err: err});
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
@ -81,12 +81,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable the border around the player.
|
||||
*/
|
||||
canvas.webplayer {
|
||||
border: none;
|
||||
outline: none;
|
||||
width: 800px;
|
||||
height: 600px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hack to make emscripten stop messing with the canvas size while in fullscreen.
|
||||
* Foiled again!
|
||||
*/
|
||||
:fullscreen canvas.webplayer {
|
||||
min-width: 100vw;
|
||||
max-width: 100vw;
|
||||
min-height: 100vh;
|
||||
max-height: 100vh;
|
||||
}
|
||||
|
||||
textarea {
|
||||
|
@ -17,58 +17,11 @@ var Module = {
|
||||
message_accum: "",
|
||||
|
||||
retroArchSend: function(msg) {
|
||||
let bytes = this.encoder.encode(msg + "\n");
|
||||
this.message_queue.push([bytes, 0]);
|
||||
this.EmscriptenSendCommand(msg);
|
||||
},
|
||||
retroArchRecv: function() {
|
||||
let out = this.message_out.shift();
|
||||
if (out == null && this.message_accum != "") {
|
||||
out = this.message_accum;
|
||||
this.message_accum = "";
|
||||
}
|
||||
return out;
|
||||
return this.EmscriptenReceiveCommandReply();
|
||||
},
|
||||
preRun: [
|
||||
function(module) {
|
||||
function stdin() {
|
||||
// Return ASCII code of character, or null if no input
|
||||
while (module.message_queue.length > 0) {
|
||||
var msg = module.message_queue[0][0];
|
||||
var index = module.message_queue[0][1];
|
||||
if (index >= msg.length) {
|
||||
module.message_queue.shift();
|
||||
} else {
|
||||
module.message_queue[0][1] = index + 1;
|
||||
// assumption: msg is a uint8array
|
||||
return msg[index];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function stdout(c) {
|
||||
if (c == null) {
|
||||
// flush
|
||||
if (module.message_accum != "") {
|
||||
module.message_out.push(module.message_accum);
|
||||
module.message_accum = "";
|
||||
}
|
||||
} else {
|
||||
let s = String.fromCharCode(c);
|
||||
if (s == "\n") {
|
||||
if (module.message_accum != "") {
|
||||
module.message_out.push(module.message_accum);
|
||||
module.message_accum = "";
|
||||
}
|
||||
} else {
|
||||
module.message_accum = module.message_accum + s;
|
||||
}
|
||||
}
|
||||
}
|
||||
module.FS.init(stdin, stdout);
|
||||
}
|
||||
],
|
||||
postRun: [],
|
||||
onRuntimeInitialized: function() {
|
||||
appInitialized();
|
||||
},
|
||||
@ -197,13 +150,13 @@ function setupFileSystem(backend) {
|
||||
// create a mountable filesystem that will server as a root mountpoint for browserfs
|
||||
var mfs = new BrowserFS.FileSystem.MountableFileSystem();
|
||||
|
||||
// create an XmlHttpRequest filesystem for the bundled data
|
||||
// create a ZipFS filesystem for the bundled data
|
||||
var zipfs = new BrowserFS.FileSystem.ZipFS(zipTOC);
|
||||
// create an XmlHttpRequest filesystem for core assets
|
||||
var xfs = new BrowserFS.FileSystem.XmlHttpRequest(".index-xhr", "assets/cores/");
|
||||
|
||||
console.log("WEBPLAYER: initializing filesystem: " + backend);
|
||||
mfs.mount('/home/web_user/retroarch/', zipfs);
|
||||
mfs.mount('/home/web_user/retroarch', zipfs);
|
||||
mfs.mount('/home/web_user/retroarch/userdata', afs);
|
||||
mfs.mount('/home/web_user/retroarch/userdata/content/downloads', xfs);
|
||||
BrowserFS.initialize(mfs);
|
||||
|
12
retroarch.c
12
retroarch.c
@ -86,6 +86,7 @@
|
||||
|
||||
#ifdef EMSCRIPTEN
|
||||
#include <emscripten/emscripten.h>
|
||||
#include "gfx/common/gl_common.h"
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_LIBNX
|
||||
@ -5959,8 +5960,10 @@ int rarch_main(int argc, char *argv[], void *data)
|
||||
}
|
||||
|
||||
#if defined(EMSCRIPTEN)
|
||||
#include "gfx/common/gl_common.h"
|
||||
|
||||
#ifdef PROXY_TO_PTHREAD
|
||||
bool platform_emscripten_is_window_hidden(void);
|
||||
#endif
|
||||
#ifdef HAVE_RWEBAUDIO
|
||||
void RWebAudioRecalibrateTime(void);
|
||||
#endif
|
||||
@ -5979,6 +5982,13 @@ 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;
|
||||
|
||||
#ifdef PROXY_TO_PTHREAD
|
||||
// ensure the same behavior when requestAnimationFrame is emulated (i.e. pause when window is hidden)
|
||||
// todo: is this an emscripten bug?
|
||||
if (!input_driver_nonblock_state && platform_emscripten_is_window_hidden())
|
||||
return;
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_RWEBAUDIO
|
||||
RWebAudioRecalibrateTime();
|
||||
#endif
|
||||
|
Loading…
x
Reference in New Issue
Block a user