Workerized emscripten retroarch (WIP) (#17484)

* workerized RA

* Workerized (non-async) web player, using OPFS

This patch eliminates the need for asyncify and uses modern filesystem
APIs instead of the deprecated, unmaintained BrowserFS.

This is a WIP patch because it won't fully work until these two
Emscripten PRs land and are released:

https://github.com/emscripten-core/emscripten/pull/23518
https://github.com/emscripten-core/emscripten/pull/23021

The former fixes an offscreen canvas context recreation bug, and the
latter adds an equivalent to BrowserFS's XHR filesystem (but without
the hazardous running-XHR-on-the-main-thread problem).

The biggest issue is that local storage of users who were using the
old version of the webplayer will be gone when they switch to the new
webplayer.  I don't have a good story for converting the old BrowserFS
IDBFS contents into the new OPFS filesystem (the move is worth doing
because OPFS supports seeking and reading only bits of a file, and
because BrowserFS is dead).

I've kept around the old libretro webplayer under
pkg/emscripten/libretro-classic, and with these make flags you can
build a non-workerized RA that uses asyncify to sleep as before:

make -f Makefile.emscripten libretro=$CORE HAVE_WORKER=0 HAVE_WASMFS=0 PTHREAD=0 HAVE_AL=1

I also moved the default directory for core content on emscripten to
not be a subdirectory of the local filesystem mount, because it's
confusing to have a subdirectory that's lazily fetched and not
mirrored to the local storage.  I think it won't impact existing users
of the classic web player because they already have a retroarch.cfg in
place.

* Get fetchfs working without manifest support

* makefile fixes
This commit is contained in:
Joe Osborn 2025-01-30 10:58:18 -08:00 committed by GitHub
parent 4b5f782fe4
commit cacd5a9a23
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 1201 additions and 182 deletions

View File

@ -1526,7 +1526,10 @@ ifeq ($(HAVE_GL_CONTEXT), 1)
endif
ifeq ($(HAVE_EMSCRIPTEN), 1)
OBJ += gfx/drivers_context/emscriptenegl_ctx.o
ifeq ($(HAVE_EGL), 1)
OBJ += gfx/drivers_context/emscriptenegl_ctx.o
endif
OBJ += gfx/drivers_context/emscriptenwebgl_ctx.o
endif
ifeq ($(HAVE_MALI_FBDEV), 1)

View File

@ -24,7 +24,8 @@ 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
HAVE_EMSCRIPTEN = 1
@ -48,6 +49,8 @@ HAVE_7ZIP = 1
HAVE_BSV_MOVIE = 1
HAVE_AL = 1
HAVE_CHD ?= 0
HAVE_WASMFS ?= 1
HAVE_WORKER ?= 1
# WARNING -- READ BEFORE ENABLING
# The rwebaudio driver is known to have several audio bugs, such as
@ -68,7 +71,7 @@ HAVE_OPENGLES3 ?= 0
ASYNC ?= 0
LTO ?= 0
PTHREAD ?= 0
PTHREAD ?= 4
STACK_SIZE ?= 4194304
INITIAL_HEAP ?= 134217728
@ -92,37 +95,20 @@ _cmd_toggle_menu,_cmd_reload_config,_cmd_toggle_grab_mouse,_cmd_toggle_game_focu
_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
EXPORTS := callMain,FS,PATH,ERRNO_CODES,stringToNewUTF8,UTF8ToString
LIBS := -s USE_ZLIB=1
LDFLAGS := -L. --no-heap-copy -s $(LIBS) -s STACK_SIZE=$(STACK_SIZE) -s INITIAL_MEMORY=$(INITIAL_HEAP) \
-s EXPORTED_RUNTIME_METHODS=callMain,FS,PATH,ERRNO_CODES,stringToNewUTF8,UTF8ToString \
-s ALLOW_MEMORY_GROWTH=1 -s EXPORTED_FUNCTIONS="$(EXPORTED_FUNCTIONS)" \
-s MODULARIZE=1 -s EXPORT_ES6=1 -s EXPORT_NAME="libretro_$(subst -,_,$(LIBRETRO))" \
--extern-pre-js emscripten/pre.js \
--js-library emscripten/library_rwebcam.js \
--js-library emscripten/library_platform_emscripten.js
ifeq ($(HAVE_RWEBAUDIO), 1)
LDFLAGS += --js-library emscripten/library_rwebaudio.js
DEFINES += -DHAVE_RWEBAUDIO
endif
ifeq ($(HAVE_AL), 1)
LDFLAGS += -lopenal
DEFINES += -DHAVE_AL
override ASYNC = 1
ifeq ($(HAVE_WASMFS), 1)
LIBS += -s WASMFS -s FORCE_FILESYSTEM=1 -lfetchfs.js -lopfs.js
EXPORTS := $(EXPORTS),FETCHFS,OPFS
endif
ifneq ($(PTHREAD), 0)
LDFLAGS += -s MAXIMUM_MEMORY=1073741824 -pthread -s PTHREAD_POOL_SIZE=$(PTHREAD)
CFLAGS += -pthread
HAVE_THREADS=1
else
HAVE_THREADS=0
endif
ifeq ($(ASYNC), 1)
LDFLAGS += -s ASYNCIFY=$(ASYNC) -s ASYNCIFY_STACK_SIZE=8192
ifeq ($(DEBUG), 1)
LDFLAGS += -s ASYNCIFY_DEBUG=1 # -s ASYNCIFY_ADVISE
ifeq ($(HAVE_WORKER), 1)
LIBS += -s PROXY_TO_PTHREAD -s USE_ES6_IMPORT_META=0 -sENVIRONMENT=worker,web
else
ifeq ($(HAVE_AL), 1)
override ASYNC = 1
endif
endif
@ -147,6 +133,45 @@ ifeq ($(HAVE_SDL2), 1)
DEFINES += -DHAVE_SDL2
endif
LDFLAGS := -L. --no-heap-copy -s $(LIBS) -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 OFFSCREENCANVAS_SUPPORT \
-s OFFSCREEN_FRAMEBUFFER \
--extern-pre-js emscripten/pre.js \
--js-library emscripten/library_rwebcam.js \
--js-library emscripten/library_platform_emscripten.js
ifeq ($(HAVE_RWEBAUDIO), 1)
LDFLAGS += --js-library emscripten/library_rwebaudio.js
DEFINES += -DHAVE_RWEBAUDIO
endif
ifeq ($(HAVE_AL), 1)
LDFLAGS += -lopenal
DEFINES += -DHAVE_AL
endif
ifneq ($(PTHREAD), 0)
LDFLAGS += -s WASM_MEM_MAX=1073741824 -pthread -s PTHREAD_POOL_SIZE=$(PTHREAD)
CFLAGS += -pthread -s SHARED_MEMORY
HAVE_THREADS=1
else
HAVE_THREADS=0
endif
ifeq ($(ASYNC), 1)
DEFINES += -DEMSCRIPTEN_ASYNCIFY
LDFLAGS += -s ASYNCIFY=$(ASYNC) -s ASYNCIFY_STACK_SIZE=8192
ifeq ($(DEBUG), 1)
LDFLAGS += -s ASYNCIFY_DEBUG=1 # -s ASYNCIFY_ADVISE
endif
endif
include Makefile.common
CFLAGS += $(DEF_FLAGS) -Ideps -Ideps/stb
@ -183,8 +208,10 @@ RARCH_OBJ := $(addprefix $(OBJDIR)/,$(OBJ))
all: $(TARGET)
$(TARGET): $(RARCH_OBJ) $(libretro)
@$(if $(libretro), mv -f $(libretro) $(libretro_new),)
$(libretro_new) : $(libretro)
mv -f $(libretro) $(libretro_new)
$(TARGET): $(RARCH_OBJ) $(libretro_new)
@$(if $(Q), $(shell echo echo "LD $@ \<obj\> $(libretro_new) $(LIBS) $(LDFLAGS)"),)
$(Q)$(LD) -o $@ $(RARCH_OBJ) $(libretro_new) $(LIBS) $(LDFLAGS)

View File

@ -235,8 +235,8 @@ static void frontend_emscripten_get_env(int *argc, char *argv[],
"config", sizeof(g_defaults.dirs[DEFAULT_DIR_MENU_CONFIG]));
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]));
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_CORE_ASSETS], base_path,
"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],
@ -308,7 +308,10 @@ int main(int argc, char *argv[])
specialHTMLTargets["!canvas"] = Module.canvas;
});
emscripten_set_canvas_element_size("!canvas", 800, 600);
emscripten_set_element_css_size("!canvas", 800.0, 600.0);
emscripten_set_main_loop(emscripten_mainloop, 0, 0);
emscripten_set_main_loop_timing(EM_TIMING_RAF, 1);
rarch_main(argc, argv, NULL);
return 0;

View File

@ -124,11 +124,9 @@ static void gfx_ctx_emscripten_destroy(void *data)
if (!emscripten)
return;
#ifdef HAVE_EGL
egl_destroy(&emscripten->egl);
#endif
free(data);
}
@ -191,7 +189,6 @@ static void *gfx_ctx_emscripten_init(void *video_driver)
#endif
return emscripten;
error:
gfx_ctx_emscripten_destroy(video_driver);
return NULL;

View File

@ -0,0 +1,279 @@
/* RetroArch - A frontend for libretro.
* Copyright (C) 2010-2014 - Hans-Kristian Arntzen
* Copyright (C) 2011-2017 - Daniel De Matteis
* Copyright (C) 2012-2015 - Michael Lelli
*
* RetroArch is free software: you can redistribute it and/or modify it under the terms
* of the GNU General Public License as published by the Free Software Found-
* ation, either version 3 of the License, or (at your option) any later version.
*
* RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
* PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with RetroArch.
* If not, see <http://www.gnu.org/licenses/>.
*/
#include <stdint.h>
#include <stdlib.h>
#include <unistd.h>
#include <emscripten/emscripten.h>
#include <emscripten/html5.h>
#ifdef HAVE_CONFIG_H
#include "../../config.h"
#endif
#include "../../retroarch.h"
#include "../../verbosity.h"
typedef struct
{
EMSCRIPTEN_WEBGL_CONTEXT_HANDLE ctx;
unsigned fb_width;
unsigned fb_height;
} emscripten_ctx_data_t;
static void gfx_ctx_emscripten_webgl_swap_interval(void *data, int interval)
{
if (interval == 0)
emscripten_set_main_loop_timing(EM_TIMING_SETIMMEDIATE, 0);
else
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;
emscripten_ctx_data_t *emscripten = (emscripten_ctx_data_t*)data;
*resize = false;
gfx_ctx_emscripten_webgl_get_canvas_size(&input_width, &input_height);
*width = (unsigned)input_width;
*height = (unsigned)input_height;
if(*width != emscripten->fb_width || *height != emscripten->fb_height) {
*resize = true;
}
emscripten->fb_width = (unsigned)*width;
emscripten->fb_height = (unsigned)*height;
*quit = false;
}
static void gfx_ctx_emscripten_webgl_swap_buffers(void *data)
{
emscripten_webgl_commit_frame();
}
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 = 2;
attrs.enableExtensionsByDefault = true;
attrs.explicitSwapControl = true;
attrs.renderViaOffscreenBackBuffer = true;
attrs.proxyContextToMainThread = EMSCRIPTEN_WEBGL_CONTEXT_PROXY_DISALLOW;
if (!emscripten)
return NULL;
emscripten->ctx = emscripten_webgl_create_context("#canvas", &attrs);
if(!emscripten->ctx) {
RARCH_LOG("[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_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) {
RARCH_LOG("[EMSCRIPTEN/WebGL]: set canvas size to %d, %d\n", width, height);
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;
RARCH_LOG("[EMSCRIPTEN/WebGL]: set canvas size to %d, %d\n", width, height);
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;
}
static bool gfx_ctx_emscripten_webgl_has_focus(void *data) {
emscripten_ctx_data_t *emscripten = (emscripten_ctx_data_t*)data;
return emscripten && emscripten->ctx;
}
static bool gfx_ctx_emscripten_webgl_suppress_screensaver(void *data, bool enable) { return false; }
static float gfx_ctx_emscripten_webgl_translate_aspect(void *data,
unsigned width, unsigned height) { return (float)width / height; }
static void gfx_ctx_emscripten_webgl_bind_hw_render(void *data, bool enable)
{
emscripten_ctx_data_t *emscripten = (emscripten_ctx_data_t*)data;
emscripten_webgl_make_context_current(emscripten->ctx);
}
static uint32_t gfx_ctx_emscripten_webgl_get_flags(void *data)
{
uint32_t flags = 0;
BIT32_SET(flags, GFX_CTX_FLAGS_SHADERS_GLSL);
return flags;
}
static void gfx_ctx_emscripten_webgl_set_flags(void *data, uint32_t flags) { }
const gfx_ctx_driver_t gfx_ctx_emscripten_webgl = {
gfx_ctx_emscripten_webgl_init,
gfx_ctx_emscripten_webgl_destroy,
gfx_ctx_emscripten_webgl_get_api,
gfx_ctx_emscripten_webgl_bind_api,
gfx_ctx_emscripten_webgl_swap_interval,
gfx_ctx_emscripten_webgl_set_video_mode,
gfx_ctx_emscripten_webgl_get_video_size,
NULL, /* get_refresh_rate */
NULL, /* get_video_output_size */
NULL, /* get_video_output_prev */
NULL, /* get_video_output_next */
NULL, /* get_metrics */
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_has_focus,
gfx_ctx_emscripten_webgl_suppress_screensaver,
false,
gfx_ctx_emscripten_webgl_swap_buffers,
gfx_ctx_emscripten_webgl_input_driver,
NULL,
NULL,
NULL,
NULL,
"webgl_emscripten",
gfx_ctx_emscripten_webgl_get_flags,
gfx_ctx_emscripten_webgl_set_flags,
gfx_ctx_emscripten_webgl_bind_hw_render,
NULL,
NULL
};

View File

@ -162,8 +162,11 @@ static const gfx_ctx_driver_t *gfx_ctx_gl_drivers[] = {
#ifdef HAVE_OSMESA
&gfx_ctx_osmesa,
#endif
#ifdef EMSCRIPTEN
#if (defined(EMSCRIPTEN) && defined(HAVE_EGL))
&gfx_ctx_emscripten,
#endif
#ifdef EMSCRIPTEN
&gfx_ctx_emscripten_webgl,
#endif
&gfx_ctx_null,
NULL

View File

@ -1384,6 +1384,7 @@ extern const gfx_ctx_driver_t gfx_ctx_cgl;
extern const gfx_ctx_driver_t gfx_ctx_cocoagl;
extern const gfx_ctx_driver_t gfx_ctx_cocoavk;
extern const gfx_ctx_driver_t gfx_ctx_emscripten;
extern const gfx_ctx_driver_t gfx_ctx_emscripten_webgl;
extern const gfx_ctx_driver_t gfx_ctx_opendingux_fbdev;
extern const gfx_ctx_driver_t gfx_ctx_khr_display;
extern const gfx_ctx_driver_t gfx_ctx_gdi;

View File

@ -417,7 +417,7 @@ static void *rwebinput_input_init(const char *joypad_driver)
rwebinput_generate_lut();
r = emscripten_set_keydown_callback(
"!canvas", rwebinput, false,
"#canvas", rwebinput, false,
rwebinput_keyboard_cb);
if (r != EMSCRIPTEN_RESULT_SUCCESS)
{
@ -426,7 +426,7 @@ static void *rwebinput_input_init(const char *joypad_driver)
}
r = emscripten_set_keyup_callback(
"!canvas", rwebinput, false,
"#canvas", rwebinput, false,
rwebinput_keyboard_cb);
if (r != EMSCRIPTEN_RESULT_SUCCESS)
{
@ -435,7 +435,7 @@ static void *rwebinput_input_init(const char *joypad_driver)
}
r = emscripten_set_keypress_callback(
"!canvas", rwebinput, false,
"#canvas", rwebinput, false,
rwebinput_keyboard_cb);
if (r != EMSCRIPTEN_RESULT_SUCCESS)
{
@ -443,7 +443,7 @@ static void *rwebinput_input_init(const char *joypad_driver)
"[EMSCRIPTEN/INPUT] failed to create keypress callback: %d\n", r);
}
r = emscripten_set_mousedown_callback("!canvas", rwebinput, false,
r = emscripten_set_mousedown_callback("#canvas", rwebinput, false,
rwebinput_mouse_cb);
if (r != EMSCRIPTEN_RESULT_SUCCESS)
{
@ -451,7 +451,7 @@ static void *rwebinput_input_init(const char *joypad_driver)
"[EMSCRIPTEN/INPUT] failed to create mousedown callback: %d\n", r);
}
r = emscripten_set_mouseup_callback("!canvas", rwebinput, false,
r = emscripten_set_mouseup_callback("#canvas", rwebinput, false,
rwebinput_mouse_cb);
if (r != EMSCRIPTEN_RESULT_SUCCESS)
{
@ -459,7 +459,7 @@ static void *rwebinput_input_init(const char *joypad_driver)
"[EMSCRIPTEN/INPUT] failed to create mouseup callback: %d\n", r);
}
r = emscripten_set_mousemove_callback("!canvas", rwebinput, false,
r = emscripten_set_mousemove_callback("#canvas", rwebinput, false,
rwebinput_mouse_cb);
if (r != EMSCRIPTEN_RESULT_SUCCESS)
{
@ -468,7 +468,7 @@ static void *rwebinput_input_init(const char *joypad_driver)
}
r = emscripten_set_wheel_callback(
"!canvas", rwebinput, false,
"#canvas", rwebinput, false,
rwebinput_wheel_cb);
if (r != EMSCRIPTEN_RESULT_SUCCESS)
{
@ -819,7 +819,7 @@ static void rwebinput_input_poll(void *data)
static void rwebinput_grab_mouse(void *data, bool state)
{
if (state)
emscripten_request_pointerlock("!canvas", EM_TRUE);
emscripten_request_pointerlock("#canvas", EM_TRUE);
else
emscripten_exit_pointerlock();
}

View File

@ -39,7 +39,7 @@
#include <psp2/kernel/threadmgr.h>
#elif defined(_3DS)
#include <3ds.h>
#elif defined(EMSCRIPTEN)
#elif (defined(EMSCRIPTEN) && defined(EMSCRIPTEN_ASYNCIFY))
#include <emscripten/emscripten.h>
#else
#include <time.h>
@ -100,7 +100,7 @@ static int nanosleepDOS(const struct timespec *rqtp, struct timespec *rmtp)
#define retro_sleep(msec) (usleep(1000 * (msec)))
#elif defined(WIIU)
#define retro_sleep(msec) (OSSleepTicks(ms_to_ticks((msec))))
#elif defined(EMSCRIPTEN)
#elif defined(EMSCRIPTEN) && defined(EMSCRIPTEN_ASYNCIFY)
#define retro_sleep(msec) (emscripten_sleep(msec))
#else
static INLINE void retro_sleep(unsigned msec)

View File

@ -0,0 +1,204 @@
<!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 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>
</body>
</html>

View File

@ -0,0 +1,34 @@
#! /usr/bin/env coffee
fs = require 'fs'
path = require 'path'
symLinks = {}
rdSync = (dpath, tree, name) ->
files = fs.readdirSync(dpath)
for file in files
# ignore non-essential directories / files
continue if file in ['.git', 'node_modules', 'bower_components', 'build'] or file[0] is '.'
fpath = dpath + '/' + file
try
# Avoid infinite loops.
lstat = fs.lstatSync(fpath)
if lstat.isSymbolicLink()
symLinks[lstat.dev] ?= {}
# Ignore if we've seen it before
continue if symLinks[lstat.dev][lstat.ino]?
symLinks[lstat.dev][lstat.ino] = 0
fstat = fs.statSync(fpath)
if fstat.isDirectory()
tree[file] = child = {}
rdSync(fpath, child, file)
else
tree[file] = null
catch e
# Ignore and move on.
return tree
fs_listing = rdSync(process.cwd(), {}, '/')
console.log(JSON.stringify(fs_listing))

View File

@ -0,0 +1,120 @@
/**
* RetroArch Web Player
*
* 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;
}
/**
* 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;
}
}
/**
* Disable the border around the player.
*/
canvas.webplayer {
border: none;
outline: none;
}
textarea {
font-family: monospace;
font-size: 0.7em;
height: 95%;
width: 95%;
border-style: none;
border-color: transparent;
overflow: auto;
resize: none;
}
/**
* Toggle Top Navigation
*/
.toggleMenu {
float: right;
}
.showMenu {
position: absolute;
right: 0;
cursor: pointer;
}
#icnShowMenu {
color: #565656 !important;
}
.navbar {
box-shadow: none;
}

View File

@ -0,0 +1,389 @@
/**
* RetroArch Web Player
*
* This provides the basic JavaScript for the RetroArch web player.
*/
var BrowserFS = BrowserFS;
var afs;
var initializationCount = 0;
var Module = {
noInitialRun: true,
arguments: ["-v", "--menu"],
encoder: new TextEncoder(),
message_queue: [],
message_out: [],
message_accum: "",
retroArchSend: function(msg) {
let bytes = this.encoder.encode(msg + "\n");
this.message_queue.push([bytes, 0]);
},
retroArchRecv: function() {
let out = this.message_out.shift();
if (out == null && this.message_accum != "") {
out = this.message_accum;
this.message_accum = "";
}
return out;
},
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();
},
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);
}
};
function cleanupStorage() {
localStorage.clear();
if (BrowserFS.FileSystem.IndexedDB.isAvailable()) {
var req = indexedDB.deleteDatabase("RetroArch");
req.onsuccess = function() {
console.log("Deleted database successfully");
};
req.onerror = function() {
console.error("Couldn't delete database");
};
req.onblocked = function() {
console.error("Couldn't delete database due to the operation being blocked");
};
}
document.getElementById("btnClean").disabled = true;
}
function idbfsInit() {
$('#icnLocal').removeClass('fa-globe');
$('#icnLocal').addClass('fa-spinner fa-spin');
var imfs = new BrowserFS.FileSystem.InMemory();
if (BrowserFS.FileSystem.IndexedDB.isAvailable()) {
afs = new BrowserFS.FileSystem.AsyncMirror(imfs,
new BrowserFS.FileSystem.IndexedDB(function(e, fs) {
if (e) {
// fallback to imfs
afs = new BrowserFS.FileSystem.InMemory();
console.error("WEBPLAYER: error: " + e + " falling back to in-memory filesystem");
appInitialized();
} else {
// initialize afs by copying files from async storage to sync storage.
afs.initialize(function(e) {
if (e) {
afs = new BrowserFS.FileSystem.InMemory();
console.error("WEBPLAYER: error: " + e + " falling back to in-memory filesystem");
appInitialized();
} else {
idbfsSyncComplete();
}
});
}
},
"RetroArch"));
}
}
function idbfsSyncComplete() {
$('#icnLocal').removeClass('fa-spinner').removeClass('fa-spin');
$('#icnLocal').addClass('fa-check');
console.log("WEBPLAYER: idbfs setup successful");
appInitialized();
}
function appInitialized() {
/* Need to wait for the file system, the wasm runtime, and the zip download
to complete before enabling the Run button. */
initializationCount++;
if (initializationCount == 3) {
setupFileSystem("browser");
preLoadingComplete();
}
}
function preLoadingComplete() {
// 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;
});
}
var zipTOC;
function zipfsInit() {
// 256 MB max bundle size
let buffer = new ArrayBuffer(256 * 1024 * 1024);
let bufferView = new Uint8Array(buffer);
let idx = 0;
// bundle should be in five parts (this can be changed later)
Promise.all([fetch("assets/frontend/bundle.zip.aa"),
fetch("assets/frontend/bundle.zip.ab"),
fetch("assets/frontend/bundle.zip.ac"),
fetch("assets/frontend/bundle.zip.ad"),
fetch("assets/frontend/bundle.zip.ae")
]).then(function(resps) {
Promise.all(resps.map((r) => r.arrayBuffer())).then(function(buffers) {
for (let buf of buffers) {
if (idx + buf.byteLength > buffer.maxByteLength) {
console.error("WEBPLAYER: error: bundle.zip is too large");
}
bufferView.set(new Uint8Array(buf), idx, buf.byteLength);
idx += buf.byteLength;
}
BrowserFS.FileSystem.ZipFS.computeIndex(BrowserFS.BFSRequire('buffer').Buffer(new Uint8Array(buffer, 0, idx)), function(toc) {
zipTOC = toc;
appInitialized();
});
})
});
}
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
var xfs1 = new BrowserFS.FileSystem.ZipFS(zipTOC);
// create an XmlHttpRequest filesystem for core assets
var xfs2 = new BrowserFS.FileSystem.XmlHttpRequest(".index-xhr", "assets/cores/");
console.log("WEBPLAYER: initializing filesystem: " + backend);
mfs.mount('/home/web_user/retroarch/userdata', afs);
mfs.mount('/home/web_user/retroarch/', xfs1);
mfs.mount('/home/web_user/retroarch/userdata/content/downloads', xfs2);
BrowserFS.initialize(mfs);
var BFS = new BrowserFS.EmscriptenFS(Module.FS, Module.PATH, Module.ERRNO_CODES);
Module.FS.mount(BFS, {
root: '/home'
}, '/home');
console.log("WEBPLAYER: " + backend + " filesystem initialization successful");
}
// 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;
}
}
function startRetroArch() {
$('.webplayer').show();
$('.webplayer-preview').hide();
document.getElementById("btnRun").disabled = true;
$('#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);
}
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');
}
}
}
}
function uploadData(data, name) {
var dataView = new Uint8Array(data);
Module.FS.createDataFile('/', name, dataView, true, false);
var data = Module.FS.readFile(name, {
encoding: 'binary'
});
Module.FS.writeFile('/home/web_user/retroarch/userdata/content/' + name, data, {
encoding: 'binary'
});
Module.FS.unlink(name);
}
function switchCore(corename) {
localStorage.setItem("core", corename);
}
function switchStorage(backend) {
if (backend != localStorage.getItem("backend")) {
localStorage.setItem("backend", backend);
location.reload();
}
}
// When the browser has loaded everything.
$(function() {
// Enable data clear
$('#btnClean').click(function() {
cleanupStorage();
});
// Enable all available ToolTips.
$('.tooltip-enable').tooltip({
placement: 'right'
});
// Allow hiding the top menu.
$('.showMenu').hide();
$('#btnHideMenu, .showMenu').click(function() {
$('nav').slideToggle('slow');
$('.showMenu').toggle('slow');
});
// 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();
}
});
// 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);
});
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);
// Load the Core's related JavaScript.
import("./" + core + "_libretro.js").then(script => {
script.default(Module).then(mod => {
Module = mod;
$('#icnRun').removeClass('fa-spinner').removeClass('fa-spin');
$('#icnRun').addClass('fa-play');
$('#lblDrop').removeClass('active');
$('#lblLocal').addClass('active');
idbfsInit();
zipfsInit();
}).catch(err => {
console.error("Couldn't instantiate module", err);
throw err;
});
}).catch(err => {
console.error("Couldn't load script", err);
throw err;
});
}

View File

@ -192,13 +192,13 @@
<img class="webplayer-preview img-fluid" src="media/canvas.png" width="960" height="720" alt="RetroArch Logo">
</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 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="//wzrd.in/standalone/browserfs@0.6.1"></script-->
<script src="browserfs.min.js"></script>
<script type="text/javascript" src="zip.min.js"></script>
<script src="libretro.js"></script>
</body>
</html>

View File

@ -3,9 +3,8 @@
*
* This provides the basic JavaScript for the RetroArch web player.
*/
var BrowserFS = BrowserFS;
var afs;
var initializationCount = 0;
var retroarch_ready = false;
var setImmediate;
var Module = {
noInitialRun: true,
@ -70,6 +69,7 @@ var Module = {
],
postRun: [],
onRuntimeInitialized: function() {
retroarch_ready = true;
appInitialized();
},
print: function(text) {
@ -86,70 +86,22 @@ var Module = {
};
function cleanupStorage() {
localStorage.clear();
if (BrowserFS.FileSystem.IndexedDB.isAvailable()) {
var req = indexedDB.deleteDatabase("RetroArch");
req.onsuccess = function() {
console.log("Deleted database successfully");
};
req.onerror = function() {
console.error("Couldn't delete database");
};
req.onblocked = function() {
console.error("Couldn't delete database due to the operation being blocked");
};
}
document.getElementById("btnClean").disabled = true;
async function cleanupStorage()
{
localStorage.clear();
let storage = await navigator.storage.getDirectory();
await storage.remove({recursive: true});
document.getElementById("btnClean").disabled = true;
}
function idbfsInit() {
$('#icnLocal').removeClass('fa-globe');
$('#icnLocal').addClass('fa-spinner fa-spin');
var imfs = new BrowserFS.FileSystem.InMemory();
if (BrowserFS.FileSystem.IndexedDB.isAvailable()) {
afs = new BrowserFS.FileSystem.AsyncMirror(imfs,
new BrowserFS.FileSystem.IndexedDB(function(e, fs) {
if (e) {
// fallback to imfs
afs = new BrowserFS.FileSystem.InMemory();
console.error("WEBPLAYER: error: " + e + " falling back to in-memory filesystem");
appInitialized();
} else {
// initialize afs by copying files from async storage to sync storage.
afs.initialize(function(e) {
if (e) {
afs = new BrowserFS.FileSystem.InMemory();
console.error("WEBPLAYER: error: " + e + " falling back to in-memory filesystem");
appInitialized();
} else {
idbfsSyncComplete();
}
});
}
},
"RetroArch"));
}
}
function idbfsSyncComplete() {
$('#icnLocal').removeClass('fa-spinner').removeClass('fa-spin');
$('#icnLocal').addClass('fa-check');
console.log("WEBPLAYER: idbfs setup successful");
appInitialized();
}
function appInitialized() {
/* Need to wait for the file system, the wasm runtime, and the zip download
to complete before enabling the Run button. */
initializationCount++;
if (initializationCount == 3) {
setupFileSystem("browser");
preLoadingComplete();
}
}
function appInitialized()
{
/* Need to wait for the wasm runtime to load before enabling the Run button. */
if (retroarch_ready)
{
setupFileSystem().then(() => { preLoadingComplete(); });
}
}
function preLoadingComplete() {
// Make the Preview image clickable to start RetroArch.
@ -163,56 +115,63 @@ function preLoadingComplete() {
});
}
var zipTOC;
function zipfsInit() {
// 256 MB max bundle size
let buffer = new ArrayBuffer(256 * 1024 * 1024);
let bufferView = new Uint8Array(buffer);
let idx = 0;
// bundle should be in five parts (this can be changed later)
Promise.all([fetch("assets/frontend/bundle.zip.aa"),
fetch("assets/frontend/bundle.zip.ab"),
fetch("assets/frontend/bundle.zip.ac"),
fetch("assets/frontend/bundle.zip.ad"),
fetch("assets/frontend/bundle.zip.ae")
]).then(function(resps) {
Promise.all(resps.map((r) => r.arrayBuffer())).then(function(buffers) {
for (let buf of buffers) {
if (idx + buf.byteLength > buffer.maxByteLength) {
console.error("WEBPLAYER: error: bundle.zip is too large");
}
bufferView.set(new Uint8Array(buf), idx, buf.byteLength);
idx += buf.byteLength;
}
BrowserFS.FileSystem.ZipFS.computeIndex(BrowserFS.BFSRequire('buffer').Buffer(new Uint8Array(buffer, 0, idx)), function(toc) {
zipTOC = toc;
appInitialized();
});
})
});
async function setupZipFS(mount) {
let buffers = await Promise.all([
fetch("assets/frontend/bundle.zip.aa").then((r) => r.arrayBuffer()),
fetch("assets/frontend/bundle.zip.ab").then((r) => r.arrayBuffer()),
fetch("assets/frontend/bundle.zip.ac").then((r) => r.arrayBuffer()),
fetch("assets/frontend/bundle.zip.ad").then((r) => r.arrayBuffer())
]);
let buffer = new ArrayBuffer(256*1024*1024);
let bufferView = new Uint8Array(buffer);
let idx = 0;
for (let buf of buffers) {
if (idx+buf.byteLength > buffer.maxByteLength) {
console.log("WEBPLAYER: error: bundle.zip is too large");
}
bufferView.set(new Uint8Array(buf), idx, buf.byteLength);
idx += buf.byteLength;
}
const zipBuf = new Uint8Array(buffer, 0, idx);
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);
Module.FS.createPreloadedFile(mount+"/"+file.filename, undefined, data, true, true);
} else if (file.directory) {
Module.FS.mkdirTree(mount+"/"+file.filename);
}
}
await zipReader.close();
}
function setupFileSystem(backend) {
// create a mountable filesystem that will server as a root mountpoint for browserfs
var mfs = new BrowserFS.FileSystem.MountableFileSystem();
function loadIndex(index, path) {
for (const key of Object.keys(index)) {
if (index[key]) {
Module.FS.mkdirTree(path+key+"/");
loadIndex(index[key], path+key+"/");
} else {
Module.FS.open(path+key, "w+");
}
}
}
// create an XmlHttpRequest filesystem for the bundled data
var xfs1 = new BrowserFS.FileSystem.ZipFS(zipTOC);
// create an XmlHttpRequest filesystem for core assets
var xfs2 = new BrowserFS.FileSystem.XmlHttpRequest(".index-xhr", "assets/cores/");
async function setupFileSystem()
{
Module.FS.mkdirTree("/home/web_user/retroarch/userdata");
console.log("WEBPLAYER: initializing filesystem: " + backend);
mfs.mount('/home/web_user/retroarch/userdata', afs);
Module.FS.mount(Module.OPFS, {}, "/home/web_user/retroarch/userdata");
Module.FS.mkdir("/home/web_user/retroarch/downloads",700);
let index = await (await fetch("assets/cores/.index-xhr")).json();
let manifest = {};
Module.FS.mount(Module.FETCHFS, {"base_url":"assets/cores"}, "/home/web_user/retroarch/downloads");
loadIndex(index, "/home/web_user/retroarch/downloads/");
mfs.mount('/home/web_user/retroarch/', xfs1);
mfs.mount('/home/web_user/retroarch/userdata/content/downloads', xfs2);
BrowserFS.initialize(mfs);
var BFS = new BrowserFS.EmscriptenFS(Module.FS, Module.PATH, Module.ERRNO_CODES);
Module.FS.mount(BFS, {
root: '/home'
}, '/home');
console.log("WEBPLAYER: " + backend + " filesystem initialization successful");
setupZipFS("/home/web_user/retroarch");
console.log("WEBPLAYER: filesystem initialization successful");
}
// Retrieve the value of the given GET parameter.
@ -242,7 +201,6 @@ function startRetroArch() {
Module.requestFullscreen(false);
Module.canvas.focus();
});
Module.canvas.focus();
Module.canvas.addEventListener("pointerdown", function() {
Module.canvas.focus();
@ -364,26 +322,26 @@ $(function() {
loadCore(core);
});
async function downloadScript(src) {
let resp = await fetch(src);
let blob = await resp.blob();
return blob;
}
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);
// Load the Core's related JavaScript.
import("./" + core + "_libretro.js").then(script => {
script.default(Module).then(mod => {
Module = mod;
$('#icnRun').removeClass('fa-spinner').removeClass('fa-spin');
$('#icnRun').addClass('fa-play');
$('#lblDrop').removeClass('active');
$('#lblLocal').addClass('active');
idbfsInit();
zipfsInit();
}).catch(err => {
console.error("Couldn't instantiate module", err);
throw err;
});
}).catch(err => {
console.error("Couldn't load script", err);
throw err;
downloadScript("./"+core+"_libretro.js").then(scriptBlob => {
Module.mainScriptUrlOrBlob = scriptBlob;
import(URL.createObjectURL(scriptBlob)).then(script => {
script.default(Module).then(mod => {
Module = mod;
$('#icnRun').removeClass('fa-spinner').removeClass('fa-spin');
$('#icnRun').addClass('fa-play');
$('#lblDrop').removeClass('active');
$('#lblLocal').addClass('active');
}).catch(err => { console.error("Couldn't instantiate module",err); throw err; });
}).catch(err => { console.error("Couldn't load script",err); throw err; });
});
}
}

1
pkg/emscripten/libretro/zip.min.js vendored Normal file

File diff suppressed because one or more lines are too long