// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin // SPDX-License-Identifier: CC-BY-NC-ND-4.0 #include "sdl_key_names.h" #include "scmversion/scmversion.h" #include "core/achievements.h" #include "core/bus.h" #include "core/controller.h" #include "core/fullscreen_ui.h" #include "core/game_list.h" #include "core/gpu.h" #include "core/gpu_backend.h" #include "core/gpu_thread.h" #include "core/host.h" #include "core/imgui_overlays.h" #include "core/settings.h" #include "core/system.h" #include "core/system_private.h" #include "util/gpu_device.h" #include "util/imgui_fullscreen.h" #include "util/imgui_manager.h" #include "util/ini_settings_interface.h" #include "util/input_manager.h" #include "util/platform_misc.h" #include "util/sdl_input_source.h" #include "imgui.h" #include "imgui_internal.h" #include "imgui_stdlib.h" #include "common/assert.h" #include "common/crash_handler.h" #include "common/error.h" #include "common/file_system.h" #include "common/log.h" #include "common/path.h" #include "common/string_util.h" #include "common/threading.h" #include "IconsEmoji.h" #include "fmt/format.h" #include #include #include #include #include #include LOG_CHANNEL(Host); namespace MiniHost { /// Use two async worker threads, should be enough for most tasks. static constexpr u32 NUM_ASYNC_WORKER_THREADS = 2; static constexpr u32 DEFAULT_WINDOW_WIDTH = 1280; static constexpr u32 DEFAULT_WINDOW_HEIGHT = 720; static constexpr u32 SETTINGS_VERSION = 3; static constexpr auto CPU_THREAD_POLL_INTERVAL = std::chrono::milliseconds(8); // how often we'll poll controllers when paused static bool ParseCommandLineParametersAndInitializeConfig(int argc, char* argv[], std::optional& autoboot); static void PrintCommandLineVersion(); static void PrintCommandLineHelp(const char* progname); static bool InitializeConfig(); static void InitializeEarlyConsole(); static void HookSignals(); static void SetAppRoot(); static void SetResourcesDirectory(); static bool SetDataDirectory(); static bool SetCriticalFolders(); static void SetDefaultSettings(SettingsInterface& si, bool system, bool controller); static std::string GetResourcePath(std::string_view name, bool allow_override); static bool PerformEarlyHardwareChecks(); static bool EarlyProcessStartup(); static void WarnAboutInterface(); static void StartCPUThread(); static void StopCPUThread(); static void ProcessCPUThreadEvents(bool block); static void ProcessCPUThreadPlatformMessages(); static void CPUThreadEntryPoint(); static void CPUThreadMainLoop(); static void GPUThreadEntryPoint(); static void UIThreadMainLoop(); static void ProcessSDLEvent(const SDL_Event* ev); static std::string GetWindowTitle(const std::string& game_title); static std::optional TranslateSDLWindowInfo(SDL_Window* win, Error* error); static bool GetSavedPlatformWindowGeometry(s32* x, s32* y, s32* width, s32* height); static void SavePlatformWindowGeometry(s32 x, s32 y, s32 width, s32 height); struct SDLHostState { // UI thread state ALIGN_TO_CACHE_LINE std::unique_ptr base_settings_interface; bool batch_mode = false; bool start_fullscreen_ui_fullscreen = false; bool was_paused_by_focus_loss = false; bool ui_thread_running = false; u32 func_event_id = 0; SDL_Window* sdl_window = nullptr; float sdl_window_scale = 0.0f; WindowInfo::PreRotation force_prerotation = WindowInfo::PreRotation::Identity; std::atomic_bool fullscreen{false}; Threading::Thread cpu_thread; Threading::Thread gpu_thread; Threading::KernelSemaphore platform_window_updated; std::mutex state_mutex; FullscreenUI::BackgroundProgressCallback* game_list_refresh_progress = nullptr; // CPU thread state. ALIGN_TO_CACHE_LINE std::atomic_bool cpu_thread_running{false}; std::mutex cpu_thread_events_mutex; std::condition_variable cpu_thread_event_done; std::condition_variable cpu_thread_event_posted; std::deque, bool>> cpu_thread_events; u32 blocking_cpu_events_pending = 0; }; static SDLHostState s_state; } // namespace MiniHost ////////////////////////////////////////////////////////////////////////// // Initialization/Shutdown ////////////////////////////////////////////////////////////////////////// bool MiniHost::PerformEarlyHardwareChecks() { Error error; const bool okay = System::PerformEarlyHardwareChecks(&error); if (okay && !error.IsValid()) [[likely]] return true; if (okay) Host::ReportErrorAsync("Hardware Check Warning", error.GetDescription()); else Host::ReportFatalError("Hardware Check Failed", error.GetDescription()); return okay; } bool MiniHost::EarlyProcessStartup() { Error error; if (!System::ProcessStartup(&error)) [[unlikely]] { Host::ReportFatalError("Process Startup Failed", error.GetDescription()); return false; } #if !__has_include("scmversion/tag.h") // // To those distributing their own builds or packages of DuckStation, and seeing this message: // // DuckStation is licensed under the CC-BY-NC-ND-4.0 license. // // This means that you do NOT have permission to re-distribute your own modified builds of DuckStation. // Modifying DuckStation for personal use is fine, but you cannot distribute builds with your changes. // As per the CC-BY-NC-ND conditions, you can re-distribute the official builds from https://www.duckstation.org/ and // https://github.com/stenzek/duckstation, so long as they are left intact, without modification. I welcome and // appreciate any pull requests made to the official repository at https://github.com/stenzek/duckstation. // // I made the decision to switch to a no-derivatives license because of numerous "forks" that were created purely for // generating money for the person who knocked it off, and always died, leaving the community with multiple builds to // choose from, most of which were out of date and broken, and endless confusion. Other forks copy/pasted upstream // changes without attribution, violating copyright. // // Thanks, and I hope you understand. // const char* message = ICON_EMOJI_WARNING "WARNING! You are not using an official release! " ICON_EMOJI_WARNING "\n\n" "DuckStation is licensed under the terms of CC-BY-NC-ND-4.0,\n" "which does not allow modified builds to be distributed.\n\n" "This build is NOT OFFICIAL and may be broken and/or malicious.\n\n" "You should download an official build from https://www.duckstation.org/."; Host::AddKeyedOSDWarning("OfficialReleaseWarning", message, Host::OSD_CRITICAL_ERROR_DURATION); #endif return true; } bool MiniHost::SetCriticalFolders() { SetAppRoot(); SetResourcesDirectory(); if (!SetDataDirectory()) return false; // logging of directories in case something goes wrong super early DEV_LOG("AppRoot Directory: {}", EmuFolders::AppRoot); DEV_LOG("DataRoot Directory: {}", EmuFolders::DataRoot); DEV_LOG("Resources Directory: {}", EmuFolders::Resources); // Write crash dumps to the data directory, since that'll be accessible for certain. CrashHandler::SetWriteDirectory(EmuFolders::DataRoot); // the resources directory should exist, bail out if not if (!FileSystem::DirectoryExists(EmuFolders::Resources.c_str())) { Host::ReportFatalError("Error", "Resources directory is missing, your installation is incomplete."); return false; } return true; } void MiniHost::SetAppRoot() { const std::string program_path = FileSystem::GetProgramPath(); INFO_LOG("Program Path: {}", program_path); EmuFolders::AppRoot = Path::Canonicalize(Path::GetDirectory(program_path)); } void MiniHost::SetResourcesDirectory() { #ifndef __APPLE__ // On Windows/Linux, these are in the binary directory. EmuFolders::Resources = Path::Combine(EmuFolders::AppRoot, "resources"); #else // On macOS, this is in the bundle resources directory. EmuFolders::Resources = Path::Canonicalize(Path::Combine(EmuFolders::AppRoot, "../Resources")); #endif } bool MiniHost::SetDataDirectory() { EmuFolders::DataRoot = Host::Internal::ComputeDataDirectory(); // make sure it exists if (!EmuFolders::DataRoot.empty() && !FileSystem::DirectoryExists(EmuFolders::DataRoot.c_str())) { // we're in trouble if we fail to create this directory... but try to hobble on with portable Error error; if (!FileSystem::EnsureDirectoryExists(EmuFolders::DataRoot.c_str(), false, &error)) { Host::ReportFatalError("Error", TinyString::from_format("Failed to create data directory: {}", error.GetDescription())); return false; } } // couldn't determine the data directory? fallback to portable. if (EmuFolders::DataRoot.empty()) EmuFolders::DataRoot = EmuFolders::AppRoot; return true; } bool MiniHost::InitializeConfig() { if (!SetCriticalFolders()) return false; std::string settings_path = Path::Combine(EmuFolders::DataRoot, "settings.ini"); const bool settings_exists = FileSystem::FileExists(settings_path.c_str()); INFO_LOG("Loading config from {}.", settings_path); s_state.base_settings_interface = std::make_unique(std::move(settings_path)); Host::Internal::SetBaseSettingsLayer(s_state.base_settings_interface.get()); u32 settings_version; if (!settings_exists || !s_state.base_settings_interface->Load() || !s_state.base_settings_interface->GetUIntValue("Main", "SettingsVersion", &settings_version) || settings_version != SETTINGS_VERSION) { if (s_state.base_settings_interface->ContainsValue("Main", "SettingsVersion")) { // NOTE: No point translating this, because there's no config loaded, so no language loaded. Host::ReportErrorAsync("Error", fmt::format("Settings version {} does not match expected version {}, resetting.", settings_version, SETTINGS_VERSION)); } s_state.base_settings_interface->SetUIntValue("Main", "SettingsVersion", SETTINGS_VERSION); SetDefaultSettings(*s_state.base_settings_interface, true, true); // Make sure we can actually save the config, and the user doesn't have some permission issue. Error error; if (!s_state.base_settings_interface->Save(&error)) { Host::ReportFatalError( "Error", fmt::format( "Failed to save configuration to\n\n{}\n\nThe error was: {}\n\nPlease ensure this directory is writable. You " "can also try portable mode by creating portable.txt in the same directory you installed DuckStation into.", s_state.base_settings_interface->GetPath(), error.GetDescription())); return false; } } EmuFolders::LoadConfig(*s_state.base_settings_interface.get()); EmuFolders::EnsureFoldersExist(); // We need to create the console window early, otherwise it appears in front of the main window. if (!Log::IsConsoleOutputEnabled() && s_state.base_settings_interface->GetBoolValue("Logging", "LogToConsole", false)) Log::SetConsoleOutputParams(true, s_state.base_settings_interface->GetBoolValue("Logging", "LogTimestamps", true)); // imgui setup, make sure it doesn't bug out ImGuiManager::SetFontPathAndRange(std::string(), {0x0020, 0x00FF, 0x2022, 0x2022, 0, 0}); return true; } void MiniHost::SetDefaultSettings(SettingsInterface& si, bool system, bool controller) { if (system) { System::SetDefaultSettings(si); EmuFolders::SetDefaults(); EmuFolders::Save(si); } if (controller) { InputManager::SetDefaultSourceConfig(si); Settings::SetDefaultControllerConfig(si); Settings::SetDefaultHotkeyConfig(si); } } void Host::ReportDebuggerMessage(std::string_view message) { ERROR_LOG("ReportDebuggerMessage(): {}", message); } std::span> Host::GetAvailableLanguageList() { return {}; } bool Host::ChangeLanguage(const char* new_language) { return false; } void Host::AddFixedInputBindings(const SettingsInterface& si) { } void Host::OnInputDeviceConnected(InputBindingKey key, std::string_view identifier, std::string_view device_name) { Host::AddKeyedOSDMessage(fmt::format("InputDeviceConnected-{}", identifier), fmt::format("Input device {0} ({1}) connected.", device_name, identifier), 10.0f); } void Host::OnInputDeviceDisconnected(InputBindingKey key, std::string_view identifier) { Host::AddKeyedOSDMessage(fmt::format("InputDeviceConnected-{}", identifier), fmt::format("Input device {} disconnected.", identifier), 10.0f); } s32 Host::Internal::GetTranslatedStringImpl(std::string_view context, std::string_view msg, std::string_view disambiguation, char* tbuf, size_t tbuf_space) { if (msg.size() > tbuf_space) return -1; else if (msg.empty()) return 0; std::memcpy(tbuf, msg.data(), msg.size()); return static_cast(msg.size()); } std::string Host::TranslatePluralToString(const char* context, const char* msg, const char* disambiguation, int count) { TinyString count_str = TinyString::from_format("{}", count); std::string ret(msg); for (;;) { std::string::size_type pos = ret.find("%n"); if (pos == std::string::npos) break; ret.replace(pos, pos + 2, count_str.view()); } return ret; } SmallString Host::TranslatePluralToSmallString(const char* context, const char* msg, const char* disambiguation, int count) { SmallString ret(msg); ret.replace("%n", TinyString::from_format("{}", count)); return ret; } std::string MiniHost::GetResourcePath(std::string_view filename, bool allow_override) { return allow_override ? EmuFolders::GetOverridableResourcePath(filename) : Path::Combine(EmuFolders::Resources, filename); } bool Host::ResourceFileExists(std::string_view filename, bool allow_override) { const std::string path = MiniHost::GetResourcePath(filename, allow_override); return FileSystem::FileExists(path.c_str()); } std::optional> Host::ReadResourceFile(std::string_view filename, bool allow_override, Error* error) { const std::string path = MiniHost::GetResourcePath(filename, allow_override); return FileSystem::ReadBinaryFile(path.c_str(), error); } std::optional Host::ReadResourceFileToString(std::string_view filename, bool allow_override, Error* error) { const std::string path = MiniHost::GetResourcePath(filename, allow_override); return FileSystem::ReadFileToString(path.c_str(), error); } std::optional Host::GetResourceFileTimestamp(std::string_view filename, bool allow_override) { const std::string path = MiniHost::GetResourcePath(filename, allow_override); FILESYSTEM_STAT_DATA sd; if (!FileSystem::StatFile(path.c_str(), &sd)) { ERROR_LOG("Failed to stat resource file '{}'", filename); return std::nullopt; } return sd.ModificationTime; } void Host::LoadSettings(const SettingsInterface& si, std::unique_lock& lock) { } void Host::CheckForSettingsChanges(const Settings& old_settings) { } void Host::CommitBaseSettingChanges() { auto lock = Host::GetSettingsLock(); Error error; if (!MiniHost::s_state.base_settings_interface->Save(&error)) ERROR_LOG("Failed to save settings: {}", error.GetDescription()); } std::optional MiniHost::TranslateSDLWindowInfo(SDL_Window* win, Error* error) { if (!win) { Error::SetStringView(error, "Window handle is null."); return std::nullopt; } const SDL_WindowFlags window_flags = SDL_GetWindowFlags(win); int window_width = 1, window_height = 1; int window_px_width = 1, window_px_height = 1; SDL_GetWindowSize(win, &window_width, &window_height); SDL_GetWindowSizeInPixels(win, &window_px_width, &window_px_height); s_state.sdl_window_scale = SDL_GetWindowDisplayScale(win); const SDL_DisplayMode* dispmode = nullptr; if (window_flags & SDL_WINDOW_FULLSCREEN) { if (!(dispmode = SDL_GetWindowFullscreenMode(win))) ERROR_LOG("SDL_GetWindowFullscreenMode() failed: {}", SDL_GetError()); } if (const SDL_DisplayID display_id = SDL_GetDisplayForWindow(win); display_id != 0) { if (!(window_flags & SDL_WINDOW_FULLSCREEN)) { if (!(dispmode = SDL_GetDesktopDisplayMode(display_id))) ERROR_LOG("SDL_GetDesktopDisplayMode() failed: {}", SDL_GetError()); } } WindowInfo wi; wi.surface_width = static_cast(window_px_width); wi.surface_height = static_cast(window_px_height); wi.surface_scale = s_state.sdl_window_scale; wi.surface_prerotation = s_state.force_prerotation; // set display refresh rate if available if (dispmode && dispmode->refresh_rate > 0.0f) { INFO_LOG("Display mode refresh rate: {} hz", dispmode->refresh_rate); wi.surface_refresh_rate = dispmode->refresh_rate; } // SDL's opengl window flag tends to make a mess of pixel formats... if (!(SDL_GetWindowFlags(win) & (SDL_WINDOW_OPENGL | SDL_WINDOW_VULKAN))) { const SDL_PropertiesID props = SDL_GetWindowProperties(win); if (props == 0) { Error::SetStringFmt(error, "SDL_GetWindowProperties() failed: {}", SDL_GetError()); return std::nullopt; } #if defined(SDL_PLATFORM_WINDOWS) wi.type = WindowInfo::Type::Win32; wi.window_handle = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WIN32_HWND_POINTER, nullptr); if (!wi.window_handle) { Error::SetStringView(error, "SDL_PROP_WINDOW_WIN32_HWND_POINTER not found."); return std::nullopt; } #elif defined(SDL_PLATFORM_MACOS) wi.type = WindowInfo::Type::MacOS; wi.window_handle = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, nullptr); if (!wi.window_handle) { Error::SetStringView(error, "SDL_PROP_WINDOW_COCOA_WINDOW_POINTER not found."); return std::nullopt; } #elif defined(SDL_PLATFORM_LINUX) || defined(SDL_PLATFORM_FREEBSD) const std::string_view video_driver = SDL_GetCurrentVideoDriver(); if (video_driver == "x11") { wi.display_connection = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_X11_DISPLAY_POINTER, nullptr); wi.window_handle = reinterpret_cast( static_cast(SDL_GetNumberProperty(props, SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0))); if (!wi.display_connection) { Error::SetStringView(error, "SDL_PROP_WINDOW_X11_DISPLAY_POINTER not found."); return std::nullopt; } else if (!wi.window_handle) { Error::SetStringView(error, "SDL_PROP_WINDOW_X11_WINDOW_NUMBER not found."); return std::nullopt; } } else if (video_driver == "wayland") { wi.display_connection = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_DISPLAY_POINTER, nullptr); wi.window_handle = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_SURFACE_POINTER, nullptr); if (!wi.display_connection) { Error::SetStringView(error, "SDL_PROP_WINDOW_WAYLAND_DISPLAY_POINTER not found."); return std::nullopt; } else if (!wi.window_handle) { Error::SetStringView(error, "SDL_PROP_WINDOW_WAYLAND_SURFACE_POINTER not found."); return std::nullopt; } } else { Error::SetStringFmt(error, "Video driver {} not supported.", video_driver); return std::nullopt; } #else #error Unsupported platform. #endif } else { // nothing handled, fall back to SDL abstraction wi.type = WindowInfo::Type::SDL; wi.window_handle = win; } return wi; } std::optional Host::AcquireRenderWindow(RenderAPI render_api, bool fullscreen, bool exclusive_fullscreen, Error* error) { using namespace MiniHost; std::optional wi; Host::RunOnUIThread([render_api, fullscreen, error, &wi]() { const std::string window_title = GetWindowTitle(System::GetGameTitle()); const SDL_PropertiesID props = SDL_CreateProperties(); SDL_SetStringProperty(props, SDL_PROP_WINDOW_CREATE_TITLE_STRING, window_title.c_str()); SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_RESIZABLE_BOOLEAN, true); SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_FOCUSABLE_BOOLEAN, true); SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_HIGH_PIXEL_DENSITY_BOOLEAN, true); if (render_api == RenderAPI::OpenGL || render_api == RenderAPI::OpenGLES) SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_OPENGL_BOOLEAN, true); else if (render_api == RenderAPI::Vulkan) SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_VULKAN_BOOLEAN, true); if (fullscreen) { SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_BORDERLESS_BOOLEAN, true); SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_FULLSCREEN_BOOLEAN, true); } if (s32 window_x, window_y, window_width, window_height; MiniHost::GetSavedPlatformWindowGeometry(&window_x, &window_y, &window_width, &window_height)) { SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_X_NUMBER, window_x); SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_Y_NUMBER, window_y); SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, window_width); SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, window_height); } else { SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, DEFAULT_WINDOW_WIDTH); SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, DEFAULT_WINDOW_HEIGHT); } s_state.sdl_window = SDL_CreateWindowWithProperties(props); SDL_DestroyProperties(props); if (s_state.sdl_window) { wi = TranslateSDLWindowInfo(s_state.sdl_window, error); if (wi.has_value()) { s_state.fullscreen.store(fullscreen, std::memory_order_release); } else { SDL_DestroyWindow(s_state.sdl_window); s_state.sdl_window = nullptr; } } else { Error::SetStringFmt(error, "SDL_CreateWindow() failed: {}", SDL_GetError()); } s_state.platform_window_updated.Post(); }); s_state.platform_window_updated.Wait(); // reload input sources, since it might use the window handle { auto lock = Host::GetSettingsLock(); InputManager::ReloadSources(*Host::GetSettingsInterface(), lock); } return wi; } void Host::ReleaseRenderWindow() { using namespace MiniHost; if (!s_state.sdl_window) return; Host::RunOnUIThread([]() { if (!s_state.fullscreen.load(std::memory_order_acquire)) { int window_x = SDL_WINDOWPOS_UNDEFINED, window_y = SDL_WINDOWPOS_UNDEFINED; int window_width = DEFAULT_WINDOW_WIDTH, window_height = DEFAULT_WINDOW_HEIGHT; SDL_GetWindowPosition(s_state.sdl_window, &window_x, &window_y); SDL_GetWindowSize(s_state.sdl_window, &window_width, &window_height); MiniHost::SavePlatformWindowGeometry(window_x, window_y, window_width, window_height); } else { s_state.fullscreen.store(false, std::memory_order_release); } SDL_DestroyWindow(s_state.sdl_window); s_state.sdl_window = nullptr; s_state.platform_window_updated.Post(); }); s_state.platform_window_updated.Wait(); } bool Host::IsFullscreen() { using namespace MiniHost; return s_state.fullscreen.load(std::memory_order_acquire); } void Host::SetFullscreen(bool enabled) { using namespace MiniHost; if (!s_state.sdl_window || s_state.fullscreen.load(std::memory_order_acquire) == enabled) return; if (!SDL_SetWindowFullscreen(s_state.sdl_window, enabled)) { ERROR_LOG("SDL_SetWindowFullscreen() failed: {}", SDL_GetError()); return; } s_state.fullscreen.store(enabled, std::memory_order_release); } void Host::BeginTextInput() { using namespace MiniHost; SDL_StartTextInput(s_state.sdl_window); } void Host::EndTextInput() { // we want to keep getting text events, SDL_StopTextInput() apparently inhibits that } bool Host::CreateAuxiliaryRenderWindow(s32 x, s32 y, u32 width, u32 height, std::string_view title, std::string_view icon_name, AuxiliaryRenderWindowUserData userdata, AuxiliaryRenderWindowHandle* handle, WindowInfo* wi, Error* error) { // not here, but could be... Error::SetStringView(error, "Not supported."); return false; } void Host::DestroyAuxiliaryRenderWindow(AuxiliaryRenderWindowHandle handle, s32* pos_x /* = nullptr */, s32* pos_y /* = nullptr */, u32* width /* = nullptr */, u32* height /* = nullptr */) { // noop } bool MiniHost::GetSavedPlatformWindowGeometry(s32* x, s32* y, s32* width, s32* height) { auto lock = Host::GetSettingsLock(); bool result = s_state.base_settings_interface->GetIntValue("SimpleHost", "WindowX", x); result = result && s_state.base_settings_interface->GetIntValue("SimpleHost", "WindowY", y); result = result && s_state.base_settings_interface->GetIntValue("SimpleHost", "WindowWidth", width); result = result && s_state.base_settings_interface->GetIntValue("SimpleHost", "WindowHeight", height); return result; } void MiniHost::SavePlatformWindowGeometry(s32 x, s32 y, s32 width, s32 height) { if (Host::IsFullscreen()) return; auto lock = Host::GetSettingsLock(); s_state.base_settings_interface->SetIntValue("SimpleHost", "WindowX", x); s_state.base_settings_interface->SetIntValue("SimpleHost", "WindowY", y); s_state.base_settings_interface->SetIntValue("SimpleHost", "WindowWidth", width); s_state.base_settings_interface->SetIntValue("SimpleHost", "WindowHeight", height); s_state.base_settings_interface->Save(); } void MiniHost::UIThreadMainLoop() { while (s_state.ui_thread_running) { SDL_Event ev; if (!SDL_WaitEvent(&ev)) continue; ProcessSDLEvent(&ev); } } void MiniHost::ProcessSDLEvent(const SDL_Event* ev) { switch (ev->type) { case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED: { Host::RunOnCPUThread( [window_width = ev->window.data1, window_height = ev->window.data2, window_scale = s_state.sdl_window_scale]() { GPUThread::ResizeDisplayWindow(window_width, window_height, window_scale); }); } break; case SDL_EVENT_WINDOW_DISPLAY_CHANGED: case SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED: { const float new_scale = SDL_GetWindowDisplayScale(s_state.sdl_window); if (new_scale != s_state.sdl_window_scale) { s_state.sdl_window_scale = new_scale; int window_width = 1, window_height = 1; SDL_GetWindowSizeInPixels(s_state.sdl_window, &window_width, &window_height); Host::RunOnCPUThread([window_width, window_height, window_scale = s_state.sdl_window_scale]() { GPUThread::ResizeDisplayWindow(window_width, window_height, window_scale); }); } } break; case SDL_EVENT_WINDOW_CLOSE_REQUESTED: { Host::RunOnCPUThread([]() { Host::RequestExitApplication(false); }); } break; case SDL_EVENT_WINDOW_FOCUS_GAINED: { Host::RunOnCPUThread([]() { if (!System::IsValid() || !s_state.was_paused_by_focus_loss) return; System::PauseSystem(false); s_state.was_paused_by_focus_loss = false; }); } break; case SDL_EVENT_WINDOW_FOCUS_LOST: { Host::RunOnCPUThread([]() { if (!System::IsRunning() || !g_settings.pause_on_focus_loss) return; s_state.was_paused_by_focus_loss = true; System::PauseSystem(true); }); } break; case SDL_EVENT_KEY_DOWN: case SDL_EVENT_KEY_UP: { Host::RunOnCPUThread([key_code = static_cast(ev->key.key), pressed = (ev->type == SDL_EVENT_KEY_DOWN)]() { InputManager::InvokeEvents(InputManager::MakeHostKeyboardKey(key_code), pressed ? 1.0f : 0.0f, GenericInputBinding::Unknown); }); } break; case SDL_EVENT_TEXT_INPUT: { if (ImGuiManager::WantsTextInput()) Host::RunOnCPUThread([text = std::string(ev->text.text)]() { ImGuiManager::AddTextInput(std::move(text)); }); } break; case SDL_EVENT_MOUSE_MOTION: { Host::RunOnCPUThread([x = static_cast(ev->motion.x), y = static_cast(ev->motion.y)]() { InputManager::UpdatePointerAbsolutePosition(0, x, y); ImGuiManager::UpdateMousePosition(x, y); }); } break; case SDL_EVENT_MOUSE_BUTTON_DOWN: case SDL_EVENT_MOUSE_BUTTON_UP: { if (ev->button.button > 0) { // swap middle/right because sdl orders them differently const u8 button = (ev->button.button == 3) ? 1 : ((ev->button.button == 2) ? 2 : (ev->button.button - 1)); Host::RunOnCPUThread([button, pressed = (ev->type == SDL_EVENT_MOUSE_BUTTON_DOWN)]() { InputManager::InvokeEvents(InputManager::MakePointerButtonKey(0, button), pressed ? 1.0f : 0.0f, GenericInputBinding::Unknown); }); } } break; case SDL_EVENT_MOUSE_WHEEL: { Host::RunOnCPUThread([x = ev->wheel.x, y = ev->wheel.y]() { if (x != 0.0f) InputManager::UpdatePointerRelativeDelta(0, InputPointerAxis::WheelX, x); if (y != 0.0f) InputManager::UpdatePointerRelativeDelta(0, InputPointerAxis::WheelY, y); }); } break; case SDL_EVENT_QUIT: { Host::RunOnCPUThread([]() { Host::RequestExitApplication(false); }); } break; default: { if (ev->type == s_state.func_event_id) { std::function* pfunc = reinterpret_cast*>(ev->user.data1); if (pfunc) { (*pfunc)(); delete pfunc; } } else if (SDLInputSource::IsHandledInputEvent(ev)) { Host::RunOnCPUThread([event_copy = *ev]() { SDLInputSource* is = static_cast(InputManager::GetInputSourceInterface(InputSourceType::SDL)); if (is) is->ProcessSDLEvent(&event_copy); }); } } break; } } void MiniHost::ProcessCPUThreadPlatformMessages() { // This is lame. On Win32, we need to pump messages, even though *we* don't have any windows // on the CPU thread, because SDL creates a hidden window for raw input for some game controllers. // If we don't do this, we don't get any controller events. #ifdef _WIN32 MSG msg; while (PeekMessageW(&msg, NULL, 0, 0, PM_REMOVE)) { TranslateMessage(&msg); DispatchMessageW(&msg); } #endif } void MiniHost::ProcessCPUThreadEvents(bool block) { std::unique_lock lock(s_state.cpu_thread_events_mutex); for (;;) { if (s_state.cpu_thread_events.empty()) { if (!block || !s_state.cpu_thread_running.load(std::memory_order_acquire)) return; // we still need to keep polling the controllers when we're paused do { ProcessCPUThreadPlatformMessages(); InputManager::PollSources(); } while (!s_state.cpu_thread_event_posted.wait_for(lock, CPU_THREAD_POLL_INTERVAL, []() { return !s_state.cpu_thread_events.empty(); })); } // return after processing all events if we had one block = false; auto event = std::move(s_state.cpu_thread_events.front()); s_state.cpu_thread_events.pop_front(); lock.unlock(); event.first(); lock.lock(); if (event.second) { s_state.blocking_cpu_events_pending--; s_state.cpu_thread_event_done.notify_one(); } } } void MiniHost::StartCPUThread() { s_state.cpu_thread_running.store(true, std::memory_order_release); s_state.cpu_thread.Start(CPUThreadEntryPoint); } void MiniHost::StopCPUThread() { if (!s_state.cpu_thread.Joinable()) return; { std::unique_lock lock(s_state.cpu_thread_events_mutex); s_state.cpu_thread_running.store(false, std::memory_order_release); s_state.cpu_thread_event_posted.notify_one(); } s_state.cpu_thread.Join(); } void MiniHost::CPUThreadEntryPoint() { Threading::SetNameOfCurrentThread("CPU Thread"); // input source setup must happen on emu thread Error error; if (!System::CPUThreadInitialize(&error, NUM_ASYNC_WORKER_THREADS)) { Host::ReportFatalError("CPU Thread Initialization Failed", error.GetDescription()); return; } // start up GPU thread s_state.gpu_thread.Start(&GPUThreadEntryPoint); // start the fullscreen UI and get it going if (GPUThread::StartFullscreenUI(s_state.start_fullscreen_ui_fullscreen, &error)) { WarnAboutInterface(); // kick a game list refresh if we're not in batch mode if (!s_state.batch_mode) Host::RefreshGameListAsync(false); CPUThreadMainLoop(); Host::CancelGameListRefresh(); } else { Host::ReportFatalError("Error", fmt::format("Failed to start fullscreen UI: {}", error.GetDescription())); } // finish any events off (e.g. shutdown system with save) ProcessCPUThreadEvents(false); if (System::IsValid()) System::ShutdownSystem(false); GPUThread::StopFullscreenUI(); GPUThread::Internal::RequestShutdown(); s_state.gpu_thread.Join(); System::CPUThreadShutdown(); // Tell the UI thread to shut down. Host::RunOnUIThread([]() { s_state.ui_thread_running = false; }); } void MiniHost::CPUThreadMainLoop() { while (s_state.cpu_thread_running.load(std::memory_order_acquire)) { if (System::IsRunning()) { System::Execute(); continue; } else if (!GPUThread::IsUsingThread() && GPUThread::IsRunningIdle()) { ProcessCPUThreadEvents(false); if (!GPUThread::IsUsingThread() && GPUThread::IsRunningIdle()) GPUThread::Internal::DoRunIdle(); } ProcessCPUThreadEvents(true); } } void MiniHost::GPUThreadEntryPoint() { Threading::SetNameOfCurrentThread("GPU Thread"); GPUThread::Internal::GPUThreadEntryPoint(); } void Host::OnSystemStarting() { MiniHost::s_state.was_paused_by_focus_loss = false; } void Host::OnSystemStarted() { } void Host::OnSystemPaused() { } void Host::OnSystemResumed() { } void Host::OnSystemDestroyed() { } void Host::OnSystemAbnormalShutdown(const std::string_view reason) { GPUThread::RunOnThread([reason = std::string(reason)]() { ImGuiFullscreen::OpenInfoMessageDialog( "Abnormal System Shutdown", fmt::format("Unfortunately, the virtual machine has abnormally shut down and cannot " "be recovered. More information about the error is below:\n\n{}", reason)); }); } void Host::OnGPUThreadRunIdleChanged(bool is_active) { } void Host::FrameDoneOnGPUThread(GPUBackend* gpu_backend, u32 frame_number) { } void Host::OnPerformanceCountersUpdated(const GPUBackend* gpu_backend) { // noop } void Host::OnAchievementsLoginRequested(Achievements::LoginRequestReason reason) { // noop } void Host::OnAchievementsLoginSuccess(const char* username, u32 points, u32 sc_points, u32 unread_messages) { // noop } void Host::OnAchievementsRefreshed() { // noop } void Host::OnAchievementsHardcoreModeChanged(bool enabled) { // noop } #ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION void Host::OnRAIntegrationMenuChanged() { // noop } #endif void Host::OnCoverDownloaderOpenRequested() { // noop } void Host::SetMouseMode(bool relative, bool hide_cursor) { // noop } void Host::OnMediaCaptureStarted() { // noop } void Host::OnMediaCaptureStopped() { // noop } void Host::PumpMessagesOnCPUThread() { MiniHost::ProcessCPUThreadEvents(false); } std::string MiniHost::GetWindowTitle(const std::string& game_title) { #if defined(_DEBUGFAST) static constexpr std::string_view suffix = " [DebugFast]"; #elif defined(_DEBUG) static constexpr std::string_view suffix = " [Debug]"; #else static constexpr std::string_view suffix = std::string_view(); #endif if (System::IsShutdown() || game_title.empty()) return fmt::format("DuckStation {}{}", g_scm_tag_str, suffix); else return fmt::format("{}{}", game_title, suffix); } void MiniHost::WarnAboutInterface() { const char* message = "This is the \"mini\" interface for DuckStation, and is missing many features.\n" " We recommend using the Qt interface instead, which you can download\n" " from https://www.duckstation.org/."; Host::AddIconOSDWarning("MiniWarning", ICON_EMOJI_WARNING, message, Host::OSD_INFO_DURATION); } void Host::OnGameChanged(const std::string& disc_path, const std::string& game_serial, const std::string& game_name, GameHash game_hash) { using namespace MiniHost; VERBOSE_LOG("Host::OnGameChanged(\"{}\", \"{}\", \"{}\")", disc_path, game_serial, game_name); if (s_state.sdl_window) SDL_SetWindowTitle(s_state.sdl_window, GetWindowTitle(game_name).c_str()); } void Host::RunOnCPUThread(std::function function, bool block /* = false */) { using namespace MiniHost; std::unique_lock lock(s_state.cpu_thread_events_mutex); s_state.cpu_thread_events.emplace_back(std::move(function), block); s_state.blocking_cpu_events_pending += BoolToUInt32(block); s_state.cpu_thread_event_posted.notify_one(); if (block) s_state.cpu_thread_event_done.wait(lock, []() { return s_state.blocking_cpu_events_pending == 0; }); } void Host::RunOnUIThread(std::function function, bool block /* = false */) { using namespace MiniHost; std::function* pfunc = new std::function(std::move(function)); SDL_Event ev; ev.user = {}; ev.type = s_state.func_event_id; ev.user.data1 = pfunc; SDL_PushEvent(&ev); } void Host::RefreshGameListAsync(bool invalidate_cache) { using namespace MiniHost; std::unique_lock lock(s_state.state_mutex); while (s_state.game_list_refresh_progress) { lock.unlock(); CancelGameListRefresh(); lock.lock(); } s_state.game_list_refresh_progress = new FullscreenUI::BackgroundProgressCallback("glrefresh"); System::QueueAsyncTask([invalidate_cache]() { GameList::Refresh(invalidate_cache, false, s_state.game_list_refresh_progress); std::unique_lock lock(s_state.state_mutex); delete s_state.game_list_refresh_progress; s_state.game_list_refresh_progress = nullptr; }); } void Host::CancelGameListRefresh() { using namespace MiniHost; { std::unique_lock lock(s_state.state_mutex); if (!s_state.game_list_refresh_progress) return; s_state.game_list_refresh_progress->SetCancelled(); } System::WaitForAllAsyncTasks(); } void Host::OnGameListEntriesChanged(std::span changed_indices) { // constantly re-querying, don't need to do anything } std::optional Host::GetTopLevelWindowInfo() { return MiniHost::TranslateSDLWindowInfo(MiniHost::s_state.sdl_window, nullptr); } void Host::RequestResetSettings(bool system, bool controller) { using namespace MiniHost; auto lock = Host::GetSettingsLock(); { SettingsInterface& si = *s_state.base_settings_interface.get(); if (system) { System::SetDefaultSettings(si); EmuFolders::SetDefaults(); EmuFolders::Save(si); } if (controller) { InputManager::SetDefaultSourceConfig(si); Settings::SetDefaultControllerConfig(si); Settings::SetDefaultHotkeyConfig(si); } } System::ApplySettings(false); } void Host::RequestExitApplication(bool allow_confirm) { Host::RunOnCPUThread([]() { System::ShutdownSystem(g_settings.save_state_on_exit); // clear the running flag, this'll break out of the main CPU loop once the VM is shutdown. MiniHost::s_state.cpu_thread_running.store(false, std::memory_order_release); }); } void Host::RequestExitBigPicture() { // sorry dude } void Host::RequestSystemShutdown(bool allow_confirm, bool save_state, bool check_memcard_busy) { // TODO: Confirm if (System::IsValid()) { Host::RunOnCPUThread([save_state]() { System::ShutdownSystem(save_state); }); } } void Host::ReportFatalError(std::string_view title, std::string_view message) { // Depending on the platform, this may not be available. std::fputs(SmallString::from_format("Fatal error: {}: {}\n", title, message).c_str(), stderr); SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, TinyString(title).c_str(), SmallString(message).c_str(), nullptr); } void Host::ReportErrorAsync(std::string_view title, std::string_view message) { std::fputs(SmallString::from_format("Error: {}: {}\n", title, message).c_str(), stderr); SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, TinyString(title).c_str(), SmallString(message).c_str(), nullptr); } void Host::RequestResizeHostDisplay(s32 width, s32 height) { using namespace MiniHost; if (!s_state.sdl_window || s_state.fullscreen.load(std::memory_order_acquire)) return; SDL_SetWindowSize(s_state.sdl_window, width, height); } void Host::OpenURL(std::string_view url) { if (!SDL_OpenURL(SmallString(url).c_str())) ERROR_LOG("SDL_OpenURL({}) failed: {}", url, SDL_GetError()); } std::string Host::GetClipboardText() { std::string ret; char* text = SDL_GetClipboardText(); if (text) { ret = text; SDL_free(text); } return ret; } bool Host::CopyTextToClipboard(std::string_view text) { if (!SDL_SetClipboardText(SmallString(text).c_str())) { ERROR_LOG("SDL_SetClipboardText({}) failed: {}", text, SDL_GetError()); return false; } return true; } std::optional InputManager::ConvertHostKeyboardStringToCode(std::string_view str) { return SDLKeyNames::GetKeyCodeForName(str); } std::optional InputManager::ConvertHostKeyboardCodeToString(u32 code) { const char* converted = SDLKeyNames::GetKeyName(code); return converted ? std::optional(converted) : std::nullopt; } const char* InputManager::ConvertHostKeyboardCodeToIcon(u32 code) { return nullptr; } bool Host::ConfirmMessage(std::string_view title, std::string_view message) { const SmallString title_copy(title); const SmallString message_copy(message); static constexpr SDL_MessageBoxButtonData bd[2] = { {SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT, 1, "Yes"}, {SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT, 2, "No"}, }; const SDL_MessageBoxData md = {SDL_MESSAGEBOX_INFORMATION, nullptr, title_copy.c_str(), message_copy.c_str(), static_cast(std::size(bd)), bd, nullptr}; int buttonid = -1; SDL_ShowMessageBox(&md, &buttonid); return (buttonid == 1); } void Host::ConfirmMessageAsync(std::string_view title, std::string_view message, ConfirmMessageAsyncCallback callback, std::string_view yes_text /* = std::string_view() */, std::string_view no_text /* = std::string_view() */) { Host::RunOnCPUThread([title = std::string(title), message = std::string(message), callback = std::move(callback), yes_text = std::string(yes_text), no_text = std::move(no_text)]() mutable { // in case we haven't started yet... if (!FullscreenUI::IsInitialized()) { callback(false); return; } // Pause system while dialog is up. const bool needs_pause = System::IsValid() && !System::IsPaused(); if (needs_pause) System::PauseSystem(true); GPUThread::RunOnThread([title = std::string(title), message = std::string(message), callback = std::move(callback), yes_text = std::string(yes_text), no_text = std::string(no_text), needs_pause]() mutable { if (!FullscreenUI::Initialize()) { callback(false); if (needs_pause) { Host::RunOnCPUThread([]() { if (System::IsValid()) System::PauseSystem(false); }); } return; } // Need to reset run idle state _again_ after displaying. auto final_callback = [callback = std::move(callback)](bool result) { FullscreenUI::UpdateRunIdleState(); callback(result); }; ImGuiFullscreen::OpenConfirmMessageDialog(std::move(title), std::move(message), std::move(final_callback), fmt::format(ICON_FA_CHECK " {}", yes_text), fmt::format(ICON_FA_TIMES " {}", no_text)); FullscreenUI::UpdateRunIdleState(); }); }); } void Host::OpenHostFileSelectorAsync(std::string_view title, bool select_directory, FileSelectorCallback callback, FileSelectorFilters filters /* = FileSelectorFilters() */, std::string_view initial_directory /* = std::string_view() */) { // TODO: Use SDL FileDialog API callback(std::string()); } const char* Host::GetDefaultFullscreenUITheme() { return ""; } bool Host::ShouldPreferHostFileSelector() { return false; } BEGIN_HOTKEY_LIST(g_host_hotkeys) END_HOTKEY_LIST() static void SignalHandler(int signal) { // First try the normal (graceful) shutdown/exit. static bool graceful_shutdown_attempted = false; if (!graceful_shutdown_attempted) { std::fprintf(stderr, "Received CTRL+C, attempting graceful shutdown. Press CTRL+C again to force.\n"); graceful_shutdown_attempted = true; Host::RequestExitApplication(false); return; } std::signal(signal, SIG_DFL); // MacOS is missing std::quick_exit() despite it being C++11... #ifndef __APPLE__ std::quick_exit(1); #else _Exit(1); #endif } void MiniHost::HookSignals() { std::signal(SIGINT, SignalHandler); std::signal(SIGTERM, SignalHandler); #ifndef _WIN32 // Ignore SIGCHLD by default on Linux, since we kick off aplay asynchronously. struct sigaction sa_chld = {}; sigemptyset(&sa_chld.sa_mask); sa_chld.sa_handler = SIG_IGN; sa_chld.sa_flags = SA_RESTART | SA_NOCLDSTOP | SA_NOCLDWAIT; sigaction(SIGCHLD, &sa_chld, nullptr); #endif } void MiniHost::InitializeEarlyConsole() { const bool was_console_enabled = Log::IsConsoleOutputEnabled(); if (!was_console_enabled) Log::SetConsoleOutputParams(true); } void MiniHost::PrintCommandLineVersion() { InitializeEarlyConsole(); std::fprintf(stderr, "DuckStation Version %s (%s)\n", g_scm_tag_str, g_scm_branch_str); std::fprintf(stderr, "https://github.com/stenzek/duckstation\n"); std::fprintf(stderr, "\n"); } void MiniHost::PrintCommandLineHelp(const char* progname) { InitializeEarlyConsole(); PrintCommandLineVersion(); std::fprintf(stderr, "Usage: %s [parameters] [--] [boot filename]\n", progname); std::fprintf(stderr, "\n"); std::fprintf(stderr, " -help: Displays this information and exits.\n"); std::fprintf(stderr, " -version: Displays version information and exits.\n"); std::fprintf(stderr, " -batch: Enables batch mode (exits after powering off).\n"); std::fprintf(stderr, " -fastboot: Force fast boot for provided filename.\n"); std::fprintf(stderr, " -slowboot: Force slow boot for provided filename.\n"); std::fprintf(stderr, " -bios: Boot into the BIOS shell.\n"); std::fprintf(stderr, " -resume: Load resume save state. If a boot filename is provided,\n" " that game's resume state will be loaded, otherwise the most\n" " recent resume save state will be loaded.\n"); std::fprintf(stderr, " -state : Loads specified save state by index. If a boot\n" " filename is provided, a per-game state will be loaded, otherwise\n" " a global state will be loaded.\n"); std::fprintf(stderr, " -statefile : Loads state from the specified filename.\n" " No boot filename is required with this option.\n"); std::fprintf(stderr, " -exe : Boot the specified exe instead of loading from disc.\n"); std::fprintf(stderr, " -fullscreen: Enters fullscreen mode immediately after starting.\n"); std::fprintf(stderr, " -nofullscreen: Prevents fullscreen mode from triggering if enabled.\n"); std::fprintf(stderr, " -earlyconsole: Creates console as early as possible, for logging.\n"); std::fprintf(stderr, " -prerotation : Prerotates output by 90/180/270 degrees.\n"); std::fprintf(stderr, " --: Signals that no more arguments will follow and the remaining\n" " parameters make up the filename. Use when the filename contains\n" " spaces or starts with a dash.\n"); std::fprintf(stderr, "\n"); } std::optional& AutoBoot(std::optional& autoboot) { if (!autoboot) autoboot.emplace(); return autoboot; } bool MiniHost::ParseCommandLineParametersAndInitializeConfig(int argc, char* argv[], std::optional& autoboot) { std::optional state_index; bool starting_bios = false; bool no_more_args = false; for (int i = 1; i < argc; i++) { if (!no_more_args) { #define CHECK_ARG(str) (std::strcmp(argv[i], (str)) == 0) #define CHECK_ARG_PARAM(str) (std::strcmp(argv[i], (str)) == 0 && ((i + 1) < argc)) if (CHECK_ARG("-help")) { PrintCommandLineHelp(argv[0]); return false; } else if (CHECK_ARG("-version")) { PrintCommandLineVersion(); return false; } else if (CHECK_ARG("-batch")) { INFO_LOG("Command Line: Using batch mode."); s_state.batch_mode = true; continue; } else if (CHECK_ARG("-bios")) { INFO_LOG("Command Line: Starting BIOS."); AutoBoot(autoboot); starting_bios = true; continue; } else if (CHECK_ARG("-fastboot")) { INFO_LOG("Command Line: Forcing fast boot."); AutoBoot(autoboot)->override_fast_boot = true; continue; } else if (CHECK_ARG("-slowboot")) { INFO_LOG("Command Line: Forcing slow boot."); AutoBoot(autoboot)->override_fast_boot = false; continue; } else if (CHECK_ARG("-resume")) { state_index = -1; INFO_LOG("Command Line: Loading resume state."); continue; } else if (CHECK_ARG_PARAM("-state")) { state_index = StringUtil::FromChars(argv[++i]); if (!state_index.has_value()) { ERROR_LOG("Invalid state index"); return false; } INFO_LOG("Command Line: Loading state index: {}", state_index.value()); continue; } else if (CHECK_ARG_PARAM("-statefile")) { AutoBoot(autoboot)->save_state = argv[++i]; INFO_LOG("Command Line: Loading state file: '{}'", autoboot->save_state); continue; } else if (CHECK_ARG_PARAM("-exe")) { AutoBoot(autoboot)->override_exe = argv[++i]; INFO_LOG("Command Line: Overriding EXE file: '{}'", autoboot->override_exe); continue; } else if (CHECK_ARG("-fullscreen")) { INFO_LOG("Command Line: Using fullscreen."); AutoBoot(autoboot)->override_fullscreen = true; s_state.start_fullscreen_ui_fullscreen = true; continue; } else if (CHECK_ARG("-nofullscreen")) { INFO_LOG("Command Line: Not using fullscreen."); AutoBoot(autoboot)->override_fullscreen = false; continue; } else if (CHECK_ARG("-earlyconsole")) { InitializeEarlyConsole(); continue; } else if (CHECK_ARG_PARAM("-prerotation")) { const char* prerotation_str = argv[++i]; if (std::strcmp(prerotation_str, "0") == 0 || StringUtil::EqualNoCase(prerotation_str, "identity")) { INFO_LOG("Command Line: Forcing surface pre-rotation to identity."); s_state.force_prerotation = WindowInfo::PreRotation::Identity; } else if (std::strcmp(prerotation_str, "90") == 0) { INFO_LOG("Command Line: Forcing surface pre-rotation to 90 degrees clockwise."); s_state.force_prerotation = WindowInfo::PreRotation::Rotate90Clockwise; } else if (std::strcmp(prerotation_str, "180") == 0) { INFO_LOG("Command Line: Forcing surface pre-rotation to 180 degrees clockwise."); s_state.force_prerotation = WindowInfo::PreRotation::Rotate180Clockwise; } else if (std::strcmp(prerotation_str, "270") == 0) { INFO_LOG("Command Line: Forcing surface pre-rotation to 270 degrees clockwise."); s_state.force_prerotation = WindowInfo::PreRotation::Rotate270Clockwise; } else { ERROR_LOG("Invalid prerotation value: {}", prerotation_str); return false; } continue; } else if (CHECK_ARG("--")) { no_more_args = true; continue; } else if (argv[i][0] == '-') { Host::ReportFatalError("Error", fmt::format("Unknown parameter: {}", argv[i])); return false; } #undef CHECK_ARG #undef CHECK_ARG_PARAM } if (autoboot && !autoboot->path.empty()) autoboot->path += ' '; AutoBoot(autoboot)->path += argv[i]; } // To do anything useful, we need the config initialized. if (!InitializeConfig()) { // NOTE: No point translating this, because no config means the language won't be loaded anyway. Host::ReportFatalError("Error", "Failed to initialize config."); return EXIT_FAILURE; } // Check the file we're starting actually exists. if (autoboot && !autoboot->path.empty() && !FileSystem::FileExists(autoboot->path.c_str())) { Host::ReportFatalError("Error", fmt::format("File '{}' does not exist.", autoboot->path)); return false; } if (state_index.has_value()) { AutoBoot(autoboot); if (autoboot->path.empty()) { // loading global state, -1 means resume the last game if (state_index.value() < 0) autoboot->save_state = System::GetMostRecentResumeSaveStatePath(); else autoboot->save_state = System::GetGlobalSaveStatePath(state_index.value()); } else { // loading game state const std::string game_serial(GameDatabase::GetSerialForPath(autoboot->path.c_str())); autoboot->save_state = System::GetGameSaveStatePath(game_serial, state_index.value()); } if (autoboot->save_state.empty() || !FileSystem::FileExists(autoboot->save_state.c_str())) { Host::ReportFatalError("Error", "The specified save state does not exist."); return false; } } // check autoboot parameters, if we set something like fullscreen without a bios // or disc, we don't want to actually start. if (autoboot && autoboot->path.empty() && autoboot->save_state.empty() && !starting_bios) autoboot.reset(); // if we don't have autoboot, we definitely don't want batch mode (because that'll skip // scanning the game list). if (s_state.batch_mode) { if (!autoboot) { Host::ReportFatalError("Error", "Cannot use batch mode, because no boot filename was specified."); return false; } // if using batch mode, immediately refresh the game list so the data is available GameList::Refresh(false, true); } return true; } #include int main(int argc, char* argv[]) { using namespace MiniHost; CrashHandler::Install(&Bus::CleanupMemoryMap); if (!PerformEarlyHardwareChecks()) return EXIT_FAILURE; if (!SDL_InitSubSystem(SDL_INIT_VIDEO | SDL_INIT_EVENTS)) { Host::ReportFatalError("Error", TinyString::from_format("SDL_InitSubSystem() failed: {}", SDL_GetError())); return EXIT_FAILURE; } s_state.func_event_id = SDL_RegisterEvents(1); if (s_state.func_event_id == static_cast(-1)) { Host::ReportFatalError("Error", TinyString::from_format("SDL_RegisterEvents() failed: {}", SDL_GetError())); return EXIT_FAILURE; } if (!EarlyProcessStartup()) return EXIT_FAILURE; std::optional autoboot; if (!ParseCommandLineParametersAndInitializeConfig(argc, argv, autoboot)) return EXIT_FAILURE; // the rest of initialization happens on the CPU thread. HookSignals(); // prevent input source polling on CPU thread... SDLInputSource::ALLOW_EVENT_POLLING = false; s_state.ui_thread_running = true; StartCPUThread(); // process autoboot early, that way we can set the fullscreen flag if (autoboot) { s_state.start_fullscreen_ui_fullscreen = s_state.start_fullscreen_ui_fullscreen || autoboot->override_fullscreen.value_or(false); Host::RunOnCPUThread([params = std::move(autoboot.value())]() mutable { Error error; if (!System::BootSystem(std::move(params), &error)) Host::ReportErrorAsync("Failed to boot system", error.GetDescription()); }); } UIThreadMainLoop(); StopCPUThread(); System::ProcessShutdown(); // Ensure log is flushed. Log::SetFileOutputParams(false, nullptr); s_state.base_settings_interface.reset(); SDL_QuitSubSystem(SDL_INIT_VIDEO | SDL_INIT_EVENTS); return EXIT_SUCCESS; }