From 37fa0d135cbbdf1f5d40388bfb6365197646b870 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Sat, 4 Jan 2025 15:33:48 +1000 Subject: [PATCH] Mini: Add duckstation-mini interface --- CMakeModules/DuckStationBuildOptions.cmake | 2 +- duckstation.sln | 24 + src/CMakeLists.txt | 8 +- src/duckstation-mini/CMakeLists.txt | 20 + src/duckstation-mini/duckstation-mini.aps | Bin 0 -> 103204 bytes src/duckstation-mini/duckstation-mini.ico | Bin 0 -> 113683 bytes .../duckstation-mini.manifest | 17 + src/duckstation-mini/duckstation-mini.rc | 110 + src/duckstation-mini/duckstation-mini.vcxproj | 38 + .../duckstation-mini.vcxproj.filters | 19 + src/duckstation-mini/mini_host.cpp | 1819 +++++++++++++++++ src/duckstation-mini/resource.h | 16 + src/duckstation-mini/sdl_key_names.h | 266 +++ 13 files changed, 2334 insertions(+), 5 deletions(-) create mode 100644 src/duckstation-mini/CMakeLists.txt create mode 100644 src/duckstation-mini/duckstation-mini.aps create mode 100644 src/duckstation-mini/duckstation-mini.ico create mode 100644 src/duckstation-mini/duckstation-mini.manifest create mode 100644 src/duckstation-mini/duckstation-mini.rc create mode 100644 src/duckstation-mini/duckstation-mini.vcxproj create mode 100644 src/duckstation-mini/duckstation-mini.vcxproj.filters create mode 100644 src/duckstation-mini/mini_host.cpp create mode 100644 src/duckstation-mini/resource.h create mode 100644 src/duckstation-mini/sdl_key_names.h diff --git a/CMakeModules/DuckStationBuildOptions.cmake b/CMakeModules/DuckStationBuildOptions.cmake index 0bc28c52a..c1b7478c6 100644 --- a/CMakeModules/DuckStationBuildOptions.cmake +++ b/CMakeModules/DuckStationBuildOptions.cmake @@ -1,8 +1,8 @@ # Renderer options. option(ENABLE_OPENGL "Build with OpenGL renderer" ON) option(ENABLE_VULKAN "Build with Vulkan renderer" ON) -option(BUILD_NOGUI_FRONTEND "Build the NoGUI frontend" OFF) option(BUILD_QT_FRONTEND "Build the Qt frontend" ON) +option(BUILD_MINI_FRONTEND "Build the Mini frontend" OFF) option(BUILD_REGTEST "Build regression test runner" OFF) option(BUILD_TESTS "Build unit tests" OFF) option(DISABLE_SSE4 "Build with SSE4 instructions disabled, reduces performance" OFF) diff --git a/duckstation.sln b/duckstation.sln index d168d2f34..a9b5a816d 100644 --- a/duckstation.sln +++ b/duckstation.sln @@ -55,6 +55,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "reshadefx", "dep\reshadefx\ EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "rapidyaml", "dep\rapidyaml\rapidyaml.vcxproj", "{1AD23A8A-4C20-434C-AE6B-0E07759EEB1E}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "duckstation-mini", "src\duckstation-mini\duckstation-mini.vcxproj", "{FA259BC0-1007-4FD9-8A47-87CC0ECB8445}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|ARM64 = Debug|ARM64 @@ -967,6 +969,28 @@ Global {1AD23A8A-4C20-434C-AE6B-0E07759EEB1E}.ReleaseLTCG-Clang-SSE2|ARM64.ActiveCfg = ReleaseLTCG-Clang|ARM64 {1AD23A8A-4C20-434C-AE6B-0E07759EEB1E}.ReleaseLTCG-Clang-SSE2|x64.ActiveCfg = ReleaseLTCG-Clang-SSE2|x64 {1AD23A8A-4C20-434C-AE6B-0E07759EEB1E}.ReleaseLTCG-Clang-SSE2|x64.Build.0 = ReleaseLTCG-Clang-SSE2|x64 + {FA259BC0-1007-4FD9-8A47-87CC0ECB8445}.Debug|ARM64.ActiveCfg = Debug-Clang|ARM64 + {FA259BC0-1007-4FD9-8A47-87CC0ECB8445}.Debug|x64.ActiveCfg = Debug|x64 + {FA259BC0-1007-4FD9-8A47-87CC0ECB8445}.Debug-Clang|ARM64.ActiveCfg = Debug-Clang|ARM64 + {FA259BC0-1007-4FD9-8A47-87CC0ECB8445}.Debug-Clang|x64.ActiveCfg = Debug-Clang|x64 + {FA259BC0-1007-4FD9-8A47-87CC0ECB8445}.Debug-Clang-SSE2|ARM64.ActiveCfg = Debug-Clang|ARM64 + {FA259BC0-1007-4FD9-8A47-87CC0ECB8445}.Debug-Clang-SSE2|x64.ActiveCfg = Debug-Clang-SSE2|x64 + {FA259BC0-1007-4FD9-8A47-87CC0ECB8445}.DebugFast|ARM64.ActiveCfg = DebugFast-Clang|ARM64 + {FA259BC0-1007-4FD9-8A47-87CC0ECB8445}.DebugFast|x64.ActiveCfg = DebugFast|x64 + {FA259BC0-1007-4FD9-8A47-87CC0ECB8445}.DebugFast-Clang|ARM64.ActiveCfg = DebugFast-Clang|ARM64 + {FA259BC0-1007-4FD9-8A47-87CC0ECB8445}.DebugFast-Clang|x64.ActiveCfg = DebugFast-Clang|x64 + {FA259BC0-1007-4FD9-8A47-87CC0ECB8445}.Devel-Clang|ARM64.ActiveCfg = Devel-Clang|ARM64 + {FA259BC0-1007-4FD9-8A47-87CC0ECB8445}.Devel-Clang|x64.ActiveCfg = Devel-Clang|x64 + {FA259BC0-1007-4FD9-8A47-87CC0ECB8445}.Release|ARM64.ActiveCfg = Release-Clang|ARM64 + {FA259BC0-1007-4FD9-8A47-87CC0ECB8445}.Release|x64.ActiveCfg = Release|x64 + {FA259BC0-1007-4FD9-8A47-87CC0ECB8445}.Release-Clang|ARM64.ActiveCfg = Release-Clang|ARM64 + {FA259BC0-1007-4FD9-8A47-87CC0ECB8445}.Release-Clang|x64.ActiveCfg = Release-Clang|x64 + {FA259BC0-1007-4FD9-8A47-87CC0ECB8445}.ReleaseLTCG|ARM64.ActiveCfg = ReleaseLTCG-Clang|ARM64 + {FA259BC0-1007-4FD9-8A47-87CC0ECB8445}.ReleaseLTCG|x64.ActiveCfg = ReleaseLTCG|x64 + {FA259BC0-1007-4FD9-8A47-87CC0ECB8445}.ReleaseLTCG-Clang|ARM64.ActiveCfg = ReleaseLTCG-Clang|ARM64 + {FA259BC0-1007-4FD9-8A47-87CC0ECB8445}.ReleaseLTCG-Clang|x64.ActiveCfg = ReleaseLTCG-Clang|x64 + {FA259BC0-1007-4FD9-8A47-87CC0ECB8445}.ReleaseLTCG-Clang-SSE2|ARM64.ActiveCfg = ReleaseLTCG-Clang|ARM64 + {FA259BC0-1007-4FD9-8A47-87CC0ECB8445}.ReleaseLTCG-Clang-SSE2|x64.ActiveCfg = ReleaseLTCG-Clang-SSE2|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 879d46bc7..61d4d7e97 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -7,14 +7,14 @@ if(WIN32 OR APPLE) add_subdirectory(updater) endif() -if(BUILD_NOGUI_FRONTEND) - add_subdirectory(duckstation-nogui) -endif() - if(BUILD_QT_FRONTEND) add_subdirectory(duckstation-qt) endif() +if(BUILD_MINI_FRONTEND) + add_subdirectory(duckstation-mini) +endif() + if(BUILD_REGTEST) add_subdirectory(duckstation-regtest) endif() diff --git a/src/duckstation-mini/CMakeLists.txt b/src/duckstation-mini/CMakeLists.txt new file mode 100644 index 000000000..ffc9272ab --- /dev/null +++ b/src/duckstation-mini/CMakeLists.txt @@ -0,0 +1,20 @@ +add_executable(duckstation-mini + mini_host.cpp + sdl_key_names.h +) + +target_link_libraries(duckstation-mini PRIVATE core util common imgui scmversion SDL3::SDL3) + +add_core_resources(duckstation-mini) + +if(WIN32) + target_sources(duckstation-mini PRIVATE + duckstation-mini.manifest + resource.h + ) + + # We want a Windows subsystem application not console. + set_target_properties(duckstation-mini PROPERTIES + WIN32_EXECUTABLE TRUE + DEBUG_POSTFIX "-debug") +endif() diff --git a/src/duckstation-mini/duckstation-mini.aps b/src/duckstation-mini/duckstation-mini.aps new file mode 100644 index 0000000000000000000000000000000000000000..147191914aa28fe649c00fe6eee012fd361ae22b GIT binary patch literal 103204 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%0AwPho2YG%&MmbBnS1}*uz_$RM)0CeCe&&vY2 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 + + + + PerMonitorV2 + true + + + + + + + + + + + \ No newline at end of file diff --git a/src/duckstation-mini/duckstation-mini.rc b/src/duckstation-mini/duckstation-mini.rc new file mode 100644 index 000000000..b4ad1577c --- /dev/null +++ b/src/duckstation-mini/duckstation-mini.rc @@ -0,0 +1,110 @@ +// Microsoft Visual C++ generated resource script. +// +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (Australia) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENA) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_AUS +#pragma code_page(1252) + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +VS_VERSION_INFO VERSIONINFO + FILEVERSION 1,0,0,1 + PRODUCTVERSION 1,0,0,1 + FILEFLAGSMASK 0x3fL +#ifdef _DEBUG + FILEFLAGS 0x1L +#else + FILEFLAGS 0x0L +#endif + FILEOS 0x40004L + FILETYPE 0x1L + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "0c0904b0" + BEGIN + VALUE "CompanyName", "https://github.com/stenzek/duckstation" + VALUE "FileDescription", "DuckStation PS1 Emulator" + VALUE "FileVersion", "1.0.0.1" + VALUE "InternalName", "duckstation-mini.exe" + VALUE "LegalCopyright", "Copyright (C) 2020-2025 Stenzek and collaborators" + VALUE "OriginalFilename", "duckstation-mini.exe" + VALUE "ProductName", "DuckStation" + VALUE "ProductVersion", "1.0.0.1" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0xc09, 1200 + END +END + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_ICON1 ICON "duckstation-mini.ico" + +#endif // English (Australia) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED + diff --git a/src/duckstation-mini/duckstation-mini.vcxproj b/src/duckstation-mini/duckstation-mini.vcxproj new file mode 100644 index 000000000..33f1c4657 --- /dev/null +++ b/src/duckstation-mini/duckstation-mini.vcxproj @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + {ee054e08-3799-4a59-a422-18259c105ffd} + + + {868b98c8-65a1-494b-8346-250a73a48c0a} + + + {57f6206d-f264-4b07-baf8-11b9bbe1f455} + + + + + + + {FA259BC0-1007-4FD9-8A47-87CC0ECB8445} + duckstation-mini + + + + + \ No newline at end of file diff --git a/src/duckstation-mini/duckstation-mini.vcxproj.filters b/src/duckstation-mini/duckstation-mini.vcxproj.filters new file mode 100644 index 000000000..b9e8cb727 --- /dev/null +++ b/src/duckstation-mini/duckstation-mini.vcxproj.filters @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/duckstation-mini/mini_host.cpp b/src/duckstation-mini/mini_host.cpp new file mode 100644 index 000000000..087d84f7e --- /dev/null +++ b/src/duckstation-mini/mini_host.cpp @@ -0,0 +1,1819 @@ +// 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(std::string settings_filename); +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 void ProcessCPUThreadEvents(bool block); +static bool PerformEarlyHardwareChecks(); +static bool EarlyProcessStartup(); +static void WarnAboutInterface(); +static void RunOnUIThread(std::function func); +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() +{ + // Already set, e.g. by -portable. + if (EmuFolders::DataRoot.empty()) + 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(std::string settings_filename) +{ + if (!SetCriticalFolders()) + return false; + + if (settings_filename.empty()) + settings_filename = Path::Combine(EmuFolders::DataRoot, "settings.ini"); + + const bool settings_exists = FileSystem::FileExists(settings_filename.c_str()); + INFO_LOG("Loading config from {}.", settings_filename); + s_state.base_settings_interface = std::make_unique(std::move(settings_filename)); + 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; + + MiniHost::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; + + MiniHost::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) + { + Host::RunOnCPUThread([button = ev->button.button - 1, 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::RunOnUIThread(std::function func) +{ + std::function* pfunc = new std::function(std::move(func)); + + SDL_Event ev; + ev.user = {}; + ev.type = s_state.func_event_id; + ev.user.data1 = pfunc; + SDL_PushEvent(&ev); +} + +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. + 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("CPU 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 +} + +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::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; + }); +} + +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::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) +{ + // TODO: Confirm + if (System::IsValid()) + { + Host::RunOnCPUThread([save_state]() { System::ShutdownSystem(save_state); }); + } +} + +void Host::ReportFatalError(std::string_view title, std::string_view message) +{ + 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) +{ + 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()); +} + +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, " -portable: Forces \"portable mode\", data in same directory.\n"); + std::fprintf(stderr, " -settings : Loads a custom settings configuration from the\n" + " specified filename. Default settings applied if file not found.\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; + std::string settings_filename; + 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("-portable")) + { + INFO_LOG("Command Line: Using portable mode."); + EmuFolders::DataRoot = EmuFolders::AppRoot; + continue; + } + else if (CHECK_ARG_PARAM("-settings")) + { + settings_filename = argv[++i]; + INFO_LOG("Command Line: Overriding settings filename: {}", settings_filename); + 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->filename.empty()) + autoboot->filename += ' '; + AutoBoot(autoboot)->filename += argv[i]; + } + + // To do anything useful, we need the config initialized. + if (!InitializeConfig(std::move(settings_filename))) + { + // 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->filename.empty() && !FileSystem::FileExists(autoboot->filename.c_str())) + { + Host::ReportFatalError("Error", fmt::format("File '{}' does not exist.", autoboot->filename)); + return false; + } + + if (state_index.has_value()) + { + AutoBoot(autoboot); + + if (autoboot->filename.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::GetGlobalSaveStateFileName(state_index.value()); + } + else + { + // loading game state + const std::string game_serial(GameDatabase::GetSerialForPath(autoboot->filename.c_str())); + autoboot->save_state = System::GetGameSaveStateFileName(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->filename.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; +} diff --git a/src/duckstation-mini/resource.h b/src/duckstation-mini/resource.h new file mode 100644 index 000000000..966189314 --- /dev/null +++ b/src/duckstation-mini/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by duckstation-sdl.rc +// +#define IDI_ICON1 102 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 103 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/src/duckstation-mini/sdl_key_names.h b/src/duckstation-mini/sdl_key_names.h new file mode 100644 index 000000000..7e7898708 --- /dev/null +++ b/src/duckstation-mini/sdl_key_names.h @@ -0,0 +1,266 @@ +// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin +// SPDX-License-Identifier: CC-BY-NC-ND-4.0 + +#pragma once + +#include "common/types.h" + +#include +#include +#include +#include +#include + +#include "SDL3/SDL.h" + +namespace SDLKeyNames { + +static const std::map s_sdl_key_names = { + {SDLK_RETURN, "Return"}, + {SDLK_ESCAPE, "Escape"}, + {SDLK_BACKSPACE, "Backspace"}, + {SDLK_TAB, "Tab"}, + {SDLK_SPACE, "Space"}, + {SDLK_EXCLAIM, "Exclam"}, + {SDLK_DBLAPOSTROPHE, "QuoteDbl"}, + {SDLK_HASH, "Hash"}, + {SDLK_PERCENT, "Percent"}, + {SDLK_DOLLAR, "Dollar"}, + {SDLK_AMPERSAND, "Ampersand"}, + {SDLK_APOSTROPHE, "Apostrophe"}, + {SDLK_LEFTPAREN, "ParenLeft"}, + {SDLK_RIGHTPAREN, "ParenRight"}, + {SDLK_ASTERISK, "Asterisk"}, + {SDLK_PLUS, "PLus"}, + {SDLK_COMMA, "Comma"}, + {SDLK_MINUS, "Minus"}, + {SDLK_PERIOD, "Period"}, + {SDLK_SLASH, "Slash"}, + {SDLK_0, "0"}, + {SDLK_1, "1"}, + {SDLK_2, "2"}, + {SDLK_3, "3"}, + {SDLK_4, "4"}, + {SDLK_5, "5"}, + {SDLK_6, "6"}, + {SDLK_7, "7"}, + {SDLK_8, "8"}, + {SDLK_9, "9"}, + {SDLK_COLON, "Colon"}, + {SDLK_SEMICOLON, "Semcolon"}, + {SDLK_LESS, "Less"}, + {SDLK_EQUALS, "Equal"}, + {SDLK_GREATER, "Greater"}, + {SDLK_QUESTION, "Question"}, + {SDLK_AT, "AT"}, + {SDLK_LEFTBRACKET, "BracketLeft"}, + {SDLK_BACKSLASH, "Backslash"}, + {SDLK_RIGHTBRACKET, "BracketRight"}, + {SDLK_CARET, "Caret"}, + {SDLK_UNDERSCORE, "Underscore"}, + {SDLK_GRAVE, "Backquote"}, + {SDLK_A, "A"}, + {SDLK_B, "B"}, + {SDLK_C, "C"}, + {SDLK_D, "D"}, + {SDLK_E, "E"}, + {SDLK_F, "F"}, + {SDLK_G, "G"}, + {SDLK_H, "H"}, + {SDLK_I, "I"}, + {SDLK_J, "J"}, + {SDLK_K, "K"}, + {SDLK_L, "L"}, + {SDLK_M, "M"}, + {SDLK_N, "N"}, + {SDLK_O, "O"}, + {SDLK_P, "P"}, + {SDLK_Q, "Q"}, + {SDLK_R, "R"}, + {SDLK_S, "S"}, + {SDLK_T, "T"}, + {SDLK_U, "U"}, + {SDLK_V, "V"}, + {SDLK_W, "W"}, + {SDLK_X, "X"}, + {SDLK_Y, "Y"}, + {SDLK_Z, "Z"}, + {SDLK_CAPSLOCK, "CapsLock"}, + {SDLK_F1, "F1"}, + {SDLK_F2, "F2"}, + {SDLK_F3, "F3"}, + {SDLK_F4, "F4"}, + {SDLK_F5, "F5"}, + {SDLK_F6, "F6"}, + {SDLK_F7, "F7"}, + {SDLK_F8, "F8"}, + {SDLK_F9, "F9"}, + {SDLK_F10, "F10"}, + {SDLK_F11, "F11"}, + {SDLK_F12, "F12"}, + {SDLK_PRINTSCREEN, "Print"}, + {SDLK_SCROLLLOCK, "ScrollLock"}, + {SDLK_PAUSE, "Pause"}, + {SDLK_INSERT, "Insert"}, + {SDLK_HOME, "Home"}, + {SDLK_PAGEUP, "PageUp"}, + {SDLK_DELETE, "Delete"}, + {SDLK_END, "End"}, + {SDLK_PAGEDOWN, "PageDown"}, + {SDLK_RIGHT, "Right"}, + {SDLK_LEFT, "Left"}, + {SDLK_DOWN, "Down"}, + {SDLK_UP, "Up"}, + {SDLK_NUMLOCKCLEAR, "NumLock"}, + {SDLK_KP_DIVIDE, "Keypad+Divide"}, + {SDLK_KP_MULTIPLY, "Keypad+Multiply"}, + {SDLK_KP_MINUS, "Keypad+Minus"}, + {SDLK_KP_PLUS, "Keypad+Plus"}, + {SDLK_KP_ENTER, "Keypad+Return"}, + {SDLK_KP_1, "Keypad+1"}, + {SDLK_KP_2, "Keypad+2"}, + {SDLK_KP_3, "Keypad+3"}, + {SDLK_KP_4, "Keypad+4"}, + {SDLK_KP_5, "Keypad+5"}, + {SDLK_KP_6, "Keypad+6"}, + {SDLK_KP_7, "Keypad+7"}, + {SDLK_KP_8, "Keypad+8"}, + {SDLK_KP_9, "Keypad+9"}, + {SDLK_KP_0, "Keypad+0"}, + {SDLK_KP_PERIOD, "Keypad+Period"}, + {SDLK_APPLICATION, "Application"}, + {SDLK_POWER, "Power"}, + {SDLK_KP_EQUALS, "Keypad+Equal"}, + {SDLK_F13, "F13"}, + {SDLK_F14, "F14"}, + {SDLK_F15, "F15"}, + {SDLK_F16, "F16"}, + {SDLK_F17, "F17"}, + {SDLK_F18, "F18"}, + {SDLK_F19, "F19"}, + {SDLK_F20, "F20"}, + {SDLK_F21, "F21"}, + {SDLK_F22, "F22"}, + {SDLK_F23, "F23"}, + {SDLK_F24, "F24"}, + {SDLK_EXECUTE, "Execute"}, + {SDLK_HELP, "Help"}, + {SDLK_MENU, "Menu"}, + {SDLK_SELECT, "Select"}, + {SDLK_STOP, "Stop"}, + {SDLK_AGAIN, "Again"}, + {SDLK_UNDO, "Undo"}, + {SDLK_CUT, "Cut"}, + {SDLK_COPY, "Copy"}, + {SDLK_PASTE, "Paste"}, + {SDLK_FIND, "Find"}, + {SDLK_MUTE, "Mute"}, + {SDLK_VOLUMEUP, "VolumeUp"}, + {SDLK_VOLUMEDOWN, "VolumeDown"}, + {SDLK_KP_COMMA, "Keypad+Comma"}, + {SDLK_KP_EQUALSAS400, "Keypad+EqualAS400"}, + {SDLK_ALTERASE, "AltErase"}, + {SDLK_SYSREQ, "SysReq"}, + {SDLK_CANCEL, "Cancel"}, + {SDLK_CLEAR, "Clear"}, + {SDLK_PRIOR, "Prior"}, + {SDLK_RETURN2, "Return2"}, + {SDLK_SEPARATOR, "Separator"}, + {SDLK_OUT, "Out"}, + {SDLK_OPER, "Oper"}, + {SDLK_CLEARAGAIN, "ClearAgain"}, + {SDLK_CRSEL, "CrSel"}, + {SDLK_EXSEL, "ExSel"}, + {SDLK_KP_00, "Keypad+00"}, + {SDLK_KP_000, "Keypad+000"}, + {SDLK_THOUSANDSSEPARATOR, "ThousandsSeparator"}, + {SDLK_DECIMALSEPARATOR, "DecimalSeparator"}, + {SDLK_CURRENCYUNIT, "CurrencyUnit"}, + {SDLK_CURRENCYSUBUNIT, "CurrencySubunit"}, + {SDLK_KP_LEFTPAREN, "Keypad+ParenLeft"}, + {SDLK_KP_RIGHTPAREN, "Keypad+ParenRight"}, + {SDLK_KP_LEFTBRACE, "Keypad+LeftBrace"}, + {SDLK_KP_RIGHTBRACE, "Keypad+RightBrace"}, + {SDLK_KP_TAB, "Keypad+Tab"}, + {SDLK_KP_BACKSPACE, "Keypad+Backspace"}, + {SDLK_KP_A, "Keypad+A"}, + {SDLK_KP_B, "Keypad+B"}, + {SDLK_KP_C, "Keypad+C"}, + {SDLK_KP_D, "Keypad+D"}, + {SDLK_KP_E, "Keypad+E"}, + {SDLK_KP_F, "Keypad+F"}, + {SDLK_KP_XOR, "Keypad+XOR"}, + {SDLK_KP_POWER, "Keypad+Power"}, + {SDLK_KP_PERCENT, "Keypad+Percent"}, + {SDLK_KP_LESS, "Keypad+Less"}, + {SDLK_KP_GREATER, "Keypad+Greater"}, + {SDLK_KP_AMPERSAND, "Keypad+Ampersand"}, + {SDLK_KP_DBLAMPERSAND, "Keypad+AmpersandDbl"}, + {SDLK_KP_VERTICALBAR, "Keypad+Bar"}, + {SDLK_KP_DBLVERTICALBAR, "Keypad+BarDbl"}, + {SDLK_KP_COLON, "Keypad+Colon"}, + {SDLK_KP_HASH, "Keypad+Hash"}, + {SDLK_KP_SPACE, "Keypad+Space"}, + {SDLK_KP_AT, "Keypad+At"}, + {SDLK_KP_EXCLAM, "Keypad+Exclam"}, + {SDLK_KP_MEMSTORE, "Keypad+MemStore"}, + {SDLK_KP_MEMRECALL, "Keypad+MemRecall"}, + {SDLK_KP_MEMCLEAR, "Keypad+MemClear"}, + {SDLK_KP_MEMADD, "Keypad+MemAdd"}, + {SDLK_KP_MEMSUBTRACT, "Keypad+MemSubtract"}, + {SDLK_KP_MEMMULTIPLY, "Keypad+MemMultiply"}, + {SDLK_KP_MEMDIVIDE, "Keypad+MemDivide"}, + {SDLK_KP_PLUSMINUS, "Keypad+PlusMinus"}, + {SDLK_KP_CLEAR, "Keypad+Clear"}, + {SDLK_KP_CLEARENTRY, "Keypad+ClearEntry"}, + {SDLK_KP_BINARY, "Keypad+Binary"}, + {SDLK_KP_OCTAL, "Keypad+Octal"}, + {SDLK_KP_DECIMAL, "Keypad+Decimal"}, + {SDLK_KP_HEXADECIMAL, "Keypad+Hexadecimal"}, + {SDLK_LCTRL, "LeftControl"}, + {SDLK_LSHIFT, "LeftShift"}, + {SDLK_LALT, "LeftAlt"}, + {SDLK_LGUI, "Super_L"}, + {SDLK_RCTRL, "RightCtrl"}, + {SDLK_RSHIFT, "RightShift"}, + {SDLK_RALT, "RightAlt"}, + {SDLK_RGUI, "RightSuper"}, + {SDLK_MODE, "Mode"}, + {SDLK_MEDIA_NEXT_TRACK, "MediaNext"}, + {SDLK_MEDIA_PREVIOUS_TRACK, "MediaPrevious"}, + {SDLK_MEDIA_STOP, "MediaStop"}, + {SDLK_MEDIA_PLAY, "MediaPlay"}, + {SDLK_MEDIA_PLAY_PAUSE, "MediaPlayPause"}, + {SDLK_MEDIA_SELECT, "MediaSelect"}, + {SDLK_MEDIA_REWIND, "MediaRewind"}, + {SDLK_MEDIA_FAST_FORWARD, "MediaFastForward"}, + {SDLK_MUTE, "VolumeMute"}, + {SDLK_AC_SEARCH, "Search"}, + {SDLK_AC_HOME, "Home"}, + {SDLK_AC_BACK, "Back"}, + {SDLK_AC_FORWARD, "Forward"}, + {SDLK_AC_STOP, "Stop"}, + {SDLK_AC_REFRESH, "Refresh"}, + {SDLK_AC_BOOKMARKS, "Bookmarks"}, + {SDLK_MEDIA_EJECT, "Eject"}, + {SDLK_SLEEP, "Sleep"}, +}; + +static const char* GetKeyName(u32 key) +{ + const auto it = s_sdl_key_names.find(key); + return it == s_sdl_key_names.end() ? nullptr : it->second; +} + +static std::optional GetKeyCodeForName(const std::string_view& key_name) +{ + for (const auto& it : s_sdl_key_names) + { + if (key_name == it.second) + return it.first; + } + + return std::nullopt; +} + +} // namespace SDLKeyNames