diff --git a/CHANGES.md b/CHANGES.md index 97d9a4d71163..d114c9b52f1a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -22,6 +22,9 @@ - CLOUDSYNC: Handle ignored directories properly - EMSCRIPTEN: Scale window to correct size - EMSCRIPTEN: Additional platform functions +- EMSCRIPTEN: Add new default video context driver: emscriptenwebgl_ctx +- EMSCRIPTEN: Add new audio driver: AudioWorklet +- EMSCRIPTEN: Add new modernized web player which will eventually replace the existing one - EMSCRIPTEN/RWEBINPUT: Add touch input support - GENERAL: Fix save state auto increment - GENERAL: Fix softpatching with periods/dots in the file name diff --git a/Makefile.common b/Makefile.common index 8b18945b65d4..be0d9c11e839 100644 --- a/Makefile.common +++ b/Makefile.common @@ -790,6 +790,9 @@ ifeq ($(HAVE_EMSCRIPTEN), 1) ifeq ($(HAVE_RWEBAUDIO), 1) OBJ += audio/drivers/rwebaudio.o endif + ifeq ($(HAVE_AUDIOWORKLET), 1) + OBJ += audio/drivers/audioworklet.o + endif endif ifeq ($(HAVE_BLUETOOTH), 1) diff --git a/Makefile.emscripten b/Makefile.emscripten index fde53d7a237f..0d44cb25803b 100644 --- a/Makefile.emscripten +++ b/Makefile.emscripten @@ -7,9 +7,7 @@ else TARGET := $(LIBRETRO)_libretro.js endif endif - -EOPT = USE_ZLIB=1 # Emscripten specific options -EOPTS = $(addprefix -s $(EMPTY), $(EOPT)) # Add '-s ' to each option +TARGET_BASE := $(subst .js,,$(TARGET)) OS = Emscripten OBJ := @@ -20,7 +18,7 @@ HAVE_PATCH = 1 HAVE_DSP_FILTER = 1 HAVE_VIDEO_FILTER = 1 HAVE_OVERLAY = 1 -HAVE_NETWORKING = 1 +HAVE_NETWORKING ?= 1 HAVE_LIBRETRODB = 1 HAVE_COMPRESSION = 1 HAVE_UPDATE_ASSETS = 1 @@ -32,10 +30,10 @@ HAVE_AUDIOMIXER = 1 HAVE_CC_RESAMPLER = 1 HAVE_EGL ?= 0 HAVE_OPENGLES = 1 -HAVE_RJPEG = 0 -HAVE_RPNG = 1 +HAVE_RJPEG = 0 +HAVE_RPNG = 1 HAVE_EMSCRIPTEN = 1 -HAVE_MENU = 1 +HAVE_MENU ?= 1 HAVE_GFX_WIDGETS = 1 HAVE_RGUI = 1 HAVE_SDL = 0 @@ -47,37 +45,63 @@ HAVE_STATIC_AUDIO_FILTERS = 1 HAVE_STB_FONT = 1 HAVE_CONFIGFILE = 1 HAVE_COMMAND = 1 -HAVE_STDIN_CMD = 1 +HAVE_STDIN_CMD ?= 1 HAVE_CHEATS = 1 HAVE_IBXM = 1 HAVE_CORE_INFO_CACHE = 1 HAVE_7ZIP = 1 HAVE_BSV_MOVIE = 1 -HAVE_AL = 1 HAVE_CHD ?= 0 HAVE_NETPLAYDISCOVERY ?= 0 + +HAVE_AL ?= 1 + +# enables pthreads, requires special headers on the web server: +# see https://web.dev/articles/coop-coep HAVE_THREADS ?= 0 +# requires HAVE_THREADS +HAVE_AUDIOWORKLET ?= 0 + # WARNING -- READ BEFORE ENABLING # The rwebaudio driver is known to have several audio bugs, such as # minor crackling, or the entire page freezing/crashing. # It works perfectly on chrome, but even firefox has really bad audio quality. # I should also note, the driver on iOS is completely broken (crashes the page). # You have been warned. -HAVE_RWEBAUDIO = 0 +HAVE_RWEBAUDIO ?= 0 + +# whether the browser thread is allowed to block to wait for audio to play, +# may lead to the issues mentioned above. +# currently this variable is only used by audioworklet; +# rwebaudio will always busywait and openal will never busywait. +ALLOW_AUDIO_BUSYWAIT ?= 0 + +# minimal asyncify; better performance than full asyncify, +# but sleeping on the main thread is only possible in some places. +MIN_ASYNC ?= 0 + +# runs RetroArch on a pthread instead of the browser thread; requires HAVE_THREADS +PROXY_TO_PTHREAD ?= 0 + +# recommended FS when using HAVE_THREADS +HAVE_WASMFS ?= 0 + +# enables OPFS (origin private file system) and FETCHFS, requires PROXY_TO_PTHREAD +HAVE_EXTRA_WASMFS ?= 0 + +# enable javascript filesystem tracking, incompatible with HAVE_WASMFS +FS_DEBUG ?= 0 # help diagnose GL problems (can cause issues in normal operation) GL_DEBUG ?= 0 -# enable javascript filesystem tracking -FS_DEBUG = 0 +# does nothing on its own, but automatically selected by some other options +WASM_WORKERS = 0 HAVE_OPENGLES ?= 1 HAVE_OPENGLES3 ?= 0 -HAVE_WASMFS ?= 0 -PROXY_TO_PTHREAD ?= 0 - ASYNC ?= 0 LTO ?= 0 PTHREAD_POOL_SIZE ?= 4 @@ -102,26 +126,32 @@ OBJDIR := obj-emscripten EXPORTED_FUNCTIONS = _main,_malloc,_free,_cmd_savefiles,_cmd_save_state,_cmd_load_state,_cmd_undo_save_state,_cmd_undo_load_state,_cmd_take_screenshot,\ _cmd_toggle_menu,_cmd_reload_config,_cmd_toggle_grab_mouse,_cmd_toggle_game_focus,_cmd_reset,_cmd_toggle_pause,_cmd_pause,_cmd_unpause,\ _cmd_set_volume,_cmd_set_shader,_cmd_cheat_set_code,_cmd_cheat_get_code,_cmd_cheat_toggle_index,_cmd_cheat_get_code_state,_cmd_cheat_realloc,\ -_cmd_cheat_get_size,_cmd_cheat_apply_cheats,_update_canvas_dimensions,_update_window_hidden,_update_power_state,_update_memory_usage,\ -EmscriptenSendCommand,EmscriptenReceiveCommandReply +_cmd_cheat_get_size,_cmd_cheat_apply_cheats,EmscriptenSendCommand,EmscriptenReceiveCommandReply EXPORTS := callMain,FS,PATH,ERRNO_CODES,ENV,stringToNewUTF8,UTF8ToString,Browser,EmscriptenSendCommand,EmscriptenReceiveCommandReply -LIBS := -s USE_ZLIB=1 +LIBS := -s USE_ZLIB=1 +CFLAGS := -s USE_ZLIB=1 -ifeq ($(HAVE_WASMFS), 1) - LIBS += -s WASMFS -s FORCE_FILESYSTEM=1 -lfetchfs.js -lopfs.js - DEFINES += -DHAVE_WASMFS +ifeq ($(HAVE_EXTRA_WASMFS), 1) + LIBS += -lfetchfs.js -lopfs.js + DEFINES += -DHAVE_EXTRA_WASMFS + override HAVE_WASMFS = 1 ifeq ($(PROXY_TO_PTHREAD), 0) - $(error ERROR: WASMFS requires PROXY_TO_PTHREAD) + $(error ERROR: HAVE_EXTRA_WASMFS requires PROXY_TO_PTHREAD) endif endif +ifeq ($(HAVE_WASMFS), 1) + LIBS += -s WASMFS -s FORCE_FILESYSTEM=1 +endif + # note: real PROXY_TO_PTHREAD is not used here; we do the pthread management ourselves ifeq ($(PROXY_TO_PTHREAD), 1) LIBS += -s OFFSCREENCANVAS_SUPPORT DEFINES += -DPROXY_TO_PTHREAD -DEMSCRIPTEN_STACK_SIZE=$(STACK_SIZE) override HAVE_THREADS = 1 + override WASM_WORKERS = 1 # use the default stack size for the browser thread; the RetroArch thread will be created with the specified stack size override STACK_SIZE = 4194304 else ifeq ($(HAVE_AL), 1) @@ -165,6 +195,40 @@ ifeq ($(HAVE_RWEBAUDIO), 1) DEFINES += -DHAVE_RWEBAUDIO endif +ifeq ($(HAVE_AUDIOWORKLET), 1) + LDFLAGS += -s AUDIO_WORKLET=1 + DEFINES += -DHAVE_AUDIOWORKLET + override WASM_WORKERS = 1 + ifeq ($(HAVE_THREADS), 0) + $(error ERROR: AUDIOWORKLET requires HAVE_THREADS) + endif + ifeq ($(PROXY_TO_PTHREAD), 1) + else ifeq ($(ASYNC), 1) + else + DEFINES += -DEMSCRIPTEN_AUDIO_EXTERNAL_BLOCK + ifeq ($(MIN_ASYNC), 1) + DEFINES += -DEMSCRIPTEN_AUDIO_ASYNC_BLOCK + else + DEFINES += -DEMSCRIPTEN_AUDIO_FAKE_BLOCK + endif + ifneq ($(ALLOW_AUDIO_BUSYWAIT), 1) + DEFINES += -DEMSCRIPTEN_AUDIO_EXTERNAL_WRITE_BLOCK + endif + endif +endif + +ifeq ($(ALLOW_AUDIO_BUSYWAIT), 1) + DEFINES += -DEMSCRIPTEN_AUDIO_BUSYWAIT +endif + +# explanation of some of these defines: +# EMSCRIPTEN_AUDIO_EXTERNAL_BLOCK: audio blocking occurs in the main loop instead of in the audio driver functions. +# EMSCRIPTEN_AUDIO_EXTERNAL_WRITE_BLOCK: along with above, enables external blocking in the write function. +# ALLOW_AUDIO_BUSYWAIT: write function will busywait. init function may still use an external block. +# EMSCRIPTEN_AUDIO_ASYNC_BLOCK: external block uses emscripten_sleep (requires MIN_ASYNC). +# EMSCRIPTEN_AUDIO_FAKE_BLOCK: external block uses main loop timing (doesn't require asyncify). +# when building with either PROXY_TO_PTHREAD or ASYNC (full asyncify), none of the above are required. + ifeq ($(HAVE_AL), 1) LDFLAGS += -lopenal DEFINES += -DHAVE_AL @@ -175,11 +239,21 @@ ifeq ($(HAVE_THREADS), 1) CFLAGS += -pthread -s SHARED_MEMORY endif +ifeq ($(WASM_WORKERS), 1) + LDFLAGS += -s WASM_WORKERS=2 +endif + ifeq ($(ASYNC), 1) - DEFINES += -DEMSCRIPTEN_ASYNCIFY + DEFINES += -DEMSCRIPTEN_ASYNCIFY -DEMSCRIPTEN_FULL_ASYNCIFY LDFLAGS += -s ASYNCIFY=1 -s ASYNCIFY_STACK_SIZE=8192 ifeq ($(DEBUG), 1) - LDFLAGS += -s ASYNCIFY_DEBUG=1 # -s ASYNCIFY_ADVISE + #LDFLAGS += -s ASYNCIFY_DEBUG=1 # broken? + endif +else ifeq ($(MIN_ASYNC), 1) + DEFINES += -DEMSCRIPTEN_ASYNCIFY -DEMSCRIPTEN_MIN_ASYNCIFY + LDFLAGS += -s ASYNCIFY=1 -s ASYNCIFY_STACK_SIZE=8192 -s ASYNCIFY_IGNORE_INDIRECT=1 -s ASYNCIFY_ADD='dynCall_*,emscripten_mainloop' -s ASYNCIFY_REMOVE='threaded_worker' + ifeq ($(DEBUG), 1) + LDFLAGS += -s ASYNCIFY_ADVISE #-s ASYNCIFY_DEBUG=1 endif endif @@ -202,7 +276,7 @@ ifneq ($(V), 1) endif ifeq ($(DEBUG), 1) - LDFLAGS += -O0 -g -gsource-map -s SAFE_HEAP=1 -s STACK_OVERFLOW_CHECK=2 -s ASSERTIONS=1 + LDFLAGS += -O0 -g -gsource-map -s SAFE_HEAP=2 -s STACK_OVERFLOW_CHECK=2 -s ASSERTIONS=1 # -O0 in cflags gives "too many locals" errors CFLAGS += -O1 -g -gsource-map else @@ -220,25 +294,37 @@ RARCH_OBJ := $(addprefix $(OBJDIR)/,$(OBJ)) all: $(TARGET) -$(libretro_new) : $(libretro) - mv -f $(libretro) $(libretro_new) +$(libretro_new): ; + +mv_libretro: + mv -f $(libretro) $(libretro_new) || true -$(TARGET): $(RARCH_OBJ) $(libretro_new) +# until emscripten adds something like WASM_WORKERS=2 but for audio worklets, DIY. +ifeq ($(HAVE_AUDIOWORKLET), 1) +$(TARGET): $(RARCH_OBJ) $(libretro_new) mv_libretro @$(if $(Q), $(shell echo echo "LD $@ \ $(libretro_new) $(LIBS) $(LDFLAGS)"),) $(Q)$(LD) -o $@ $(RARCH_OBJ) $(libretro_new) $(LIBS) $(LDFLAGS) + $(Q)tr -d '\n' < "$(TARGET_BASE).aw.js" | sed -e "s/[\/&]/\\\\&/g" -e "s/'/\\\\\\\\&/g" > _audioworklet.js + $(Q)sed -i.bak -e "s/\"$(TARGET_BASE)\.aw\.js\"/URL.createObjectURL(new Blob(['$$(cat _audioworklet.js)'],{type:'text\/javascript'}))/" -- "$@" + $(Q)rm -f "$(TARGET_BASE).aw.js" _audioworklet.js "$@".bak +else +$(TARGET): $(RARCH_OBJ) $(libretro_new) mv_libretro + @$(if $(Q), $(shell echo echo "LD $@ \ $(libretro_new) $(LIBS) $(LDFLAGS)"),) + $(Q)$(LD) -o $@ $(RARCH_OBJ) $(libretro_new) $(LIBS) $(LDFLAGS) +endif $(OBJDIR)/%.o: %.c @mkdir -p $(dir $@) @$(if $(Q), $(shell echo echo CC $<),) - $(Q)$(CC) $(CFLAGS) $(DEFINES) $(EOPTS) -c -o $@ $< + $(Q)$(CC) $(CFLAGS) $(DEFINES) -c -o $@ $< $(OBJDIR)/%.o: %.cpp @mkdir -p $(dir $@) @$(if $(Q), $(shell echo echo CXX $<),) - $(Q)$(CXX) $(CXXFLAGS) $(DEFINES) $(EOPTS) -c -o $@ $< + $(Q)$(CXX) $(CXXFLAGS) $(DEFINES) -c -o $@ $< clean: rm -rf $(OBJDIR) rm -f $(TARGET) -.PHONY: all clean +.PHONY: all clean mv_libretro diff --git a/audio/audio_driver.c b/audio/audio_driver.c index 52d9643ff4c9..f6448633d6dc 100644 --- a/audio/audio_driver.c +++ b/audio/audio_driver.c @@ -147,9 +147,12 @@ audio_driver_t *audio_drivers[] = { #ifdef WIIU &audio_ax, #endif -#if defined(EMSCRIPTEN) && defined(HAVE_RWEBAUDIO) +#if defined(HAVE_RWEBAUDIO) &audio_rwebaudio, #endif +#if defined(HAVE_AUDIOWORKLET) + &audio_audioworklet, +#endif #if defined(PSP) || defined(VITA) || defined(ORBIS) &audio_psp, #endif @@ -463,19 +466,19 @@ static void audio_driver_flush( = avail; audio_st->source_ratio_current = audio_st->source_ratio_original * adjust; - } #if 0 - if (verbosity_is_enabled()) - { - RARCH_LOG_OUTPUT("[Audio]: Audio buffer is %u%% full\n", - (unsigned)(100 - (avail * 100) / - audio_st->buffer_size)); - RARCH_LOG_OUTPUT("[Audio]: New rate: %lf, Orig rate: %lf\n", - audio_st->source_ratio_current, - audio_st->source_ratio_original); - } + if (verbosity_is_enabled()) + { + RARCH_LOG_OUTPUT("[Audio]: Audio buffer is %u%% full\n", + (unsigned)(100 - (avail * 100) / + audio_st->buffer_size)); + RARCH_LOG_OUTPUT("[Audio]: New rate: %lf, Orig rate: %lf\n", + audio_st->source_ratio_current, + audio_st->source_ratio_original); + } #endif + } } src_data.ratio = audio_st->source_ratio_current; diff --git a/audio/audio_driver.h b/audio/audio_driver.h index 8e2492decc1f..676744798a82 100644 --- a/audio/audio_driver.h +++ b/audio/audio_driver.h @@ -439,6 +439,7 @@ extern audio_driver_t audio_switch_thread; extern audio_driver_t audio_switch_libnx_audren; extern audio_driver_t audio_switch_libnx_audren_thread; extern audio_driver_t audio_rwebaudio; +extern audio_driver_t audio_audioworklet; audio_driver_state_t *audio_state_get_ptr(void); diff --git a/audio/drivers/audioworklet.c b/audio/drivers/audioworklet.c new file mode 100644 index 000000000000..d5425ff88a4b --- /dev/null +++ b/audio/drivers/audioworklet.c @@ -0,0 +1,521 @@ +/* RetroArch - A frontend for libretro. + * Copyright (C) 2025 - OlyB + * + * 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 . + */ + +#include +#include +#include +#include + +#include +#include +#include +#include "../../frontend/drivers/platform_emscripten.h" + +#include +#include + +#include "../audio_driver.h" +#include "../../verbosity.h" + +#define WORKLET_STACK_SIZE 4096 + +/* additional buffer size (for EMSCRIPTEN_AUDIO_EXTERNAL_WRITE_BLOCK only) */ +/* if this is too small, frames may be dropped and content could run too fast. */ +/* very large slow-motion rate values may be too large for this; avoid anything higher than 6 or 7. */ +#define EXTERNAL_BLOCK_BUFFER_MS 128 + +typedef struct audioworklet_data +{ + uint8_t *worklet_stack; + uint32_t write_avail_bytes; /* atomic */ + size_t visible_buffer_size; +#ifdef EMSCRIPTEN_AUDIO_EXTERNAL_WRITE_BLOCK + size_t write_avail_diff; +#endif +#ifdef PROXY_TO_PTHREAD + emscripten_lock_t trywrite_lock; + emscripten_condvar_t trywrite_cond; +#endif + emscripten_lock_t buffer_lock; + EMSCRIPTEN_WEBAUDIO_T context; + float *tmpbuf; + fifo_buffer_t *buffer; + unsigned rate; + unsigned latency; + bool nonblock; + bool initing; +#ifdef EMSCRIPTEN_AUDIO_FAKE_BLOCK + bool block_requested; +#endif + volatile bool running; /* currently only used by RetroArch */ + volatile bool driver_running; /* whether the driver is running (buffer allocated) */ + volatile bool context_running; /* whether the AudioContext is running */ + volatile bool init_done; + volatile bool init_error; +} audioworklet_data_t; + +/* We only ever want to create 1 worklet, so we need to keep its data even if the driver is inactive. */ +static audioworklet_data_t *audioworklet_static_data = NULL; + +/* Note that we cannot allocate any heap in here. */ +static bool audioworklet_process_cb(int numInputs, const AudioSampleFrame *inputs, + int numOutputs, AudioSampleFrame *outputs, + int numParams, const AudioParamFrame *params, + void *data) +{ + audioworklet_data_t *audioworklet = (audioworklet_data_t*)data; + size_t avail; + size_t max_read; + unsigned writing_frames = 0; + int i; + + /* TODO: do we need to pay attention to audioworklet->running here too? */ + if (audioworklet->driver_running) + { + /* can't use Atomics.wait in AudioWorklet */ + /* busyspin is safe as of emscripten 4.0.4 */ + if (!emscripten_lock_busyspin_wait_acquire(&audioworklet->buffer_lock, 2.5)) + { + printf("[WARN] [AudioWorklet] Worklet: could not acquire lock\n"); + return true; + } + + avail = FIFO_READ_AVAIL(audioworklet->buffer); + max_read = MIN(avail, outputs[0].samplesPerChannel * 2 * sizeof(float)); + + if (max_read) + { + fifo_read(audioworklet->buffer, audioworklet->tmpbuf, max_read); + emscripten_atomic_add_u32(&audioworklet->write_avail_bytes, max_read); + } + emscripten_lock_release(&audioworklet->buffer_lock); +#ifdef PROXY_TO_PTHREAD + emscripten_condvar_signal(&audioworklet->trywrite_cond, 1); +#endif + + writing_frames = max_read / 2 / sizeof(float); + for (i = 0; i < writing_frames; i++) + { + outputs[0].data[i] = audioworklet->tmpbuf[i * 2]; + outputs[0].data[outputs[0].samplesPerChannel + i] = audioworklet->tmpbuf[i * 2 + 1]; + } + } + + if (writing_frames < outputs[0].samplesPerChannel) + { + int zero_frames = outputs[0].samplesPerChannel - writing_frames; + memset(outputs[0].data + writing_frames, 0, zero_frames * sizeof(float)); + memset(outputs[0].data + writing_frames + outputs[0].samplesPerChannel, 0, zero_frames * sizeof(float)); + } + + return true; +} + +static void audioworklet_processor_inited_cb(EMSCRIPTEN_WEBAUDIO_T context, bool success, void *data) +{ + audioworklet_data_t *audioworklet = (audioworklet_data_t*)data; + int outputChannelCounts[1] = { 2 }; + EmscriptenAudioWorkletNodeCreateOptions opts = { 0, 1, outputChannelCounts }; + EMSCRIPTEN_AUDIO_WORKLET_NODE_T worklet_node; + + if (!success) + { + RARCH_ERR("[AudioWorklet] Failed to init AudioWorkletProcessor!\n"); + audioworklet->init_error = true; + audioworklet->init_done = true; + return; + } + + worklet_node = emscripten_create_wasm_audio_worklet_node(context, "retroarch", &opts, audioworklet_process_cb, audioworklet); + emscripten_audio_node_connect(worklet_node, context, 0, 0); + + audioworklet->init_done = true; +} + +static void audioworklet_thread_inited_cb(EMSCRIPTEN_WEBAUDIO_T context, bool success, void *data) +{ + audioworklet_data_t *audioworklet = (audioworklet_data_t*)data; + WebAudioWorkletProcessorCreateOptions opts = { "retroarch", 0 }; + + if (!success) + { + RARCH_ERR("[AudioWorklet] Failed to init worklet thread! Is the worklet file in the right place?\n"); + audioworklet->init_error = true; + audioworklet->init_done = true; + return; + } + + emscripten_create_wasm_audio_worklet_processor_async(context, &opts, audioworklet_processor_inited_cb, audioworklet); +} + +static void audioworklet_ctx_statechange_cb(void *data, bool state) +{ + audioworklet_data_t *audioworklet = (audioworklet_data_t*)data; + audioworklet->context_running = state; +} + +static void audioworklet_ctx_create(void *data) +{ + audioworklet_data_t *audioworklet = (audioworklet_data_t*)data; + + audioworklet->context = emscripten_create_audio_context(0); + + audioworklet->tmpbuf = memalign(16, emscripten_audio_context_quantum_size(audioworklet->context) * 2 * sizeof(float)); + audioworklet->rate = EM_ASM_INT({ + return emscriptenGetAudioObject($0).sampleRate; + }, audioworklet->context); + audioworklet->context_running = EM_ASM_INT({ + let ac = emscriptenGetAudioObject($0); + ac.addEventListener("statechange", function() { + getWasmTableEntry($2)($1, ac.state == "running"); + }); + return ac.state == "running"; + }, audioworklet->context, audioworklet, audioworklet_ctx_statechange_cb); + + emscripten_start_wasm_audio_worklet_thread_async(audioworklet->context, + audioworklet->worklet_stack, WORKLET_STACK_SIZE, audioworklet_thread_inited_cb, audioworklet); +} + +static void audioworklet_alloc_buffer(void *data) +{ + audioworklet_data_t *audioworklet = (audioworklet_data_t*)data; + + size_t buffer_size; + audioworklet->visible_buffer_size = (audioworklet->latency * audioworklet->rate * 2 * sizeof(float)) / 1000; + buffer_size = audioworklet->visible_buffer_size; +#ifdef EMSCRIPTEN_AUDIO_EXTERNAL_WRITE_BLOCK + audioworklet->write_avail_diff = (EXTERNAL_BLOCK_BUFFER_MS * audioworklet->rate * 2 * sizeof(float)) / 1000; + buffer_size += audioworklet->write_avail_diff; +#endif + audioworklet->buffer = fifo_new(buffer_size); + emscripten_atomic_store_u32(&audioworklet->write_avail_bytes, buffer_size); + RARCH_LOG("[AudioWorklet] Buffer size: %lu bytes.\n", audioworklet->visible_buffer_size); +} + +static void audioworklet_init_error(void *data) +{ + audioworklet_data_t *audioworklet = (audioworklet_data_t*)data; + + RARCH_ERR("[AudioWorklet] Failed to initialize driver!\n"); + free(audioworklet->worklet_stack); + free(audioworklet->tmpbuf); + free(audioworklet); +} + +static bool audioworklet_resume_ctx(void *data) +{ + audioworklet_data_t *audioworklet = (audioworklet_data_t*)data; + + if (!audioworklet->context_running) + { + MAIN_THREAD_ASYNC_EM_ASM({ + emscriptenGetAudioObject($0).resume(); + }, audioworklet->context); + } + return audioworklet->context_running; +} + +static void *audioworklet_init(const char *device, unsigned rate, + unsigned latency, unsigned block_frames, unsigned *new_rate) +{ + audioworklet_data_t *audioworklet; + if (audioworklet_static_data) + { + if (audioworklet_static_data->driver_running || audioworklet_static_data->initing) + { + RARCH_ERR("[AudioWorklet] Tried to start already running driver!\n"); + return NULL; + } + RARCH_LOG("[AudioWorklet] Reusing old context.\n"); + audioworklet = audioworklet_static_data; + audioworklet->latency = latency; + *new_rate = audioworklet->rate; + RARCH_LOG("[AudioWorklet] Device rate: %d Hz.\n", *new_rate); + audioworklet_alloc_buffer(audioworklet); + audioworklet_resume_ctx(audioworklet); + audioworklet->driver_running = true; + return audioworklet; + } + + audioworklet = (audioworklet_data_t*)calloc(1, sizeof(audioworklet_data_t)); + if (!audioworklet) + return NULL; + audioworklet->worklet_stack = memalign(16, WORKLET_STACK_SIZE); + if (!audioworklet->worklet_stack) + return NULL; + audioworklet_static_data = audioworklet; + + audioworklet->latency = latency; + platform_emscripten_run_on_browser_thread_sync(audioworklet_ctx_create, audioworklet); + *new_rate = audioworklet->rate; + RARCH_LOG("[AudioWorklet] Device rate: %d Hz.\n", *new_rate); + audioworklet->initing = true; + audioworklet_alloc_buffer(audioworklet); + emscripten_lock_init(&audioworklet->buffer_lock); +#ifdef PROXY_TO_PTHREAD + emscripten_lock_init(&audioworklet->trywrite_lock); + emscripten_condvar_init(&audioworklet->trywrite_cond); +#endif + +#ifndef EMSCRIPTEN_AUDIO_EXTERNAL_BLOCK + /* TODO: can MIN_ASYNCIFY block here too? */ + while (!audioworklet->init_done) + retro_sleep(1); + audioworklet->initing = false; + if (audioworklet->init_error) + { + audioworklet_init_error(audioworklet); + return NULL; + } + audioworklet->driver_running = true; +#elif defined(EMSCRIPTEN_AUDIO_FAKE_BLOCK) + audioworklet->block_requested = true; + platform_emscripten_enter_fake_block(1); +#endif + /* external block: will be handled later */ + + return audioworklet; +} + +static ssize_t audioworklet_write(void *data, const void *s, size_t ss) +{ + audioworklet_data_t *audioworklet = (audioworklet_data_t*)data; + const float *samples = (const float*)s; + size_t num_frames = ss / 2 / sizeof(float); + size_t written = 0; + size_t to_write_frames; + size_t to_write_bytes; + size_t avail; + size_t max_write; + + /* too early! might happen with external blocking */ + if (!audioworklet->driver_running) + return 0; + + /* don't write audio if the context isn't running, just try to start it */ + if (!audioworklet_resume_ctx(audioworklet)) + return 0; + + while (num_frames) + { +#ifdef PROXY_TO_PTHREAD + if (!emscripten_lock_wait_acquire(&audioworklet->buffer_lock, 2500000)) +#else + if (!emscripten_lock_busyspin_wait_acquire(&audioworklet->buffer_lock, 2.5)) +#endif + { + RARCH_WARN("[AudioWorklet] Main thread: could not acquire lock\n"); + break; + } + + avail = FIFO_WRITE_AVAIL(audioworklet->buffer); + max_write = avail; +#ifdef EMSCRIPTEN_AUDIO_EXTERNAL_WRITE_BLOCK + /* make sure we don't write into the blocking buffer for nonblock */ + if (audioworklet->nonblock) + { + if (max_write > audioworklet->write_avail_diff) + max_write -= audioworklet->write_avail_diff; + else + max_write = 0; + } +#endif + to_write_frames = MIN(num_frames, max_write / 2 / sizeof(float)); + if (to_write_frames) + { + to_write_bytes = to_write_frames * 2 * sizeof(float); + avail -= to_write_bytes; + fifo_write(audioworklet->buffer, samples, to_write_bytes); + emscripten_atomic_store_u32(&audioworklet->write_avail_bytes, (uint32_t)avail); + num_frames -= to_write_frames; + samples += (to_write_frames * 2); + written += to_write_frames; + } + + emscripten_lock_release(&audioworklet->buffer_lock); + +#ifdef EMSCRIPTEN_AUDIO_EXTERNAL_WRITE_BLOCK +#ifdef EMSCRIPTEN_AUDIO_FAKE_BLOCK + /* see if we're over the threshold to go to fake block */ + if (avail < audioworklet->write_avail_diff) + { + audioworklet->block_requested = true; + platform_emscripten_enter_fake_block(1); + } +#endif + if (num_frames && !audioworklet->nonblock) + RARCH_WARN("[AudioWorklet] Dropping %lu frames.\n", num_frames); + break; +#endif + if (audioworklet->nonblock || !num_frames) + break; +#if defined(PROXY_TO_PTHREAD) + emscripten_condvar_wait(&audioworklet->trywrite_cond, &audioworklet->trywrite_lock, 3000000); +#elif defined(EMSCRIPTEN_FULL_ASYNCIFY) + retro_sleep(1); +#else /* equivalent to defined(EMSCRIPTEN_AUDIO_BUSYWAIT) */ + while (emscripten_atomic_load_u32(&audioworklet->write_avail_bytes) < 2 * sizeof(float)) + audioworklet_resume_ctx(audioworklet); +#endif + /* try resuming, on the off chance that the context was interrupted while blocking */ + audioworklet_resume_ctx(audioworklet); + } + + return written; +} + +#ifdef EMSCRIPTEN_AUDIO_EXTERNAL_BLOCK +/* returns true if fake block should continue */ +bool audioworklet_external_block(void) +{ + audioworklet_data_t *audioworklet = audioworklet_static_data; + +#ifdef EMSCRIPTEN_AUDIO_FAKE_BLOCK + if (!audioworklet->block_requested) + return false; +#endif + + while (audioworklet->initing && !audioworklet->init_done) +#ifdef EMSCRIPTEN_AUDIO_ASYNC_BLOCK + retro_sleep(1); +#else + return true; +#endif + if (audioworklet->init_done && !audioworklet->driver_running) + { + audioworklet->initing = false; + if (audioworklet->init_error) + { + audioworklet_init_error(audioworklet); + abort(); + return false; + } + audioworklet->driver_running = true; + } +#ifdef EMSCRIPTEN_AUDIO_EXTERNAL_WRITE_BLOCK + if (!audioworklet->driver_running) + return false; + + while (emscripten_atomic_load_u32(&audioworklet->write_avail_bytes) < audioworklet->write_avail_diff) + { + audioworklet_resume_ctx(audioworklet); +#ifdef EMSCRIPTEN_AUDIO_ASYNC_BLOCK + retro_sleep(1); +#else + return true; +#endif + } +#endif + +#ifdef EMSCRIPTEN_AUDIO_FAKE_BLOCK + audioworklet->block_requested = false; + platform_emscripten_exit_fake_block(); + return true; /* return to RAF if needed */ +#endif + return false; +} +#endif + +static bool audioworklet_stop(void *data) +{ + audioworklet_data_t *audioworklet = (audioworklet_data_t*)data; + audioworklet->running = false; + return true; +} + +static bool audioworklet_start(void *data, bool is_shutdown) +{ + audioworklet_data_t *audioworklet = (audioworklet_data_t*)data; + audioworklet->running = true; + return true; +} + +static bool audioworklet_alive(void *data) +{ + audioworklet_data_t *audioworklet = (audioworklet_data_t*)data; + return audioworklet->running; +} + +static void audioworklet_set_nonblock_state(void *data, bool state) +{ + audioworklet_data_t *audioworklet = (audioworklet_data_t*)data; + audioworklet->nonblock = state; +} + +static void audioworklet_free(void *data) +{ + audioworklet_data_t *audioworklet = (audioworklet_data_t*)data; + + /* that's not good... this shouldn't happen? */ + if (!audioworklet->driver_running) + { + RARCH_ERR("[AudioWorklet] Tried to free before done initing!\n"); + return; + } + +#ifdef PROXY_TO_PTHREAD + if (!emscripten_lock_wait_acquire(&audioworklet->buffer_lock, 10000000)) +#else + if (!emscripten_lock_busyspin_wait_acquire(&audioworklet->buffer_lock, 10)) +#endif + { + RARCH_ERR("[AudioWorklet] Main thread: could not acquire lock to free buffer!\n"); + return; + } + audioworklet->driver_running = false; + fifo_free(audioworklet->buffer); + emscripten_lock_release(&audioworklet->buffer_lock); + MAIN_THREAD_ASYNC_EM_ASM({ + emscriptenGetAudioObject($0).suspend(); + }, audioworklet->context); +} + +static size_t audioworklet_write_avail(void *data) +{ + audioworklet_data_t *audioworklet = (audioworklet_data_t*)data; + +#ifdef EMSCRIPTEN_AUDIO_EXTERNAL_WRITE_BLOCK + size_t avail = emscripten_atomic_load_u32(&audioworklet->write_avail_bytes); + if (avail > audioworklet->write_avail_diff) + return avail - audioworklet->write_avail_diff; + return 0; +#else + return emscripten_atomic_load_u32(&audioworklet->write_avail_bytes); +#endif +} + +static size_t audioworklet_buffer_size(void *data) +{ + audioworklet_data_t *audioworklet = (audioworklet_data_t*)data; + return audioworklet->visible_buffer_size; +} + +static bool audioworklet_use_float(void *data) { return true; } + +audio_driver_t audio_audioworklet = { + audioworklet_init, + audioworklet_write, + audioworklet_stop, + audioworklet_start, + audioworklet_alive, + audioworklet_set_nonblock_state, + audioworklet_free, + audioworklet_use_float, + "audioworklet", + NULL, + NULL, + audioworklet_write_avail, + audioworklet_buffer_size +}; diff --git a/command.c b/command.c index 405f46d6b866..1d0110cb85d2 100644 --- a/command.c +++ b/command.c @@ -364,8 +364,7 @@ command_t* command_stdin_new(void) #endif #if defined(EMSCRIPTEN) -void PlatformEmscriptenCommandReply(const char *, size_t); -int PlatformEmscriptenCommandRead(char **, size_t); +#include "frontend/drivers/platform_emscripten.h" typedef struct { char command_buf[CMD_BUF_SIZE]; @@ -374,7 +373,7 @@ typedef struct static void emscripten_command_reply(command_t *_cmd, const char *s, size_t len) { - PlatformEmscriptenCommandReply(s, len); + platform_emscripten_command_reply(s, len); } static void emscripten_command_free(command_t *handle) @@ -386,7 +385,7 @@ static void emscripten_command_free(command_t *handle) static void command_emscripten_poll(command_t *handle) { command_emscripten_t *emscriptencmd = (command_emscripten_t*)handle->userptr; - ptrdiff_t msg_len = PlatformEmscriptenCommandRead((char **)(&emscriptencmd->command_buf), CMD_BUF_SIZE); + ptrdiff_t msg_len = platform_emscripten_command_read((char **)(&emscriptencmd->command_buf), CMD_BUF_SIZE); if (msg_len == 0) return; command_parse_msg(handle, emscriptencmd->command_buf); @@ -416,8 +415,6 @@ command_t* command_emscripten_new(void) } #endif - - bool command_get_config_param(command_t *cmd, const char* arg) { size_t _len; diff --git a/config.def.h b/config.def.h index 2e36cc09a339..5f9814a5325d 100644 --- a/config.def.h +++ b/config.def.h @@ -1174,7 +1174,7 @@ /* Desired audio latency in milliseconds. Might not be honored * if driver can't provide given latency. */ -#if defined(ANDROID) || defined(EMSCRIPTEN) || defined(RETROFW) || defined(MIYOO) +#if defined(ANDROID) || defined(RETROFW) || defined(MIYOO) || (defined(EMSCRIPTEN) && !defined(HAVE_AUDIOWORKLET)) /* For most Android devices, 64ms is way too low. */ #define DEFAULT_OUT_LATENCY 128 #define DEFAULT_IN_LATENCY 128 diff --git a/configuration.c b/configuration.c index 885c4f91357c..496099d66a62 100644 --- a/configuration.c +++ b/configuration.c @@ -145,6 +145,7 @@ enum audio_driver_enum AUDIO_WII, AUDIO_WIIU, AUDIO_RWEBAUDIO, + AUDIO_AUDIOWORKLET, AUDIO_PSP, AUDIO_PS2, AUDIO_CTR, @@ -549,7 +550,9 @@ static const enum audio_driver_enum AUDIO_DEFAULT_DRIVER = AUDIO_DSOUND; static const enum audio_driver_enum AUDIO_DEFAULT_DRIVER = AUDIO_AL; #elif defined(HAVE_SL) static const enum audio_driver_enum AUDIO_DEFAULT_DRIVER = AUDIO_SL; -#elif defined(EMSCRIPTEN) +#elif defined(HAVE_AUDIOWORKLET) +static const enum audio_driver_enum AUDIO_DEFAULT_DRIVER = AUDIO_AUDIOWORKLET; +#elif defined(HAVE_RWEBAUDIO) static const enum audio_driver_enum AUDIO_DEFAULT_DRIVER = AUDIO_RWEBAUDIO; #elif defined(HAVE_SDL) static const enum audio_driver_enum AUDIO_DEFAULT_DRIVER = AUDIO_SDL; @@ -974,6 +977,8 @@ const char *config_get_default_audio(void) #endif case AUDIO_RWEBAUDIO: return "rwebaudio"; + case AUDIO_AUDIOWORKLET: + return "audioworklet"; case AUDIO_JACK: return "jack"; case AUDIO_NULL: diff --git a/dist-scripts/dist-cores.sh b/dist-scripts/dist-cores.sh index cc4a13143ced..1c08c451d970 100755 --- a/dist-scripts/dist-cores.sh +++ b/dist-scripts/dist-cores.sh @@ -221,7 +221,7 @@ for f in `ls -v *_${platform}.${EXT}`; do heap_mem=134217728 if [ $name = "mupen64plus_next" ] ; then gles3=1 - #async=1 + async=1 #proxy_to_pthread=0 stack_mem=134217728 heap_mem=268435456 diff --git a/emscripten/library_platform_emscripten.js b/emscripten/library_platform_emscripten.js index fb86595c3c13..3d015a564999 100644 --- a/emscripten/library_platform_emscripten.js +++ b/emscripten/library_platform_emscripten.js @@ -3,7 +3,7 @@ var LibraryPlatformEmscripten = { $RPE: { powerStateChange: function(e) { - _update_power_state(true, Number.isFinite(e.target.dischargingTime) ? e.target.dischargingTime : 0x7FFFFFFF, e.target.level, e.target.charging); + _platform_emscripten_update_power_state(true, Number.isFinite(e.target.dischargingTime) ? e.target.dischargingTime : 0x7FFFFFFF, e.target.level, e.target.charging); }, updateMemoryUsage: function() { @@ -11,13 +11,14 @@ var LibraryPlatformEmscripten = { var used = BigInt(performance.memory.usedJSHeapSize || 0); var limit = BigInt(performance.memory.jsHeapSizeLimit || 0); // emscripten currently only supports passing 32 bit ints, so pack it - _update_memory_usage(Number(used & 0xFFFFFFFFn), Number(used >> 32n), Number(limit & 0xFFFFFFFFn), Number(limit >> 32n)); + _platform_emscripten_update_memory_usage(Number(used & 0xFFFFFFFFn), Number(used >> 32n), Number(limit & 0xFFFFFFFFn), Number(limit >> 32n)); setTimeout(RPE.updateMemoryUsage, 5000); }, command_queue: [], command_reply_queue: [] }, + PlatformEmscriptenWatchCanvasSizeAndDpr__deps: ["platform_emscripten_update_canvas_dimensions"], PlatformEmscriptenWatchCanvasSizeAndDpr: function(dpr) { if (RPE.observer) { RPE.observer.unobserve(Module.canvas); @@ -37,7 +38,7 @@ var LibraryPlatformEmscripten = { } // doubles are too big to pass as an argument to exported functions {{{ makeSetValue("dpr", "0", "window.devicePixelRatio", "double") }}}; - _update_canvas_dimensions(width, height, dpr); + _platform_emscripten_update_canvas_dimensions(width, height, dpr); }); RPE.observer.observe(Module.canvas); window.addEventListener("resize", function() { @@ -46,12 +47,14 @@ var LibraryPlatformEmscripten = { }, false); }, + PlatformEmscriptenWatchWindowVisibility__deps: ["platform_emscripten_update_window_hidden"], PlatformEmscriptenWatchWindowVisibility: function() { document.addEventListener("visibilitychange", function() { - _update_window_hidden(document.visibilityState == "hidden"); + _platform_emscripten_update_window_hidden(document.visibilityState == "hidden"); }, false); }, + PlatformEmscriptenPowerStateInit__deps: ["platform_emscripten_update_power_state"], PlatformEmscriptenPowerStateInit: function() { if (!navigator.getBattery) return; navigator.getBattery().then(function(battery) { @@ -61,15 +64,16 @@ var LibraryPlatformEmscripten = { }); }, + PlatformEmscriptenMemoryUsageInit__deps: ["platform_emscripten_update_memory_usage"], PlatformEmscriptenMemoryUsageInit: function() { if (!performance.memory) return; RPE.updateMemoryUsage(); }, - $EmscriptenSendCommand__deps: ["PlatformEmscriptenCommandRaiseFlag"], + $EmscriptenSendCommand__deps: ["platform_emscripten_command_raise_flag"], $EmscriptenSendCommand: function(str) { RPE.command_queue.push(str); - _PlatformEmscriptenCommandRaiseFlag(); + _platform_emscripten_command_raise_flag(); }, $EmscriptenReceiveCommandReply: function() { diff --git a/emscripten/pre.js b/emscripten/pre.js index ae4d12534ff1..863d45b54584 100644 --- a/emscripten/pre.js +++ b/emscripten/pre.js @@ -1,2 +1,6 @@ // To work around a bug in emscripten's polyfills for setImmediate in strict mode var setImmediate; + +// To work around a deadlock in firefox +// Use platform_emscripten_has_async_atomics() to determine actual availability +if (Atomics && !Atomics.waitAsync) Atomics.waitAsync = true; diff --git a/frontend/drivers/platform_emscripten.c b/frontend/drivers/platform_emscripten.c index 60c0a253a46c..fdefe8d74bd3 100644 --- a/frontend/drivers/platform_emscripten.c +++ b/frontend/drivers/platform_emscripten.c @@ -54,11 +54,12 @@ #include "../../cheat_manager.h" #include "../../audio/audio_driver.h" -#ifdef HAVE_WASMFS +#ifdef HAVE_EXTRA_WASMFS #include #endif #ifdef PROXY_TO_PTHREAD +#include #include #include #include @@ -69,6 +70,8 @@ #define PLATFORM_GETVAL(type, addr) *addr #endif +#include "platform_emscripten.h" + void emscripten_mainloop(void); void PlatformEmscriptenWatchCanvasSizeAndDpr(double *dpr); void PlatformEmscriptenWatchWindowVisibility(void); @@ -77,13 +80,20 @@ void PlatformEmscriptenMemoryUsageInit(void); typedef struct { +#ifdef PROXY_TO_PTHREAD + pthread_t program_thread_id; + emscripten_lock_t raf_lock; + emscripten_condvar_t raf_cond; +#endif uint64_t memory_used; uint64_t memory_limit; double device_pixel_ratio; + int raf_interval; int canvas_width; int canvas_height; int power_state_discharge_time; float power_state_level; + bool has_async_atomics; volatile bool power_state_charging; volatile bool power_state_supported; volatile bool window_hidden; @@ -222,7 +232,7 @@ void cmd_cheat_apply_cheats(void) /* javascript callbacks */ -void update_canvas_dimensions(int width, int height, double *dpr) +void platform_emscripten_update_canvas_dimensions(int width, int height, double *dpr) { printf("[INFO] Setting real canvas size: %d x %d\n", width, height); emscripten_set_canvas_element_size("#canvas", width, height); @@ -233,14 +243,14 @@ void update_canvas_dimensions(int width, int height, double *dpr) PLATFORM_SETVAL(f64, &emscripten_platform_data->device_pixel_ratio, *dpr); } -void update_window_hidden(bool hidden) +void platform_emscripten_update_window_hidden(bool hidden) { if (!emscripten_platform_data) return; emscripten_platform_data->window_hidden = hidden; } -void update_power_state(bool supported, int discharge_time, float level, bool charging) +void platform_emscripten_update_power_state(bool supported, int discharge_time, float level, bool charging) { if (!emscripten_platform_data) return; @@ -250,7 +260,7 @@ void update_power_state(bool supported, int discharge_time, float level, bool ch PLATFORM_SETVAL(f32, &emscripten_platform_data->power_state_level, level); } -void update_memory_usage(uint32_t used1, uint32_t used2, uint32_t limit1, uint32_t limit2) +void platform_emscripten_update_memory_usage(uint32_t used1, uint32_t used2, uint32_t limit1, uint32_t limit2) { if (!emscripten_platform_data) return; @@ -258,7 +268,7 @@ void update_memory_usage(uint32_t used1, uint32_t used2, uint32_t limit1, uint32 PLATFORM_SETVAL(u64, &emscripten_platform_data->memory_limit, limit1 | ((uint64_t)limit2 << 32)); } -void PlatformEmscriptenCommandRaiseFlag() +void platform_emscripten_command_raise_flag() { if (!emscripten_platform_data) return; @@ -266,8 +276,36 @@ void PlatformEmscriptenCommandRaiseFlag() } /* platform specific c helpers */ +/* see platform_emscripten.h for documentation. */ + +void platform_emscripten_run_on_browser_thread_sync(void (*func)(void*), void* arg) +{ +#ifdef PROXY_TO_PTHREAD + emscripten_proxy_sync(emscripten_proxy_get_system_queue(), emscripten_main_runtime_thread_id(), func, arg); +#else + func(arg); +#endif +} + +void platform_emscripten_run_on_browser_thread_async(void (*func)(void*), void* arg) +{ +#ifdef PROXY_TO_PTHREAD + emscripten_proxy_async(emscripten_proxy_get_system_queue(), emscripten_main_runtime_thread_id(), func, arg); +#else + emscripten_async_call(func, arg, 0); +#endif +} + +void platform_emscripten_run_on_program_thread_async(void (*func)(void*), void* arg) +{ +#ifdef PROXY_TO_PTHREAD + emscripten_proxy_async(emscripten_proxy_get_system_queue(), emscripten_platform_data->program_thread_id, func, arg); +#else + emscripten_async_call(func, arg, 0); +#endif +} -void PlatformEmscriptenCommandReply(const char *msg, size_t len) +void platform_emscripten_command_reply(const char *msg, size_t len) { MAIN_THREAD_EM_ASM({ var message = UTF8ToString($0, $1); @@ -275,7 +313,7 @@ void PlatformEmscriptenCommandReply(const char *msg, size_t len) }, msg, len); } -size_t PlatformEmscriptenCommandRead(char **into, size_t max_len) +size_t platform_emscripten_command_read(char **into, size_t max_len) { if (!emscripten_platform_data || !emscripten_platform_data->command_flag) return 0; @@ -316,27 +354,62 @@ double platform_emscripten_get_dpr(void) return PLATFORM_GETVAL(f64, &emscripten_platform_data->device_pixel_ratio); } +bool platform_emscripten_has_async_atomics(void) +{ + return emscripten_platform_data->has_async_atomics; +} + bool platform_emscripten_is_window_hidden(void) { return emscripten_platform_data->window_hidden; } -void platform_emscripten_run_on_browser_thread_sync(void (*func)(void*), void* arg) +bool platform_emscripten_should_drop_iter(void) { + return (emscripten_platform_data->window_hidden && emscripten_platform_data->raf_interval); +} + #ifdef PROXY_TO_PTHREAD - emscripten_proxy_sync(emscripten_proxy_get_system_queue(), emscripten_main_runtime_thread_id(), func, arg); + +static void set_raf_interval(void *data) +{ + emscripten_set_main_loop_timing(EM_TIMING_RAF, (int)data); +} + +void platform_emscripten_wait_for_frame(void) +{ + if (emscripten_platform_data->raf_interval) + emscripten_condvar_waitinf(&emscripten_platform_data->raf_cond, &emscripten_platform_data->raf_lock); +} + #else - func(arg); -#endif + +void platform_emscripten_enter_fake_block(int ms) +{ + if (ms == 0) + emscripten_set_main_loop_timing(EM_TIMING_SETIMMEDIATE, 0); + else + emscripten_set_main_loop_timing(EM_TIMING_SETTIMEOUT, ms); } -void platform_emscripten_run_on_browser_thread_async(void (*func)(void*), void* arg) +void platform_emscripten_exit_fake_block(void) { + command_event(CMD_EVENT_VIDEO_SET_BLOCKING_STATE, NULL); +} + +#endif + +void platform_emscripten_set_main_loop_interval(int interval) +{ + emscripten_platform_data->raf_interval = interval; #ifdef PROXY_TO_PTHREAD - emscripten_proxy_async(emscripten_proxy_get_system_queue(), emscripten_main_runtime_thread_id(), func, arg); + if (interval != 0) + platform_emscripten_run_on_browser_thread_sync(set_raf_interval, (void *)interval); #else - // for now, not async - func(arg); + if (interval == 0) + emscripten_set_main_loop_timing(EM_TIMING_SETIMMEDIATE, 0); + else + emscripten_set_main_loop_timing(EM_TIMING_RAF, interval); #endif } @@ -354,7 +427,7 @@ static void frontend_emscripten_get_env(int *argc, char *argv[], { size_t _len = strlcpy(base_path, home, sizeof(base_path)); strlcpy(base_path + _len, "/retroarch", sizeof(base_path) - _len); -#ifndef HAVE_WASMFS +#ifndef HAVE_EXTRA_WASMFS /* can be removed when the new web player replaces the old one */ _len = strlcpy(user_path, home, sizeof(user_path)); strlcpy(user_path + _len, "/retroarch/userdata", sizeof(user_path) - _len); @@ -370,7 +443,7 @@ static void frontend_emscripten_get_env(int *argc, char *argv[], else { strlcpy(base_path, "retroarch", sizeof(base_path)); -#ifndef HAVE_WASMFS +#ifndef HAVE_EXTRA_WASMFS /* can be removed when the new web player replaces the old one */ strlcpy(user_path, "retroarch/userdata", sizeof(user_path)); strlcpy(bundle_path, "retroarch/bundle", sizeof(bundle_path)); @@ -445,7 +518,7 @@ static void frontend_emscripten_get_env(int *argc, char *argv[], static enum frontend_powerstate frontend_emscripten_get_powerstate(int *seconds, int *percent) { enum frontend_powerstate ret = FRONTEND_POWERSTATE_NONE; - int level; + float level; if (!emscripten_platform_data || !emscripten_platform_data->power_state_supported) return ret; @@ -486,8 +559,8 @@ static uint64_t frontend_emscripten_get_free_mem(void) /* program entry and startup */ -#ifdef HAVE_WASMFS -void platform_emscripten_mount_filesystems(void) +#ifdef HAVE_EXTRA_WASMFS +static void platform_emscripten_mount_filesystems(void) { char *opfs_mount = getenv("OPFS_MOUNT"); char *fetch_manifest = getenv("FETCH_MANIFEST"); @@ -619,16 +692,20 @@ void platform_emscripten_mount_filesystems(void) free(line); } } -#endif /* HAVE_WASMFS */ +#endif /* HAVE_EXTRA_WASMFS */ static int thread_main(int argc, char *argv[]) { -#ifdef HAVE_WASMFS +#ifdef HAVE_EXTRA_WASMFS platform_emscripten_mount_filesystems(); #endif emscripten_set_main_loop(emscripten_mainloop, 0, 0); +#ifdef PROXY_TO_PTHREAD + emscripten_set_main_loop_timing(EM_TIMING_SETIMMEDIATE, 0); +#else emscripten_set_main_loop_timing(EM_TIMING_RAF, 1); +#endif rarch_main(argc, argv, NULL); return 0; @@ -642,9 +719,15 @@ static char** _main_argv; static void *main_pthread(void* arg) { emscripten_set_thread_name(pthread_self(), "Application main thread"); + emscripten_platform_data->program_thread_id = pthread_self(); thread_main(_main_argc, _main_argv); return NULL; } + +static void raf_signaler(void) +{ + emscripten_condvar_signal(&emscripten_platform_data->raf_cond, 1); +} #endif int main(int argc, char *argv[]) @@ -654,15 +737,28 @@ int main(int argc, char *argv[]) pthread_attr_t attr; pthread_t thread; #endif - // this never gets freed + /* this never gets freed */ emscripten_platform_data = (emscripten_platform_data_t *)calloc(1, sizeof(emscripten_platform_data_t)); + emscripten_platform_data->has_async_atomics = EM_ASM_INT({ + return Atomics?.waitAsync?.toString().includes("[native code]"); + }); + PlatformEmscriptenWatchCanvasSizeAndDpr(malloc(sizeof(double))); PlatformEmscriptenWatchWindowVisibility(); PlatformEmscriptenPowerStateInit(); PlatformEmscriptenMemoryUsageInit(); + emscripten_platform_data->raf_interval = 1; #ifdef PROXY_TO_PTHREAD + /* run requestAnimationFrame on the browser thread, as some browsers (chrome on linux) */ + /* seem to have issues running at full speed with requestAnimationFrame in workers. */ + /* instead, we run the RetroArch main loop with setImmediate and just wait on a signal if we need RAF */ + emscripten_lock_init(&emscripten_platform_data->raf_lock); + emscripten_condvar_init(&emscripten_platform_data->raf_cond); + emscripten_set_main_loop(raf_signaler, 0, 0); + emscripten_set_main_loop_timing(EM_TIMING_RAF, 1); + _main_argc = argc; _main_argv = argv; pthread_attr_init(&attr); diff --git a/frontend/drivers/platform_emscripten.h b/frontend/drivers/platform_emscripten.h new file mode 100644 index 000000000000..add2ad03c639 --- /dev/null +++ b/frontend/drivers/platform_emscripten.h @@ -0,0 +1,131 @@ +/* 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 . + */ + +#ifndef _PLATFORM_EMSCRIPTEN_H +#define _PLATFORM_EMSCRIPTEN_H + +#include + +/** + * Synchronously run a function on the browser thread. + * + * @param func Function to call + * @param arg Argument to pass to \c func + */ +void platform_emscripten_run_on_browser_thread_sync(void (*func)(void*), void* arg); + +/** + * Asynchronously run a function on the browser thread. Returns immediately. + * + * @param func Function to call + * @param arg Argument to pass to \c func + */ +void platform_emscripten_run_on_browser_thread_async(void (*func)(void*), void* arg); + +/** + * Asynchronously run a function on the program thread. Returns immediately. + * + * @param func Function to call + * @param arg Argument to pass to \c func + */ +void platform_emscripten_run_on_program_thread_async(void (*func)(void*), void* arg); + +/** + * Send command reply. + * + * @param msg Reply string + * @param len Length of \c msg + */ +void platform_emscripten_command_reply(const char *msg, size_t len); + +/** + * Read a command from the command queue. + * + * @param into Pointer to store command string + * @param max_len Length of \c *into + * @return Length of stored command. + * Returns 0 if there are no commands in queue. + */ +size_t platform_emscripten_command_read(char **into, size_t max_len); + +/** + * Get the real screen dimensions of the canvas on the page. + * + * @param width Pointer to store canvas width + * @param height Pointer to store canvas height + */ +void platform_emscripten_get_canvas_size(int *width, int *height); + +/** + * Get the ratio of CSS pixels to real screen pixels. Useful for input scaling. + * + * @return devicePixelRatio + */ +double platform_emscripten_get_dpr(void); + +/** + * Check if the browser supports Atomics.waitAsync. + * + * @return True if async atomics are available. + */ +bool platform_emscripten_has_async_atomics(void); + +/** + * Check if the window is hidden. + * + * @return True if the window is hidden. + */ +bool platform_emscripten_is_window_hidden(void); + +/** + * Whether the frame should be dropped. + * Currently returns true if the window is hidden and vsync is on. + * + * @return True if the frame should be dropped. + */ +bool platform_emscripten_should_drop_iter(void); + +/** + * Block until a vsync interval, if vsync is enabled. + * PROXY_TO_PTHREAD only. + */ +void platform_emscripten_wait_for_frame(void); + +/** + * Enter a loop that blocks the main loop function. + * !PROXY_TO_PTHREAD only. + * + * @param ms Milliseconds between iterations. + * Use 1 for balanced performance and CPU usage. + */ +void platform_emscripten_enter_fake_block(int ms); + +/** + * Exit the main loop blocking function. + * !PROXY_TO_PTHREAD only. + */ +void platform_emscripten_exit_fake_block(void); + +/** + * Set the vsync interval for the main loop. + * + * @param interval Vsync interval to set. + * Use 0 to disable vsync. + */ +void platform_emscripten_set_main_loop_interval(int interval); + +#endif diff --git a/gfx/drivers_context/emscriptenegl_ctx.c b/gfx/drivers_context/emscriptenegl_ctx.c index c6659d6f8357..2cfbfd066c4e 100644 --- a/gfx/drivers_context/emscriptenegl_ctx.c +++ b/gfx/drivers_context/emscriptenegl_ctx.c @@ -21,6 +21,7 @@ #include #include +#include "../../frontend/drivers/platform_emscripten.h" #ifdef HAVE_CONFIG_H #include "../../config.h" @@ -33,8 +34,6 @@ #include "../common/egl_common.h" #endif -void platform_emscripten_get_canvas_size(int *width, int *height); - typedef struct { #ifdef HAVE_EGL @@ -46,10 +45,7 @@ typedef struct static void gfx_ctx_emscripten_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); + platform_emscripten_set_main_loop_interval(interval); } static void gfx_ctx_emscripten_check_window(void *data, bool *quit, diff --git a/gfx/drivers_context/emscriptenwebgl_ctx.c b/gfx/drivers_context/emscriptenwebgl_ctx.c index 171abd804f5a..26e5304015d0 100644 --- a/gfx/drivers_context/emscriptenwebgl_ctx.c +++ b/gfx/drivers_context/emscriptenwebgl_ctx.c @@ -21,6 +21,7 @@ #include #include +#include "../../frontend/drivers/platform_emscripten.h" #ifdef HAVE_CONFIG_H #include "../../config.h" @@ -29,9 +30,6 @@ #include "../../retroarch.h" #include "../../verbosity.h" -void platform_emscripten_get_canvas_size(int *width, int *height); -double platform_emscripten_get_dpr(void); - typedef struct { EMSCRIPTEN_WEBGL_CONTEXT_HANDLE ctx; @@ -41,10 +39,7 @@ typedef struct 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); + platform_emscripten_set_main_loop_interval(interval); } static void gfx_ctx_emscripten_webgl_check_window(void *data, bool *quit, diff --git a/griffin/griffin.c b/griffin/griffin.c index b2cabed22698..82e74922f468 100644 --- a/griffin/griffin.c +++ b/griffin/griffin.c @@ -868,7 +868,7 @@ AUDIO #include "../audio/drivers/gx_audio.c" #elif defined(__wiiu__) #include "../audio/drivers/wiiu_audio.c" -#elif defined(EMSCRIPTEN) +#elif defined(HAVE_RWEBAUDIO) #include "../audio/drivers/rwebaudio.c" #elif defined(PSP) || defined(VITA) || defined(ORBIS) #include "../audio/drivers/psp_audio.c" diff --git a/input/drivers/rwebinput_input.c b/input/drivers/rwebinput_input.c index f0a2cb15fcde..55982a056f88 100644 --- a/input/drivers/rwebinput_input.c +++ b/input/drivers/rwebinput_input.c @@ -25,6 +25,7 @@ #include #include +#include "../../frontend/drivers/platform_emscripten.h" #include "../input_driver.h" #include "../input_types.h" @@ -45,8 +46,6 @@ #define MAX_TOUCH 32 -double platform_emscripten_get_dpr(void); - typedef struct rwebinput_key_to_code_map_entry { const char *key; @@ -732,14 +731,18 @@ static int16_t rwebinput_input_state( return 0; } +static void rwebinput_remove_event_listeners(void *data) +{ + /* *currently* not automatically proxied in the case of PROXY_TO_PTHREAD */ + emscripten_html5_remove_all_event_listeners(); +} + static void rwebinput_input_free(void *data) { rwebinput_input_t *rwebinput = (rwebinput_input_t*)data; - emscripten_html5_remove_all_event_listeners(); - + platform_emscripten_run_on_browser_thread_sync(rwebinput_remove_event_listeners, NULL); free(rwebinput->keyboard.events); - free(data); } diff --git a/input/drivers_joypad/rwebpad_joypad.c b/input/drivers_joypad/rwebpad_joypad.c index 72777334fdd6..09e75258cca4 100644 --- a/input/drivers_joypad/rwebpad_joypad.c +++ b/input/drivers_joypad/rwebpad_joypad.c @@ -19,6 +19,7 @@ #include #include #include +#include "../../frontend/drivers/platform_emscripten.h" #include "../input_driver.h" @@ -29,9 +30,14 @@ #define NUM_BUTTONS 64 #define NUM_AXES 64 +typedef struct +{ + struct EmscriptenGamepadEvent pads[DEFAULT_MAX_PADS]; + bool live_pads[DEFAULT_MAX_PADS]; +} rwebpad_joypad_data_t; + /* TODO/FIXME - static globals */ -static struct EmscriptenGamepadEvent _pads[DEFAULT_MAX_PADS]; -static bool _live_pads[DEFAULT_MAX_PADS] = {0}; +static rwebpad_joypad_data_t *rwebpad_joypad_data = NULL; static EM_BOOL rwebpad_gamepad_cb(int event_type, const EmscriptenGamepadEvent *gamepad_event, void *user_data) @@ -42,8 +48,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; + rwebpad_joypad_data->pads[gamepad_event->index] = *gamepad_event; + rwebpad_joypad_data->live_pads[gamepad_event->index] = true; input_autoconfigure_connect( gamepad_event->id, /* name */ NULL, /* display name */ @@ -53,7 +59,7 @@ static EM_BOOL rwebpad_gamepad_cb(int event_type, pid); /* pid */ break; case EMSCRIPTEN_EVENT_GAMEPADDISCONNECTED: - _live_pads[gamepad_event->index] = false; + rwebpad_joypad_data->live_pads[gamepad_event->index] = false; input_autoconfigure_disconnect(gamepad_event->index, rwebpad_joypad.ident); break; @@ -70,7 +76,14 @@ static void *rwebpad_joypad_init(void *data) if (r == EMSCRIPTEN_RESULT_NOT_SUPPORTED) return NULL; - /* callbacks needs to be registered for gamepads to connect */ + if (!rwebpad_joypad_data) + { + rwebpad_joypad_data = (rwebpad_joypad_data_t*)calloc(1, sizeof(rwebpad_joypad_data_t)); + if (!rwebpad_joypad_data) + return NULL; + } + + /* callbacks need to be registered for gamepads to connect */ r = emscripten_set_gamepadconnected_callback(NULL, false, rwebpad_gamepad_cb); @@ -82,32 +95,32 @@ static void *rwebpad_joypad_init(void *data) static const char *rwebpad_joypad_name(unsigned pad) { - if (pad >= DEFAULT_MAX_PADS || !_live_pads[pad]) { + if (pad >= DEFAULT_MAX_PADS || !rwebpad_joypad_data->live_pads[pad]) { return ""; } - return _pads[pad].id; + return rwebpad_joypad_data->pads[pad].id; } static int32_t rwebpad_joypad_button(unsigned port, uint16_t joykey) { EmscriptenGamepadEvent gamepad_state; - if (port >= DEFAULT_MAX_PADS || !_live_pads[port]) + if (port >= DEFAULT_MAX_PADS || !rwebpad_joypad_data->live_pads[port]) return 0; - gamepad_state = _pads[port]; + gamepad_state = rwebpad_joypad_data->pads[port]; if (joykey < gamepad_state.numButtons) return gamepad_state.digitalButton[joykey]; return 0; } -static void rwebpad_joypad_get_buttons(unsigned port_num, input_bits_t *state) +static void rwebpad_joypad_get_buttons(unsigned port, input_bits_t *state) { EmscriptenGamepadEvent gamepad_state; unsigned i; - if (port_num >= DEFAULT_MAX_PADS || !_live_pads[port_num]) { + if (port >= DEFAULT_MAX_PADS || !rwebpad_joypad_data->live_pads[port]) { BIT256_CLEAR_ALL_PTR(state); return; } - gamepad_state = _pads[port_num]; + gamepad_state = rwebpad_joypad_data->pads[port]; for (i = 0; i < gamepad_state.numButtons; i++) { if (gamepad_state.digitalButton[i]) @@ -138,10 +151,10 @@ static int16_t rwebpad_joypad_axis_state( static int16_t rwebpad_joypad_axis(unsigned port, uint32_t joyaxis) { - if (port >= DEFAULT_MAX_PADS || !_live_pads[port]) { + if (port >= DEFAULT_MAX_PADS || !rwebpad_joypad_data->live_pads[port]) { return 0; } - return rwebpad_joypad_axis_state(&_pads[port], port, joyaxis); + return rwebpad_joypad_axis_state(&rwebpad_joypad_data->pads[port], port, joyaxis); } static int16_t rwebpad_joypad_state( @@ -153,9 +166,9 @@ static int16_t rwebpad_joypad_state( EmscriptenGamepadEvent gamepad_state; int16_t ret = 0; uint16_t port_idx = joypad_info->joy_idx; - if (port_idx >= DEFAULT_MAX_PADS || !_live_pads[port]) + if (port_idx >= DEFAULT_MAX_PADS || !rwebpad_joypad_data->live_pads[port]) return 0; - gamepad_state = _pads[port]; + gamepad_state = rwebpad_joypad_data->pads[port]; for (i = 0; i < RARCH_FIRST_CUSTOM_BIND; i++) { /* Auto-binds are per joypad, not per user. */ @@ -179,19 +192,25 @@ static int16_t rwebpad_joypad_state( return ret; } -static void rwebpad_joypad_poll(void) +static void rwebpad_joypad_do_poll(void *data) { + int i; emscripten_sample_gamepad_data(); - for (int i = 0; i < DEFAULT_MAX_PADS; i++) { - if (_live_pads[i]) { - emscripten_get_gamepad_status(i, &_pads[i]); - } + for (i = 0; i < DEFAULT_MAX_PADS; i++) + { + if (rwebpad_joypad_data->live_pads[i]) + emscripten_get_gamepad_status(i, &rwebpad_joypad_data->pads[i]); } } +static void rwebpad_joypad_poll(void) +{ + platform_emscripten_run_on_browser_thread_sync(rwebpad_joypad_do_poll, NULL); +} + static bool rwebpad_joypad_query_pad(unsigned pad) { - return _live_pads[pad]; + return rwebpad_joypad_data->live_pads[pad]; } static void rwebpad_joypad_destroy(void) { } diff --git a/intl/msg_hash_lbl.h b/intl/msg_hash_lbl.h index 2f4086e86a11..1700fcdd0cb5 100644 --- a/intl/msg_hash_lbl.h +++ b/intl/msg_hash_lbl.h @@ -240,6 +240,10 @@ MSG_HASH( MENU_ENUM_LABEL_AUDIO_DRIVER_RWEBAUDIO, "rwebaudio" ) +MSG_HASH( + MENU_ENUM_LABEL_AUDIO_DRIVER_AUDIOWORKLET, + "audioworklet" + ) MSG_HASH( MENU_ENUM_LABEL_AUDIO_DRIVER_JACK, "jack" diff --git a/libretro-common/include/retro_timers.h b/libretro-common/include/retro_timers.h index e519ad617f9d..4882b483eac8 100644 --- a/libretro-common/include/retro_timers.h +++ b/libretro-common/include/retro_timers.h @@ -39,8 +39,9 @@ #include #elif defined(_3DS) #include <3ds.h> -#elif (defined(EMSCRIPTEN) && defined(EMSCRIPTEN_ASYNCIFY)) +#elif defined(EMSCRIPTEN) #include +#include #else #include #endif @@ -100,8 +101,18 @@ static int nanosleepDOS(const struct timespec *rqtp, struct timespec *rmtp) #define retro_sleep(msec) (usleep(1000 * (msec))) #elif defined(WIIU) #define retro_sleep(msec) (OSSleepTicks(ms_to_ticks((msec)))) -#elif defined(EMSCRIPTEN) && defined(EMSCRIPTEN_ASYNCIFY) +#elif defined(EMSCRIPTEN) && defined(EMSCRIPTEN_ASYNCIFY) && !defined(HAVE_THREADS) #define retro_sleep(msec) (emscripten_sleep(msec)) +#elif defined(EMSCRIPTEN) && defined(EMSCRIPTEN_ASYNCIFY) +static INLINE void retro_sleep(unsigned msec) +{ + if (emscripten_is_main_browser_thread()) + emscripten_sleep(msec); + else + emscripten_thread_sleep(msec); +} +#elif defined(EMSCRIPTEN) +#define retro_sleep(msec) (emscripten_thread_sleep(msec)) #else static INLINE void retro_sleep(unsigned msec) { diff --git a/msg_hash.h b/msg_hash.h index 642b9886f6c8..0aea04997f6a 100644 --- a/msg_hash.h +++ b/msg_hash.h @@ -2660,6 +2660,7 @@ enum msg_hash_enums MENU_ENUM_LABEL_AUDIO_DRIVER_CTR, MENU_ENUM_LABEL_AUDIO_DRIVER_SWITCH, MENU_ENUM_LABEL_AUDIO_DRIVER_RWEBAUDIO, + MENU_ENUM_LABEL_AUDIO_DRIVER_AUDIOWORKLET, MENU_ENUM_LABEL_AUDIO_DRIVER_JACK, MENU_ENUM_LABEL_AUDIO_DRIVER_NULL, @@ -2691,6 +2692,7 @@ enum msg_hash_enums MENU_ENUM_LABEL_HELP_AUDIO_DRIVER_CTR, MENU_ENUM_LABEL_HELP_AUDIO_DRIVER_SWITCH, MENU_ENUM_LABEL_HELP_AUDIO_DRIVER_RWEBAUDIO, + MENU_ENUM_LABEL_HELP_AUDIO_DRIVER_AUDIOWORKLET, MENU_ENUM_LABEL_HELP_AUDIO_DRIVER_JACK, MENU_ENUM_LABEL_HELP_AUDIO_DRIVER_NULL, diff --git a/pkg/emscripten/libretro-thread/libretro.js b/pkg/emscripten/libretro-thread/libretro.js index e1ec07ebb1b4..324c4cc44a8e 100644 --- a/pkg/emscripten/libretro-thread/libretro.js +++ b/pkg/emscripten/libretro-thread/libretro.js @@ -208,14 +208,18 @@ function uploadFiles(accept) { input.type = "file"; input.setAttribute("multiple", ""); if (accept) input.accept = accept; - input.onchange = async function() { + input.style.setProperty("display", "none", "important"); + document.body.appendChild(input); + input.addEventListener("change", async function() { let files = []; for (const file of this.files) { files.push({path: file.name, data: await readFile(file)}); } + document.body.removeChild(input); resolve(files); - } + }); input.oncancel = function() { + document.body.removeChild(input); resolve([]); } input.click(); @@ -351,12 +355,20 @@ async function appInitialized() { }); } -function loadCore(core) { +async function downloadScript(src) { + let resp = await fetch(src); + let blob = await resp.blob(); + return blob; +} + +async function loadCore(core) { // Make the core the selected core in the UI. const coreTitle = document.querySelector('#core-selector a[data-core="' + core + '"]')?.textContent; if (coreTitle) coreSelectorCurrent.textContent = coreTitle; const fileExt = (core == "retroarch") ? ".js" : "_libretro.js"; - import("./" + core + fileExt).then(script => { + const url = URL.createObjectURL(await downloadScript("./" + core + fileExt)); + Module.mainScriptUrlOrBlob = url; + import(url).then(script => { script.default(Module).then(mod => { Module = mod; }).catch(err => { console.error("Couldn't instantiate module", err); throw err; }); diff --git a/pkg/emscripten/libretro-thread/libretro.worker.js b/pkg/emscripten/libretro-thread/libretro.worker.js index 9ba3088b4ff9..881ed8f7059c 100644 --- a/pkg/emscripten/libretro-thread/libretro.worker.js +++ b/pkg/emscripten/libretro-thread/libretro.worker.js @@ -140,7 +140,11 @@ FS.rm = async function() { const child = path.substr(dir_end + 1); const parent_dir = await getDirHandle(parent); if (!parent_dir) continue; - await parent_dir.removeEntry(child, {recursive: true}); + try { + await parent_dir.removeEntry(child, {recursive: true}); + } catch (e) { + continue; + } } } @@ -158,16 +162,17 @@ FS.stat = async function(path) { /* data migration */ -function idbExists(name) { +function idbExists(dbName, objStoreName) { return new Promise((resolve, reject) => { - let request = indexedDB.open(name); + let request = indexedDB.open(dbName); request.onupgradeneeded = function(e) { e.target.transaction.abort(); resolve(false); } request.onsuccess = function(e) { + let exists = objStoreName ? Array.from(e.target.result.objectStoreNames).includes(objStoreName) : true; e.target.result.close(); - resolve(true); + resolve(exists); } request.onerror = function(e) { reject(e); @@ -252,7 +257,7 @@ async function indexMigrateTree(dir, maxDepth) { // look for and migrate any data to the OPFS from the old BrowserFS in IndexedDB async function tryMigrateFromIdbfs() { - if (await FS.stat("/retroarch/.migration-finished") == "file" || !(await idbExists("RetroArch"))) return; + if (await FS.stat("/retroarch/.migration-finished") == "file" || !(await idbExists("RetroArch", "RetroArch"))) return; debugLog("Migrating data from BrowserFS IndexedDB"); await initBfsIdbfs("RetroArch"); const files = await indexMigrateTree("/", 5); @@ -301,8 +306,7 @@ async function tryLoadBundle() { if (await FS.stat(timestampFile) == "file") timestamp = new TextDecoder().decode(await FS.readFile(timestampFile)); - // debuggers beware: the network tab's "Disable cache" option disables If-Modified-Since too - let resp = await fetch(bundlePath[0], {headers: {"If-Modified-Since": timestamp}}); + let resp = await fetch(bundlePath[0], {headers: {"If-Modified-Since": timestamp, "Cache-Control": "public, max-age=0"}}); if (resp.status == 200) { debugLog("Got new bundle"); timestamp = resp.headers.get("last-modified"); diff --git a/retroarch.c b/retroarch.c index 40c2b6703ed4..1577940750c1 100644 --- a/retroarch.c +++ b/retroarch.c @@ -86,6 +86,7 @@ #ifdef EMSCRIPTEN #include +#include "frontend/drivers/platform_emscripten.h" #include "gfx/common/gl_common.h" #endif @@ -5997,8 +5998,8 @@ int rarch_main(int argc, char *argv[], void *data) #if defined(EMSCRIPTEN) -#ifdef PROXY_TO_PTHREAD -bool platform_emscripten_is_window_hidden(void); +#if defined(EMSCRIPTEN_AUDIO_EXTERNAL_BLOCK) && defined(HAVE_AUDIOWORKLET) +bool audioworklet_external_block(void); #endif #ifdef HAVE_RWEBAUDIO void RWebAudioRecalibrateTime(void); @@ -6018,10 +6019,12 @@ void emscripten_mainloop(void) bool runloop_is_slowmotion = (runloop_flags & RUNLOOP_FLAG_SLOWMOTION) ? true : false; bool runloop_is_paused = (runloop_flags & RUNLOOP_FLAG_PAUSED) ? true : false; -#ifdef PROXY_TO_PTHREAD - // ensure the same behavior when requestAnimationFrame is emulated (i.e. pause when window is hidden) - // todo: is this an emscripten bug? - if (!input_driver_nonblock_state && platform_emscripten_is_window_hidden()) + /* firefox especially seems to bug without this */ + if (platform_emscripten_should_drop_iter()) + return; + +#if defined(EMSCRIPTEN_AUDIO_FAKE_BLOCK) && defined(HAVE_AUDIOWORKLET) + if (audioworklet_external_block()) return; #endif @@ -6051,8 +6054,18 @@ void emscripten_mainloop(void) ret = runloop_iterate(); +#if defined(EMSCRIPTEN_AUDIO_ASYNC_BLOCK) && defined(HAVE_AUDIOWORKLET) + audioworklet_external_block(); +#endif + task_queue_check(); +#ifdef PROXY_TO_PTHREAD + /* sync with requestAnimationFrame on browser thread if vsync is on */ + /* performance seems better with this wait at the end of the iter */ + platform_emscripten_wait_for_frame(); +#endif + if (ret != -1) return;