Path: blob/master/src/duckstation-mini/mini_host.cpp
4802 views
// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin <[email protected]>1// SPDX-License-Identifier: CC-BY-NC-ND-4.023#include "scmversion/scmversion.h"45#include "core/achievements.h"6#include "core/bus.h"7#include "core/controller.h"8#include "core/fullscreen_ui.h"9#include "core/game_list.h"10#include "core/gpu.h"11#include "core/gpu_backend.h"12#include "core/gpu_thread.h"13#include "core/host.h"14#include "core/imgui_overlays.h"15#include "core/settings.h"16#include "core/system.h"17#include "core/system_private.h"1819#include "util/gpu_device.h"20#include "util/imgui_fullscreen.h"21#include "util/imgui_manager.h"22#include "util/ini_settings_interface.h"23#include "util/input_manager.h"24#include "util/platform_misc.h"25#include "util/sdl_input_source.h"2627#include "imgui.h"28#include "imgui_internal.h"29#include "imgui_stdlib.h"3031#include "common/assert.h"32#include "common/crash_handler.h"33#include "common/error.h"34#include "common/file_system.h"35#include "common/log.h"36#include "common/path.h"37#include "common/string_util.h"38#include "common/threading.h"39#include "common/time_helpers.h"4041#include "IconsEmoji.h"42#include "fmt/format.h"4344#include <SDL3/SDL.h>45#include <cinttypes>46#include <cmath>47#include <condition_variable>48#include <csignal>49#include <ctime>50#include <thread>5152LOG_CHANNEL(Host);5354namespace MiniHost {5556/// Use two async worker threads, should be enough for most tasks.57static constexpr u32 NUM_ASYNC_WORKER_THREADS = 2;5859// static constexpr u32 DEFAULT_WINDOW_WIDTH = 1280;60// static constexpr u32 DEFAULT_WINDOW_HEIGHT = 720;61static constexpr u32 DEFAULT_WINDOW_WIDTH = 1920;62static constexpr u32 DEFAULT_WINDOW_HEIGHT = 1080;6364static constexpr u32 SETTINGS_VERSION = 3;65static constexpr auto CPU_THREAD_POLL_INTERVAL =66std::chrono::milliseconds(8); // how often we'll poll controllers when paused6768static bool ParseCommandLineParametersAndInitializeConfig(int argc, char* argv[],69std::optional<SystemBootParameters>& autoboot);70static void PrintCommandLineVersion();71static void PrintCommandLineHelp(const char* progname);72static bool InitializeConfig();73static void InitializeEarlyConsole();74static void HookSignals();75static void SetAppRoot();76static void SetResourcesDirectory();77static bool SetDataDirectory();78static bool SetCriticalFolders();79static void SetDefaultSettings(SettingsInterface& si, bool system, bool controller);80static std::string GetResourcePath(std::string_view name, bool allow_override);81static bool PerformEarlyHardwareChecks();82static bool EarlyProcessStartup();83static void WarnAboutInterface();84static void StartCPUThread();85static void StopCPUThread();86static void ProcessCPUThreadEvents(bool block);87static void ProcessCPUThreadPlatformMessages();88static void CPUThreadEntryPoint();89static void CPUThreadMainLoop();90static void GPUThreadEntryPoint();91static void UIThreadMainLoop();92static void ProcessSDLEvent(const SDL_Event* ev);93static std::string GetWindowTitle(const std::string& game_title);94static std::optional<WindowInfo> TranslateSDLWindowInfo(SDL_Window* win, Error* error);95static bool GetSavedPlatformWindowGeometry(s32* x, s32* y, s32* width, s32* height);96static void SavePlatformWindowGeometry(s32 x, s32 y, s32 width, s32 height);9798struct SDLHostState99{100// UI thread state101ALIGN_TO_CACHE_LINE INISettingsInterface base_settings_interface;102bool batch_mode = false;103bool start_fullscreen_ui_fullscreen = false;104bool was_paused_by_focus_loss = false;105bool ui_thread_running = false;106107u32 func_event_id = 0;108109SDL_Window* sdl_window = nullptr;110float sdl_window_scale = 0.0f;111WindowInfo::PreRotation force_prerotation = WindowInfo::PreRotation::Identity;112std::atomic_bool fullscreen{false};113114Threading::Thread cpu_thread;115Threading::Thread gpu_thread;116Threading::KernelSemaphore platform_window_updated;117118std::mutex state_mutex;119FullscreenUI::BackgroundProgressCallback* game_list_refresh_progress = nullptr;120121// CPU thread state.122ALIGN_TO_CACHE_LINE std::atomic_bool cpu_thread_running{false};123std::mutex cpu_thread_events_mutex;124std::condition_variable cpu_thread_event_done;125std::condition_variable cpu_thread_event_posted;126std::deque<std::pair<std::function<void()>, bool>> cpu_thread_events;127u32 blocking_cpu_events_pending = 0;128};129130static SDLHostState s_state;131} // namespace MiniHost132133//////////////////////////////////////////////////////////////////////////134// Initialization/Shutdown135//////////////////////////////////////////////////////////////////////////136137bool MiniHost::PerformEarlyHardwareChecks()138{139Error error;140const bool okay = System::PerformEarlyHardwareChecks(&error);141if (okay && !error.IsValid()) [[likely]]142return true;143144if (okay)145Host::ReportErrorAsync("Hardware Check Warning", error.GetDescription());146else147Host::ReportFatalError("Hardware Check Failed", error.GetDescription());148149return okay;150}151152bool MiniHost::EarlyProcessStartup()153{154Error error;155if (!System::ProcessStartup(&error)) [[unlikely]]156{157Host::ReportFatalError("Process Startup Failed", error.GetDescription());158return false;159}160161#if !__has_include("scmversion/tag.h")162//163// To those distributing their own builds or packages of DuckStation, and seeing this message:164//165// DuckStation is licensed under the CC-BY-NC-ND-4.0 license.166//167// This means that you do NOT have permission to re-distribute your own modified builds of DuckStation.168// Modifying DuckStation for personal use is fine, but you cannot distribute builds with your changes.169// As per the CC-BY-NC-ND conditions, you can re-distribute the official builds from https://www.duckstation.org/ and170// https://github.com/stenzek/duckstation, so long as they are left intact, without modification. I welcome and171// appreciate any pull requests made to the official repository at https://github.com/stenzek/duckstation.172//173// I made the decision to switch to a no-derivatives license because of numerous "forks" that were created purely for174// generating money for the person who knocked it off, and always died, leaving the community with multiple builds to175// choose from, most of which were out of date and broken, and endless confusion. Other forks copy/pasted upstream176// changes without attribution, violating copyright.177//178// Thanks, and I hope you understand.179//180181const char* message = ICON_EMOJI_WARNING "WARNING! You are not using an official release! " ICON_EMOJI_WARNING "\n\n"182"DuckStation is licensed under the terms of CC-BY-NC-ND-4.0,\n"183"which does not allow modified builds to be distributed.\n\n"184"This build is NOT OFFICIAL and may be broken and/or malicious.\n\n"185"You should download an official build from https://www.duckstation.org/.";186187Host::AddKeyedOSDWarning("OfficialReleaseWarning", message, Host::OSD_CRITICAL_ERROR_DURATION);188#endif189190return true;191}192193bool MiniHost::SetCriticalFolders()194{195SetAppRoot();196SetResourcesDirectory();197if (!SetDataDirectory())198return false;199200// logging of directories in case something goes wrong super early201DEV_LOG("AppRoot Directory: {}", EmuFolders::AppRoot);202DEV_LOG("DataRoot Directory: {}", EmuFolders::DataRoot);203DEV_LOG("Resources Directory: {}", EmuFolders::Resources);204205// Write crash dumps to the data directory, since that'll be accessible for certain.206CrashHandler::SetWriteDirectory(EmuFolders::DataRoot);207208// the resources directory should exist, bail out if not209if (!FileSystem::DirectoryExists(EmuFolders::Resources.c_str()))210{211Host::ReportFatalError("Error", "Resources directory is missing, your installation is incomplete.");212return false;213}214215return true;216}217218void MiniHost::SetAppRoot()219{220const std::string program_path = FileSystem::GetProgramPath();221INFO_LOG("Program Path: {}", program_path);222223EmuFolders::AppRoot = Path::Canonicalize(Path::GetDirectory(program_path));224}225226void MiniHost::SetResourcesDirectory()227{228#ifndef __APPLE__229// On Windows/Linux, these are in the binary directory.230EmuFolders::Resources = Path::Combine(EmuFolders::AppRoot, "resources");231#else232// On macOS, this is in the bundle resources directory.233EmuFolders::Resources = Path::Canonicalize(Path::Combine(EmuFolders::AppRoot, "../Resources"));234#endif235}236237bool MiniHost::SetDataDirectory()238{239EmuFolders::DataRoot = Host::Internal::ComputeDataDirectory();240241// make sure it exists242if (!EmuFolders::DataRoot.empty() && !FileSystem::DirectoryExists(EmuFolders::DataRoot.c_str()))243{244// we're in trouble if we fail to create this directory... but try to hobble on with portable245Error error;246if (!FileSystem::EnsureDirectoryExists(EmuFolders::DataRoot.c_str(), false, &error))247{248Host::ReportFatalError("Error",249TinyString::from_format("Failed to create data directory: {}", error.GetDescription()));250return false;251}252}253254// couldn't determine the data directory? fallback to portable.255if (EmuFolders::DataRoot.empty())256EmuFolders::DataRoot = EmuFolders::AppRoot;257258return true;259}260261bool MiniHost::InitializeConfig()262{263if (!SetCriticalFolders())264return false;265266std::string settings_path = Path::Combine(EmuFolders::DataRoot, "settings.ini");267const bool settings_exists = FileSystem::FileExists(settings_path.c_str());268INFO_LOG("Loading config from {}.", settings_path);269s_state.base_settings_interface.SetPath(std::move(settings_path));270Host::Internal::SetBaseSettingsLayer(&s_state.base_settings_interface);271272u32 settings_version;273if (!settings_exists || !s_state.base_settings_interface.Load() ||274!s_state.base_settings_interface.GetUIntValue("Main", "SettingsVersion", &settings_version) ||275settings_version != SETTINGS_VERSION)276{277if (s_state.base_settings_interface.ContainsValue("Main", "SettingsVersion"))278{279// NOTE: No point translating this, because there's no config loaded, so no language loaded.280Host::ReportErrorAsync("Error", fmt::format("Settings version {} does not match expected version {}, resetting.",281settings_version, SETTINGS_VERSION));282}283284s_state.base_settings_interface.SetUIntValue("Main", "SettingsVersion", SETTINGS_VERSION);285SetDefaultSettings(s_state.base_settings_interface, true, true);286287// Make sure we can actually save the config, and the user doesn't have some permission issue.288Error error;289if (!s_state.base_settings_interface.Save(&error))290{291Host::ReportFatalError(292"Error",293fmt::format(294"Failed to save configuration to\n\n{}\n\nThe error was: {}\n\nPlease ensure this directory is writable. You "295"can also try portable mode by creating portable.txt in the same directory you installed DuckStation into.",296s_state.base_settings_interface.GetPath(), error.GetDescription()));297return false;298}299}300301EmuFolders::LoadConfig(s_state.base_settings_interface);302EmuFolders::EnsureFoldersExist();303304// We need to create the console window early, otherwise it appears in front of the main window.305if (!Log::IsConsoleOutputEnabled() && s_state.base_settings_interface.GetBoolValue("Logging", "LogToConsole", false))306Log::SetConsoleOutputParams(true, s_state.base_settings_interface.GetBoolValue("Logging", "LogTimestamps", true));307308return true;309}310311void MiniHost::SetDefaultSettings(SettingsInterface& si, bool system, bool controller)312{313if (system)314{315System::SetDefaultSettings(si);316EmuFolders::SetDefaults();317EmuFolders::Save(si);318}319320if (controller)321{322InputManager::SetDefaultSourceConfig(si);323Settings::SetDefaultControllerConfig(si);324Settings::SetDefaultHotkeyConfig(si);325}326}327328void Host::ReportDebuggerMessage(std::string_view message)329{330ERROR_LOG("ReportDebuggerMessage(): {}", message);331}332333std::span<const std::pair<const char*, const char*>> Host::GetAvailableLanguageList()334{335return {};336}337338const char* Host::GetLanguageName(std::string_view language_code)339{340return "";341}342343bool Host::ChangeLanguage(const char* new_language)344{345return false;346}347348void Host::AddFixedInputBindings(const SettingsInterface& si)349{350}351352void Host::OnInputDeviceConnected(InputBindingKey key, std::string_view identifier, std::string_view device_name)353{354Host::AddKeyedOSDMessage(fmt::format("InputDeviceConnected-{}", identifier),355fmt::format("Input device {0} ({1}) connected.", device_name, identifier), 10.0f);356}357358void Host::OnInputDeviceDisconnected(InputBindingKey key, std::string_view identifier)359{360Host::AddKeyedOSDMessage(fmt::format("InputDeviceConnected-{}", identifier),361fmt::format("Input device {} disconnected.", identifier), 10.0f);362}363364s32 Host::Internal::GetTranslatedStringImpl(std::string_view context, std::string_view msg,365std::string_view disambiguation, char* tbuf, size_t tbuf_space)366{367if (msg.size() > tbuf_space)368return -1;369else if (msg.empty())370return 0;371372std::memcpy(tbuf, msg.data(), msg.size());373return static_cast<s32>(msg.size());374}375376std::string Host::TranslatePluralToString(const char* context, const char* msg, const char* disambiguation, int count)377{378TinyString count_str = TinyString::from_format("{}", count);379380std::string ret(msg);381for (;;)382{383std::string::size_type pos = ret.find("%n");384if (pos == std::string::npos)385break;386387ret.replace(pos, pos + 2, count_str.view());388}389390return ret;391}392393SmallString Host::TranslatePluralToSmallString(const char* context, const char* msg, const char* disambiguation,394int count)395{396SmallString ret(msg);397ret.replace("%n", TinyString::from_format("{}", count));398return ret;399}400401std::string MiniHost::GetResourcePath(std::string_view filename, bool allow_override)402{403return allow_override ? EmuFolders::GetOverridableResourcePath(filename) :404Path::Combine(EmuFolders::Resources, filename);405}406407bool Host::ResourceFileExists(std::string_view filename, bool allow_override)408{409const std::string path = MiniHost::GetResourcePath(filename, allow_override);410return FileSystem::FileExists(path.c_str());411}412413std::optional<DynamicHeapArray<u8>> Host::ReadResourceFile(std::string_view filename, bool allow_override, Error* error)414{415const std::string path = MiniHost::GetResourcePath(filename, allow_override);416return FileSystem::ReadBinaryFile(path.c_str(), error);417}418419std::optional<std::string> Host::ReadResourceFileToString(std::string_view filename, bool allow_override, Error* error)420{421const std::string path = MiniHost::GetResourcePath(filename, allow_override);422return FileSystem::ReadFileToString(path.c_str(), error);423}424425std::optional<std::time_t> Host::GetResourceFileTimestamp(std::string_view filename, bool allow_override)426{427const std::string path = MiniHost::GetResourcePath(filename, allow_override);428FILESYSTEM_STAT_DATA sd;429if (!FileSystem::StatFile(path.c_str(), &sd))430{431ERROR_LOG("Failed to stat resource file '{}'", filename);432return std::nullopt;433}434435return sd.ModificationTime;436}437438void Host::LoadSettings(const SettingsInterface& si, std::unique_lock<std::mutex>& lock)439{440}441442void Host::CheckForSettingsChanges(const Settings& old_settings)443{444}445446void Host::CommitBaseSettingChanges()447{448auto lock = Host::GetSettingsLock();449Error error;450if (!MiniHost::s_state.base_settings_interface.Save(&error))451ERROR_LOG("Failed to save settings: {}", error.GetDescription());452}453454std::optional<WindowInfo> MiniHost::TranslateSDLWindowInfo(SDL_Window* win, Error* error)455{456if (!win)457{458Error::SetStringView(error, "Window handle is null.");459return std::nullopt;460}461462const SDL_WindowFlags window_flags = SDL_GetWindowFlags(win);463int window_width = 1, window_height = 1;464int window_px_width = 1, window_px_height = 1;465SDL_GetWindowSize(win, &window_width, &window_height);466SDL_GetWindowSizeInPixels(win, &window_px_width, &window_px_height);467s_state.sdl_window_scale = SDL_GetWindowDisplayScale(win);468469const SDL_DisplayMode* dispmode = nullptr;470471if (window_flags & SDL_WINDOW_FULLSCREEN)472{473if (!(dispmode = SDL_GetWindowFullscreenMode(win)))474ERROR_LOG("SDL_GetWindowFullscreenMode() failed: {}", SDL_GetError());475}476477if (const SDL_DisplayID display_id = SDL_GetDisplayForWindow(win); display_id != 0)478{479if (!(window_flags & SDL_WINDOW_FULLSCREEN))480{481if (!(dispmode = SDL_GetDesktopDisplayMode(display_id)))482ERROR_LOG("SDL_GetDesktopDisplayMode() failed: {}", SDL_GetError());483}484}485486WindowInfo wi;487wi.surface_width = static_cast<u16>(window_px_width);488wi.surface_height = static_cast<u16>(window_px_height);489wi.surface_scale = s_state.sdl_window_scale;490wi.surface_prerotation = s_state.force_prerotation;491492// set display refresh rate if available493if (dispmode && dispmode->refresh_rate > 0.0f)494{495INFO_LOG("Display mode refresh rate: {} hz", dispmode->refresh_rate);496wi.surface_refresh_rate = dispmode->refresh_rate;497}498499// SDL's opengl window flag tends to make a mess of pixel formats...500if (!(SDL_GetWindowFlags(win) & (SDL_WINDOW_OPENGL | SDL_WINDOW_VULKAN)))501{502const SDL_PropertiesID props = SDL_GetWindowProperties(win);503if (props == 0)504{505Error::SetStringFmt(error, "SDL_GetWindowProperties() failed: {}", SDL_GetError());506return std::nullopt;507}508509#if defined(SDL_PLATFORM_WINDOWS)510wi.type = WindowInfo::Type::Win32;511wi.window_handle = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WIN32_HWND_POINTER, nullptr);512if (!wi.window_handle)513{514Error::SetStringView(error, "SDL_PROP_WINDOW_WIN32_HWND_POINTER not found.");515return std::nullopt;516}517#elif defined(SDL_PLATFORM_MACOS)518wi.type = WindowInfo::Type::MacOS;519wi.window_handle = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, nullptr);520if (!wi.window_handle)521{522Error::SetStringView(error, "SDL_PROP_WINDOW_COCOA_WINDOW_POINTER not found.");523return std::nullopt;524}525#elif defined(SDL_PLATFORM_LINUX) || defined(SDL_PLATFORM_FREEBSD)526const std::string_view video_driver = SDL_GetCurrentVideoDriver();527if (video_driver == "x11")528{529wi.display_connection = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_X11_DISPLAY_POINTER, nullptr);530wi.window_handle = reinterpret_cast<void*>(531static_cast<intptr_t>(SDL_GetNumberProperty(props, SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0)));532if (!wi.display_connection)533{534Error::SetStringView(error, "SDL_PROP_WINDOW_X11_DISPLAY_POINTER not found.");535return std::nullopt;536}537else if (!wi.window_handle)538{539Error::SetStringView(error, "SDL_PROP_WINDOW_X11_WINDOW_NUMBER not found.");540return std::nullopt;541}542}543else if (video_driver == "wayland")544{545wi.display_connection = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_DISPLAY_POINTER, nullptr);546wi.window_handle = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_SURFACE_POINTER, nullptr);547if (!wi.display_connection)548{549Error::SetStringView(error, "SDL_PROP_WINDOW_WAYLAND_DISPLAY_POINTER not found.");550return std::nullopt;551}552else if (!wi.window_handle)553{554Error::SetStringView(error, "SDL_PROP_WINDOW_WAYLAND_SURFACE_POINTER not found.");555return std::nullopt;556}557}558else559{560Error::SetStringFmt(error, "Video driver {} not supported.", video_driver);561return std::nullopt;562}563#else564#error Unsupported platform.565#endif566}567else568{569// nothing handled, fall back to SDL abstraction570wi.type = WindowInfo::Type::SDL;571wi.window_handle = win;572}573574return wi;575}576577std::optional<WindowInfo> Host::AcquireRenderWindow(RenderAPI render_api, bool fullscreen, bool exclusive_fullscreen,578Error* error)579{580using namespace MiniHost;581582std::optional<WindowInfo> wi;583584Host::RunOnUIThread([render_api, fullscreen, error, &wi]() {585const std::string window_title = GetWindowTitle(System::GetGameTitle());586const SDL_PropertiesID props = SDL_CreateProperties();587SDL_SetStringProperty(props, SDL_PROP_WINDOW_CREATE_TITLE_STRING, window_title.c_str());588589SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_RESIZABLE_BOOLEAN, true);590SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_FOCUSABLE_BOOLEAN, true);591SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_HIGH_PIXEL_DENSITY_BOOLEAN, true);592593if (render_api == RenderAPI::OpenGL || render_api == RenderAPI::OpenGLES)594SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_OPENGL_BOOLEAN, true);595else if (render_api == RenderAPI::Vulkan)596SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_VULKAN_BOOLEAN, true);597598if (fullscreen)599{600SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_BORDERLESS_BOOLEAN, true);601SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_FULLSCREEN_BOOLEAN, true);602}603604if (s32 window_x, window_y, window_width, window_height;605MiniHost::GetSavedPlatformWindowGeometry(&window_x, &window_y, &window_width, &window_height))606{607SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_X_NUMBER, window_x);608SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_Y_NUMBER, window_y);609SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, window_width);610SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, window_height);611}612else613{614SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, DEFAULT_WINDOW_WIDTH);615SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, DEFAULT_WINDOW_HEIGHT);616}617618s_state.sdl_window = SDL_CreateWindowWithProperties(props);619SDL_DestroyProperties(props);620621if (s_state.sdl_window)622{623wi = TranslateSDLWindowInfo(s_state.sdl_window, error);624if (wi.has_value())625{626s_state.fullscreen.store(fullscreen, std::memory_order_release);627}628else629{630SDL_DestroyWindow(s_state.sdl_window);631s_state.sdl_window = nullptr;632}633}634else635{636Error::SetStringFmt(error, "SDL_CreateWindow() failed: {}", SDL_GetError());637}638639s_state.platform_window_updated.Post();640});641642s_state.platform_window_updated.Wait();643644// reload input sources, since it might use the window handle645{646auto lock = Host::GetSettingsLock();647InputManager::ReloadSources(*Host::GetSettingsInterface(), lock);648}649650return wi;651}652653void Host::ReleaseRenderWindow()654{655using namespace MiniHost;656657if (!s_state.sdl_window)658return;659660Host::RunOnUIThread([]() {661if (!s_state.fullscreen.load(std::memory_order_acquire))662{663int window_x = SDL_WINDOWPOS_UNDEFINED, window_y = SDL_WINDOWPOS_UNDEFINED;664int window_width = DEFAULT_WINDOW_WIDTH, window_height = DEFAULT_WINDOW_HEIGHT;665SDL_GetWindowPosition(s_state.sdl_window, &window_x, &window_y);666SDL_GetWindowSize(s_state.sdl_window, &window_width, &window_height);667MiniHost::SavePlatformWindowGeometry(window_x, window_y, window_width, window_height);668}669else670{671s_state.fullscreen.store(false, std::memory_order_release);672}673674SDL_DestroyWindow(s_state.sdl_window);675s_state.sdl_window = nullptr;676677s_state.platform_window_updated.Post();678});679680s_state.platform_window_updated.Wait();681}682683bool Host::IsFullscreen()684{685using namespace MiniHost;686687return s_state.fullscreen.load(std::memory_order_acquire);688}689690void Host::SetFullscreen(bool enabled)691{692using namespace MiniHost;693694if (!s_state.sdl_window || s_state.fullscreen.load(std::memory_order_acquire) == enabled)695return;696697if (!SDL_SetWindowFullscreen(s_state.sdl_window, enabled))698{699ERROR_LOG("SDL_SetWindowFullscreen() failed: {}", SDL_GetError());700return;701}702703s_state.fullscreen.store(enabled, std::memory_order_release);704}705706void Host::BeginTextInput()707{708using namespace MiniHost;709710SDL_StartTextInput(s_state.sdl_window);711}712713void Host::EndTextInput()714{715// we want to keep getting text events, SDL_StopTextInput() apparently inhibits that716}717718bool Host::CreateAuxiliaryRenderWindow(s32 x, s32 y, u32 width, u32 height, std::string_view title,719std::string_view icon_name, AuxiliaryRenderWindowUserData userdata,720AuxiliaryRenderWindowHandle* handle, WindowInfo* wi, Error* error)721{722// not here, but could be...723Error::SetStringView(error, "Not supported.");724return false;725}726727void Host::DestroyAuxiliaryRenderWindow(AuxiliaryRenderWindowHandle handle, s32* pos_x /* = nullptr */,728s32* pos_y /* = nullptr */, u32* width /* = nullptr */,729u32* height /* = nullptr */)730{731// noop732}733734bool MiniHost::GetSavedPlatformWindowGeometry(s32* x, s32* y, s32* width, s32* height)735{736const auto lock = Host::GetSettingsLock();737738bool result = s_state.base_settings_interface.GetIntValue("UI", "MainWindowX", x);739result = result && s_state.base_settings_interface.GetIntValue("UI", "MainWindowY", y);740result = result && s_state.base_settings_interface.GetIntValue("UI", "MainWindowWidth", width);741result = result && s_state.base_settings_interface.GetIntValue("UI", "MainWindowHeight", height);742return result;743}744745void MiniHost::SavePlatformWindowGeometry(s32 x, s32 y, s32 width, s32 height)746{747if (Host::IsFullscreen())748return;749750const auto lock = Host::GetSettingsLock();751s_state.base_settings_interface.SetIntValue("UI", "MainWindowX", x);752s_state.base_settings_interface.SetIntValue("UI", "MainWindowY", y);753s_state.base_settings_interface.SetIntValue("UI", "MainWindowWidth", width);754s_state.base_settings_interface.SetIntValue("UI", "MainWindowHeight", height);755}756757void MiniHost::UIThreadMainLoop()758{759while (s_state.ui_thread_running)760{761SDL_Event ev;762if (!SDL_WaitEvent(&ev))763continue;764765ProcessSDLEvent(&ev);766}767}768769void MiniHost::ProcessSDLEvent(const SDL_Event* ev)770{771switch (ev->type)772{773case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:774{775Host::RunOnCPUThread(776[window_width = ev->window.data1, window_height = ev->window.data2, window_scale = s_state.sdl_window_scale]() {777GPUThread::ResizeDisplayWindow(window_width, window_height, window_scale);778});779}780break;781782case SDL_EVENT_WINDOW_DISPLAY_CHANGED:783case SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED:784{785const float new_scale = SDL_GetWindowDisplayScale(s_state.sdl_window);786if (new_scale != s_state.sdl_window_scale)787{788s_state.sdl_window_scale = new_scale;789790int window_width = 1, window_height = 1;791SDL_GetWindowSizeInPixels(s_state.sdl_window, &window_width, &window_height);792Host::RunOnCPUThread([window_width, window_height, window_scale = s_state.sdl_window_scale]() {793GPUThread::ResizeDisplayWindow(window_width, window_height, window_scale);794});795}796}797break;798799case SDL_EVENT_WINDOW_CLOSE_REQUESTED:800{801Host::RunOnCPUThread([]() { Host::RequestExitApplication(false); });802}803break;804805case SDL_EVENT_WINDOW_FOCUS_GAINED:806{807Host::RunOnCPUThread([]() {808if (!System::IsValid() || !s_state.was_paused_by_focus_loss)809return;810811System::PauseSystem(false);812s_state.was_paused_by_focus_loss = false;813});814}815break;816817case SDL_EVENT_WINDOW_FOCUS_LOST:818{819Host::RunOnCPUThread([]() {820if (!System::IsRunning() || !g_settings.pause_on_focus_loss)821return;822823s_state.was_paused_by_focus_loss = true;824System::PauseSystem(true);825});826}827break;828829case SDL_EVENT_KEY_DOWN:830case SDL_EVENT_KEY_UP:831{832if (const std::optional<u32> key = InputManager::ConvertHostNativeKeyCodeToKeyCode(ev->key.raw))833{834Host::RunOnCPUThread([key_code = key.value(), pressed = (ev->type == SDL_EVENT_KEY_DOWN)]() {835InputManager::InvokeEvents(InputManager::MakeHostKeyboardKey(key_code), pressed ? 1.0f : 0.0f,836GenericInputBinding::Unknown);837});838}839}840break;841842case SDL_EVENT_TEXT_INPUT:843{844if (ImGuiManager::WantsTextInput())845Host::RunOnCPUThread([text = std::string(ev->text.text)]() { ImGuiManager::AddTextInput(std::move(text)); });846}847break;848849case SDL_EVENT_MOUSE_MOTION:850{851Host::RunOnCPUThread([x = static_cast<float>(ev->motion.x), y = static_cast<float>(ev->motion.y)]() {852InputManager::UpdatePointerAbsolutePosition(0, x, y);853ImGuiManager::UpdateMousePosition(x, y);854});855}856break;857858case SDL_EVENT_MOUSE_BUTTON_DOWN:859case SDL_EVENT_MOUSE_BUTTON_UP:860{861if (ev->button.button > 0)862{863// swap middle/right because sdl orders them differently864const u8 button = (ev->button.button == 3) ? 1 : ((ev->button.button == 2) ? 2 : (ev->button.button - 1));865Host::RunOnCPUThread([button, pressed = (ev->type == SDL_EVENT_MOUSE_BUTTON_DOWN)]() {866InputManager::InvokeEvents(InputManager::MakePointerButtonKey(0, button), pressed ? 1.0f : 0.0f,867GenericInputBinding::Unknown);868});869}870}871break;872873case SDL_EVENT_MOUSE_WHEEL:874{875Host::RunOnCPUThread([x = ev->wheel.x, y = ev->wheel.y]() {876if (x != 0.0f)877InputManager::UpdatePointerRelativeDelta(0, InputPointerAxis::WheelX, x);878if (y != 0.0f)879InputManager::UpdatePointerRelativeDelta(0, InputPointerAxis::WheelY, y);880});881}882break;883884case SDL_EVENT_QUIT:885{886Host::RunOnCPUThread([]() { Host::RequestExitApplication(false); });887}888break;889890default:891{892if (ev->type == s_state.func_event_id)893{894std::function<void()>* pfunc = reinterpret_cast<std::function<void()>*>(ev->user.data1);895if (pfunc)896{897(*pfunc)();898delete pfunc;899}900}901else if (SDLInputSource::IsHandledInputEvent(ev))902{903Host::RunOnCPUThread([event_copy = *ev]() {904SDLInputSource* is =905static_cast<SDLInputSource*>(InputManager::GetInputSourceInterface(InputSourceType::SDL));906if (is)907is->ProcessSDLEvent(&event_copy);908});909}910}911break;912}913}914915void MiniHost::ProcessCPUThreadPlatformMessages()916{917// This is lame. On Win32, we need to pump messages, even though *we* don't have any windows918// on the CPU thread, because SDL creates a hidden window for raw input for some game controllers.919// If we don't do this, we don't get any controller events.920#ifdef _WIN32921MSG msg;922while (PeekMessageW(&msg, NULL, 0, 0, PM_REMOVE))923{924TranslateMessage(&msg);925DispatchMessageW(&msg);926}927#endif928}929930void MiniHost::ProcessCPUThreadEvents(bool block)931{932std::unique_lock lock(s_state.cpu_thread_events_mutex);933934for (;;)935{936if (s_state.cpu_thread_events.empty())937{938if (!block || !s_state.cpu_thread_running.load(std::memory_order_acquire))939return;940941// we still need to keep polling the controllers when we're paused942do943{944ProcessCPUThreadPlatformMessages();945InputManager::PollSources();946} while (!s_state.cpu_thread_event_posted.wait_for(lock, CPU_THREAD_POLL_INTERVAL,947[]() { return !s_state.cpu_thread_events.empty(); }));948}949950// return after processing all events if we had one951block = false;952953auto event = std::move(s_state.cpu_thread_events.front());954s_state.cpu_thread_events.pop_front();955lock.unlock();956event.first();957lock.lock();958959if (event.second)960{961s_state.blocking_cpu_events_pending--;962s_state.cpu_thread_event_done.notify_one();963}964}965}966967void MiniHost::StartCPUThread()968{969s_state.cpu_thread_running.store(true, std::memory_order_release);970s_state.cpu_thread.Start(CPUThreadEntryPoint);971}972973void MiniHost::StopCPUThread()974{975if (!s_state.cpu_thread.Joinable())976return;977978{979std::unique_lock lock(s_state.cpu_thread_events_mutex);980s_state.cpu_thread_running.store(false, std::memory_order_release);981s_state.cpu_thread_event_posted.notify_one();982}983984s_state.cpu_thread.Join();985}986987void MiniHost::CPUThreadEntryPoint()988{989Threading::SetNameOfCurrentThread("CPU Thread");990991// input source setup must happen on emu thread992Error error;993if (!System::CPUThreadInitialize(&error, NUM_ASYNC_WORKER_THREADS))994{995Host::ReportFatalError("CPU Thread Initialization Failed", error.GetDescription());996return;997}998999// start up GPU thread1000s_state.gpu_thread.Start(&GPUThreadEntryPoint);10011002// start the fullscreen UI and get it going1003if (GPUThread::StartFullscreenUI(s_state.start_fullscreen_ui_fullscreen, &error))1004{1005WarnAboutInterface();10061007// kick a game list refresh if we're not in batch mode1008if (!s_state.batch_mode)1009Host::RefreshGameListAsync(false);10101011CPUThreadMainLoop();10121013Host::CancelGameListRefresh();1014}1015else1016{1017Host::ReportFatalError("Error", fmt::format("Failed to start fullscreen UI: {}", error.GetDescription()));1018}10191020// finish any events off (e.g. shutdown system with save)1021ProcessCPUThreadEvents(false);10221023if (System::IsValid())1024System::ShutdownSystem(false);10251026GPUThread::StopFullscreenUI();1027GPUThread::Internal::RequestShutdown();1028s_state.gpu_thread.Join();10291030System::CPUThreadShutdown();10311032// Tell the UI thread to shut down.1033Host::RunOnUIThread([]() { s_state.ui_thread_running = false; });1034}10351036void MiniHost::CPUThreadMainLoop()1037{1038while (s_state.cpu_thread_running.load(std::memory_order_acquire))1039{1040if (System::IsRunning())1041{1042System::Execute();1043continue;1044}1045else if (!GPUThread::IsUsingThread() && GPUThread::IsRunningIdle())1046{1047ProcessCPUThreadEvents(false);1048if (!GPUThread::IsUsingThread() && GPUThread::IsRunningIdle())1049GPUThread::Internal::DoRunIdle();1050}10511052ProcessCPUThreadEvents(true);1053}1054}10551056void MiniHost::GPUThreadEntryPoint()1057{1058Threading::SetNameOfCurrentThread("GPU Thread");1059GPUThread::Internal::GPUThreadEntryPoint();1060}10611062void Host::OnSystemStarting()1063{1064MiniHost::s_state.was_paused_by_focus_loss = false;1065}10661067void Host::OnSystemStarted()1068{1069}10701071void Host::OnSystemPaused()1072{1073}10741075void Host::OnSystemResumed()1076{1077}10781079void Host::OnSystemStopping()1080{1081}10821083void Host::OnSystemDestroyed()1084{1085}10861087void Host::OnSystemAbnormalShutdown(const std::string_view reason)1088{1089GPUThread::RunOnThread([reason = std::string(reason)]() {1090ImGuiFullscreen::OpenInfoMessageDialog(1091"Abnormal System Shutdown", fmt::format("Unfortunately, the virtual machine has abnormally shut down and cannot "1092"be recovered. More information about the error is below:\n\n{}",1093reason));1094});1095}10961097void Host::OnGPUThreadRunIdleChanged(bool is_active)1098{1099}11001101void Host::FrameDoneOnGPUThread(GPUBackend* gpu_backend, u32 frame_number)1102{1103}11041105void Host::OnPerformanceCountersUpdated(const GPUBackend* gpu_backend)1106{1107// noop1108}11091110void Host::OnAchievementsLoginRequested(Achievements::LoginRequestReason reason)1111{1112// noop1113}11141115void Host::OnAchievementsLoginSuccess(const char* username, u32 points, u32 sc_points, u32 unread_messages)1116{1117// noop1118}11191120void Host::OnAchievementsRefreshed()1121{1122// noop1123}11241125void Host::OnAchievementsActiveChanged(bool active)1126{1127// noop1128}11291130void Host::OnAchievementsHardcoreModeChanged(bool enabled)1131{1132// noop1133}11341135void Host::OnAchievementsAllProgressRefreshed()1136{1137// noop1138}11391140#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION11411142void Host::OnRAIntegrationMenuChanged()1143{1144// noop1145}11461147#endif11481149void Host::SetMouseMode(bool relative, bool hide_cursor)1150{1151// noop1152}11531154void Host::OnMediaCaptureStarted()1155{1156// noop1157}11581159void Host::OnMediaCaptureStopped()1160{1161// noop1162}11631164void Host::PumpMessagesOnCPUThread()1165{1166MiniHost::ProcessCPUThreadEvents(false);1167}11681169std::string MiniHost::GetWindowTitle(const std::string& game_title)1170{1171#if defined(_DEBUGFAST)1172static constexpr std::string_view suffix = " [DebugFast]";1173#elif defined(_DEBUG)1174static constexpr std::string_view suffix = " [Debug]";1175#else1176static constexpr std::string_view suffix = std::string_view();1177#endif11781179if (System::IsShutdown() || game_title.empty())1180return fmt::format("DuckStation {}{}", g_scm_version_str, suffix);1181else1182return fmt::format("{}{}", game_title, suffix);1183}11841185void MiniHost::WarnAboutInterface()1186{1187const char* message = "This is the \"mini\" interface for DuckStation, and is missing many features.\n"1188" We recommend using the Qt interface instead, which you can download\n"1189" from https://www.duckstation.org/.";1190Host::AddIconOSDWarning("MiniWarning", ICON_EMOJI_WARNING, message, Host::OSD_INFO_DURATION);1191}11921193void Host::OnSystemGameChanged(const std::string& disc_path, const std::string& game_serial,1194const std::string& game_name, GameHash game_hash)1195{1196using namespace MiniHost;11971198VERBOSE_LOG("Host::OnGameChanged(\"{}\", \"{}\", \"{}\")", disc_path, game_serial, game_name);1199if (s_state.sdl_window)1200SDL_SetWindowTitle(s_state.sdl_window, GetWindowTitle(game_name).c_str());1201}12021203void Host::OnSystemUndoStateAvailabilityChanged(bool available, u64 timestamp)1204{1205//1206}12071208void Host::RunOnCPUThread(std::function<void()> function, bool block /* = false */)1209{1210using namespace MiniHost;12111212std::unique_lock lock(s_state.cpu_thread_events_mutex);1213s_state.cpu_thread_events.emplace_back(std::move(function), block);1214s_state.blocking_cpu_events_pending += BoolToUInt32(block);1215s_state.cpu_thread_event_posted.notify_one();1216if (block)1217s_state.cpu_thread_event_done.wait(lock, []() { return s_state.blocking_cpu_events_pending == 0; });1218}12191220void Host::RunOnUIThread(std::function<void()> function, bool block /* = false */)1221{1222using namespace MiniHost;12231224std::function<void()>* pfunc = new std::function<void()>(std::move(function));12251226SDL_Event ev;1227ev.user = {};1228ev.type = s_state.func_event_id;1229ev.user.data1 = pfunc;1230SDL_PushEvent(&ev);1231}12321233void Host::RefreshGameListAsync(bool invalidate_cache)1234{1235using namespace MiniHost;12361237std::unique_lock lock(s_state.state_mutex);12381239while (s_state.game_list_refresh_progress)1240{1241lock.unlock();1242CancelGameListRefresh();1243lock.lock();1244}12451246s_state.game_list_refresh_progress = new FullscreenUI::BackgroundProgressCallback("glrefresh");1247System::QueueAsyncTask([invalidate_cache]() {1248GameList::Refresh(invalidate_cache, false, s_state.game_list_refresh_progress);12491250std::unique_lock lock(s_state.state_mutex);1251delete s_state.game_list_refresh_progress;1252s_state.game_list_refresh_progress = nullptr;1253});1254}12551256void Host::CancelGameListRefresh()1257{1258using namespace MiniHost;12591260{1261std::unique_lock lock(s_state.state_mutex);1262if (!s_state.game_list_refresh_progress)1263return;12641265s_state.game_list_refresh_progress->SetCancelled();1266}12671268System::WaitForAllAsyncTasks();1269}12701271void Host::OnGameListEntriesChanged(std::span<const u32> changed_indices)1272{1273// constantly re-querying, don't need to do anything1274}12751276std::optional<WindowInfo> Host::GetTopLevelWindowInfo()1277{1278return MiniHost::TranslateSDLWindowInfo(MiniHost::s_state.sdl_window, nullptr);1279}12801281void Host::RequestResetSettings(bool system, bool controller)1282{1283using namespace MiniHost;12841285auto lock = Host::GetSettingsLock();1286{1287SettingsInterface& si = s_state.base_settings_interface;12881289if (system)1290{1291System::SetDefaultSettings(si);1292EmuFolders::SetDefaults();1293EmuFolders::Save(si);1294}12951296if (controller)1297{1298InputManager::SetDefaultSourceConfig(si);1299Settings::SetDefaultControllerConfig(si);1300Settings::SetDefaultHotkeyConfig(si);1301}1302}13031304System::ApplySettings(false);1305}13061307void Host::RequestExitApplication(bool allow_confirm)1308{1309Host::RunOnCPUThread([]() {1310System::ShutdownSystem(g_settings.save_state_on_exit);13111312// clear the running flag, this'll break out of the main CPU loop once the VM is shutdown.1313MiniHost::s_state.cpu_thread_running.store(false, std::memory_order_release);1314});1315}13161317void Host::RequestExitBigPicture()1318{1319// sorry dude1320}13211322void Host::RequestSystemShutdown(bool allow_confirm, bool save_state, bool check_memcard_busy)1323{1324// TODO: Confirm1325if (System::IsValid())1326{1327Host::RunOnCPUThread([save_state]() { System::ShutdownSystem(save_state); });1328}1329}13301331void Host::ReportFatalError(std::string_view title, std::string_view message)1332{1333// Depending on the platform, this may not be available.1334std::fputs(SmallString::from_format("Fatal error: {}: {}\n", title, message).c_str(), stderr);1335SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, TinyString(title).c_str(), SmallString(message).c_str(), nullptr);1336}13371338void Host::ReportErrorAsync(std::string_view title, std::string_view message)1339{1340std::fputs(SmallString::from_format("Error: {}: {}\n", title, message).c_str(), stderr);1341SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, TinyString(title).c_str(), SmallString(message).c_str(), nullptr);1342}13431344void Host::RequestResizeHostDisplay(s32 width, s32 height)1345{1346using namespace MiniHost;13471348if (!s_state.sdl_window || s_state.fullscreen.load(std::memory_order_acquire))1349return;13501351SDL_SetWindowSize(s_state.sdl_window, width, height);1352}13531354void Host::OpenURL(std::string_view url)1355{1356if (!SDL_OpenURL(SmallString(url).c_str()))1357ERROR_LOG("SDL_OpenURL({}) failed: {}", url, SDL_GetError());1358}13591360std::string Host::GetClipboardText()1361{1362std::string ret;13631364char* text = SDL_GetClipboardText();1365if (text)1366{1367ret = text;1368SDL_free(text);1369}13701371return ret;1372}13731374bool Host::CopyTextToClipboard(std::string_view text)1375{1376if (!SDL_SetClipboardText(SmallString(text).c_str()))1377{1378ERROR_LOG("SDL_SetClipboardText({}) failed: {}", text, SDL_GetError());1379return false;1380}13811382return true;1383}13841385std::string Host::FormatNumber(NumberFormatType type, s64 value)1386{1387std::string ret;13881389if (type >= NumberFormatType::ShortDate && type <= NumberFormatType::LongDateTime)1390{1391const char* format;1392switch (type)1393{1394case NumberFormatType::ShortDate:1395format = "%x";1396break;13971398case NumberFormatType::LongDate:1399format = "%A %B %e %Y";1400break;14011402case NumberFormatType::ShortTime:1403case NumberFormatType::LongTime:1404format = "%X";1405break;14061407case NumberFormatType::ShortDateTime:1408format = "%X %x";1409break;14101411case NumberFormatType::LongDateTime:1412format = "%c";1413break;14141415DefaultCaseIsUnreachable();1416}14171418ret.resize(128);14191420if (const std::optional<std::tm> ltime = Common::LocalTime(static_cast<std::time_t>(value)))1421ret.resize(std::strftime(ret.data(), ret.size(), format, <ime.value()));1422else1423ret = "Invalid";1424}1425else1426{1427ret = fmt::format("{}", value);1428}14291430return ret;1431}14321433std::string Host::FormatNumber(NumberFormatType type, double value)1434{1435return fmt::format("{}", value);1436}14371438bool Host::ConfirmMessage(std::string_view title, std::string_view message)1439{1440const SmallString title_copy(title);1441const SmallString message_copy(message);14421443static constexpr SDL_MessageBoxButtonData bd[2] = {1444{SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT, 1, "Yes"},1445{SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT, 2, "No"},1446};1447const SDL_MessageBoxData md = {SDL_MESSAGEBOX_INFORMATION,1448nullptr,1449title_copy.c_str(),1450message_copy.c_str(),1451static_cast<int>(std::size(bd)),1452bd,1453nullptr};14541455int buttonid = -1;1456SDL_ShowMessageBox(&md, &buttonid);1457return (buttonid == 1);1458}14591460void Host::ConfirmMessageAsync(std::string_view title, std::string_view message, ConfirmMessageAsyncCallback callback,1461std::string_view yes_text /* = std::string_view() */,1462std::string_view no_text /* = std::string_view() */)1463{1464Host::RunOnCPUThread([title = std::string(title), message = std::string(message), callback = std::move(callback),1465yes_text = std::string(yes_text), no_text = std::move(no_text)]() mutable {1466// in case we haven't started yet...1467if (!FullscreenUI::IsInitialized())1468{1469callback(false);1470return;1471}14721473// Pause system while dialog is up.1474const bool needs_pause = System::IsValid() && !System::IsPaused();1475if (needs_pause)1476System::PauseSystem(true);14771478GPUThread::RunOnThread([title = std::string(title), message = std::string(message), callback = std::move(callback),1479yes_text = std::string(yes_text), no_text = std::string(no_text), needs_pause]() mutable {1480if (!FullscreenUI::Initialize())1481{1482callback(false);14831484if (needs_pause)1485{1486Host::RunOnCPUThread([]() {1487if (System::IsValid())1488System::PauseSystem(false);1489});1490}14911492return;1493}14941495// Need to reset run idle state _again_ after displaying.1496auto final_callback = [callback = std::move(callback)](bool result) {1497FullscreenUI::UpdateRunIdleState();1498callback(result);1499};15001501ImGuiFullscreen::OpenConfirmMessageDialog(std::move(title), std::move(message), std::move(final_callback),1502fmt::format(ICON_FA_CHECK " {}", yes_text),1503fmt::format(ICON_FA_XMARK " {}", no_text));1504FullscreenUI::UpdateRunIdleState();1505});1506});1507}15081509void Host::OpenHostFileSelectorAsync(std::string_view title, bool select_directory, FileSelectorCallback callback,1510FileSelectorFilters filters /* = FileSelectorFilters() */,1511std::string_view initial_directory /* = std::string_view() */)1512{1513// TODO: Use SDL FileDialog API1514callback(std::string());1515}15161517const char* Host::GetDefaultFullscreenUITheme()1518{1519return "";1520}15211522bool Host::ShouldPreferHostFileSelector()1523{1524return false;1525}15261527BEGIN_HOTKEY_LIST(g_host_hotkeys)1528END_HOTKEY_LIST()15291530static void SignalHandler(int signal)1531{1532// First try the normal (graceful) shutdown/exit.1533static bool graceful_shutdown_attempted = false;1534if (!graceful_shutdown_attempted)1535{1536std::fprintf(stderr, "Received CTRL+C, attempting graceful shutdown. Press CTRL+C again to force.\n");1537graceful_shutdown_attempted = true;1538Host::RequestExitApplication(false);1539return;1540}15411542std::signal(signal, SIG_DFL);15431544// MacOS is missing std::quick_exit() despite it being C++11...1545#ifndef __APPLE__1546std::quick_exit(1);1547#else1548_Exit(1);1549#endif1550}15511552void MiniHost::HookSignals()1553{1554std::signal(SIGINT, SignalHandler);1555std::signal(SIGTERM, SignalHandler);15561557#ifndef _WIN321558// Ignore SIGCHLD by default on Linux, since we kick off aplay asynchronously.1559struct sigaction sa_chld = {};1560sigemptyset(&sa_chld.sa_mask);1561sa_chld.sa_handler = SIG_IGN;1562sa_chld.sa_flags = SA_RESTART | SA_NOCLDSTOP | SA_NOCLDWAIT;1563sigaction(SIGCHLD, &sa_chld, nullptr);1564#endif1565}15661567void MiniHost::InitializeEarlyConsole()1568{1569const bool was_console_enabled = Log::IsConsoleOutputEnabled();1570if (!was_console_enabled)1571Log::SetConsoleOutputParams(true);1572}15731574void MiniHost::PrintCommandLineVersion()1575{1576InitializeEarlyConsole();15771578std::fprintf(stderr, "DuckStation Version %s (%s)\n", g_scm_tag_str, g_scm_branch_str);1579std::fprintf(stderr, "https://github.com/stenzek/duckstation\n");1580std::fprintf(stderr, "\n");1581}15821583void MiniHost::PrintCommandLineHelp(const char* progname)1584{1585InitializeEarlyConsole();15861587PrintCommandLineVersion();1588std::fprintf(stderr, "Usage: %s [parameters] [--] [boot filename]\n", progname);1589std::fprintf(stderr, "\n");1590std::fprintf(stderr, " -help: Displays this information and exits.\n");1591std::fprintf(stderr, " -version: Displays version information and exits.\n");1592std::fprintf(stderr, " -batch: Enables batch mode (exits after powering off).\n");1593std::fprintf(stderr, " -fastboot: Force fast boot for provided filename.\n");1594std::fprintf(stderr, " -slowboot: Force slow boot for provided filename.\n");1595std::fprintf(stderr, " -bios: Boot into the BIOS shell.\n");1596std::fprintf(stderr, " -resume: Load resume save state. If a boot filename is provided,\n"1597" that game's resume state will be loaded, otherwise the most\n"1598" recent resume save state will be loaded.\n");1599std::fprintf(stderr, " -state <index>: Loads specified save state by index. If a boot\n"1600" filename is provided, a per-game state will be loaded, otherwise\n"1601" a global state will be loaded.\n");1602std::fprintf(stderr, " -statefile <filename>: Loads state from the specified filename.\n"1603" No boot filename is required with this option.\n");1604std::fprintf(stderr, " -exe <filename>: Boot the specified exe instead of loading from disc.\n");1605std::fprintf(stderr, " -fullscreen: Enters fullscreen mode immediately after starting.\n");1606std::fprintf(stderr, " -nofullscreen: Prevents fullscreen mode from triggering if enabled.\n");1607std::fprintf(stderr, " -earlyconsole: Creates console as early as possible, for logging.\n");1608std::fprintf(stderr, " -prerotation <degrees>: Prerotates output by 90/180/270 degrees.\n");1609std::fprintf(stderr, " --: Signals that no more arguments will follow and the remaining\n"1610" parameters make up the filename. Use when the filename contains\n"1611" spaces or starts with a dash.\n");1612std::fprintf(stderr, "\n");1613}16141615std::optional<SystemBootParameters>& AutoBoot(std::optional<SystemBootParameters>& autoboot)1616{1617if (!autoboot)1618autoboot.emplace();16191620return autoboot;1621}16221623bool MiniHost::ParseCommandLineParametersAndInitializeConfig(int argc, char* argv[],1624std::optional<SystemBootParameters>& autoboot)1625{1626std::optional<s32> state_index;1627bool starting_bios = false;1628bool no_more_args = false;16291630for (int i = 1; i < argc; i++)1631{1632if (!no_more_args)1633{1634#define CHECK_ARG(str) (std::strcmp(argv[i], (str)) == 0)1635#define CHECK_ARG_PARAM(str) (std::strcmp(argv[i], (str)) == 0 && ((i + 1) < argc))16361637if (CHECK_ARG("-help"))1638{1639PrintCommandLineHelp(argv[0]);1640return false;1641}1642else if (CHECK_ARG("-version"))1643{1644PrintCommandLineVersion();1645return false;1646}1647else if (CHECK_ARG("-batch"))1648{1649INFO_LOG("Command Line: Using batch mode.");1650s_state.batch_mode = true;1651continue;1652}1653else if (CHECK_ARG("-bios"))1654{1655INFO_LOG("Command Line: Starting BIOS.");1656AutoBoot(autoboot);1657starting_bios = true;1658continue;1659}1660else if (CHECK_ARG("-fastboot"))1661{1662INFO_LOG("Command Line: Forcing fast boot.");1663AutoBoot(autoboot)->override_fast_boot = true;1664continue;1665}1666else if (CHECK_ARG("-slowboot"))1667{1668INFO_LOG("Command Line: Forcing slow boot.");1669AutoBoot(autoboot)->override_fast_boot = false;1670continue;1671}1672else if (CHECK_ARG("-resume"))1673{1674state_index = -1;1675INFO_LOG("Command Line: Loading resume state.");1676continue;1677}1678else if (CHECK_ARG_PARAM("-state"))1679{1680state_index = StringUtil::FromChars<s32>(argv[++i]);1681if (!state_index.has_value())1682{1683ERROR_LOG("Invalid state index");1684return false;1685}16861687INFO_LOG("Command Line: Loading state index: {}", state_index.value());1688continue;1689}1690else if (CHECK_ARG_PARAM("-statefile"))1691{1692AutoBoot(autoboot)->save_state = argv[++i];1693INFO_LOG("Command Line: Loading state file: '{}'", autoboot->save_state);1694continue;1695}1696else if (CHECK_ARG_PARAM("-exe"))1697{1698AutoBoot(autoboot)->override_exe = argv[++i];1699INFO_LOG("Command Line: Overriding EXE file: '{}'", autoboot->override_exe);1700continue;1701}1702else if (CHECK_ARG("-fullscreen"))1703{1704INFO_LOG("Command Line: Using fullscreen.");1705AutoBoot(autoboot)->override_fullscreen = true;1706s_state.start_fullscreen_ui_fullscreen = true;1707continue;1708}1709else if (CHECK_ARG("-nofullscreen"))1710{1711INFO_LOG("Command Line: Not using fullscreen.");1712AutoBoot(autoboot)->override_fullscreen = false;1713continue;1714}1715else if (CHECK_ARG("-earlyconsole"))1716{1717InitializeEarlyConsole();1718continue;1719}1720else if (CHECK_ARG_PARAM("-prerotation"))1721{1722const char* prerotation_str = argv[++i];1723if (std::strcmp(prerotation_str, "0") == 0 || StringUtil::EqualNoCase(prerotation_str, "identity"))1724{1725INFO_LOG("Command Line: Forcing surface pre-rotation to identity.");1726s_state.force_prerotation = WindowInfo::PreRotation::Identity;1727}1728else if (std::strcmp(prerotation_str, "90") == 0)1729{1730INFO_LOG("Command Line: Forcing surface pre-rotation to 90 degrees clockwise.");1731s_state.force_prerotation = WindowInfo::PreRotation::Rotate90Clockwise;1732}1733else if (std::strcmp(prerotation_str, "180") == 0)1734{1735INFO_LOG("Command Line: Forcing surface pre-rotation to 180 degrees clockwise.");1736s_state.force_prerotation = WindowInfo::PreRotation::Rotate180Clockwise;1737}1738else if (std::strcmp(prerotation_str, "270") == 0)1739{1740INFO_LOG("Command Line: Forcing surface pre-rotation to 270 degrees clockwise.");1741s_state.force_prerotation = WindowInfo::PreRotation::Rotate270Clockwise;1742}1743else1744{1745ERROR_LOG("Invalid prerotation value: {}", prerotation_str);1746return false;1747}17481749continue;1750}1751else if (CHECK_ARG("--"))1752{1753no_more_args = true;1754continue;1755}1756else if (argv[i][0] == '-')1757{1758Host::ReportFatalError("Error", fmt::format("Unknown parameter: {}", argv[i]));1759return false;1760}17611762#undef CHECK_ARG1763#undef CHECK_ARG_PARAM1764}17651766if (autoboot && !autoboot->path.empty())1767autoboot->path += ' ';1768AutoBoot(autoboot)->path += argv[i];1769}17701771// To do anything useful, we need the config initialized.1772if (!InitializeConfig())1773{1774// NOTE: No point translating this, because no config means the language won't be loaded anyway.1775Host::ReportFatalError("Error", "Failed to initialize config.");1776return EXIT_FAILURE;1777}17781779// Check the file we're starting actually exists.17801781if (autoboot && !autoboot->path.empty() && !FileSystem::FileExists(autoboot->path.c_str()))1782{1783Host::ReportFatalError("Error", fmt::format("File '{}' does not exist.", autoboot->path));1784return false;1785}17861787if (state_index.has_value())1788{1789AutoBoot(autoboot);17901791if (autoboot->path.empty())1792{1793// loading global state, -1 means resume the last game1794if (state_index.value() < 0)1795autoboot->save_state = System::GetMostRecentResumeSaveStatePath();1796else1797autoboot->save_state = System::GetGlobalSaveStatePath(state_index.value());1798}1799else1800{1801// loading game state1802const std::string game_serial(GameDatabase::GetSerialForPath(autoboot->path.c_str()));1803autoboot->save_state = System::GetGameSaveStatePath(game_serial, state_index.value());1804}18051806if (autoboot->save_state.empty() || !FileSystem::FileExists(autoboot->save_state.c_str()))1807{1808Host::ReportFatalError("Error", "The specified save state does not exist.");1809return false;1810}1811}18121813// check autoboot parameters, if we set something like fullscreen without a bios1814// or disc, we don't want to actually start.1815if (autoboot && autoboot->path.empty() && autoboot->save_state.empty() && !starting_bios)1816autoboot.reset();18171818// if we don't have autoboot, we definitely don't want batch mode (because that'll skip1819// scanning the game list).1820if (s_state.batch_mode)1821{1822if (!autoboot)1823{1824Host::ReportFatalError("Error", "Cannot use batch mode, because no boot filename was specified.");1825return false;1826}18271828// if using batch mode, immediately refresh the game list so the data is available1829GameList::Refresh(false, true);1830}18311832return true;1833}18341835#include <SDL3/SDL_main.h>18361837int main(int argc, char* argv[])1838{1839using namespace MiniHost;18401841CrashHandler::Install(&Bus::CleanupMemoryMap);18421843if (!PerformEarlyHardwareChecks())1844return EXIT_FAILURE;18451846if (!SDL_InitSubSystem(SDL_INIT_VIDEO | SDL_INIT_EVENTS))1847{1848Host::ReportFatalError("Error", TinyString::from_format("SDL_InitSubSystem() failed: {}", SDL_GetError()));1849return EXIT_FAILURE;1850}18511852s_state.func_event_id = SDL_RegisterEvents(1);1853if (s_state.func_event_id == static_cast<u32>(-1))1854{1855Host::ReportFatalError("Error", TinyString::from_format("SDL_RegisterEvents() failed: {}", SDL_GetError()));1856return EXIT_FAILURE;1857}18581859if (!EarlyProcessStartup())1860return EXIT_FAILURE;18611862std::optional<SystemBootParameters> autoboot;1863if (!ParseCommandLineParametersAndInitializeConfig(argc, argv, autoboot))1864return EXIT_FAILURE;18651866// the rest of initialization happens on the CPU thread.1867HookSignals();18681869// prevent input source polling on CPU thread...1870SDLInputSource::ALLOW_EVENT_POLLING = false;1871s_state.ui_thread_running = true;1872StartCPUThread();18731874// process autoboot early, that way we can set the fullscreen flag1875if (autoboot)1876{1877s_state.start_fullscreen_ui_fullscreen =1878s_state.start_fullscreen_ui_fullscreen || autoboot->override_fullscreen.value_or(false);1879Host::RunOnCPUThread([params = std::move(autoboot.value())]() mutable {1880Error error;1881if (!System::BootSystem(std::move(params), &error))1882Host::ReportErrorAsync("Failed to boot system", error.GetDescription());1883});1884}18851886UIThreadMainLoop();18871888StopCPUThread();18891890System::ProcessShutdown();18911892// Ensure log is flushed.1893Log::SetFileOutputParams(false, nullptr);18941895if (s_state.base_settings_interface.IsDirty())1896s_state.base_settings_interface.Save();18971898SDL_QuitSubSystem(SDL_INIT_VIDEO | SDL_INIT_EVENTS);18991900return EXIT_SUCCESS;1901}190219031904