Threaded emscripten improvements

This commit is contained in:
BinBashBanana 2025-03-05 10:39:43 -08:00
parent a6316465a3
commit f6eb1ed5c5
18 changed files with 1965 additions and 1149 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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">&times;</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>&nbsp;
<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>&nbsp; Run
</li>
<li id="btnAdd" class="disabled">
<span class="fa fa-plus" id="icnAdd"></span>&nbsp; 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>&nbsp; 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>&nbsp; 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>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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