From 9da56ebaba2f42bc4e2b69c8a1ed397e4527d5cf Mon Sep 17 00:00:00 2001 From: lizzie Date: Wed, 10 Jun 2026 00:03:43 +0000 Subject: [PATCH] emscripten friendly sdl loop --- docs/Caveats.md | 2 + src/yuzu_cmd/CMakeLists.txt | 5 +- src/yuzu_cmd/emu_window/emu_window_sdl3.cpp | 24 +-- src/yuzu_cmd/emu_window/emu_window_sdl3.h | 3 +- src/yuzu_cmd/yuzu.cpp | 168 ++++++++++---------- tools/miniserver.js | 99 +++++++----- 6 files changed, 155 insertions(+), 146 deletions(-) mode change 100644 => 100755 tools/miniserver.js diff --git a/docs/Caveats.md b/docs/Caveats.md index f82fbbfe74..ab8ea4d0f3 100644 --- a/docs/Caveats.md +++ b/docs/Caveats.md @@ -258,6 +258,8 @@ If running under Firefox and you hit "out of memory" on dev console, close the e To run the binary (after building) you should be fine with `node ./eden-cli.js`. For obvious reasons no Qt frontend is available on WASM, support for Vulkan is done charily via [llvmpipe2wasm](https://github.com/Devsh-Graphics-Programming/llvmpipe2wasm). +If you run into the error "acorn.js can't be found" check [this associated issue](https://github.com/emscripten-core/emscripten/issues/13368), the fix in short is `npm --global install acorn`. On FreeBSD you could run `npm` under root, or you could do the sane thing and do `sudo chown -R $USER /usr/local/lib/node_modules/ /usr/local/bin/` (remember to restore permissions afterwards!) unless you wish to run `npm` under root which is generally a bad idea. + 2026-06-09: As of writing, no Dynarmic-based JIT is possible on this target, full interpreted emulation is the only reasonable option. While there is some efforts on making a JIT like [here](https://github.com/wingo/wasm-jit) or [here](https://wingolog.org/archives/2022/08/18/just-in-time-code-generation-within-webassembly), the result is so latency expensive we're better off using an interpreter instead. ## Windows diff --git a/src/yuzu_cmd/CMakeLists.txt b/src/yuzu_cmd/CMakeLists.txt index 8db965b80d..616b865e73 100644 --- a/src/yuzu_cmd/CMakeLists.txt +++ b/src/yuzu_cmd/CMakeLists.txt @@ -77,12 +77,13 @@ if (NOT MSVC) endif() if (PLATFORM_EMSCRIPTEN) - # 10GB is required... yikes! + # 10GB is required at max... yikes! target_link_options(yuzu-cmd PRIVATE -sALLOW_MEMORY_GROWTH=1 + -sINITIAL_MEMORY=33554432 -sMAXIMUM_MEMORY=10737418240 -sGLOBAL_BASE=16777216 - -sEXPORTED_RUNTIME_METHODS="['FS']" + -sEXPORTED_RUNTIME_METHODS=['FS'] -sPTHREAD_POOL_SIZE_STRICT=0 -sPTHREAD_POOL_SIZE=navigator.hardwareConcurrency) endif() diff --git a/src/yuzu_cmd/emu_window/emu_window_sdl3.cpp b/src/yuzu_cmd/emu_window/emu_window_sdl3.cpp index ee34b62e90..789d93b564 100644 --- a/src/yuzu_cmd/emu_window/emu_window_sdl3.cpp +++ b/src/yuzu_cmd/emu_window/emu_window_sdl3.cpp @@ -32,9 +32,6 @@ EmuWindow_SDL3::EmuWindow_SDL3(InputCommon::InputSubsystem* input_subsystem_, Co } EmuWindow_SDL3::~EmuWindow_SDL3() { -#ifdef __EMSCRIPTEN__ - emscripten_cancel_main_loop(); -#endif system.HIDCore().UnloadInputDevices(); input_subsystem->Shutdown(); SDL_Quit(); @@ -169,23 +166,8 @@ void EmuWindow_SDL3::Fullscreen() { } } -void EmuWindow_SDL3::WaitEvent() { +void EmuWindow_SDL3::OnEvent(SDL_Event& event) { // Called on main thread - SDL_Event event; - - if (!SDL_WaitEvent(&event)) { - const char* error = SDL_GetError(); - if (!error || strcmp(error, "") == 0) { - // https://github.com/libsdl-org/SDL/issues/5780 - // Sometimes SDL will return without actually having hit an error condition; - // just ignore it in this case. - return; - } - - LOG_CRITICAL(Frontend, "SDL_WaitEvent failed: {}", error); - exit(1); - } - switch (event.type) { case SDL_EVENT_WINDOW_RESIZED: case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED: @@ -255,6 +237,9 @@ void EmuWindow_SDL3::WaitEvent() { // Credits to Samantas5855 and others for this function. void EmuWindow_SDL3::SetWindowIcon() { +#if defined(__EMSCRIPTEN__) || defined(__wasi__) + // Icons do not work yet +#else SDL_IOStream* const yuzu_icon_stream = SDL_IOFromConstMem((void*)yuzu_icon, yuzu_icon_size); if (yuzu_icon_stream == nullptr) { LOG_WARNING(Frontend, "Failed to create Eden icon stream."); @@ -268,6 +253,7 @@ void EmuWindow_SDL3::SetWindowIcon() { // The icon is attached to the window pointer SDL_SetWindowIcon(render_window, window_icon); SDL_DestroySurface(window_icon); +#endif } void EmuWindow_SDL3::OnMinimalClientAreaChangeRequest(std::pair minimal_size) { diff --git a/src/yuzu_cmd/emu_window/emu_window_sdl3.h b/src/yuzu_cmd/emu_window/emu_window_sdl3.h index 8e809a3f02..25a5e17193 100644 --- a/src/yuzu_cmd/emu_window/emu_window_sdl3.h +++ b/src/yuzu_cmd/emu_window/emu_window_sdl3.h @@ -13,6 +13,7 @@ #include "core/frontend/graphics_context.h" struct SDL_Window; +union SDL_Event; namespace Core { class System; @@ -35,7 +36,7 @@ public: bool IsShown() const override; /// Wait for the next event on the main thread. - void WaitEvent(); + void OnEvent(SDL_Event& event); // Sets the window icon from yuzu.bmp void SetWindowIcon(); diff --git a/src/yuzu_cmd/yuzu.cpp b/src/yuzu_cmd/yuzu.cpp index dbe8872b7a..aa94de0a80 100644 --- a/src/yuzu_cmd/yuzu.cpp +++ b/src/yuzu_cmd/yuzu.cpp @@ -11,6 +11,8 @@ #ifdef __EMSCRIPTEN__ #include #endif +#define SDL_MAIN_USE_CALLBACKS 1 +#include #include @@ -43,9 +45,7 @@ #ifdef _WIN32 // windows.h needs to be included before shellapi.h #include - #include - #include "common/windows/timer_resolution.h" #endif @@ -178,8 +178,15 @@ static void OnStatusMessageReceived(const Network::StatusMessageEntry& msg) { std::cout << std::endl << "* " << message << std::endl << std::endl; } -/// Application entry point -int main(int argc, char** argv) { +struct SdlState { + Common::DetachedTasks detached_tasks{}; + Core::System system{}; + std::unique_ptr emu_window; +}; + +extern "C" SDL_AppResult SDL_AppInit(void **appstate, int argc, char **argv) { + SdlState* state = new SdlState(); + #ifdef _WIN32 if (AttachConsole(ATTACH_PARENT_PROCESS)) { freopen("CONOUT$", "wb", stdout); @@ -190,16 +197,14 @@ int main(int argc, char** argv) { Common::Log::Initialize(); Common::Log::SetColorConsoleBackendEnabled(true); Common::Log::Start(); - Common::DetachedTasks detached_tasks; int option_index = 0; #ifdef _WIN32 int argc_w; auto argv_w = CommandLineToArgvW(GetCommandLineW(), &argc_w); - if (argv_w == nullptr) { LOG_CRITICAL(Frontend, "Failed to get command line arguments"); - return -1; + return SDL_APP_FAILURE; } #endif std::string filepath; @@ -247,7 +252,7 @@ int main(int argc, char** argv) { break; case 'h': PrintHelp(argv[0]); - return 0; + return SDL_APP_FAILURE; case 'g': filepath = std::string(optarg); break; @@ -264,7 +269,7 @@ int main(int argc, char** argv) { if (!std::regex_match(str_arg, re)) { std::cout << "Wrong format for option --multiplayer\n"; PrintHelp(argv[0]); - return 0; + return SDL_APP_FAILURE; } std::smatch match; @@ -274,17 +279,16 @@ int main(int argc, char** argv) { password = match[2]; address = match[3]; if (!match[4].str().empty()) { - port = static_cast(std::strtoul(match[4].str().c_str(), nullptr, 0)); + port = u16(std::strtoul(match[4].str().c_str(), nullptr, 0)); } std::regex nickname_re("^[a-zA-Z0-9._\\- ]+$"); if (!std::regex_match(nickname, nickname_re)) { - std::cout - << "Nickname is not valid. Must be 4 to 20 alphanumeric characters.\n"; - return 0; + LOG_ERROR(Frontend, "Nickname is not valid. Must be 4 to 20 alphanumeric characters"); + return SDL_APP_FAILURE; } if (address.empty()) { - std::cout << "Address to room must not be empty.\n"; - return 0; + LOG_ERROR(Frontend, "Address to room must not be empty"); + return SDL_APP_FAILURE; } break; } @@ -297,7 +301,7 @@ int main(int argc, char** argv) { break; case 'v': PrintVersion(); - return 0; + return SDL_APP_FAILURE; } } else { #ifdef _WIN32 @@ -341,79 +345,73 @@ int main(int argc, char** argv) { if (filepath.empty()) { LOG_CRITICAL(Frontend, "Failed to load ROM: No ROM specified"); - return -1; + return SDL_APP_FAILURE; } - Core::System system{}; - system.Initialize(); + state->system.Initialize(); InputCommon::InputSubsystem input_subsystem{}; // Apply the command line arguments - system.ApplySettings(); + state->system.ApplySettings(); - std::unique_ptr emu_window; switch (Settings::values.renderer_backend.GetValue()) { #ifdef HAS_OPENGL case Settings::RendererBackend::OpenGL_GLSL: case Settings::RendererBackend::OpenGL_GLASM: case Settings::RendererBackend::OpenGL_SPIRV: - emu_window = std::make_unique(&input_subsystem, system, fullscreen); + state->emu_window = std::make_unique(&input_subsystem, state->system, fullscreen); break; #endif case Settings::RendererBackend::Vulkan: - emu_window = std::make_unique(&input_subsystem, system, fullscreen); + state->emu_window = std::make_unique(&input_subsystem, state->system, fullscreen); break; case Settings::RendererBackend::Null: - emu_window = std::make_unique(&input_subsystem, system, fullscreen); + state->emu_window = std::make_unique(&input_subsystem, state->system, fullscreen); break; default: LOG_CRITICAL(Frontend, "Invalid renderer backend"); - return -1; + return SDL_APP_FAILURE; } #ifdef _WIN32 Common::Windows::SetCurrentTimerResolutionToMaximum(); - system.CoreTiming().SetTimerResolutionNs(Common::Windows::GetCurrentTimerResolution()); + state->system.CoreTiming().SetTimerResolutionNs(Common::Windows::GetCurrentTimerResolution()); #endif - system.SetContentProvider(std::make_unique()); - system.SetFilesystem(std::make_shared()); - system.GetFileSystemController().CreateFactories(*system.GetFilesystem()); - system.GetUserChannel().clear(); + state->system.SetContentProvider(std::make_unique()); + state->system.SetFilesystem(std::make_shared()); + state->system.GetFileSystemController().CreateFactories(*state->system.GetFilesystem()); + state->system.GetUserChannel().clear(); Service::AM::FrontendAppletParameters load_parameters{ .applet_id = Service::AM::AppletId::Application, }; - const Core::SystemResultStatus load_result{system.Load(*emu_window, filepath, load_parameters)}; - + const Core::SystemResultStatus load_result = state->system.Load(*state->emu_window, filepath, load_parameters); switch (load_result) { - case Core::SystemResultStatus::ErrorGetLoader: - LOG_CRITICAL(Frontend, "Failed to obtain loader for {}!", filepath); - return -1; - case Core::SystemResultStatus::ErrorLoader: - LOG_CRITICAL(Frontend, "Failed to load ROM!"); - return -1; - case Core::SystemResultStatus::ErrorNotInitialized: - LOG_CRITICAL(Frontend, "CPUCore not initialized"); - return -1; - case Core::SystemResultStatus::ErrorVideoCore: - LOG_CRITICAL(Frontend, "Failed to initialize VideoCore!"); - return -1; case Core::SystemResultStatus::Success: break; // Expected case + case Core::SystemResultStatus::ErrorGetLoader: + LOG_CRITICAL(Frontend, "Failed to obtain loader for {}!", filepath); + return SDL_APP_FAILURE; + case Core::SystemResultStatus::ErrorLoader: + LOG_CRITICAL(Frontend, "Failed to load ROM!"); + return SDL_APP_FAILURE; + case Core::SystemResultStatus::ErrorNotInitialized: + LOG_CRITICAL(Frontend, "CPUCore not initialized"); + return SDL_APP_FAILURE; + case Core::SystemResultStatus::ErrorVideoCore: + LOG_CRITICAL(Frontend, "Failed to initialize VideoCore!"); + return SDL_APP_FAILURE; default: - if (static_cast(load_result) > - static_cast(Core::SystemResultStatus::ErrorLoader)) { - const u16 loader_id = static_cast(Core::SystemResultStatus::ErrorLoader); - const u16 error_id = static_cast(load_result) - loader_id; - LOG_CRITICAL(Frontend, - "While attempting to load the ROM requested, an error occurred. Please " - "refer to the Eden wiki for more information or the Eden discord for " - "additional help.\n\nError Code: {:04X}-{:04X}\nError Description: {}", - loader_id, error_id, static_cast(error_id)); - } - break; + const u16 loader_id = u16(Core::SystemResultStatus::ErrorLoader); + const u16 error_id = u16(load_result) - loader_id; + LOG_CRITICAL(Frontend, + "While attempting to load the ROM requested, an error occurred. Please " + "refer to the Eden wiki for more information or the Eden discord for " + "additional help.\n\nError Code: {:04X}-{:04X}\nError Description: {}", + loader_id, error_id, Loader::ResultStatus(error_id)); + return SDL_APP_FAILURE; } if (use_multiplayer) { @@ -422,49 +420,47 @@ int main(int argc, char** argv) { member->BindOnStatusMessageReceived(OnStatusMessageReceived); member->BindOnStateChanged(OnStateChanged); member->BindOnError(OnNetworkError); - LOG_DEBUG(Network, "Start connection to {}:{} with nickname {}", address, port, - nickname); + LOG_DEBUG(Network, "Start connection to {}:{} with nickname {}", address, port, nickname); member->Join(nickname, address.c_str(), port, 0, Network::NoPreferredIP, password); } else { LOG_ERROR(Network, "Could not access RoomMember"); - return 0; + return SDL_APP_FAILURE; } } // Core is loaded, start the GPU (makes the GPU contexts current to this thread) - system.GPU().Start(); - system.GetCpuManager().OnGpuReady(); + state->system.GPU().Start(); + state->system.GetCpuManager().OnGpuReady(); if (Settings::values.use_disk_shader_cache.GetValue()) { - system.Renderer().ReadRasterizer()->LoadDiskResources( - system.GetApplicationProcessProgramID(), std::stop_token{}, + state->system.Renderer().ReadRasterizer()->LoadDiskResources( + state->system.GetApplicationProcessProgramID(), std::stop_token{}, [](VideoCore::LoadCallbackStage, size_t value, size_t total) {}); } - system.RegisterExitCallback([&] { - // Just exit right away. - exit(0); - }); - void(system.Run()); - if (system.DebuggerEnabled()) { - system.InitializeDebugger(); - } - -#ifdef __EMSCRIPTEN__ - // Required so lambda fits snuggly into our "main loop" - static EmuWindow_SDL3* static_ems_emu_window = emu_window.get(); - emscripten_set_main_loop([]() { - static_ems_emu_window->WaitEvent(); - }, 0, 1); -#else - while (emu_window->IsOpen()) - emu_window->WaitEvent(); -#endif - system.DetachDebugger(); - void(system.Pause()); - system.ShutdownMainProcess(); - detached_tasks.WaitForAllTasks(); - return 0; + // don't do anything, SDL3 already exists for us :D + state->system.RegisterExitCallback([] {}); + void(state->system.Run()); + if (state->system.DebuggerEnabled()) + state->system.InitializeDebugger(); + return SDL_APP_SUCCESS; +} +extern "C" SDL_AppResult SDL_AppIterate(void *appstate) { + SdlState *state = (SdlState *)appstate; + return state->emu_window->IsOpen() ? SDL_APP_CONTINUE : SDL_APP_SUCCESS; +} +extern "C" SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event) { + SdlState *state = (SdlState *)appstate; + state->emu_window->OnEvent(*event); + return SDL_APP_SUCCESS; +} +extern "C" void SDL_AppQuit(void *appstate, SDL_AppResult result) { + SdlState *state = (SdlState *)appstate; + state->system.DetachDebugger(); + void(state->system.Pause()); + state->system.ShutdownMainProcess(); + state->detached_tasks.WaitForAllTasks(); + delete state; } #define VMA_IMPLEMENTATION diff --git a/tools/miniserver.js b/tools/miniserver.js old mode 100644 new mode 100755 index 6f4bd35a14..f16d463573 --- a/tools/miniserver.js +++ b/tools/miniserver.js @@ -1,4 +1,4 @@ -#!/usr/local/env node +#!/usr/bin/env node // SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later @@ -21,51 +21,61 @@ const server = createServer((req, res) => { eden-cli + + - - `); } else if (req.url === '/eden-cli.js') { @@ -86,12 +96,25 @@ function finished_body() { }); res.end(content, 'utf-8'); }); + } else if (req.url === '/game.nro') { + readFile(nro_file, (err, content) => { + res.writeHead(200, { + 'Content-Type': 'application/wasm', + 'Cross-Origin-Opener-Policy': 'same-origin', + 'Cross-Origin-Embedder-Policy': 'require-corp' + }); + res.end(content, 'utf-8'); + }); + } else { + res.writeHead(404, {}); + res.end('', 'utf-8'); } }); const build_dir = process.argv[2]; -if (build_dir === undefined) { - console.log(`Usage: ${process.argv[0]} ${process.argv[1]} [build directory]`); +const nro_file = process.argv[3]; +if (typeof build_dir == "undefined" || typeof nro_file == "undefined") { + console.log(`Usage: ${process.argv[0]} ${process.argv[1]} [build directory] [NRO file]`); } else { server.listen(2210, () => { console.log(`${process.argv[0]} ${process.argv[1]} http://localhost:2210`);