Enable pthreads on Emscripten (#17586)

* 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

* fix scaling, remove zip dependency

* Support asset/cheats/etc downloaders for emscripten

- Add http transfer support for emscripten
  - At the task_http level, not the net_http level --- so no netplay
    or webdav.
- Change default paths to be more like other platforms
- Gives us smaller bundles and a faster boot time
- Had to work around a task queue bug on Emscripten
  - I made the smallest possible change to do it, but it may be better
    to fix in rthread.c

* Load an emscripten file_packager package on first run

If no ozone assets are present, load a libretro_minimal package
created using Emscripten's built-in file packager.

* updated readme, removed indexer from wasmfs libretro-web

* Put back zip dependency, load asset bundle into opfs on first run

* fix upload path

* Remove unused function

* easy testing setup for two multithreaded conditions

1. make PROXY_TO_PTHREAD=1 (slower)
2. make PROXY_TO_PTHREAD=0 (bad audio, because doesn't sleep in
openal.c)

* Remove condition on sleep in openal

also make input_driver check existence of drv->axis, drv->button
before calling them.

* Fix resizing under EGL

* Don't force config file path on emscripten

* Add time.h include to netplay, default HAVE_NETPLAYDISCOVERY to 0

* Remove nearly all proxied joypad calls under emscripten

* Fix file uploads under firefox

* Fix safari API uses, but Safari still hangs in OPFS filesystem mount

I think this can be fixed by moving the backend creation off the main
thread.

* Move filesystem init into emscripten C entry point

* Setup filesystems off of main thread

* re-set default player to async

Also improve Safari compatibility under proxy-to-pthread condition

* Safari upload file fixes

* Remove some excess prints

* Fix typo
This commit is contained in:
Joe Osborn 2025-02-19 15:59:25 -08:00 committed by GitHub
parent 6ec4ffb2fa
commit 56014a27d6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 1748 additions and 175 deletions

View File

@ -1536,7 +1536,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)
@ -2203,11 +2206,16 @@ ifeq ($(HAVE_NETWORKING), 1)
$(LIBRETRO_COMM_DIR)/net/net_socket.o \
core_updater_list.o \
network/natt.o \
tasks/task_http.o \
tasks/task_netplay_lan_scan.o \
tasks/task_netplay_nat_traversal.o \
tasks/task_netplay_find_content.o
ifeq ($(HAVE_EMSCRIPTEN), 1)
OBJ += tasks/task_http_emscripten.o
else
OBJ += tasks/task_http.o
endif
ifeq ($(HAVE_MENU), 1)
OBJ += tasks/task_pl_thumbnail_download.o
endif

View File

@ -19,12 +19,18 @@ HAVE_PATCH = 1
HAVE_DSP_FILTER = 1
HAVE_VIDEO_FILTER = 1
HAVE_OVERLAY = 1
HAVE_NETWORKING = 1
HAVE_LIBRETRODB = 1
HAVE_COMPRESSION = 1
HAVE_UPDATE_ASSETS = 1
HAVE_ONLINE_UPDATER = 1
HAVE_GLSL = 1
HAVE_SCREENSHOTS = 1
HAVE_REWIND = 1
HAVE_AUDIOMIXER = 1
HAVE_CC_RESAMPLER = 1
HAVE_EGL = 1
HAVE_EGL ?= 1
HAVE_OPENGLES = 1
HAVE_RJPEG = 0
HAVE_RPNG = 1
HAVE_EMSCRIPTEN = 1
@ -48,6 +54,12 @@ 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
@ -61,12 +73,12 @@ HAVE_RWEBAUDIO = 0
GL_DEBUG ?= 0
# enable javascript filesystem tracking
FS_DEBUG = 1
FS_DEBUG = 0
HAVE_OPENGLES ?= 1
HAVE_OPENGLES3 ?= 0
ASYNC ?= 0
ASYNC ?= 1
LTO ?= 0
PTHREAD ?= 0
@ -92,59 +104,84 @@ _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
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
EXPORTS := callMain,FS,PATH,ERRNO_CODES,ENV,stringToNewUTF8,UTF8ToString,Browser,GL
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
endif
LIBS := -s USE_ZLIB=1 -lbrowser.js
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_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)
endif
endif
ifeq ($(PROXY_TO_PTHREAD),1)
LIBS += -sUSE_ES6_IMPORT_META=0 -sENVIRONMENT=worker,web
LIBS += -sPROXY_TO_PTHREAD -sOFFSCREENCANVAS_SUPPORT
DEFINES += -DUSE_OFFSCREENCANVAS=1 -DPROXY_TO_PTHREAD=1
else
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 \
--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
LDFLAGS += -s FULL_ES3=1 -s MIN_WEBGL_VERSION=2 -s MAX_WEBGL_VERSION=2 -lGL
else
LDFLAGS += -s FULL_ES2=1
LDFLAGS += -s FULL_ES2=1 -s MIN_WEBGL_VERSION=1 -s MAX_WEBGL_VERSION=2 -lGL
endif
endif
ifeq ($(GL_DEBUG), 1)
LDFLAGS += -s GL_ASSERTIONS=1 -s GL_DEBUG=1
LDFLAGS += -s GL_ASSERTIONS=1 -s GL_DEBUG=1 -DHAVE_GL_DEBUG_ES=1
endif
ifeq ($(FS_DEBUG), 1)
LDFLAGS += -s FS_DEBUG=1
endif
ifeq ($(HAVE_SDL2), 1)
LIBS += -s USE_SDL=2
DEFINES += -DHAVE_SDL2
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
@ -183,8 +220,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

@ -1833,7 +1833,11 @@
#define DEFAULT_BUILDBOT_SERVER_URL ""
#endif
#ifdef EMSCRIPTEN
#define DEFAULT_BUILDBOT_ASSETS_SERVER_URL "https://buildbot.libretro.com/assets/"
#else
#define DEFAULT_BUILDBOT_ASSETS_SERVER_URL "http://buildbot.libretro.com/assets/"
#endif
#define DEFAULT_DISCORD_APP_ID "475456035851599874"

View File

@ -15,28 +15,6 @@ var LibraryPlatformEmscripten = {
}
},
PlatformEmscriptenWatchCanvasSize: function() {
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);
}
Module.setCanvasSize(width, height);
Module.print("Setting real canvas size: " + width + " x " + height);
});
RPE.observer.observe(Module.canvas);
window.addEventListener("resize", function(e) {
RPE.observer.unobserve(Module.canvas);
RPE.observer.observe(Module.canvas);
}, false);
},
PlatformEmscriptenPowerStateInit: function() {
if (!navigator.getBattery) return;
navigator.getBattery().then(function(battery) {

View File

@ -17,7 +17,13 @@
#include <emscripten/emscripten.h>
#include <emscripten/html5.h>
#if HAVE_WASMFS
#include <emscripten/wasmfs.h>
#endif
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <file/config_file.h>
#include <queues/task_queue.h>
@ -50,7 +56,33 @@
#include "../../audio/audio_driver.h"
void emscripten_mainloop(void);
void PlatformEmscriptenWatchCanvasSize(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);
@ -262,17 +294,101 @@ static void frontend_emscripten_get_env(int *argc, char *argv[],
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_CACHE], "/tmp/",
"retroarch", sizeof(g_defaults.dirs[DEFAULT_DIR_CACHE]));
/* history and main config */
/* history */
strlcpy(g_defaults.dirs[DEFAULT_DIR_CONTENT_HISTORY],
user_path, sizeof(g_defaults.dirs[DEFAULT_DIR_CONTENT_HISTORY]));
fill_pathname_join(g_defaults.path_config, user_path,
FILE_PATH_MAIN_CONFIG, sizeof(g_defaults.path_config));
#ifndef IS_SALAMANDER
dir_check_defaults("custom.ini");
#endif
}
typedef struct args {
int argc;
char **argv;
} args_t;
static bool retro_started = false;
static bool filesystem_ready = false;
#if HAVE_WASMFS
void PlatformEmscriptenMountFilesystems(void *info) {
char *opfs_mount = getenv("OPFS");
char *fetch_manifest = getenv("FETCH_MANIFEST");
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);
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);
abort();
}
abort();
}
}
if(fetch_manifest) {
/* fetch_manifest should be a path to a manifest file.
manifest files have this format:
URL PATH
URL PATH
URL PATH
...
Where URL may not contain spaces, but PATH may.
*/
int max_line_len = 1024;
printf("[FetchFS] read fetch manifest from %s\n",fetch_manifest);
FILE *file = fopen(fetch_manifest, O_RDONLY);
char *line = calloc(sizeof(char), max_line_len);
size_t len = 0;
while (getline(&line, &len, file) != -1) {
char *path = strstr(line, " ");
backend_t fetch;
int fd;
if (!path) {
printf("Manifest file has invalid line %s\n",line);
return;
}
*path = '\0';
path += 1;
printf("Fetch %s from %s\n", path, line);
{
char *parent = strdup(path);
path_parent_dir(parent, strlen(parent));
if(!path_mkdir(parent)) {
printf("mkdir error %d\n",errno);
abort();
}
free(parent);
}
fetch = wasmfs_create_fetch_backend(line, 8*1024*1024);
fd = wasmfs_create_file(path, 0777, fetch);
close(fd);
}
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)
{
enum frontend_powerstate ret = FRONTEND_POWERSTATE_NONE;
@ -303,17 +419,42 @@ static uint64_t frontend_emscripten_get_free_mem(void)
return PlatformEmscriptenGetFreeMem();
}
void emscripten_bootup_mainloop(void *argptr) {
if(filesystem_ready) {
args_t *args = (args_t*)argptr;
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);
}
}
int main(int argc, char *argv[])
{
args_t *args = calloc(sizeof(args_t), 1);
PlatformEmscriptenWatchCanvasSize();
PlatformEmscriptenPowerStateInit();
EM_ASM({
specialHTMLTargets["!canvas"] = Module.canvas;
});
emscripten_set_main_loop(emscripten_mainloop, 0, 0);
rarch_main(argc, argv, NULL);
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;
}

View File

@ -52,7 +52,7 @@ static void gfx_ctx_emscripten_swap_interval(void *data, int interval)
static void gfx_ctx_emscripten_get_canvas_size(int *width, int *height)
{
EMSCRIPTEN_RESULT r = emscripten_get_canvas_element_size("!canvas", width, height);
EMSCRIPTEN_RESULT r = emscripten_get_canvas_element_size("#canvas", width, height);
if (r != EMSCRIPTEN_RESULT_SUCCESS)
{
@ -70,11 +70,10 @@ static void gfx_ctx_emscripten_check_window(void *data, bool *quit,
emscripten_ctx_data_t *emscripten = (emscripten_ctx_data_t*)data;
gfx_ctx_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;
*resize = false;
}
static void gfx_ctx_emscripten_swap_buffers(void *data)
@ -94,9 +93,10 @@ static void gfx_ctx_emscripten_get_video_size(void *data,
if (!emscripten)
return;
*width = emscripten->fb_width;
*height = emscripten->fb_height;
int w, h;
gfx_ctx_emscripten_get_canvas_size(&w, &h);
*width = w;
*height = h;
}
static bool gfx_ctx_emscripten_get_metrics(void *data,
@ -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,298 @@
/* 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;
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;
}
static void gfx_ctx_emscripten_webgl_swap_buffers(void *data)
{
#ifdef USE_OFFSCREENCANVAS
emscripten_webgl_commit_frame();
#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;
}
static bool gfx_ctx_emscripten_webgl_get_metrics(void *data,
enum display_metric_types type, float *value)
{
switch (type)
{
// there is no way to get the actual DPI in emscripten, so return a standard value instead.
// this is needed for menu touch/pointer swipe scrolling to work.
case DISPLAY_METRIC_DPI:
*value = 150.0f;
break;
default:
*value = 0.0f;
return false;
}
return true;
}
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 */
gfx_ctx_emscripten_webgl_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, /* get_context_data */
NULL /* make_current */
};

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)
{
@ -476,7 +476,7 @@ static void *rwebinput_input_init(const char *joypad_driver)
"[EMSCRIPTEN/INPUT] failed to create wheel callback: %d\n", r);
}
r = emscripten_set_touchstart_callback("!canvas", rwebinput, false,
r = emscripten_set_touchstart_callback("#canvas", rwebinput, false,
rwebinput_touch_cb);
if (r != EMSCRIPTEN_RESULT_SUCCESS)
{
@ -484,7 +484,7 @@ static void *rwebinput_input_init(const char *joypad_driver)
"[EMSCRIPTEN/INPUT] failed to create touchstart callback: %d\n", r);
}
r = emscripten_set_touchend_callback("!canvas", rwebinput, false,
r = emscripten_set_touchend_callback("#canvas", rwebinput, false,
rwebinput_touch_cb);
if (r != EMSCRIPTEN_RESULT_SUCCESS)
{
@ -492,7 +492,7 @@ static void *rwebinput_input_init(const char *joypad_driver)
"[EMSCRIPTEN/INPUT] failed to create touchend callback: %d\n", r);
}
r = emscripten_set_touchmove_callback("!canvas", rwebinput, false,
r = emscripten_set_touchmove_callback("#canvas", rwebinput, false,
rwebinput_touch_cb);
if (r != EMSCRIPTEN_RESULT_SUCCESS)
{
@ -500,7 +500,7 @@ static void *rwebinput_input_init(const char *joypad_driver)
"[EMSCRIPTEN/INPUT] failed to create touchmove callback: %d\n", r);
}
r = emscripten_set_touchcancel_callback("!canvas", rwebinput, false,
r = emscripten_set_touchcancel_callback("#canvas", rwebinput, false,
rwebinput_touch_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

@ -26,6 +26,12 @@
#include "../../verbosity.h"
#define CLAMPDOUBLE(x) MIN(1.0, MAX(-1.0, (x)))
#define NUM_BUTTONS 64
#define NUM_AXES 64
/* TODO/FIXME - static globals */
static struct EmscriptenGamepadEvent _pads[DEFAULT_MAX_PADS];
static bool _live_pads[DEFAULT_MAX_PADS] = {0};
static EM_BOOL rwebpad_gamepad_cb(int event_type,
const EmscriptenGamepadEvent *gamepad_event, void *user_data)
@ -36,6 +42,8 @@ static EM_BOOL rwebpad_gamepad_cb(int event_type,
switch (event_type)
{
case EMSCRIPTEN_EVENT_GAMEPADCONNECTED:
_pads[gamepad_event->index] = *gamepad_event;
_live_pads[gamepad_event->index] = true;
input_autoconfigure_connect(
gamepad_event->id, /* name */
NULL, /* display name */
@ -45,6 +53,7 @@ static EM_BOOL rwebpad_gamepad_cb(int event_type,
pid); /* pid */
break;
case EMSCRIPTEN_EVENT_GAMEPADDISCONNECTED:
_live_pads[gamepad_event->index] = false;
input_autoconfigure_disconnect(gamepad_event->index,
rwebpad_joypad.ident);
break;
@ -68,28 +77,23 @@ static void *rwebpad_joypad_init(void *data)
r = emscripten_set_gamepaddisconnected_callback(NULL, false,
rwebpad_gamepad_cb);
return (void*)-1;
return (void*)(-1);
}
static const char *rwebpad_joypad_name(unsigned pad)
{
static EmscriptenGamepadEvent gamepad_state;
EMSCRIPTEN_RESULT r = emscripten_get_gamepad_status(pad, &gamepad_state);
if (r == EMSCRIPTEN_RESULT_SUCCESS)
return gamepad_state.id;
return "";
if (pad >= DEFAULT_MAX_PADS || !_live_pads[pad]) {
return "";
}
return _pads[pad].id;
}
static int32_t rwebpad_joypad_button(unsigned port, uint16_t joykey)
{
EmscriptenGamepadEvent gamepad_state;
EMSCRIPTEN_RESULT r = emscripten_get_gamepad_status(
port, &gamepad_state);
if (port >= DEFAULT_MAX_PADS)
return 0;
if (r != EMSCRIPTEN_RESULT_SUCCESS)
if (port >= DEFAULT_MAX_PADS || !_live_pads[port])
return 0;
gamepad_state = _pads[port];
if (joykey < gamepad_state.numButtons)
return gamepad_state.digitalButton[joykey];
return 0;
@ -98,21 +102,17 @@ static int32_t rwebpad_joypad_button(unsigned port, uint16_t joykey)
static void rwebpad_joypad_get_buttons(unsigned port_num, input_bits_t *state)
{
EmscriptenGamepadEvent gamepad_state;
EMSCRIPTEN_RESULT r = emscripten_get_gamepad_status(
port_num, &gamepad_state);
if (r == EMSCRIPTEN_RESULT_SUCCESS)
{
unsigned i;
for (i = 0; i < gamepad_state.numButtons; i++)
{
if (gamepad_state.digitalButton[i])
BIT256_SET_PTR(state, i);
}
}
else
unsigned i;
if (port_num >= DEFAULT_MAX_PADS || !_live_pads[port_num]) {
BIT256_CLEAR_ALL_PTR(state);
return;
}
gamepad_state = _pads[port_num];
for (i = 0; i < gamepad_state.numButtons; i++)
{
if (gamepad_state.digitalButton[i])
BIT256_SET_PTR(state, i);
}
}
static int16_t rwebpad_joypad_axis_state(
@ -138,11 +138,10 @@ static int16_t rwebpad_joypad_axis_state(
static int16_t rwebpad_joypad_axis(unsigned port, uint32_t joyaxis)
{
EmscriptenGamepadEvent gamepad_state;
EMSCRIPTEN_RESULT r = emscripten_get_gamepad_status(port, &gamepad_state);
if (r != EMSCRIPTEN_RESULT_SUCCESS)
if (port >= DEFAULT_MAX_PADS || !_live_pads[port]) {
return 0;
return rwebpad_joypad_axis_state(&gamepad_state, port, joyaxis);
}
return rwebpad_joypad_axis_state(&_pads[port], port, joyaxis);
}
static int16_t rwebpad_joypad_state(
@ -154,13 +153,9 @@ static int16_t rwebpad_joypad_state(
EmscriptenGamepadEvent gamepad_state;
int16_t ret = 0;
uint16_t port_idx = joypad_info->joy_idx;
EMSCRIPTEN_RESULT r = emscripten_get_gamepad_status(
port_idx, &gamepad_state);
if (r != EMSCRIPTEN_RESULT_SUCCESS)
if (port_idx >= DEFAULT_MAX_PADS || !_live_pads[port])
return 0;
if (port_idx >= DEFAULT_MAX_PADS)
return 0;
gamepad_state = _pads[port];
for (i = 0; i < RARCH_FIRST_CUSTOM_BIND; i++)
{
/* Auto-binds are per joypad, not per user. */
@ -187,16 +182,16 @@ static int16_t rwebpad_joypad_state(
static void rwebpad_joypad_poll(void)
{
emscripten_sample_gamepad_data();
for (int i = 0; i < DEFAULT_MAX_PADS; i++) {
if (_live_pads[i]) {
emscripten_get_gamepad_status(i, &_pads[i]);
}
}
}
static bool rwebpad_joypad_query_pad(unsigned pad)
{
EmscriptenGamepadEvent gamepad_state;
EMSCRIPTEN_RESULT r = emscripten_get_gamepad_status(pad, &gamepad_state);
if (r == EMSCRIPTEN_RESULT_SUCCESS)
return gamepad_state.connected == EM_TRUE;
return false;
return _live_pads[pad];
}
static void rwebpad_joypad_destroy(void) { }

View File

@ -1076,16 +1076,16 @@ static int16_t input_joypad_analog_axis(
? joypad_info->auto_binds[ident_y_plus].joyaxis
: bind_y_plus->joyaxis;
/* normalized magnitude for radial scaled analog deadzone */
if (x_axis_plus != AXIS_NONE)
if (x_axis_plus != AXIS_NONE && drv->axis)
x = drv->axis(
joypad_info->joy_idx, x_axis_plus);
if (x_axis_minus != AXIS_NONE)
if (x_axis_minus != AXIS_NONE && drv->axis)
x += drv->axis(joypad_info->joy_idx,
x_axis_minus);
if (y_axis_plus != AXIS_NONE)
if (y_axis_plus != AXIS_NONE && drv->axis)
y = drv->axis(
joypad_info->joy_idx, y_axis_plus);
if (y_axis_minus != AXIS_NONE)
if (y_axis_minus != AXIS_NONE && drv->axis)
y += drv->axis(
joypad_info->joy_idx, y_axis_minus);
normal_mag = (1.0f / 0x7fff) * sqrt(x * x + y * y);
@ -1113,9 +1113,9 @@ static int16_t input_joypad_analog_axis(
uint16_t key_plus = (bind_plus->joykey == NO_BTN)
? joypad_info->auto_binds[ident_plus].joykey
: bind_plus->joykey;
if (drv->button(joypad_info->joy_idx, key_plus))
if (drv->button && drv->button(joypad_info->joy_idx, key_plus))
res = 0x7fff;
if (drv->button(joypad_info->joy_idx, key_minus))
if (drv->button && drv->button(joypad_info->joy_idx, key_minus))
res += -0x7fff;
}

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

@ -35,7 +35,9 @@
#ifdef HAVE_THREADS
#include <rthreads/rthreads.h>
#endif
#ifdef EMSCRIPTEN
#include <retro_timers.h>
#endif
#ifdef HAVE_GCD
#include <dispatch/dispatch.h>
#endif
@ -522,8 +524,14 @@ static void threaded_worker(void *userdata)
}
slock_unlock(running_lock);
task->handler(task);
#ifdef EMSCRIPTEN
/* Workaround emscripten pthread bug where not parking the
thread will prevent other important stuff from
happening. Maybe due to lack of signals implementation in
emscripten's pthreads? */
retro_sleep(1);
#endif
slock_lock(property_lock);
finished = ((task->flags & RETRO_TASK_FLG_FINISHED) > 0) ? true : false;

View File

@ -28,6 +28,7 @@
#endif
#include <retro_timers.h>
#include <time.h>
#include <math/float_minmax.h>
#include <string/stdstring.h>

View File

@ -2,7 +2,7 @@
The RetroArch Web Player is RetroArch compiled through [Emscripten](https://emscripten.org/). The following outlines how to compile RetroArch using Emscripten, and running it in your browser.
## Compiling
## Compiling the Single-Threaded Player
To compile RetroArch with Emscripten, you'll first have to [download and install the Emscripten SDK](https://emscripten.org/docs/getting_started/downloads.html) at 3.1.46:
@ -35,7 +35,7 @@ cp fceumm_libretro.{js,wasm} pkg/emscripten/libretro
The emscripten build in the retroarch tree does not contain the necessary web assets for a complete RetroArch installation. You'll need the asset package from the latest emscripten nightly build ( https://buildbot.libretro.com/nightly/emscripten/ ); take its `assets/` folder and put it into `pkg/emscripten/libretro`. This `assets/` folder should contain a `frontend/` directory and a `cores/` directory.
If you're building your own frontend asset bundle (i.e. modifying `frontend/bundle/`), you'll need to turn the bundle into zipped partfiles. Open a terminal in `assets/frontend` and `zip -r9 bundle.zip bundle && split -b 30M bundle.zip bundle.zip.` (this should work on Mac and Linux, please file a PR with instructions for Windows).
If you're building your own frontend asset bundle (i.e. modifying `frontend/bundle/`), you'll need to turn the bundle into zipped partfiles. Open a terminal in `assets/frontend/` and `zip -r9 bundle.zip bundle && cd .. && split -b 30M bundle.zip bundle.zip.` (this should work on Mac and Linux, please file a PR with instructions for Windows).
If you want to add more built-in core content files to `assets/cores`, you need to re-run the indexer script:
@ -83,11 +83,11 @@ emmake make -f Makefile platform=emscripten
cp melonds_libretro_emscripten.bc ~/retroarch/RetroArch/libretro_emscripten.bc
```
Now build the frontend with the pthreads env variable: (2 is the number of workers this can be any integer)
Now build the frontend with the pthreads env variable: (2 is the number of workers; this can be any integer, but many browsers limit the number of workers)
```
cd ~/retroarch/RetroArch
pthread=2 emmake make -f Makefile.emscripten LIBRETRO=melonds && cp melonds_libretro.* pkg/emscripten/libretro
emmake make -f Makefile.emscripten LIBRETRO=melonds PTHREAD=2 && cp melonds_libretro.* pkg/emscripten/libretro
```
Your resulting output will be located in:
@ -95,12 +95,11 @@ Your resulting output will be located in:
```
~/retroarch/RetroArch/pkg/emscripten/libretro/melonds_libretro.js
~/retroarch/RetroArch/pkg/emscripten/libretro/melonds_libretro.wasm
~/retroarch/RetroArch/pkg/emscripten/libretro/melonds_libretro.worker.js
```
## Setting up your webserver (Threaded)
Unless loading from `localhost` you will need to server the content from an HTTPS endpoint with a valid SSL certificate. This is a security limitation imposed by the browser. Along with that you will need to set content control policies with special headers in your server:
To support multithreaded builds, you will need to serve the content from an HTTPS endpoint with a valid SSL certificate. This is a security limitation imposed by the browser. Along with that you will need to set content control policies with special headers in your server:
In Nodejs with express:
@ -108,6 +107,7 @@ In Nodejs with express:
app.use(function(req, res, next) {
res.header("Cross-Origin-Embedder-Policy", "require-corp");
res.header("Cross-Origin-Opener-Policy", "same-origin");
res.header("Cross-Origin-Resource-Policy", "same-origin");
next();
});
```
@ -117,4 +117,50 @@ In NGINX: (site config under `server {`)
```
add_header Cross-Origin-Opener-Policy same-origin;
add_header Cross-Origin-Embedder-Policy require-corp;
add_header Cross-Origin-Resource-Policy same-origin;
```
Node http-server:
```
http-server . -S \
--header "Cross-Origin-Opener-Policy: same-origin" \
--header "Cross-Origin-Embedder-Policy: require-corp" \
--header "Cross-Origin-Resource-Policy: same-origin"
```
# Compiling the Multi-Threaded Frontend
To compile the multi-threaded RetroArch frontend with Emscripten and make use of wasmfs and other features, you'll first have to [download and install the Emscripten SDK](https://emscripten.org/docs/getting_started/downloads.html). Currently, we need the "top of tree" or latest version of Emscripten:
```
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
./emsdk install tot
./emsdk activate tot
source emsdk_env.sh
```
After emsdk is installed you will need to build an emulator core, move that output into Retroarch, and use helper scripts to produce web ready assets, in this example we will be building [https://github.com/libretro/libretro-fceumm](https://github.com/libretro/libretro-fceumm):
```
mkdir ~/retroarch
cd ~/retroarch
git clone https://github.com/libretro/libretro-fceumm.git
cd libretro-fceumm
emmake make -f Makefile.libretro platform=emscripten
git clone https://github.com/libretro/RetroArch.git ~/retroarch/RetroArch
cp ~/retroarch/libretro-fceumm/fceumm_libretro_emscripten.bc ~/retroarch/RetroArch/libretro_emscripten.bc
cd ~/retroarch
emmake make -f Makefile.emscripten LIBRETRO=fceumm PROXY_TO_PTHREAD=1 PTHREAD=4 HAVE_WASMFS=1 ASYNC=0 HAVE_EGL=0 -j all
cp fceumm_libretro.{js,wasm} pkg/emscripten/libretro-thread
```
## Dependencies
The emscripten build in the retroarch tree does not contain the necessary web assets for a complete RetroArch installation. While it supports the regular desktop asset and content downloaders, we also provide a small bundle of UI assets for first launch. You can obtain these files from the nightly Emscripten build on the buildbot, or make them yourself by `zip -r bundle-minimal.zip bundle-minimal` (essentially, just the `assets/ozone`, `assets/pkg`, and `assets/sounds` folders from the regular asset package).
## Usage
Hosting the threaded web build is the same as for the multi-threaded emulators above; SSL and proper CORS headers are required.

View File

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

View File

@ -0,0 +1,136 @@
/**
* 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;
}
}
canvas.webplayer {
border: none;
outline: none;
width: 800px;
height: 600px;
}
#canvas_div {
width: 100%;
height: 100%;
position: absolute;
}
/**
* 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 {
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,298 @@
/**
* RetroArch Web Player
*
* This provides the basic JavaScript for the RetroArch web player.
*/
var filesystem_ready = false;
var retroarch_ready = false;
var setImmediate;
var Module = {
noInitialRun: true,
arguments: ["-v", "--menu"],
noImageDecoding: true,
noAudioDecoding: true,
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) {
Module.ENV['OPFS'] = "/home/web_user/retroarch";
},
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() {
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);
}
};
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;
}
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;
});
}
// 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) {
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();
}
}
// 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);
});
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);
downloadScript("./"+core+"_libretro.js").then(scriptBlob => {
Module.mainScriptUrlOrBlob = scriptBlob;
import(URL.createObjectURL(scriptBlob)).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; });
});
}
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

@ -0,0 +1,66 @@
importScripts("zip-no-worker.min.js");
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();
}
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;
}
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();
}
onmessage = async (msg) => {
if(msg.data.command == "load_bundle") {
let old_timestamp = msg.data;
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("/home/web_user/retroarch/userdata/content/"+msg.data.name, new Uint8Array(msg.data.data));
postMessage({command:"uploaded_file",name:msg.data.name});
}
}

File diff suppressed because one or more lines are too long

View File

@ -192,10 +192,10 @@
<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>

View File

@ -81,22 +81,12 @@
}
}
/**
* 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

@ -9,7 +9,7 @@ var initializationCount = 0;
var Module = {
noInitialRun: true,
arguments: ["-v", "--menu"],
arguments: ["-v", "--menu", "-c", "/home/web_user/retroarch/userdata/retroarch.cfg"],
encoder: new TextEncoder(),
message_queue: [],
@ -203,9 +203,8 @@ function setupFileSystem(backend) {
var xfs2 = new BrowserFS.FileSystem.XmlHttpRequest(".index-xhr", "assets/cores/");
console.log("WEBPLAYER: initializing filesystem: " + backend);
mfs.mount('/home/web_user/retroarch/bundle', xfs1);
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);
@ -386,4 +385,4 @@ function loadCore(core) {
console.error("Couldn't load script", err);
throw err;
});
}
}

View File

@ -0,0 +1,360 @@
/* RetroArch - A frontend for libretro.
* Copyright (C) 2011-2017 - Daniel De Matteis
*
* 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/>.
*/
#ifndef EMSCRIPTEN
#error "task_http_emscripten only makes sense in emscripten builds"
#endif
#include <stdlib.h>
#include "verbosity.h"
#include <emscripten/emscripten.h>
#include <string/stdstring.h>
#include <compat/strl.h>
#include <file/file_path.h>
#include <retro_timers.h>
#include <retro_miscellaneous.h>
#ifdef RARCH_INTERNAL
#include "../gfx/video_display_server.h"
#endif
#include "task_file_transfer.h"
#include "tasks_internal.h"
struct http_handle
{
int handle;
char connection_url[NAME_MAX_LENGTH];
http_transfer_data_t *response;
};
typedef struct http_handle http_handle_t;
static void task_http_transfer_handler(retro_task_t *task)
{
http_handle_t *http = (http_handle_t*)task->state;
uint8_t flg = task_get_flags(task);
if ((flg & RETRO_TASK_FLG_CANCELLED) > 0)
goto task_finished;
if (http->response || (http->handle == -1))
goto task_finished;
return;
task_finished:
task_set_flags(task, RETRO_TASK_FLG_FINISHED, true);
if (http->response)
{
if ((flg & RETRO_TASK_FLG_CANCELLED) > 0)
{
string_list_free(http->response->headers);
free(http->response->data);
free(http->response);
http->response = NULL;
task_set_error(task,
strldup("Task cancelled.", sizeof("Task cancelled.")));
}
else
{
bool mute;
task_set_data(task, http->response);
mute = ((task->flags & RETRO_TASK_FLG_MUTE) > 0);
if (!mute && http->response->status >= 400)
task_set_error(task, strldup("Download failed.",
sizeof("Download failed.")));
}
}
free(http);
}
static void http_transfer_progress_cb(retro_task_t *task)
{
if (task)
video_display_server_set_window_progress(task->progress,
((task->flags & RETRO_TASK_FLG_FINISHED) > 0));
}
static bool task_http_finder(retro_task_t *task, void *user_data)
{
http_handle_t *http = NULL;
if (task && (task->handler == task_http_transfer_handler) && user_data)
if ((http = (http_handle_t*)task->state))
return string_is_equal(http->connection_url, (const char*)user_data);
return false;
}
static void task_http_transfer_cleanup(retro_task_t *task)
{
http_transfer_data_t* data = (http_transfer_data_t*)task_get_data(task);
if (data)
{
string_list_free(data->headers);
if (data->data)
free(data->data);
free(data);
}
}
void wget_onload_cb(unsigned handle, void *t_ptr, void *data, unsigned len) {
retro_task_t *task = (retro_task_t *)t_ptr;
http_handle_t *http = (http_handle_t*)task->state;
http_transfer_data_t *resp;
if (!(resp = (http_transfer_data_t*)malloc(sizeof(*resp)))) {
http->handle = -1;
return;
} else {
resp->data = data;
resp->len = len;
resp->status = 200;
resp->headers = NULL; // sorry webdav
http->response = resp;
}
}
void wget_onerror_cb(unsigned handle, void *t_ptr, int status, const char *err) {
retro_task_t *task = (retro_task_t *)t_ptr;
http_handle_t *http = (http_handle_t*)task->state;
bool mute = ((task->flags & RETRO_TASK_FLG_MUTE) > 0);
if (!mute)
task_set_error(task, strldup("Download failed.",
sizeof("Download failed.")));
http->handle = -1;
}
void wget_onprogress_cb(unsigned handle, void *t_ptr, int pos, int tot) {
retro_task_t *task = (retro_task_t *)t_ptr;
if (tot == 0)
task_set_progress(task, -1);
else if (pos < (((size_t)-1) / 100))
/* prefer multiply then divide for more accurate results */
task_set_progress(task, (signed)(pos * 100 / tot));
else
/* but invert the logic if it would cause an overflow */
task_set_progress(task, MIN((signed)pos / (tot / 100), 100));
}
static void *task_push_http_transfer_generic(
const char *url, const char *method,
const char *data, const char *user_agent,
const char *headers,
bool mute,
retro_task_callback_t cb, void *user_data)
{
retro_task_t *t = NULL;
http_handle_t *http = NULL;
int wget_handle = -1;
if (!url)
return NULL;
if (!string_is_equal(method, "GET"))
{
/* POST requests usually mutate the server, so assume multiple calls are
* intended, even if they're duplicated. Additionally, they may differ
* only by the POST data, and task_http_finder doesn't look at that, so
* unique requests could be misclassified as duplicates.
*/
}
else
{
task_finder_data_t find_data;
find_data.func = task_http_finder;
find_data.userdata = (void*)url;
/* Concurrent download of the same file is not allowed */
if (task_queue_find(&find_data))
return NULL;
}
if (!(http = (http_handle_t*)malloc(sizeof(*http))))
goto error;
http->handle = -1;
http->response = NULL;
http->connection_url[0] = '\0';
strlcpy(http->connection_url, url, sizeof(http->connection_url));
if (!(t = task_init()))
goto error;
t->handler = task_http_transfer_handler;
t->state = http;
t->callback = cb;
t->progress_cb = http_transfer_progress_cb;
t->cleanup = task_http_transfer_cleanup;
t->user_data = user_data;
t->progress = -1;
if (mute)
t->flags |= RETRO_TASK_FLG_MUTE;
else
t->flags &= ~RETRO_TASK_FLG_MUTE;
wget_handle = emscripten_async_wget2_data(url, method, data, t, false, wget_onload_cb, wget_onerror_cb, wget_onprogress_cb);
http->handle = wget_handle;
task_queue_push(t);
return t;
error:
if (http)
free(http);
if (t)
free(t);
return NULL;
}
void* task_push_http_transfer(const char *url, bool mute,
const char *type,
retro_task_callback_t cb, void *user_data)
{
return task_push_http_transfer_generic(url, type ? type : "GET", NULL, NULL, NULL, mute, cb, user_data);
}
void *task_push_webdav_stat(const char *url, bool mute, const char *headers,
retro_task_callback_t cb, void *user_data)
{
RARCH_ERR("[http] response headers not supported, webdav won't work\n");
return task_push_http_transfer_generic(url, "OPTIONS", NULL, NULL, headers, mute, cb, user_data);
}
void* task_push_webdav_mkdir(const char *url, bool mute,
const char *headers,
retro_task_callback_t cb, void *user_data)
{
RARCH_ERR("[http] response headers not supported, webdav won't work\n");
return task_push_http_transfer_generic(url, "MKCOL", NULL, NULL, headers, mute, cb, user_data);
}
void* task_push_webdav_put(const char *url,
const void *put_data, size_t len, bool mute,
const char *headers, retro_task_callback_t cb, void *user_data)
{
char expect[1024]; /* TODO/FIXME - check size */
size_t _len;
RARCH_ERR("[http] response headers not supported, webdav won't work\n");
_len = strlcpy(expect, "Expect: 100-continue\r\n", sizeof(expect));
if (headers)
{
strlcpy(expect + _len, headers, sizeof(expect) - _len);
}
return task_push_http_transfer_generic(url, "PUT", put_data, NULL, expect, mute, cb, user_data);
}
void* task_push_webdav_delete(const char *url, bool mute,
const char *headers,
retro_task_callback_t cb, void *user_data)
{
RARCH_ERR("[http] response headers not supported, webdav won't work\n");
return task_push_http_transfer_generic(url, "DELETE", NULL, NULL, headers, mute, cb, user_data);
}
void *task_push_webdav_move(const char *url,
const char *dest, bool mute, const char *headers,
retro_task_callback_t cb, void *user_data)
{
size_t _len;
char dest_header[PATH_MAX_LENGTH + 512];
RARCH_ERR("[http] response headers not supported, webdav won't work\n");
_len = strlcpy(dest_header, "Destination: ", sizeof(dest_header));
_len += strlcpy(dest_header + _len, dest, sizeof(dest_header) - _len);
_len += strlcpy(dest_header + _len, "\r\n", sizeof(dest_header) - _len);
if (headers)
strlcpy(dest_header + _len, headers, sizeof(dest_header) - _len);
return task_push_http_transfer_generic(url, "MOVE", NULL, NULL, dest_header, mute, cb, user_data);
}
void* task_push_http_transfer_file(const char* url, bool mute,
const char* type,
retro_task_callback_t cb, file_transfer_t* transfer_data)
{
size_t _len;
const char *s = NULL;
char tmp[NAME_MAX_LENGTH] = "";
retro_task_t *t = NULL;
if (string_is_empty(url))
return NULL;
if (!(t = (retro_task_t*)task_push_http_transfer_generic(
/* should be using type but some callers now rely on type being ignored */
url, "GET",
NULL, NULL, NULL,
mute, cb, transfer_data)))
return NULL;
if (transfer_data)
s = transfer_data->path;
else
s = url;
_len = strlcpy(tmp, msg_hash_to_str(MSG_DOWNLOADING), sizeof(tmp));
tmp[ _len] = ' ';
tmp[++_len] = '\0';
if (string_ends_with_size(s, ".index",
strlen(s), STRLEN_CONST(".index")))
s = msg_hash_to_str(MSG_INDEX_FILE);
strlcpy(tmp + _len, s, sizeof(tmp) - _len);
t->title = strdup(tmp);
return t;
}
void* task_push_http_transfer_with_user_agent(const char *url, bool mute,
const char *type, const char *user_agent,
retro_task_callback_t cb, void *user_data)
{
return task_push_http_transfer_generic(url, type ? type : "GET", NULL, user_agent, NULL, mute, cb, user_data);
}
void* task_push_http_transfer_with_headers(const char *url, bool mute,
const char *type, const char *headers,
retro_task_callback_t cb, void *user_data)
{
return task_push_http_transfer_generic(url, type ? type : "GET", NULL, NULL, headers, mute, cb, user_data);
}
void* task_push_http_post_transfer(const char *url,
const char *post_data, bool mute,
const char *type, retro_task_callback_t cb, void *user_data)
{
return task_push_http_transfer_generic(url, type ? type : "POST", post_data, NULL, NULL, mute, cb, user_data);
}
void* task_push_http_post_transfer_with_user_agent(const char *url,
const char *post_data, bool mute,
const char *type, const char *user_agent,
retro_task_callback_t cb, void *user_data)
{
return task_push_http_transfer_generic(url, type ? type : "POST", post_data, user_agent, NULL, mute, cb, user_data);
}
void* task_push_http_post_transfer_with_headers(const char *url,
const char *post_data, bool mute,
const char *type, const char *headers,
retro_task_callback_t cb, void *user_data)
{
return task_push_http_transfer_generic(url, type ? type : "POST", post_data, NULL, headers, mute, cb, user_data);
}