From c423fdfcd781d14569e6285a76f6550dda3923ab Mon Sep 17 00:00:00 2001 From: lizzie Date: Thu, 18 Jun 2026 18:17:21 +0000 Subject: [PATCH] [eden-cli] rework loop to use new SDL3 event dispatch system Signed-off-by: lizzie --- docs/user/CommandLine.md | 3 + src/yuzu_cmd/emu_window/emu_window_sdl3.cpp | 90 ++++------ src/yuzu_cmd/emu_window/emu_window_sdl3.h | 9 +- src/yuzu_cmd/yuzu.cpp | 178 ++++++++++++-------- 4 files changed, 147 insertions(+), 133 deletions(-) diff --git a/docs/user/CommandLine.md b/docs/user/CommandLine.md index 6e1f0f237c..51542115e3 100644 --- a/docs/user/CommandLine.md +++ b/docs/user/CommandLine.md @@ -27,3 +27,6 @@ There are two main applications, an SDL-based app (`eden-cli`) and a Qt based ap - `--user/-u`: Specify the user index. - `--version/-v`: Display version and quit. - `--input-profile/-i`: Specifies input profile name to use (for player #0 only). +- `--null-render/-n`: Forces the usage of the "Null" render backend irrespective of settings. +- `--filter/-x`: Sets the debug log filter irrespective of settings. +- `--singlecore/-s`: Forces single-core regardless of settings. diff --git a/src/yuzu_cmd/emu_window/emu_window_sdl3.cpp b/src/yuzu_cmd/emu_window/emu_window_sdl3.cpp index cd390d29f9..72101fcb47 100644 --- a/src/yuzu_cmd/emu_window/emu_window_sdl3.cpp +++ b/src/yuzu_cmd/emu_window/emu_window_sdl3.cpp @@ -5,6 +5,9 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include +#ifdef __EMSCRIPTEN__ +#include +#endif #include "common/logging.h" #include "common/scm_rev.h" @@ -26,9 +29,16 @@ EmuWindow_SDL3::EmuWindow_SDL3(InputCommon::InputSubsystem* input_subsystem_, Co LOG_CRITICAL(Frontend, "Failed to initialize SDL3: {}, Exiting...", SDL_GetError()); exit(1); } + titlebar_timer = SDL_AddTimer(2000, [](void *userdata, SDL_TimerID, Uint32) { + auto* this_ = (EmuWindow_SDL3*)userdata; + auto const results = this_->system.GetAndResetPerfStats(); + auto const title = fmt::format("{} | {}-{} | FPS: {:.0f} ({:.0f}%)", Common::g_build_fullname, Common::g_scm_branch, Common::g_scm_desc, results.average_game_fps, results.emulation_speed * 100.0f); + SDL_SetWindowTitle(this_->render_window, title.c_str()); + }, this); } EmuWindow_SDL3::~EmuWindow_SDL3() { + SDL_RemoveTimer(titlebar_timer); system.HIDCore().UnloadInputDevices(); input_subsystem->Shutdown(); SDL_Quit(); @@ -130,8 +140,7 @@ void EmuWindow_SDL3::Fullscreen() { switch (Settings::values.fullscreen_mode.GetValue()) { case Settings::FullscreenMode::Exclusive: // Set window size to render size before entering fullscreen in exclusive mode. - if (const SDL_DisplayMode* display_mode_ptr = - SDL_GetDesktopDisplayMode(SDL_GetDisplayForWindow(render_window))) { + if (const SDL_DisplayMode* display_mode_ptr = SDL_GetDesktopDisplayMode(SDL_GetDisplayForWindow(render_window))) { display_mode = *display_mode_ptr; SDL_SetWindowSize(render_window, display_mode.w, display_mode.h); SDL_SetWindowFullscreenMode(render_window, &display_mode); @@ -163,92 +172,60 @@ void EmuWindow_SDL3::Fullscreen() { } } -void EmuWindow_SDL3::WaitEvent() { - // 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); - } - +void EmuWindow_SDL3::OnEvent(SDL_Event& event) { + // Notice how we skip the "update title" aspect on most events + // this is because some WMs do NOT like changing titles while resizing + // so let's just... not do that, thanks :) + // Afterall we don't really expect the user to pay attention to the titlebar + // while they're moving a lot of shit around... switch (event.type) { case SDL_EVENT_WINDOW_RESIZED: case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED: case SDL_EVENT_WINDOW_MAXIMIZED: case SDL_EVENT_WINDOW_RESTORED: - OnResize(); - break; + return OnResize(); case SDL_EVENT_WINDOW_MINIMIZED: is_shown = false; - OnResize(); - break; + return OnResize(); case SDL_EVENT_WINDOW_EXPOSED: is_shown = true; - OnResize(); - break; + return OnResize(); case SDL_EVENT_WINDOW_CLOSE_REQUESTED: is_open = false; - break; + return; case SDL_EVENT_KEY_DOWN: case SDL_EVENT_KEY_UP: - OnKeyEvent(static_cast(event.key.scancode), event.key.down ? 1 : 0); - break; + return OnKeyEvent(int(event.key.scancode), event.key.down ? 1 : 0); case SDL_EVENT_MOUSE_MOTION: // ignore if it came from touch if (event.button.which != SDL_TOUCH_MOUSEID) OnMouseMotion(event.motion.x, event.motion.y); - break; + return; case SDL_EVENT_MOUSE_BUTTON_DOWN: case SDL_EVENT_MOUSE_BUTTON_UP: // ignore if it came from touch - if (event.button.which != SDL_TOUCH_MOUSEID) { - OnMouseButton(event.button.button, event.button.down ? 1 : 0, - static_cast(event.button.x), static_cast(event.button.y)); - } - break; + if (event.button.which != SDL_TOUCH_MOUSEID) + OnMouseButton(event.button.button, event.button.down ? 1 : 0, s32(event.button.x), s32(event.button.y)); + return; case SDL_EVENT_FINGER_DOWN: - OnFingerDown(event.tfinger.x, event.tfinger.y, - static_cast(event.tfinger.touchID)); - break; + return OnFingerDown(event.tfinger.x, event.tfinger.y, std::size_t(event.tfinger.touchID)); case SDL_EVENT_FINGER_MOTION: - OnFingerMotion(event.tfinger.x, event.tfinger.y, - static_cast(event.tfinger.touchID)); - break; + return OnFingerMotion(event.tfinger.x, event.tfinger.y, std::size_t(event.tfinger.touchID)); case SDL_EVENT_FINGER_UP: - OnFingerUp(); - break; + return OnFingerUp(); case SDL_EVENT_QUIT: is_open = false; - break; + return; default: break; } - - const u32 current_time = SDL_GetTicks(); - if (current_time > last_time + 2000) { - const auto results = system.GetAndResetPerfStats(); - const auto title = fmt::format("{} | {}-{} | FPS: {:.0f} ({:.0f}%)", - Common::g_build_fullname, - Common::g_scm_branch, - Common::g_scm_desc, - results.average_game_fps, - results.emulation_speed * 100.0); - SDL_SetWindowTitle(render_window, title.c_str()); - last_time = current_time; - } } // 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."); @@ -262,6 +239,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..3c7870b1b8 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(); @@ -80,6 +81,9 @@ protected: /// Called when a configuration change affects the minimal size of the window void OnMinimalClientAreaChangeRequest(std::pair minimal_size) override; + /// Periodic changer of titlebar (independent of event loop) + SDL_TimerID titlebar_timer; + /// Is the window still open? bool is_open = true; @@ -89,9 +93,6 @@ protected: /// Internal SDL3 render window SDL_Window* render_window{}; - /// Keeps track of how often to update the title bar during gameplay - u32 last_time = 0; - /// Input subsystem to use with this window. InputCommon::InputSubsystem* input_subsystem; diff --git a/src/yuzu_cmd/yuzu.cpp b/src/yuzu_cmd/yuzu.cpp index d20c126159..66977f7531 100644 --- a/src/yuzu_cmd/yuzu.cpp +++ b/src/yuzu_cmd/yuzu.cpp @@ -8,6 +8,12 @@ #include #include #include +#include "common/settings_enums.h" +#ifdef __EMSCRIPTEN__ +#include +#endif +#define SDL_MAIN_USE_CALLBACKS 1 +#include #include @@ -40,9 +46,7 @@ #ifdef _WIN32 // windows.h needs to be included before shellapi.h #include - #include - #include "common/windows/timer_resolution.h" #endif @@ -175,8 +179,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); @@ -187,16 +198,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; @@ -206,10 +215,13 @@ int main(int argc, char** argv) { std::optional override_gdb_port{}; bool use_multiplayer = false; bool fullscreen = false; + bool force_null_render = false; + bool force_single_core = false; std::string nickname{}; std::string password{}; std::string address{}; std::string input_profile{}; + std::optional log_filter{}; u16 port = Network::DefaultRoomPort; static struct option long_options[] = { @@ -224,6 +236,9 @@ int main(int argc, char** argv) { {"user", required_argument, 0, 'u'}, {"version", no_argument, 0, 'v'}, {"input-profile", no_argument, 0, 'i'}, + {"null-render", no_argument, 0, 'n'}, + {"singlecore", no_argument, 0, 's'}, + {"filter", no_argument, 0, 'x'}, {0, 0, 0, 0}, // clang-format on }; @@ -244,7 +259,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; @@ -261,7 +276,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; @@ -271,17 +286,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; } @@ -294,7 +308,17 @@ int main(int argc, char** argv) { break; case 'v': PrintVersion(); - return 0; + return SDL_APP_FAILURE; + case 'n': + force_null_render = true; + break; + case 's': + force_single_core = true; + break; + case 'x': + log_filter = argv[optind]; + ++optind; + break; } } else { #ifdef _WIN32 @@ -311,7 +335,7 @@ int main(int argc, char** argv) { // apply the log_filter setting // the logger was initialized before and doesn't pick up the filter on its own Common::Log::Filter filter; - filter.ParseFilterString(Settings::values.log_filter.GetValue()); + filter.ParseFilterString(log_filter.value_or(Settings::values.log_filter.GetValue())); Common::Log::SetGlobalFilter(filter); if (!program_args.empty()) { @@ -332,85 +356,87 @@ int main(int argc, char** argv) { Settings::values.gdbstub_port = *override_gdb_port; } + if (force_single_core) { + Settings::values.use_multi_core = false; + } + + if (force_null_render) { + Settings::values.renderer_backend = Settings::RendererBackend::Null; + } + #ifdef _WIN32 LocalFree(argv_w); #endif 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::Success: + break; // Expected case case Core::SystemResultStatus::ErrorGetLoader: LOG_CRITICAL(Frontend, "Failed to obtain loader for {}!", filepath); - return -1; + return SDL_APP_FAILURE; case Core::SystemResultStatus::ErrorLoader: LOG_CRITICAL(Frontend, "Failed to load ROM!"); - return -1; + return SDL_APP_FAILURE; case Core::SystemResultStatus::ErrorNotInitialized: LOG_CRITICAL(Frontend, "CPUCore not initialized"); - return -1; + return SDL_APP_FAILURE; case Core::SystemResultStatus::ErrorVideoCore: LOG_CRITICAL(Frontend, "Failed to initialize VideoCore!"); - return -1; - case Core::SystemResultStatus::Success: - break; // Expected case + 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) { @@ -419,41 +445,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(); - } - while (emu_window->IsOpen()) { - emu_window->WaitEvent(); - } - 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