Path: blob/master/src/duckstation-mini/mini_host.cpp
4246 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"3940#include "IconsEmoji.h"41#include "fmt/format.h"4243#include <SDL3/SDL.h>44#include <cinttypes>45#include <cmath>46#include <condition_variable>47#include <csignal>48#include <ctime>49#include <thread>5051LOG_CHANNEL(Host);5253namespace MiniHost {5455/// Use two async worker threads, should be enough for most tasks.56static constexpr u32 NUM_ASYNC_WORKER_THREADS = 2;5758// static constexpr u32 DEFAULT_WINDOW_WIDTH = 1280;59// static constexpr u32 DEFAULT_WINDOW_HEIGHT = 720;60static constexpr u32 DEFAULT_WINDOW_WIDTH = 1920;61static constexpr u32 DEFAULT_WINDOW_HEIGHT = 1080;6263static constexpr u32 SETTINGS_VERSION = 3;64static constexpr auto CPU_THREAD_POLL_INTERVAL =65std::chrono::milliseconds(8); // how often we'll poll controllers when paused6667static bool ParseCommandLineParametersAndInitializeConfig(int argc, char* argv[],68std::optional<SystemBootParameters>& autoboot);69static void PrintCommandLineVersion();70static void PrintCommandLineHelp(const char* progname);71static bool InitializeConfig();72static void InitializeEarlyConsole();73static void HookSignals();74static void SetAppRoot();75static void SetResourcesDirectory();76static bool SetDataDirectory();77static bool SetCriticalFolders();78static void SetDefaultSettings(SettingsInterface& si, bool system, bool controller);79static std::string GetResourcePath(std::string_view name, bool allow_override);80static bool PerformEarlyHardwareChecks();81static bool EarlyProcessStartup();82static void WarnAboutInterface();83static void StartCPUThread();84static void StopCPUThread();85static void ProcessCPUThreadEvents(bool block);86static void ProcessCPUThreadPlatformMessages();87static void CPUThreadEntryPoint();88static void CPUThreadMainLoop();89static void GPUThreadEntryPoint();90static void UIThreadMainLoop();91static void ProcessSDLEvent(const SDL_Event* ev);92static std::string GetWindowTitle(const std::string& game_title);93static std::optional<WindowInfo> TranslateSDLWindowInfo(SDL_Window* win, Error* error);94static bool GetSavedPlatformWindowGeometry(s32* x, s32* y, s32* width, s32* height);95static void SavePlatformWindowGeometry(s32 x, s32 y, s32 width, s32 height);9697struct SDLHostState98{99// UI thread state100ALIGN_TO_CACHE_LINE INISettingsInterface base_settings_interface;101bool batch_mode = false;102bool start_fullscreen_ui_fullscreen = false;103bool was_paused_by_focus_loss = false;104bool ui_thread_running = false;105106u32 func_event_id = 0;107108SDL_Window* sdl_window = nullptr;109float sdl_window_scale = 0.0f;110WindowInfo::PreRotation force_prerotation = WindowInfo::PreRotation::Identity;111std::atomic_bool fullscreen{false};112113Threading::Thread cpu_thread;114Threading::Thread gpu_thread;115Threading::KernelSemaphore platform_window_updated;116117std::mutex state_mutex;118FullscreenUI::BackgroundProgressCallback* game_list_refresh_progress = nullptr;119120// CPU thread state.121ALIGN_TO_CACHE_LINE std::atomic_bool cpu_thread_running{false};122std::mutex cpu_thread_events_mutex;123std::condition_variable cpu_thread_event_done;124std::condition_variable cpu_thread_event_posted;125std::deque<std::pair<std::function<void()>, bool>> cpu_thread_events;126u32 blocking_cpu_events_pending = 0;127};128129static SDLHostState s_state;130} // namespace MiniHost131132//////////////////////////////////////////////////////////////////////////133// Initialization/Shutdown134//////////////////////////////////////////////////////////////////////////135136bool MiniHost::PerformEarlyHardwareChecks()137{138Error error;139const bool okay = System::PerformEarlyHardwareChecks(&error);140if (okay && !error.IsValid()) [[likely]]141return true;142143if (okay)144Host::ReportErrorAsync("Hardware Check Warning", error.GetDescription());145else146Host::ReportFatalError("Hardware Check Failed", error.GetDescription());147148return okay;149}150151bool MiniHost::EarlyProcessStartup()152{153Error error;154if (!System::ProcessStartup(&error)) [[unlikely]]155{156Host::ReportFatalError("Process Startup Failed", error.GetDescription());157return false;158}159160#if !__has_include("scmversion/tag.h")161//162// To those distributing their own builds or packages of DuckStation, and seeing this message:163//164// DuckStation is licensed under the CC-BY-NC-ND-4.0 license.165//166// This means that you do NOT have permission to re-distribute your own modified builds of DuckStation.167// Modifying DuckStation for personal use is fine, but you cannot distribute builds with your changes.168// As per the CC-BY-NC-ND conditions, you can re-distribute the official builds from https://www.duckstation.org/ and169// https://github.com/stenzek/duckstation, so long as they are left intact, without modification. I welcome and170// appreciate any pull requests made to the official repository at https://github.com/stenzek/duckstation.171//172// I made the decision to switch to a no-derivatives license because of numerous "forks" that were created purely for173// generating money for the person who knocked it off, and always died, leaving the community with multiple builds to174// choose from, most of which were out of date and broken, and endless confusion. Other forks copy/pasted upstream175// changes without attribution, violating copyright.176//177// Thanks, and I hope you understand.178//179180const char* message = ICON_EMOJI_WARNING "WARNING! You are not using an official release! " ICON_EMOJI_WARNING "\n\n"181"DuckStation is licensed under the terms of CC-BY-NC-ND-4.0,\n"182"which does not allow modified builds to be distributed.\n\n"183"This build is NOT OFFICIAL and may be broken and/or malicious.\n\n"184"You should download an official build from https://www.duckstation.org/.";185186Host::AddKeyedOSDWarning("OfficialReleaseWarning", message, Host::OSD_CRITICAL_ERROR_DURATION);187#endif188189return true;190}191192bool MiniHost::SetCriticalFolders()193{194SetAppRoot();195SetResourcesDirectory();196if (!SetDataDirectory())197return false;198199// logging of directories in case something goes wrong super early200DEV_LOG("AppRoot Directory: {}", EmuFolders::AppRoot);201DEV_LOG("DataRoot Directory: {}", EmuFolders::DataRoot);202DEV_LOG("Resources Directory: {}", EmuFolders::Resources);203204// Write crash dumps to the data directory, since that'll be accessible for certain.205CrashHandler::SetWriteDirectory(EmuFolders::DataRoot);206207// the resources directory should exist, bail out if not208if (!FileSystem::DirectoryExists(EmuFolders::Resources.c_str()))209{210Host::ReportFatalError("Error", "Resources directory is missing, your installation is incomplete.");211return false;212}213214return true;215}216217void MiniHost::SetAppRoot()218{219const std::string program_path = FileSystem::GetProgramPath();220INFO_LOG("Program Path: {}", program_path);221222EmuFolders::AppRoot = Path::Canonicalize(Path::GetDirectory(program_path));223}224225void MiniHost::SetResourcesDirectory()226{227#ifndef __APPLE__228// On Windows/Linux, these are in the binary directory.229EmuFolders::Resources = Path::Combine(EmuFolders::AppRoot, "resources");230#else231// On macOS, this is in the bundle resources directory.232EmuFolders::Resources = Path::Canonicalize(Path::Combine(EmuFolders::AppRoot, "../Resources"));233#endif234}235236bool MiniHost::SetDataDirectory()237{238EmuFolders::DataRoot = Host::Internal::ComputeDataDirectory();239240// make sure it exists241if (!EmuFolders::DataRoot.empty() && !FileSystem::DirectoryExists(EmuFolders::DataRoot.c_str()))242{243// we're in trouble if we fail to create this directory... but try to hobble on with portable244Error error;245if (!FileSystem::EnsureDirectoryExists(EmuFolders::DataRoot.c_str(), false, &error))246{247Host::ReportFatalError("Error",248TinyString::from_format("Failed to create data directory: {}", error.GetDescription()));249return false;250}251}252253// couldn't determine the data directory? fallback to portable.254if (EmuFolders::DataRoot.empty())255EmuFolders::DataRoot = EmuFolders::AppRoot;256257return true;258}259260bool MiniHost::InitializeConfig()261{262if (!SetCriticalFolders())263return false;264265std::string settings_path = Path::Combine(EmuFolders::DataRoot, "settings.ini");266const bool settings_exists = FileSystem::FileExists(settings_path.c_str());267INFO_LOG("Loading config from {}.", settings_path);268s_state.base_settings_interface.SetPath(std::move(settings_path));269Host::Internal::SetBaseSettingsLayer(&s_state.base_settings_interface);270271u32 settings_version;272if (!settings_exists || !s_state.base_settings_interface.Load() ||273!s_state.base_settings_interface.GetUIntValue("Main", "SettingsVersion", &settings_version) ||274settings_version != SETTINGS_VERSION)275{276if (s_state.base_settings_interface.ContainsValue("Main", "SettingsVersion"))277{278// NOTE: No point translating this, because there's no config loaded, so no language loaded.279Host::ReportErrorAsync("Error", fmt::format("Settings version {} does not match expected version {}, resetting.",280settings_version, SETTINGS_VERSION));281}282283s_state.base_settings_interface.SetUIntValue("Main", "SettingsVersion", SETTINGS_VERSION);284SetDefaultSettings(s_state.base_settings_interface, true, true);285286// Make sure we can actually save the config, and the user doesn't have some permission issue.287Error error;288if (!s_state.base_settings_interface.Save(&error))289{290Host::ReportFatalError(291"Error",292fmt::format(293"Failed to save configuration to\n\n{}\n\nThe error was: {}\n\nPlease ensure this directory is writable. You "294"can also try portable mode by creating portable.txt in the same directory you installed DuckStation into.",295s_state.base_settings_interface.GetPath(), error.GetDescription()));296return false;297}298}299300EmuFolders::LoadConfig(s_state.base_settings_interface);301EmuFolders::EnsureFoldersExist();302303// We need to create the console window early, otherwise it appears in front of the main window.304if (!Log::IsConsoleOutputEnabled() && s_state.base_settings_interface.GetBoolValue("Logging", "LogToConsole", false))305Log::SetConsoleOutputParams(true, s_state.base_settings_interface.GetBoolValue("Logging", "LogTimestamps", true));306307return true;308}309310void MiniHost::SetDefaultSettings(SettingsInterface& si, bool system, bool controller)311{312if (system)313{314System::SetDefaultSettings(si);315EmuFolders::SetDefaults();316EmuFolders::Save(si);317}318319if (controller)320{321InputManager::SetDefaultSourceConfig(si);322Settings::SetDefaultControllerConfig(si);323Settings::SetDefaultHotkeyConfig(si);324}325}326327void Host::ReportDebuggerMessage(std::string_view message)328{329ERROR_LOG("ReportDebuggerMessage(): {}", message);330}331332std::span<const std::pair<const char*, const char*>> Host::GetAvailableLanguageList()333{334return {};335}336337const char* Host::GetLanguageName(std::string_view language_code)338{339return "";340}341342bool Host::ChangeLanguage(const char* new_language)343{344return false;345}346347void Host::AddFixedInputBindings(const SettingsInterface& si)348{349}350351void Host::OnInputDeviceConnected(InputBindingKey key, std::string_view identifier, std::string_view device_name)352{353Host::AddKeyedOSDMessage(fmt::format("InputDeviceConnected-{}", identifier),354fmt::format("Input device {0} ({1}) connected.", device_name, identifier), 10.0f);355}356357void Host::OnInputDeviceDisconnected(InputBindingKey key, std::string_view identifier)358{359Host::AddKeyedOSDMessage(fmt::format("InputDeviceConnected-{}", identifier),360fmt::format("Input device {} disconnected.", identifier), 10.0f);361}362363s32 Host::Internal::GetTranslatedStringImpl(std::string_view context, std::string_view msg,364std::string_view disambiguation, char* tbuf, size_t tbuf_space)365{366if (msg.size() > tbuf_space)367return -1;368else if (msg.empty())369return 0;370371std::memcpy(tbuf, msg.data(), msg.size());372return static_cast<s32>(msg.size());373}374375std::string Host::TranslatePluralToString(const char* context, const char* msg, const char* disambiguation, int count)376{377TinyString count_str = TinyString::from_format("{}", count);378379std::string ret(msg);380for (;;)381{382std::string::size_type pos = ret.find("%n");383if (pos == std::string::npos)384break;385386ret.replace(pos, pos + 2, count_str.view());387}388389return ret;390}391392SmallString Host::TranslatePluralToSmallString(const char* context, const char* msg, const char* disambiguation,393int count)394{395SmallString ret(msg);396ret.replace("%n", TinyString::from_format("{}", count));397return ret;398}399400std::string MiniHost::GetResourcePath(std::string_view filename, bool allow_override)401{402return allow_override ? EmuFolders::GetOverridableResourcePath(filename) :403Path::Combine(EmuFolders::Resources, filename);404}405406bool Host::ResourceFileExists(std::string_view filename, bool allow_override)407{408const std::string path = MiniHost::GetResourcePath(filename, allow_override);409return FileSystem::FileExists(path.c_str());410}411412std::optional<DynamicHeapArray<u8>> Host::ReadResourceFile(std::string_view filename, bool allow_override, Error* error)413{414const std::string path = MiniHost::GetResourcePath(filename, allow_override);415return FileSystem::ReadBinaryFile(path.c_str(), error);416}417418std::optional<std::string> Host::ReadResourceFileToString(std::string_view filename, bool allow_override, Error* error)419{420const std::string path = MiniHost::GetResourcePath(filename, allow_override);421return FileSystem::ReadFileToString(path.c_str(), error);422}423424std::optional<std::time_t> Host::GetResourceFileTimestamp(std::string_view filename, bool allow_override)425{426const std::string path = MiniHost::GetResourcePath(filename, allow_override);427FILESYSTEM_STAT_DATA sd;428if (!FileSystem::StatFile(path.c_str(), &sd))429{430ERROR_LOG("Failed to stat resource file '{}'", filename);431return std::nullopt;432}433434return sd.ModificationTime;435}436437void Host::LoadSettings(const SettingsInterface& si, std::unique_lock<std::mutex>& lock)438{439}440441void Host::CheckForSettingsChanges(const Settings& old_settings)442{443}444445void Host::CommitBaseSettingChanges()446{447auto lock = Host::GetSettingsLock();448Error error;449if (!MiniHost::s_state.base_settings_interface.Save(&error))450ERROR_LOG("Failed to save settings: {}", error.GetDescription());451}452453std::optional<WindowInfo> MiniHost::TranslateSDLWindowInfo(SDL_Window* win, Error* error)454{455if (!win)456{457Error::SetStringView(error, "Window handle is null.");458return std::nullopt;459}460461const SDL_WindowFlags window_flags = SDL_GetWindowFlags(win);462int window_width = 1, window_height = 1;463int window_px_width = 1, window_px_height = 1;464SDL_GetWindowSize(win, &window_width, &window_height);465SDL_GetWindowSizeInPixels(win, &window_px_width, &window_px_height);466s_state.sdl_window_scale = SDL_GetWindowDisplayScale(win);467468const SDL_DisplayMode* dispmode = nullptr;469470if (window_flags & SDL_WINDOW_FULLSCREEN)471{472if (!(dispmode = SDL_GetWindowFullscreenMode(win)))473ERROR_LOG("SDL_GetWindowFullscreenMode() failed: {}", SDL_GetError());474}475476if (const SDL_DisplayID display_id = SDL_GetDisplayForWindow(win); display_id != 0)477{478if (!(window_flags & SDL_WINDOW_FULLSCREEN))479{480if (!(dispmode = SDL_GetDesktopDisplayMode(display_id)))481ERROR_LOG("SDL_GetDesktopDisplayMode() failed: {}", SDL_GetError());482}483}484485WindowInfo wi;486wi.surface_width = static_cast<u16>(window_px_width);487wi.surface_height = static_cast<u16>(window_px_height);488wi.surface_scale = s_state.sdl_window_scale;489wi.surface_prerotation = s_state.force_prerotation;490491// set display refresh rate if available492if (dispmode && dispmode->refresh_rate > 0.0f)493{494INFO_LOG("Display mode refresh rate: {} hz", dispmode->refresh_rate);495wi.surface_refresh_rate = dispmode->refresh_rate;496}497498// SDL's opengl window flag tends to make a mess of pixel formats...499if (!(SDL_GetWindowFlags(win) & (SDL_WINDOW_OPENGL | SDL_WINDOW_VULKAN)))500{501const SDL_PropertiesID props = SDL_GetWindowProperties(win);502if (props == 0)503{504Error::SetStringFmt(error, "SDL_GetWindowProperties() failed: {}", SDL_GetError());505return std::nullopt;506}507508#if defined(SDL_PLATFORM_WINDOWS)509wi.type = WindowInfo::Type::Win32;510wi.window_handle = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WIN32_HWND_POINTER, nullptr);511if (!wi.window_handle)512{513Error::SetStringView(error, "SDL_PROP_WINDOW_WIN32_HWND_POINTER not found.");514return std::nullopt;515}516#elif defined(SDL_PLATFORM_MACOS)517wi.type = WindowInfo::Type::MacOS;518wi.window_handle = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, nullptr);519if (!wi.window_handle)520{521Error::SetStringView(error, "SDL_PROP_WINDOW_COCOA_WINDOW_POINTER not found.");522return std::nullopt;523}524#elif defined(SDL_PLATFORM_LINUX) || defined(SDL_PLATFORM_FREEBSD)525const std::string_view video_driver = SDL_GetCurrentVideoDriver();526if (video_driver == "x11")527{528wi.display_connection = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_X11_DISPLAY_POINTER, nullptr);529wi.window_handle = reinterpret_cast<void*>(530static_cast<intptr_t>(SDL_GetNumberProperty(props, SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0)));531if (!wi.display_connection)532{533Error::SetStringView(error, "SDL_PROP_WINDOW_X11_DISPLAY_POINTER not found.");534return std::nullopt;535}536else if (!wi.window_handle)537{538Error::SetStringView(error, "SDL_PROP_WINDOW_X11_WINDOW_NUMBER not found.");539return std::nullopt;540}541}542else if (video_driver == "wayland")543{544wi.display_connection = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_DISPLAY_POINTER, nullptr);545wi.window_handle = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_SURFACE_POINTER, nullptr);546if (!wi.display_connection)547{548Error::SetStringView(error, "SDL_PROP_WINDOW_WAYLAND_DISPLAY_POINTER not found.");549return std::nullopt;550}551else if (!wi.window_handle)552{553Error::SetStringView(error, "SDL_PROP_WINDOW_WAYLAND_SURFACE_POINTER not found.");554return std::nullopt;555}556}557else558{559Error::SetStringFmt(error, "Video driver {} not supported.", video_driver);560return std::nullopt;561}562#else563#error Unsupported platform.564#endif565}566else567{568// nothing handled, fall back to SDL abstraction569wi.type = WindowInfo::Type::SDL;570wi.window_handle = win;571}572573return wi;574}575576std::optional<WindowInfo> Host::AcquireRenderWindow(RenderAPI render_api, bool fullscreen, bool exclusive_fullscreen,577Error* error)578{579using namespace MiniHost;580581std::optional<WindowInfo> wi;582583Host::RunOnUIThread([render_api, fullscreen, error, &wi]() {584const std::string window_title = GetWindowTitle(System::GetGameTitle());585const SDL_PropertiesID props = SDL_CreateProperties();586SDL_SetStringProperty(props, SDL_PROP_WINDOW_CREATE_TITLE_STRING, window_title.c_str());587588SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_RESIZABLE_BOOLEAN, true);589SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_FOCUSABLE_BOOLEAN, true);590SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_HIGH_PIXEL_DENSITY_BOOLEAN, true);591592if (render_api == RenderAPI::OpenGL || render_api == RenderAPI::OpenGLES)593SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_OPENGL_BOOLEAN, true);594else if (render_api == RenderAPI::Vulkan)595SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_VULKAN_BOOLEAN, true);596597if (fullscreen)598{599SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_BORDERLESS_BOOLEAN, true);600SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_FULLSCREEN_BOOLEAN, true);601}602603if (s32 window_x, window_y, window_width, window_height;604MiniHost::GetSavedPlatformWindowGeometry(&window_x, &window_y, &window_width, &window_height))605{606SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_X_NUMBER, window_x);607SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_Y_NUMBER, window_y);608SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, window_width);609SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, window_height);610}611else612{613SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, DEFAULT_WINDOW_WIDTH);614SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, DEFAULT_WINDOW_HEIGHT);615}616617s_state.sdl_window = SDL_CreateWindowWithProperties(props);618SDL_DestroyProperties(props);619620if (s_state.sdl_window)621{622wi = TranslateSDLWindowInfo(s_state.sdl_window, error);623if (wi.has_value())624{625s_state.fullscreen.store(fullscreen, std::memory_order_release);626}627else628{629SDL_DestroyWindow(s_state.sdl_window);630s_state.sdl_window = nullptr;631}632}633else634{635Error::SetStringFmt(error, "SDL_CreateWindow() failed: {}", SDL_GetError());636}637638s_state.platform_window_updated.Post();639});640641s_state.platform_window_updated.Wait();642643// reload input sources, since it might use the window handle644{645auto lock = Host::GetSettingsLock();646InputManager::ReloadSources(*Host::GetSettingsInterface(), lock);647}648649return wi;650}651652void Host::ReleaseRenderWindow()653{654using namespace MiniHost;655656if (!s_state.sdl_window)657return;658659Host::RunOnUIThread([]() {660if (!s_state.fullscreen.load(std::memory_order_acquire))661{662int window_x = SDL_WINDOWPOS_UNDEFINED, window_y = SDL_WINDOWPOS_UNDEFINED;663int window_width = DEFAULT_WINDOW_WIDTH, window_height = DEFAULT_WINDOW_HEIGHT;664SDL_GetWindowPosition(s_state.sdl_window, &window_x, &window_y);665SDL_GetWindowSize(s_state.sdl_window, &window_width, &window_height);666MiniHost::SavePlatformWindowGeometry(window_x, window_y, window_width, window_height);667}668else669{670s_state.fullscreen.store(false, std::memory_order_release);671}672673SDL_DestroyWindow(s_state.sdl_window);674s_state.sdl_window = nullptr;675676s_state.platform_window_updated.Post();677});678679s_state.platform_window_updated.Wait();680}681682bool Host::IsFullscreen()683{684using namespace MiniHost;685686return s_state.fullscreen.load(std::memory_order_acquire);687}688689void Host::SetFullscreen(bool enabled)690{691using namespace MiniHost;692693if (!s_state.sdl_window || s_state.fullscreen.load(std::memory_order_acquire) == enabled)694return;695696if (!SDL_SetWindowFullscreen(s_state.sdl_window, enabled))697{698ERROR_LOG("SDL_SetWindowFullscreen() failed: {}", SDL_GetError());699return;700}701702s_state.fullscreen.store(enabled, std::memory_order_release);703}704705void Host::BeginTextInput()706{707using namespace MiniHost;708709SDL_StartTextInput(s_state.sdl_window);710}711712void Host::EndTextInput()713{714// we want to keep getting text events, SDL_StopTextInput() apparently inhibits that715}716717bool Host::CreateAuxiliaryRenderWindow(s32 x, s32 y, u32 width, u32 height, std::string_view title,718std::string_view icon_name, AuxiliaryRenderWindowUserData userdata,719AuxiliaryRenderWindowHandle* handle, WindowInfo* wi, Error* error)720{721// not here, but could be...722Error::SetStringView(error, "Not supported.");723return false;724}725726void Host::DestroyAuxiliaryRenderWindow(AuxiliaryRenderWindowHandle handle, s32* pos_x /* = nullptr */,727s32* pos_y /* = nullptr */, u32* width /* = nullptr */,728u32* height /* = nullptr */)729{730// noop731}732733bool MiniHost::GetSavedPlatformWindowGeometry(s32* x, s32* y, s32* width, s32* height)734{735const auto lock = Host::GetSettingsLock();736737bool result = s_state.base_settings_interface.GetIntValue("UI", "MainWindowX", x);738result = result && s_state.base_settings_interface.GetIntValue("UI", "MainWindowY", y);739result = result && s_state.base_settings_interface.GetIntValue("UI", "MainWindowWidth", width);740result = result && s_state.base_settings_interface.GetIntValue("UI", "MainWindowHeight", height);741return result;742}743744void MiniHost::SavePlatformWindowGeometry(s32 x, s32 y, s32 width, s32 height)745{746if (Host::IsFullscreen())747return;748749const auto lock = Host::GetSettingsLock();750s_state.base_settings_interface.SetIntValue("UI", "MainWindowX", x);751s_state.base_settings_interface.SetIntValue("UI", "MainWindowY", y);752s_state.base_settings_interface.SetIntValue("UI", "MainWindowWidth", width);753s_state.base_settings_interface.SetIntValue("UI", "MainWindowHeight", height);754}755756void MiniHost::UIThreadMainLoop()757{758while (s_state.ui_thread_running)759{760SDL_Event ev;761if (!SDL_WaitEvent(&ev))762continue;763764ProcessSDLEvent(&ev);765}766}767768void MiniHost::ProcessSDLEvent(const SDL_Event* ev)769{770switch (ev->type)771{772case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:773{774Host::RunOnCPUThread(775[window_width = ev->window.data1, window_height = ev->window.data2, window_scale = s_state.sdl_window_scale]() {776GPUThread::ResizeDisplayWindow(window_width, window_height, window_scale);777});778}779break;780781case SDL_EVENT_WINDOW_DISPLAY_CHANGED:782case SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED:783{784const float new_scale = SDL_GetWindowDisplayScale(s_state.sdl_window);785if (new_scale != s_state.sdl_window_scale)786{787s_state.sdl_window_scale = new_scale;788789int window_width = 1, window_height = 1;790SDL_GetWindowSizeInPixels(s_state.sdl_window, &window_width, &window_height);791Host::RunOnCPUThread([window_width, window_height, window_scale = s_state.sdl_window_scale]() {792GPUThread::ResizeDisplayWindow(window_width, window_height, window_scale);793});794}795}796break;797798case SDL_EVENT_WINDOW_CLOSE_REQUESTED:799{800Host::RunOnCPUThread([]() { Host::RequestExitApplication(false); });801}802break;803804case SDL_EVENT_WINDOW_FOCUS_GAINED:805{806Host::RunOnCPUThread([]() {807if (!System::IsValid() || !s_state.was_paused_by_focus_loss)808return;809810System::PauseSystem(false);811s_state.was_paused_by_focus_loss = false;812});813}814break;815816case SDL_EVENT_WINDOW_FOCUS_LOST:817{818Host::RunOnCPUThread([]() {819if (!System::IsRunning() || !g_settings.pause_on_focus_loss)820return;821822s_state.was_paused_by_focus_loss = true;823System::PauseSystem(true);824});825}826break;827828case SDL_EVENT_KEY_DOWN:829case SDL_EVENT_KEY_UP:830{831if (const std::optional<u32> key = InputManager::ConvertHostNativeKeyCodeToKeyCode(ev->key.raw))832{833Host::RunOnCPUThread([key_code = key.value(), pressed = (ev->type == SDL_EVENT_KEY_DOWN)]() {834InputManager::InvokeEvents(InputManager::MakeHostKeyboardKey(key_code), pressed ? 1.0f : 0.0f,835GenericInputBinding::Unknown);836});837}838}839break;840841case SDL_EVENT_TEXT_INPUT:842{843if (ImGuiManager::WantsTextInput())844Host::RunOnCPUThread([text = std::string(ev->text.text)]() { ImGuiManager::AddTextInput(std::move(text)); });845}846break;847848case SDL_EVENT_MOUSE_MOTION:849{850Host::RunOnCPUThread([x = static_cast<float>(ev->motion.x), y = static_cast<float>(ev->motion.y)]() {851InputManager::UpdatePointerAbsolutePosition(0, x, y);852ImGuiManager::UpdateMousePosition(x, y);853});854}855break;856857case SDL_EVENT_MOUSE_BUTTON_DOWN:858case SDL_EVENT_MOUSE_BUTTON_UP:859{860if (ev->button.button > 0)861{862// swap middle/right because sdl orders them differently863const u8 button = (ev->button.button == 3) ? 1 : ((ev->button.button == 2) ? 2 : (ev->button.button - 1));864Host::RunOnCPUThread([button, pressed = (ev->type == SDL_EVENT_MOUSE_BUTTON_DOWN)]() {865InputManager::InvokeEvents(InputManager::MakePointerButtonKey(0, button), pressed ? 1.0f : 0.0f,866GenericInputBinding::Unknown);867});868}869}870break;871872case SDL_EVENT_MOUSE_WHEEL:873{874Host::RunOnCPUThread([x = ev->wheel.x, y = ev->wheel.y]() {875if (x != 0.0f)876InputManager::UpdatePointerRelativeDelta(0, InputPointerAxis::WheelX, x);877if (y != 0.0f)878InputManager::UpdatePointerRelativeDelta(0, InputPointerAxis::WheelY, y);879});880}881break;882883case SDL_EVENT_QUIT:884{885Host::RunOnCPUThread([]() { Host::RequestExitApplication(false); });886}887break;888889default:890{891if (ev->type == s_state.func_event_id)892{893std::function<void()>* pfunc = reinterpret_cast<std::function<void()>*>(ev->user.data1);894if (pfunc)895{896(*pfunc)();897delete pfunc;898}899}900else if (SDLInputSource::IsHandledInputEvent(ev))901{902Host::RunOnCPUThread([event_copy = *ev]() {903SDLInputSource* is =904static_cast<SDLInputSource*>(InputManager::GetInputSourceInterface(InputSourceType::SDL));905if (is)906is->ProcessSDLEvent(&event_copy);907});908}909}910break;911}912}913914void MiniHost::ProcessCPUThreadPlatformMessages()915{916// This is lame. On Win32, we need to pump messages, even though *we* don't have any windows917// on the CPU thread, because SDL creates a hidden window for raw input for some game controllers.918// If we don't do this, we don't get any controller events.919#ifdef _WIN32920MSG msg;921while (PeekMessageW(&msg, NULL, 0, 0, PM_REMOVE))922{923TranslateMessage(&msg);924DispatchMessageW(&msg);925}926#endif927}928929void MiniHost::ProcessCPUThreadEvents(bool block)930{931std::unique_lock lock(s_state.cpu_thread_events_mutex);932933for (;;)934{935if (s_state.cpu_thread_events.empty())936{937if (!block || !s_state.cpu_thread_running.load(std::memory_order_acquire))938return;939940// we still need to keep polling the controllers when we're paused941do942{943ProcessCPUThreadPlatformMessages();944InputManager::PollSources();945} while (!s_state.cpu_thread_event_posted.wait_for(lock, CPU_THREAD_POLL_INTERVAL,946[]() { return !s_state.cpu_thread_events.empty(); }));947}948949// return after processing all events if we had one950block = false;951952auto event = std::move(s_state.cpu_thread_events.front());953s_state.cpu_thread_events.pop_front();954lock.unlock();955event.first();956lock.lock();957958if (event.second)959{960s_state.blocking_cpu_events_pending--;961s_state.cpu_thread_event_done.notify_one();962}963}964}965966void MiniHost::StartCPUThread()967{968s_state.cpu_thread_running.store(true, std::memory_order_release);969s_state.cpu_thread.Start(CPUThreadEntryPoint);970}971972void MiniHost::StopCPUThread()973{974if (!s_state.cpu_thread.Joinable())975return;976977{978std::unique_lock lock(s_state.cpu_thread_events_mutex);979s_state.cpu_thread_running.store(false, std::memory_order_release);980s_state.cpu_thread_event_posted.notify_one();981}982983s_state.cpu_thread.Join();984}985986void MiniHost::CPUThreadEntryPoint()987{988Threading::SetNameOfCurrentThread("CPU Thread");989990// input source setup must happen on emu thread991Error error;992if (!System::CPUThreadInitialize(&error, NUM_ASYNC_WORKER_THREADS))993{994Host::ReportFatalError("CPU Thread Initialization Failed", error.GetDescription());995return;996}997998// start up GPU thread999s_state.gpu_thread.Start(&GPUThreadEntryPoint);10001001// start the fullscreen UI and get it going1002if (GPUThread::StartFullscreenUI(s_state.start_fullscreen_ui_fullscreen, &error))1003{1004WarnAboutInterface();10051006// kick a game list refresh if we're not in batch mode1007if (!s_state.batch_mode)1008Host::RefreshGameListAsync(false);10091010CPUThreadMainLoop();10111012Host::CancelGameListRefresh();1013}1014else1015{1016Host::ReportFatalError("Error", fmt::format("Failed to start fullscreen UI: {}", error.GetDescription()));1017}10181019// finish any events off (e.g. shutdown system with save)1020ProcessCPUThreadEvents(false);10211022if (System::IsValid())1023System::ShutdownSystem(false);10241025GPUThread::StopFullscreenUI();1026GPUThread::Internal::RequestShutdown();1027s_state.gpu_thread.Join();10281029System::CPUThreadShutdown();10301031// Tell the UI thread to shut down.1032Host::RunOnUIThread([]() { s_state.ui_thread_running = false; });1033}10341035void MiniHost::CPUThreadMainLoop()1036{1037while (s_state.cpu_thread_running.load(std::memory_order_acquire))1038{1039if (System::IsRunning())1040{1041System::Execute();1042continue;1043}1044else if (!GPUThread::IsUsingThread() && GPUThread::IsRunningIdle())1045{1046ProcessCPUThreadEvents(false);1047if (!GPUThread::IsUsingThread() && GPUThread::IsRunningIdle())1048GPUThread::Internal::DoRunIdle();1049}10501051ProcessCPUThreadEvents(true);1052}1053}10541055void MiniHost::GPUThreadEntryPoint()1056{1057Threading::SetNameOfCurrentThread("GPU Thread");1058GPUThread::Internal::GPUThreadEntryPoint();1059}10601061void Host::OnSystemStarting()1062{1063MiniHost::s_state.was_paused_by_focus_loss = false;1064}10651066void Host::OnSystemStarted()1067{1068}10691070void Host::OnSystemPaused()1071{1072}10731074void Host::OnSystemResumed()1075{1076}10771078void Host::OnSystemStopping()1079{1080}10811082void Host::OnSystemDestroyed()1083{1084}10851086void Host::OnSystemAbnormalShutdown(const std::string_view reason)1087{1088GPUThread::RunOnThread([reason = std::string(reason)]() {1089ImGuiFullscreen::OpenInfoMessageDialog(1090"Abnormal System Shutdown", fmt::format("Unfortunately, the virtual machine has abnormally shut down and cannot "1091"be recovered. More information about the error is below:\n\n{}",1092reason));1093});1094}10951096void Host::OnGPUThreadRunIdleChanged(bool is_active)1097{1098}10991100void Host::FrameDoneOnGPUThread(GPUBackend* gpu_backend, u32 frame_number)1101{1102}11031104void Host::OnPerformanceCountersUpdated(const GPUBackend* gpu_backend)1105{1106// noop1107}11081109void Host::OnAchievementsLoginRequested(Achievements::LoginRequestReason reason)1110{1111// noop1112}11131114void Host::OnAchievementsLoginSuccess(const char* username, u32 points, u32 sc_points, u32 unread_messages)1115{1116// noop1117}11181119void Host::OnAchievementsRefreshed()1120{1121// noop1122}11231124void Host::OnAchievementsActiveChanged(bool active)1125{1126// noop1127}11281129void Host::OnAchievementsHardcoreModeChanged(bool enabled)1130{1131// noop1132}11331134void Host::OnAchievementsAllProgressRefreshed()1135{1136// noop1137}11381139#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION11401141void Host::OnRAIntegrationMenuChanged()1142{1143// noop1144}11451146#endif11471148void Host::SetMouseMode(bool relative, bool hide_cursor)1149{1150// noop1151}11521153void Host::OnMediaCaptureStarted()1154{1155// noop1156}11571158void Host::OnMediaCaptureStopped()1159{1160// noop1161}11621163void Host::PumpMessagesOnCPUThread()1164{1165MiniHost::ProcessCPUThreadEvents(false);1166}11671168std::string MiniHost::GetWindowTitle(const std::string& game_title)1169{1170#if defined(_DEBUGFAST)1171static constexpr std::string_view suffix = " [DebugFast]";1172#elif defined(_DEBUG)1173static constexpr std::string_view suffix = " [Debug]";1174#else1175static constexpr std::string_view suffix = std::string_view();1176#endif11771178if (System::IsShutdown() || game_title.empty())1179return fmt::format("DuckStation {}{}", g_scm_tag_str, suffix);1180else1181return fmt::format("{}{}", game_title, suffix);1182}11831184void MiniHost::WarnAboutInterface()1185{1186const char* message = "This is the \"mini\" interface for DuckStation, and is missing many features.\n"1187" We recommend using the Qt interface instead, which you can download\n"1188" from https://www.duckstation.org/.";1189Host::AddIconOSDWarning("MiniWarning", ICON_EMOJI_WARNING, message, Host::OSD_INFO_DURATION);1190}11911192void Host::OnSystemGameChanged(const std::string& disc_path, const std::string& game_serial,1193const std::string& game_name, GameHash game_hash)1194{1195using namespace MiniHost;11961197VERBOSE_LOG("Host::OnGameChanged(\"{}\", \"{}\", \"{}\")", disc_path, game_serial, game_name);1198if (s_state.sdl_window)1199SDL_SetWindowTitle(s_state.sdl_window, GetWindowTitle(game_name).c_str());1200}12011202void Host::OnSystemUndoStateAvailabilityChanged(bool available, u64 timestamp)1203{1204//1205}12061207void Host::RunOnCPUThread(std::function<void()> function, bool block /* = false */)1208{1209using namespace MiniHost;12101211std::unique_lock lock(s_state.cpu_thread_events_mutex);1212s_state.cpu_thread_events.emplace_back(std::move(function), block);1213s_state.blocking_cpu_events_pending += BoolToUInt32(block);1214s_state.cpu_thread_event_posted.notify_one();1215if (block)1216s_state.cpu_thread_event_done.wait(lock, []() { return s_state.blocking_cpu_events_pending == 0; });1217}12181219void Host::RunOnUIThread(std::function<void()> function, bool block /* = false */)1220{1221using namespace MiniHost;12221223std::function<void()>* pfunc = new std::function<void()>(std::move(function));12241225SDL_Event ev;1226ev.user = {};1227ev.type = s_state.func_event_id;1228ev.user.data1 = pfunc;1229SDL_PushEvent(&ev);1230}12311232void Host::RefreshGameListAsync(bool invalidate_cache)1233{1234using namespace MiniHost;12351236std::unique_lock lock(s_state.state_mutex);12371238while (s_state.game_list_refresh_progress)1239{1240lock.unlock();1241CancelGameListRefresh();1242lock.lock();1243}12441245s_state.game_list_refresh_progress = new FullscreenUI::BackgroundProgressCallback("glrefresh");1246System::QueueAsyncTask([invalidate_cache]() {1247GameList::Refresh(invalidate_cache, false, s_state.game_list_refresh_progress);12481249std::unique_lock lock(s_state.state_mutex);1250delete s_state.game_list_refresh_progress;1251s_state.game_list_refresh_progress = nullptr;1252});1253}12541255void Host::CancelGameListRefresh()1256{1257using namespace MiniHost;12581259{1260std::unique_lock lock(s_state.state_mutex);1261if (!s_state.game_list_refresh_progress)1262return;12631264s_state.game_list_refresh_progress->SetCancelled();1265}12661267System::WaitForAllAsyncTasks();1268}12691270void Host::OnGameListEntriesChanged(std::span<const u32> changed_indices)1271{1272// constantly re-querying, don't need to do anything1273}12741275std::optional<WindowInfo> Host::GetTopLevelWindowInfo()1276{1277return MiniHost::TranslateSDLWindowInfo(MiniHost::s_state.sdl_window, nullptr);1278}12791280void Host::RequestResetSettings(bool system, bool controller)1281{1282using namespace MiniHost;12831284auto lock = Host::GetSettingsLock();1285{1286SettingsInterface& si = s_state.base_settings_interface;12871288if (system)1289{1290System::SetDefaultSettings(si);1291EmuFolders::SetDefaults();1292EmuFolders::Save(si);1293}12941295if (controller)1296{1297InputManager::SetDefaultSourceConfig(si);1298Settings::SetDefaultControllerConfig(si);1299Settings::SetDefaultHotkeyConfig(si);1300}1301}13021303System::ApplySettings(false);1304}13051306void Host::RequestExitApplication(bool allow_confirm)1307{1308Host::RunOnCPUThread([]() {1309System::ShutdownSystem(g_settings.save_state_on_exit);13101311// clear the running flag, this'll break out of the main CPU loop once the VM is shutdown.1312MiniHost::s_state.cpu_thread_running.store(false, std::memory_order_release);1313});1314}13151316void Host::RequestExitBigPicture()1317{1318// sorry dude1319}13201321void Host::RequestSystemShutdown(bool allow_confirm, bool save_state, bool check_memcard_busy)1322{1323// TODO: Confirm1324if (System::IsValid())1325{1326Host::RunOnCPUThread([save_state]() { System::ShutdownSystem(save_state); });1327}1328}13291330void Host::ReportFatalError(std::string_view title, std::string_view message)1331{1332// Depending on the platform, this may not be available.1333std::fputs(SmallString::from_format("Fatal error: {}: {}\n", title, message).c_str(), stderr);1334SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, TinyString(title).c_str(), SmallString(message).c_str(), nullptr);1335}13361337void Host::ReportErrorAsync(std::string_view title, std::string_view message)1338{1339std::fputs(SmallString::from_format("Error: {}: {}\n", title, message).c_str(), stderr);1340SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, TinyString(title).c_str(), SmallString(message).c_str(), nullptr);1341}13421343void Host::RequestResizeHostDisplay(s32 width, s32 height)1344{1345using namespace MiniHost;13461347if (!s_state.sdl_window || s_state.fullscreen.load(std::memory_order_acquire))1348return;13491350SDL_SetWindowSize(s_state.sdl_window, width, height);1351}13521353void Host::OpenURL(std::string_view url)1354{1355if (!SDL_OpenURL(SmallString(url).c_str()))1356ERROR_LOG("SDL_OpenURL({}) failed: {}", url, SDL_GetError());1357}13581359std::string Host::GetClipboardText()1360{1361std::string ret;13621363char* text = SDL_GetClipboardText();1364if (text)1365{1366ret = text;1367SDL_free(text);1368}13691370return ret;1371}13721373bool Host::CopyTextToClipboard(std::string_view text)1374{1375if (!SDL_SetClipboardText(SmallString(text).c_str()))1376{1377ERROR_LOG("SDL_SetClipboardText({}) failed: {}", text, SDL_GetError());1378return false;1379}13801381return true;1382}13831384std::string Host::FormatNumber(NumberFormatType type, s64 value)1385{1386std::string ret;13871388if (type >= NumberFormatType::ShortDate && type <= NumberFormatType::LongDateTime)1389{1390const char* format;1391switch (type)1392{1393case NumberFormatType::ShortDate:1394format = "%x";1395break;13961397case NumberFormatType::LongDate:1398format = "%A %B %e %Y";1399break;14001401case NumberFormatType::ShortTime:1402case NumberFormatType::LongTime:1403format = "%X";1404break;14051406case NumberFormatType::ShortDateTime:1407format = "%X %x";1408break;14091410case NumberFormatType::LongDateTime:1411format = "%c";1412break;14131414DefaultCaseIsUnreachable();1415}14161417struct tm ttime = {};1418const std::time_t tvalue = static_cast<std::time_t>(value);1419#ifdef _MSC_VER1420localtime_s(&ttime, &tvalue);1421#else1422localtime_r(&tvalue, &ttime);1423#endif14241425char buf[128];1426std::strftime(buf, std::size(buf), format, &ttime);1427ret.assign(buf);1428}1429else1430{1431ret = fmt::format("{}", value);1432}14331434return ret;1435}14361437std::string Host::FormatNumber(NumberFormatType type, double value)1438{1439return fmt::format("{}", value);1440}14411442bool Host::ConfirmMessage(std::string_view title, std::string_view message)1443{1444const SmallString title_copy(title);1445const SmallString message_copy(message);14461447static constexpr SDL_MessageBoxButtonData bd[2] = {1448{SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT, 1, "Yes"},1449{SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT, 2, "No"},1450};1451const SDL_MessageBoxData md = {SDL_MESSAGEBOX_INFORMATION,1452nullptr,1453title_copy.c_str(),1454message_copy.c_str(),1455static_cast<int>(std::size(bd)),1456bd,1457nullptr};14581459int buttonid = -1;1460SDL_ShowMessageBox(&md, &buttonid);1461return (buttonid == 1);1462}14631464void Host::ConfirmMessageAsync(std::string_view title, std::string_view message, ConfirmMessageAsyncCallback callback,1465std::string_view yes_text /* = std::string_view() */,1466std::string_view no_text /* = std::string_view() */)1467{1468Host::RunOnCPUThread([title = std::string(title), message = std::string(message), callback = std::move(callback),1469yes_text = std::string(yes_text), no_text = std::move(no_text)]() mutable {1470// in case we haven't started yet...1471if (!FullscreenUI::IsInitialized())1472{1473callback(false);1474return;1475}14761477// Pause system while dialog is up.1478const bool needs_pause = System::IsValid() && !System::IsPaused();1479if (needs_pause)1480System::PauseSystem(true);14811482GPUThread::RunOnThread([title = std::string(title), message = std::string(message), callback = std::move(callback),1483yes_text = std::string(yes_text), no_text = std::string(no_text), needs_pause]() mutable {1484if (!FullscreenUI::Initialize())1485{1486callback(false);14871488if (needs_pause)1489{1490Host::RunOnCPUThread([]() {1491if (System::IsValid())1492System::PauseSystem(false);1493});1494}14951496return;1497}14981499// Need to reset run idle state _again_ after displaying.1500auto final_callback = [callback = std::move(callback)](bool result) {1501FullscreenUI::UpdateRunIdleState();1502callback(result);1503};15041505ImGuiFullscreen::OpenConfirmMessageDialog(std::move(title), std::move(message), std::move(final_callback),1506fmt::format(ICON_FA_CHECK " {}", yes_text),1507fmt::format(ICON_FA_XMARK " {}", no_text));1508FullscreenUI::UpdateRunIdleState();1509});1510});1511}15121513void Host::OpenHostFileSelectorAsync(std::string_view title, bool select_directory, FileSelectorCallback callback,1514FileSelectorFilters filters /* = FileSelectorFilters() */,1515std::string_view initial_directory /* = std::string_view() */)1516{1517// TODO: Use SDL FileDialog API1518callback(std::string());1519}15201521const char* Host::GetDefaultFullscreenUITheme()1522{1523return "";1524}15251526bool Host::ShouldPreferHostFileSelector()1527{1528return false;1529}15301531BEGIN_HOTKEY_LIST(g_host_hotkeys)1532END_HOTKEY_LIST()15331534static void SignalHandler(int signal)1535{1536// First try the normal (graceful) shutdown/exit.1537static bool graceful_shutdown_attempted = false;1538if (!graceful_shutdown_attempted)1539{1540std::fprintf(stderr, "Received CTRL+C, attempting graceful shutdown. Press CTRL+C again to force.\n");1541graceful_shutdown_attempted = true;1542Host::RequestExitApplication(false);1543return;1544}15451546std::signal(signal, SIG_DFL);15471548// MacOS is missing std::quick_exit() despite it being C++11...1549#ifndef __APPLE__1550std::quick_exit(1);1551#else1552_Exit(1);1553#endif1554}15551556void MiniHost::HookSignals()1557{1558std::signal(SIGINT, SignalHandler);1559std::signal(SIGTERM, SignalHandler);15601561#ifndef _WIN321562// Ignore SIGCHLD by default on Linux, since we kick off aplay asynchronously.1563struct sigaction sa_chld = {};1564sigemptyset(&sa_chld.sa_mask);1565sa_chld.sa_handler = SIG_IGN;1566sa_chld.sa_flags = SA_RESTART | SA_NOCLDSTOP | SA_NOCLDWAIT;1567sigaction(SIGCHLD, &sa_chld, nullptr);1568#endif1569}15701571void MiniHost::InitializeEarlyConsole()1572{1573const bool was_console_enabled = Log::IsConsoleOutputEnabled();1574if (!was_console_enabled)1575Log::SetConsoleOutputParams(true);1576}15771578void MiniHost::PrintCommandLineVersion()1579{1580InitializeEarlyConsole();15811582std::fprintf(stderr, "DuckStation Version %s (%s)\n", g_scm_tag_str, g_scm_branch_str);1583std::fprintf(stderr, "https://github.com/stenzek/duckstation\n");1584std::fprintf(stderr, "\n");1585}15861587void MiniHost::PrintCommandLineHelp(const char* progname)1588{1589InitializeEarlyConsole();15901591PrintCommandLineVersion();1592std::fprintf(stderr, "Usage: %s [parameters] [--] [boot filename]\n", progname);1593std::fprintf(stderr, "\n");1594std::fprintf(stderr, " -help: Displays this information and exits.\n");1595std::fprintf(stderr, " -version: Displays version information and exits.\n");1596std::fprintf(stderr, " -batch: Enables batch mode (exits after powering off).\n");1597std::fprintf(stderr, " -fastboot: Force fast boot for provided filename.\n");1598std::fprintf(stderr, " -slowboot: Force slow boot for provided filename.\n");1599std::fprintf(stderr, " -bios: Boot into the BIOS shell.\n");1600std::fprintf(stderr, " -resume: Load resume save state. If a boot filename is provided,\n"1601" that game's resume state will be loaded, otherwise the most\n"1602" recent resume save state will be loaded.\n");1603std::fprintf(stderr, " -state <index>: Loads specified save state by index. If a boot\n"1604" filename is provided, a per-game state will be loaded, otherwise\n"1605" a global state will be loaded.\n");1606std::fprintf(stderr, " -statefile <filename>: Loads state from the specified filename.\n"1607" No boot filename is required with this option.\n");1608std::fprintf(stderr, " -exe <filename>: Boot the specified exe instead of loading from disc.\n");1609std::fprintf(stderr, " -fullscreen: Enters fullscreen mode immediately after starting.\n");1610std::fprintf(stderr, " -nofullscreen: Prevents fullscreen mode from triggering if enabled.\n");1611std::fprintf(stderr, " -earlyconsole: Creates console as early as possible, for logging.\n");1612std::fprintf(stderr, " -prerotation <degrees>: Prerotates output by 90/180/270 degrees.\n");1613std::fprintf(stderr, " --: Signals that no more arguments will follow and the remaining\n"1614" parameters make up the filename. Use when the filename contains\n"1615" spaces or starts with a dash.\n");1616std::fprintf(stderr, "\n");1617}16181619std::optional<SystemBootParameters>& AutoBoot(std::optional<SystemBootParameters>& autoboot)1620{1621if (!autoboot)1622autoboot.emplace();16231624return autoboot;1625}16261627bool MiniHost::ParseCommandLineParametersAndInitializeConfig(int argc, char* argv[],1628std::optional<SystemBootParameters>& autoboot)1629{1630std::optional<s32> state_index;1631bool starting_bios = false;1632bool no_more_args = false;16331634for (int i = 1; i < argc; i++)1635{1636if (!no_more_args)1637{1638#define CHECK_ARG(str) (std::strcmp(argv[i], (str)) == 0)1639#define CHECK_ARG_PARAM(str) (std::strcmp(argv[i], (str)) == 0 && ((i + 1) < argc))16401641if (CHECK_ARG("-help"))1642{1643PrintCommandLineHelp(argv[0]);1644return false;1645}1646else if (CHECK_ARG("-version"))1647{1648PrintCommandLineVersion();1649return false;1650}1651else if (CHECK_ARG("-batch"))1652{1653INFO_LOG("Command Line: Using batch mode.");1654s_state.batch_mode = true;1655continue;1656}1657else if (CHECK_ARG("-bios"))1658{1659INFO_LOG("Command Line: Starting BIOS.");1660AutoBoot(autoboot);1661starting_bios = true;1662continue;1663}1664else if (CHECK_ARG("-fastboot"))1665{1666INFO_LOG("Command Line: Forcing fast boot.");1667AutoBoot(autoboot)->override_fast_boot = true;1668continue;1669}1670else if (CHECK_ARG("-slowboot"))1671{1672INFO_LOG("Command Line: Forcing slow boot.");1673AutoBoot(autoboot)->override_fast_boot = false;1674continue;1675}1676else if (CHECK_ARG("-resume"))1677{1678state_index = -1;1679INFO_LOG("Command Line: Loading resume state.");1680continue;1681}1682else if (CHECK_ARG_PARAM("-state"))1683{1684state_index = StringUtil::FromChars<s32>(argv[++i]);1685if (!state_index.has_value())1686{1687ERROR_LOG("Invalid state index");1688return false;1689}16901691INFO_LOG("Command Line: Loading state index: {}", state_index.value());1692continue;1693}1694else if (CHECK_ARG_PARAM("-statefile"))1695{1696AutoBoot(autoboot)->save_state = argv[++i];1697INFO_LOG("Command Line: Loading state file: '{}'", autoboot->save_state);1698continue;1699}1700else if (CHECK_ARG_PARAM("-exe"))1701{1702AutoBoot(autoboot)->override_exe = argv[++i];1703INFO_LOG("Command Line: Overriding EXE file: '{}'", autoboot->override_exe);1704continue;1705}1706else if (CHECK_ARG("-fullscreen"))1707{1708INFO_LOG("Command Line: Using fullscreen.");1709AutoBoot(autoboot)->override_fullscreen = true;1710s_state.start_fullscreen_ui_fullscreen = true;1711continue;1712}1713else if (CHECK_ARG("-nofullscreen"))1714{1715INFO_LOG("Command Line: Not using fullscreen.");1716AutoBoot(autoboot)->override_fullscreen = false;1717continue;1718}1719else if (CHECK_ARG("-earlyconsole"))1720{1721InitializeEarlyConsole();1722continue;1723}1724else if (CHECK_ARG_PARAM("-prerotation"))1725{1726const char* prerotation_str = argv[++i];1727if (std::strcmp(prerotation_str, "0") == 0 || StringUtil::EqualNoCase(prerotation_str, "identity"))1728{1729INFO_LOG("Command Line: Forcing surface pre-rotation to identity.");1730s_state.force_prerotation = WindowInfo::PreRotation::Identity;1731}1732else if (std::strcmp(prerotation_str, "90") == 0)1733{1734INFO_LOG("Command Line: Forcing surface pre-rotation to 90 degrees clockwise.");1735s_state.force_prerotation = WindowInfo::PreRotation::Rotate90Clockwise;1736}1737else if (std::strcmp(prerotation_str, "180") == 0)1738{1739INFO_LOG("Command Line: Forcing surface pre-rotation to 180 degrees clockwise.");1740s_state.force_prerotation = WindowInfo::PreRotation::Rotate180Clockwise;1741}1742else if (std::strcmp(prerotation_str, "270") == 0)1743{1744INFO_LOG("Command Line: Forcing surface pre-rotation to 270 degrees clockwise.");1745s_state.force_prerotation = WindowInfo::PreRotation::Rotate270Clockwise;1746}1747else1748{1749ERROR_LOG("Invalid prerotation value: {}", prerotation_str);1750return false;1751}17521753continue;1754}1755else if (CHECK_ARG("--"))1756{1757no_more_args = true;1758continue;1759}1760else if (argv[i][0] == '-')1761{1762Host::ReportFatalError("Error", fmt::format("Unknown parameter: {}", argv[i]));1763return false;1764}17651766#undef CHECK_ARG1767#undef CHECK_ARG_PARAM1768}17691770if (autoboot && !autoboot->path.empty())1771autoboot->path += ' ';1772AutoBoot(autoboot)->path += argv[i];1773}17741775// To do anything useful, we need the config initialized.1776if (!InitializeConfig())1777{1778// NOTE: No point translating this, because no config means the language won't be loaded anyway.1779Host::ReportFatalError("Error", "Failed to initialize config.");1780return EXIT_FAILURE;1781}17821783// Check the file we're starting actually exists.17841785if (autoboot && !autoboot->path.empty() && !FileSystem::FileExists(autoboot->path.c_str()))1786{1787Host::ReportFatalError("Error", fmt::format("File '{}' does not exist.", autoboot->path));1788return false;1789}17901791if (state_index.has_value())1792{1793AutoBoot(autoboot);17941795if (autoboot->path.empty())1796{1797// loading global state, -1 means resume the last game1798if (state_index.value() < 0)1799autoboot->save_state = System::GetMostRecentResumeSaveStatePath();1800else1801autoboot->save_state = System::GetGlobalSaveStatePath(state_index.value());1802}1803else1804{1805// loading game state1806const std::string game_serial(GameDatabase::GetSerialForPath(autoboot->path.c_str()));1807autoboot->save_state = System::GetGameSaveStatePath(game_serial, state_index.value());1808}18091810if (autoboot->save_state.empty() || !FileSystem::FileExists(autoboot->save_state.c_str()))1811{1812Host::ReportFatalError("Error", "The specified save state does not exist.");1813return false;1814}1815}18161817// check autoboot parameters, if we set something like fullscreen without a bios1818// or disc, we don't want to actually start.1819if (autoboot && autoboot->path.empty() && autoboot->save_state.empty() && !starting_bios)1820autoboot.reset();18211822// if we don't have autoboot, we definitely don't want batch mode (because that'll skip1823// scanning the game list).1824if (s_state.batch_mode)1825{1826if (!autoboot)1827{1828Host::ReportFatalError("Error", "Cannot use batch mode, because no boot filename was specified.");1829return false;1830}18311832// if using batch mode, immediately refresh the game list so the data is available1833GameList::Refresh(false, true);1834}18351836return true;1837}18381839#include <SDL3/SDL_main.h>18401841int main(int argc, char* argv[])1842{1843using namespace MiniHost;18441845CrashHandler::Install(&Bus::CleanupMemoryMap);18461847if (!PerformEarlyHardwareChecks())1848return EXIT_FAILURE;18491850if (!SDL_InitSubSystem(SDL_INIT_VIDEO | SDL_INIT_EVENTS))1851{1852Host::ReportFatalError("Error", TinyString::from_format("SDL_InitSubSystem() failed: {}", SDL_GetError()));1853return EXIT_FAILURE;1854}18551856s_state.func_event_id = SDL_RegisterEvents(1);1857if (s_state.func_event_id == static_cast<u32>(-1))1858{1859Host::ReportFatalError("Error", TinyString::from_format("SDL_RegisterEvents() failed: {}", SDL_GetError()));1860return EXIT_FAILURE;1861}18621863if (!EarlyProcessStartup())1864return EXIT_FAILURE;18651866std::optional<SystemBootParameters> autoboot;1867if (!ParseCommandLineParametersAndInitializeConfig(argc, argv, autoboot))1868return EXIT_FAILURE;18691870// the rest of initialization happens on the CPU thread.1871HookSignals();18721873// prevent input source polling on CPU thread...1874SDLInputSource::ALLOW_EVENT_POLLING = false;1875s_state.ui_thread_running = true;1876StartCPUThread();18771878// process autoboot early, that way we can set the fullscreen flag1879if (autoboot)1880{1881s_state.start_fullscreen_ui_fullscreen =1882s_state.start_fullscreen_ui_fullscreen || autoboot->override_fullscreen.value_or(false);1883Host::RunOnCPUThread([params = std::move(autoboot.value())]() mutable {1884Error error;1885if (!System::BootSystem(std::move(params), &error))1886Host::ReportErrorAsync("Failed to boot system", error.GetDescription());1887});1888}18891890UIThreadMainLoop();18911892StopCPUThread();18931894System::ProcessShutdown();18951896// Ensure log is flushed.1897Log::SetFileOutputParams(false, nullptr);18981899if (s_state.base_settings_interface.IsDirty())1900s_state.base_settings_interface.Save();19011902SDL_QuitSubSystem(SDL_INIT_VIDEO | SDL_INIT_EVENTS);19031904return EXIT_SUCCESS;1905}190619071908