Path: blob/master/src/duckstation-mini/mini_host.cpp
7367 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/core_private.h"9#include "core/cpu_core.h"10#include "core/fullscreenui.h"11#include "core/fullscreenui_widgets.h"12#include "core/game_list.h"13#include "core/gpu.h"14#include "core/gpu_backend.h"15#include "core/host.h"16#include "core/imgui_overlays.h"17#include "core/settings.h"18#include "core/system.h"19#include "core/system_private.h"20#include "core/video_thread.h"2122#include "util/cd_image.h"23#include "util/gpu_device.h"24#include "util/imgui_manager.h"25#include "util/input_manager.h"26#include "util/sdl_input_source.h"27#include "util/translation.h"2829#include "imgui.h"30#include "imgui_internal.h"31#include "imgui_stdlib.h"3233#include "common/assert.h"34#include "common/crash_handler.h"35#include "common/error.h"36#include "common/file_system.h"37#include "common/log.h"38#include "common/path.h"39#include "common/string_util.h"40#include "common/task_queue.h"41#include "common/threading.h"42#include "common/time_helpers.h"4344#include "IconsEmoji.h"45#include "fmt/format.h"4647#include <SDL3/SDL.h>48#include <cinttypes>49#include <cmath>50#include <condition_variable>51#include <csignal>52#include <ctime>53#include <thread>5455#ifdef _WIN3256#include "common/windows_headers.h"57#endif5859LOG_CHANNEL(Host);6061namespace MiniHost {6263/// Use two async worker threads, should be enough for most tasks.64static constexpr u32 NUM_ASYNC_WORKER_THREADS = 2;6566// static constexpr u32 DEFAULT_WINDOW_WIDTH = 1280;67// static constexpr u32 DEFAULT_WINDOW_HEIGHT = 720;68static constexpr u32 DEFAULT_WINDOW_WIDTH = 1920;69static constexpr u32 DEFAULT_WINDOW_HEIGHT = 1080;7071static constexpr auto CORE_THREAD_POLL_INTERVAL =72std::chrono::milliseconds(8); // how often we'll poll controllers when paused7374static bool ParseCommandLineParametersAndInitializeConfig(int argc, char* argv[],75std::optional<SystemBootParameters>& autoboot);76static void PrintCommandLineVersion();77static void PrintCommandLineHelp(const char* progname);78static bool InitializeFoldersAndConfig(Error* error);79static void InitializeEarlyConsole();80static void HookSignals();81static std::string GetResourcePath(std::string_view name, bool allow_override);82static bool PerformEarlyHardwareChecks();83static bool EarlyProcessStartup();84static void WarnAboutInterface();85static void StartCoreThread();86static void StopCoreThread();87static void ProcessCoreThreadEvents(bool block);88static void ProcessCoreThreadPlatformMessages();89static void CoreThreadEntryPoint();90static void CoreThreadMainLoop();91static void VideoThreadEntryPoint();92static void UIThreadMainLoop();93static void ProcessSDLEvent(const SDL_Event* ev);94static std::string GetWindowTitle(const std::string& game_title);95static std::optional<WindowInfo> TranslateSDLWindowInfo(SDL_Window* win, Error* error);96static bool GetSavedPlatformWindowGeometry(s32* x, s32* y, s32* width, s32* height);97static void SavePlatformWindowGeometry(s32 x, s32 y, s32 width, s32 height);9899struct SDLHostState100{101// UI thread state102ALIGN_TO_CACHE_LINE bool 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;111float sdl_window_refresh_rate = 0.0f;112WindowInfoPrerotation force_prerotation = WindowInfoPrerotation::Identity;113114Threading::Thread core_thread;115Threading::Thread video_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 core_thread_running{false};123std::mutex core_thread_events_mutex;124std::condition_variable core_thread_event_done;125std::condition_variable core_thread_event_posted;126std::deque<std::pair<std::function<void()>, bool>> core_thread_events;127u32 blocking_core_events_pending = 0;128};129130static SDLHostState s_state;131ALIGN_TO_CACHE_LINE static TaskQueue s_async_task_queue;132133} // namespace MiniHost134135//////////////////////////////////////////////////////////////////////////136// Initialization/Shutdown137//////////////////////////////////////////////////////////////////////////138139bool MiniHost::PerformEarlyHardwareChecks()140{141Error error;142const bool okay = System::PerformEarlyHardwareChecks(&error);143if (okay && !error.IsValid()) [[likely]]144return true;145146if (okay)147Host::ReportErrorAsync("Hardware Check Warning", error.GetDescription());148else149Host::ReportFatalError("Hardware Check Failed", error.GetDescription());150151return okay;152}153154bool MiniHost::EarlyProcessStartup()155{156Error error;157if (!System::ProcessStartup(&error)) [[unlikely]]158{159Host::ReportFatalError("Process Startup Failed", error.GetDescription());160return false;161}162163#if !__has_include("scmversion/tag.h")164//165// To those distributing their own builds or packages of DuckStation, and seeing this message:166//167// DuckStation is licensed under the CC-BY-NC-ND-4.0 license.168//169// This means that you do NOT have permission to re-distribute your own modified builds of DuckStation.170// Modifying DuckStation for personal use is fine, but you cannot distribute builds with your changes.171// As per the CC-BY-NC-ND conditions, you can re-distribute the official builds from https://www.duckstation.org/ and172// https://github.com/stenzek/duckstation, so long as they are left intact, without modification. I welcome and173// appreciate any pull requests made to the official repository at https://github.com/stenzek/duckstation.174//175// I made the decision to switch to a no-derivatives license because of numerous "forks" that were created purely for176// generating money for the person who knocked it off, and always died, leaving the community with multiple builds to177// choose from, most of which were out of date and broken, and endless confusion. Other forks copy/pasted upstream178// changes without attribution, violating copyright.179//180// Thanks, and I hope you understand.181//182183const char* title = "WARNING! You are not using an official release!";184const char* message = "DuckStation is licensed under the terms of CC-BY-NC-ND-4.0,\n"185"which does not allow modified builds to be distributed.\n\n"186"This build is NOT OFFICIAL and may be broken and/or malicious.\n\n"187"You should download an official build from https://www.duckstation.org/.";188189Host::AddIconOSDMessage(OSDMessageType::Error, "OfficialReleaseWarning", ICON_EMOJI_WARNING, title, message);190#endif191192return true;193}194195bool MiniHost::InitializeFoldersAndConfig(Error* error)196{197// Path to the resources directory relative to the application binary.198// On Windows/Linux, these are in the binary directory.199// On macOS, this is in the bundle resources directory.200#ifndef __APPLE__201static constexpr const char* RESOURCES_RELATIVE_PATH = "resources";202#else203static constexpr const char* RESOURCES_RELATIVE_PATH = "../Resources";204#endif205206if (!Core::SetCriticalFolders(RESOURCES_RELATIVE_PATH, error))207return false;208209Error config_error;210if (!Core::InitializeBaseSettingsLayer(Core::GetBaseSettingsPath(), &config_error))211return false;212213return true;214}215216void Host::SetDefaultSettings(SettingsInterface& si)217{218}219220void Host::OnSettingsResetToDefault(bool host, bool system, bool controller)221{222Host::RunOnCoreThread([]() { System::ApplySettings(false); });223}224225void Host::ReportDebuggerEvent(CPU::DebuggerEvent event, std::string_view message)226{227ERROR_LOG("ReportDebuggerEvent(): {}", message);228}229230std::span<const std::pair<const char*, const char*>> Host::GetAvailableLanguageList()231{232return {};233}234235const char* Host::GetLanguageName(std::string_view language_code)236{237return "";238}239240bool Host::ChangeLanguage(const char* new_language)241{242return false;243}244245void Host::AddFixedInputBindings(const SettingsInterface& si)246{247}248249void Host::OnInputDeviceConnected(InputBindingKey key, std::string_view identifier, std::string_view device_name)250{251}252253void Host::OnInputDeviceDisconnected(InputBindingKey key, std::string_view identifier)254{255}256257s32 Host::Internal::GetTranslatedStringImpl(std::string_view context, std::string_view msg,258std::string_view disambiguation, char* tbuf, size_t tbuf_space)259{260if (msg.size() > tbuf_space)261return -1;262else if (msg.empty())263return 0;264265std::memcpy(tbuf, msg.data(), msg.size());266return static_cast<s32>(msg.size());267}268269std::string Host::TranslatePluralToString(const char* context, const char* msg, const char* disambiguation, int count)270{271TinyString count_str = TinyString::from_format("{}", count);272273std::string ret(msg);274for (;;)275{276std::string::size_type pos = ret.find("%n");277if (pos == std::string::npos)278break;279280ret.replace(pos, pos + 2, count_str.view());281}282283return ret;284}285286SmallString Host::TranslatePluralToSmallString(const char* context, const char* msg, const char* disambiguation,287int count)288{289SmallString ret(msg);290ret.replace("%n", TinyString::from_format("{}", count));291return ret;292}293294std::string MiniHost::GetResourcePath(std::string_view filename, bool allow_override)295{296return allow_override ? EmuFolders::GetOverridableResourcePath(filename) :297Path::Combine(EmuFolders::Resources, filename);298}299300bool Host::ResourceFileExists(std::string_view filename, bool allow_override)301{302const std::string path = MiniHost::GetResourcePath(filename, allow_override);303return FileSystem::FileExists(path.c_str());304}305306std::optional<DynamicHeapArray<u8>> Host::ReadResourceFile(std::string_view filename, bool allow_override, Error* error)307{308const std::string path = MiniHost::GetResourcePath(filename, allow_override);309return FileSystem::ReadBinaryFile(path.c_str(), error);310}311312std::optional<std::string> Host::ReadResourceFileToString(std::string_view filename, bool allow_override, Error* error)313{314const std::string path = MiniHost::GetResourcePath(filename, allow_override);315return FileSystem::ReadFileToString(path.c_str(), error);316}317318std::optional<std::time_t> Host::GetResourceFileTimestamp(std::string_view filename, bool allow_override)319{320const std::string path = MiniHost::GetResourcePath(filename, allow_override);321FILESYSTEM_STAT_DATA sd;322if (!FileSystem::StatFile(path.c_str(), &sd))323{324ERROR_LOG("Failed to stat resource file '{}'", filename);325return std::nullopt;326}327328return sd.ModificationTime;329}330331void Host::LoadSettings(const SettingsInterface& si, std::unique_lock<std::mutex>& lock)332{333}334335void Host::CheckForSettingsChanges(const Settings& old_settings)336{337}338339void Host::CommitBaseSettingChanges()340{341const auto lock = Core::GetSettingsLock();342Error error;343if (!Core::SaveBaseSettingsLayer(&error))344ERROR_LOG("Failed to save settings: {}", error.GetDescription());345}346347std::optional<WindowInfo> MiniHost::TranslateSDLWindowInfo(SDL_Window* win, Error* error)348{349if (!win)350{351Error::SetStringView(error, "Window handle is null.");352return std::nullopt;353}354355const SDL_WindowFlags window_flags = SDL_GetWindowFlags(win);356int window_width = 1, window_height = 1;357int window_px_width = 1, window_px_height = 1;358SDL_GetWindowSize(win, &window_width, &window_height);359SDL_GetWindowSizeInPixels(win, &window_px_width, &window_px_height);360s_state.sdl_window_scale = SDL_GetWindowDisplayScale(win);361362const SDL_DisplayMode* dispmode = nullptr;363364if (window_flags & SDL_WINDOW_FULLSCREEN)365{366if (!(dispmode = SDL_GetWindowFullscreenMode(win)))367ERROR_LOG("SDL_GetWindowFullscreenMode() failed: {}", SDL_GetError());368}369370if (const SDL_DisplayID display_id = SDL_GetDisplayForWindow(win); display_id != 0)371{372if (!(window_flags & SDL_WINDOW_FULLSCREEN))373{374if (!(dispmode = SDL_GetDesktopDisplayMode(display_id)))375ERROR_LOG("SDL_GetDesktopDisplayMode() failed: {}", SDL_GetError());376}377}378379WindowInfo wi;380wi.surface_width = static_cast<u16>(window_px_width);381wi.surface_height = static_cast<u16>(window_px_height);382wi.surface_scale = s_state.sdl_window_scale;383wi.surface_prerotation = s_state.force_prerotation;384385// set display refresh rate if available386if (dispmode && dispmode->refresh_rate > 0.0f)387{388INFO_LOG("Display mode refresh rate: {} hz", dispmode->refresh_rate);389wi.surface_refresh_rate = dispmode->refresh_rate;390s_state.sdl_window_refresh_rate = dispmode->refresh_rate;391}392else393{394s_state.sdl_window_refresh_rate = 0.0f;395}396397// SDL's opengl window flag tends to make a mess of pixel formats...398if (!(SDL_GetWindowFlags(win) & (SDL_WINDOW_OPENGL | SDL_WINDOW_VULKAN)))399{400const SDL_PropertiesID props = SDL_GetWindowProperties(win);401if (props == 0)402{403Error::SetStringFmt(error, "SDL_GetWindowProperties() failed: {}", SDL_GetError());404return std::nullopt;405}406407#if defined(SDL_PLATFORM_WINDOWS)408wi.type = WindowInfoType::Win32;409wi.window_handle = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WIN32_HWND_POINTER, nullptr);410if (!wi.window_handle)411{412Error::SetStringView(error, "SDL_PROP_WINDOW_WIN32_HWND_POINTER not found.");413return std::nullopt;414}415#elif defined(SDL_PLATFORM_MACOS)416wi.type = WindowInfoType::MacOS;417wi.window_handle = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, nullptr);418if (!wi.window_handle)419{420Error::SetStringView(error, "SDL_PROP_WINDOW_COCOA_WINDOW_POINTER not found.");421return std::nullopt;422}423#elif defined(SDL_PLATFORM_LINUX) || defined(SDL_PLATFORM_FREEBSD)424const std::string_view video_driver = SDL_GetCurrentVideoDriver();425if (video_driver == "x11")426{427wi.display_connection = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_X11_DISPLAY_POINTER, nullptr);428wi.window_handle = reinterpret_cast<void*>(429static_cast<intptr_t>(SDL_GetNumberProperty(props, SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0)));430if (!wi.display_connection)431{432Error::SetStringView(error, "SDL_PROP_WINDOW_X11_DISPLAY_POINTER not found.");433return std::nullopt;434}435else if (!wi.window_handle)436{437Error::SetStringView(error, "SDL_PROP_WINDOW_X11_WINDOW_NUMBER not found.");438return std::nullopt;439}440}441else if (video_driver == "wayland")442{443wi.display_connection = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_DISPLAY_POINTER, nullptr);444wi.window_handle = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_SURFACE_POINTER, nullptr);445if (!wi.display_connection)446{447Error::SetStringView(error, "SDL_PROP_WINDOW_WAYLAND_DISPLAY_POINTER not found.");448return std::nullopt;449}450else if (!wi.window_handle)451{452Error::SetStringView(error, "SDL_PROP_WINDOW_WAYLAND_SURFACE_POINTER not found.");453return std::nullopt;454}455}456else457{458Error::SetStringFmt(error, "Video driver {} not supported.", video_driver);459return std::nullopt;460}461#else462#error Unsupported platform.463#endif464}465else466{467// nothing handled, fall back to SDL abstraction468wi.type = WindowInfoType::SDL;469wi.window_handle = win;470}471472return wi;473}474475std::optional<WindowInfo> Host::AcquireRenderWindow(RenderAPI render_api, bool fullscreen, bool exclusive_fullscreen,476Error* error)477{478using namespace MiniHost;479480std::optional<WindowInfo> wi;481482Host::RunOnUIThread([render_api, fullscreen, error, &wi]() {483const std::string window_title = GetWindowTitle(System::GetGameTitle());484const SDL_PropertiesID props = SDL_CreateProperties();485SDL_SetStringProperty(props, SDL_PROP_WINDOW_CREATE_TITLE_STRING, window_title.c_str());486487SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_RESIZABLE_BOOLEAN, true);488SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_FOCUSABLE_BOOLEAN, true);489SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_HIGH_PIXEL_DENSITY_BOOLEAN, true);490491if (render_api == RenderAPI::OpenGL || render_api == RenderAPI::OpenGLES)492SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_OPENGL_BOOLEAN, true);493else if (render_api == RenderAPI::Vulkan)494SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_VULKAN_BOOLEAN, true);495496if (fullscreen)497{498SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_BORDERLESS_BOOLEAN, true);499SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_FULLSCREEN_BOOLEAN, true);500}501502if (s32 window_x, window_y, window_width, window_height;503MiniHost::GetSavedPlatformWindowGeometry(&window_x, &window_y, &window_width, &window_height))504{505SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_X_NUMBER, window_x);506SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_Y_NUMBER, window_y);507SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, window_width);508SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, window_height);509}510else511{512SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, DEFAULT_WINDOW_WIDTH);513SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, DEFAULT_WINDOW_HEIGHT);514}515516s_state.sdl_window = SDL_CreateWindowWithProperties(props);517SDL_DestroyProperties(props);518519if (s_state.sdl_window)520{521wi = TranslateSDLWindowInfo(s_state.sdl_window, error);522if (!wi.has_value())523{524SDL_DestroyWindow(s_state.sdl_window);525s_state.sdl_window = nullptr;526}527}528else529{530Error::SetStringFmt(error, "SDL_CreateWindow() failed: {}", SDL_GetError());531}532533s_state.platform_window_updated.Post();534});535536s_state.platform_window_updated.Wait();537538// reload input sources, since it might use the window handle539Host::RunOnCoreThread(&System::ReloadInputSources);540541return wi;542}543544WindowInfoType Host::GetRenderWindowInfoType()545{546// Assume SDL for GL/Vulkan.547return WindowInfoType::SDL;548}549550void Host::ReleaseRenderWindow()551{552using namespace MiniHost;553554if (!s_state.sdl_window)555return;556557Host::RunOnUIThread([]() {558if (!(SDL_GetWindowFlags(s_state.sdl_window) & SDL_WINDOW_FULLSCREEN))559{560int window_x = SDL_WINDOWPOS_UNDEFINED, window_y = SDL_WINDOWPOS_UNDEFINED;561int window_width = DEFAULT_WINDOW_WIDTH, window_height = DEFAULT_WINDOW_HEIGHT;562SDL_GetWindowPosition(s_state.sdl_window, &window_x, &window_y);563SDL_GetWindowSize(s_state.sdl_window, &window_width, &window_height);564MiniHost::SavePlatformWindowGeometry(window_x, window_y, window_width, window_height);565}566567SDL_DestroyWindow(s_state.sdl_window);568s_state.sdl_window = nullptr;569570s_state.platform_window_updated.Post();571});572573s_state.platform_window_updated.Wait();574}575576bool Host::CanChangeFullscreenMode(bool new_fullscreen_state)577{578return true;579}580581void Host::BeginTextInput()582{583using namespace MiniHost;584585SDL_StartTextInput(s_state.sdl_window);586}587588void Host::EndTextInput()589{590// we want to keep getting text events, SDL_StopTextInput() apparently inhibits that591}592593bool Host::CreateAuxiliaryRenderWindow(s32 x, s32 y, u32 width, u32 height, std::string_view title,594std::string_view icon_name, AuxiliaryRenderWindowUserData userdata,595AuxiliaryRenderWindowHandle* handle, WindowInfo* wi, Error* error)596{597// not here, but could be...598Error::SetStringView(error, "Not supported.");599return false;600}601602void Host::DestroyAuxiliaryRenderWindow(AuxiliaryRenderWindowHandle handle, s32* pos_x /* = nullptr */,603s32* pos_y /* = nullptr */, u32* width /* = nullptr */,604u32* height /* = nullptr */)605{606// noop607}608609bool MiniHost::GetSavedPlatformWindowGeometry(s32* x, s32* y, s32* width, s32* height)610{611const auto lock = Core::GetSettingsLock();612613SettingsInterface* si = Core::GetBaseSettingsLayer();614bool result = si->GetIntValue("UI", "MainWindowX", x);615result = result && si->GetIntValue("UI", "MainWindowY", y);616result = result && si->GetIntValue("UI", "MainWindowWidth", width);617result = result && si->GetIntValue("UI", "MainWindowHeight", height);618return result;619}620621void MiniHost::SavePlatformWindowGeometry(s32 x, s32 y, s32 width, s32 height)622{623const auto lock = Core::GetSettingsLock();624SettingsInterface* si = Core::GetBaseSettingsLayer();625si->SetIntValue("UI", "MainWindowX", x);626si->SetIntValue("UI", "MainWindowY", y);627si->SetIntValue("UI", "MainWindowWidth", width);628si->SetIntValue("UI", "MainWindowHeight", height);629}630631void MiniHost::UIThreadMainLoop()632{633while (s_state.ui_thread_running)634{635SDL_Event ev;636if (!SDL_WaitEvent(&ev))637continue;638639ProcessSDLEvent(&ev);640}641}642643void MiniHost::ProcessSDLEvent(const SDL_Event* ev)644{645switch (ev->type)646{647case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:648{649Host::RunOnCoreThread([window_width = ev->window.data1, window_height = ev->window.data2,650window_scale = s_state.sdl_window_scale,651window_refresh_rate = s_state.sdl_window_refresh_rate]() {652VideoThread::ResizeRenderWindow(window_width, window_height, window_scale, window_refresh_rate);653});654}655break;656657case SDL_EVENT_WINDOW_DISPLAY_CHANGED:658case SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED:659{660const float new_scale = SDL_GetWindowDisplayScale(s_state.sdl_window);661if (new_scale != s_state.sdl_window_scale)662{663s_state.sdl_window_scale = new_scale;664665int window_width = 1, window_height = 1;666SDL_GetWindowSizeInPixels(s_state.sdl_window, &window_width, &window_height);667Host::RunOnCoreThread([window_width, window_height, window_scale = s_state.sdl_window_scale,668window_refresh_rate = s_state.sdl_window_refresh_rate]() {669VideoThread::ResizeRenderWindow(window_width, window_height, window_scale, window_refresh_rate);670});671}672}673break;674675case SDL_EVENT_WINDOW_CLOSE_REQUESTED:676{677Host::RunOnCoreThread([]() { Host::RequestExitApplication(false); });678}679break;680681case SDL_EVENT_WINDOW_FOCUS_GAINED:682{683Host::RunOnCoreThread([]() {684if (!System::IsValid() || !s_state.was_paused_by_focus_loss)685return;686687System::PauseSystem(false);688s_state.was_paused_by_focus_loss = false;689});690}691break;692693case SDL_EVENT_WINDOW_FOCUS_LOST:694{695Host::RunOnCoreThread([]() {696if (!System::IsRunning() || !g_settings.pause_on_focus_loss)697return;698699s_state.was_paused_by_focus_loss = true;700System::PauseSystem(true);701});702}703break;704705case SDL_EVENT_KEY_DOWN:706case SDL_EVENT_KEY_UP:707{708if (const std::optional<u32> key = InputManager::ConvertHostNativeKeyCodeToKeyCode(ev->key.raw))709{710Host::RunOnCoreThread([key_code = key.value(), pressed = (ev->type == SDL_EVENT_KEY_DOWN)]() {711InputManager::InvokeEvents(InputManager::MakeHostKeyboardKey(key_code), pressed ? 1.0f : 0.0f,712GenericInputBinding::Unknown);713});714}715}716break;717718case SDL_EVENT_TEXT_INPUT:719{720if (ImGuiManager::WantsTextInput())721Host::RunOnCoreThread([text = std::string(ev->text.text)]() { ImGuiManager::AddTextInput(std::move(text)); });722}723break;724725case SDL_EVENT_MOUSE_MOTION:726{727Host::RunOnCoreThread([x = static_cast<float>(ev->motion.x), y = static_cast<float>(ev->motion.y)]() {728InputManager::UpdatePointerAbsolutePosition(0, x, y);729ImGuiManager::UpdateMousePosition(x, y);730});731}732break;733734case SDL_EVENT_MOUSE_BUTTON_DOWN:735case SDL_EVENT_MOUSE_BUTTON_UP:736{737if (ev->button.button > 0)738{739// swap middle/right because sdl orders them differently740const u8 button = (ev->button.button == 3) ? 1 : ((ev->button.button == 2) ? 2 : (ev->button.button - 1));741Host::RunOnCoreThread([button, pressed = (ev->type == SDL_EVENT_MOUSE_BUTTON_DOWN)]() {742InputManager::InvokeEvents(InputManager::MakePointerButtonKey(0, button), pressed ? 1.0f : 0.0f,743GenericInputBinding::Unknown);744});745}746}747break;748749case SDL_EVENT_MOUSE_WHEEL:750{751Host::RunOnCoreThread([x = ev->wheel.x, y = ev->wheel.y]() {752if (x != 0.0f)753InputManager::UpdatePointerWheelRelativeDelta(0, InputPointerAxis::WheelX, x);754if (y != 0.0f)755InputManager::UpdatePointerWheelRelativeDelta(0, InputPointerAxis::WheelY, y);756});757}758break;759760case SDL_EVENT_QUIT:761{762Host::RunOnCoreThread([]() { Host::RequestExitApplication(false); });763}764break;765766default:767{768if (ev->type == s_state.func_event_id)769{770std::function<void()>* pfunc = reinterpret_cast<std::function<void()>*>(ev->user.data1);771if (pfunc)772{773(*pfunc)();774delete pfunc;775}776}777else if (SDLInputSource::IsHandledInputEvent(ev))778{779Host::RunOnCoreThread([event_copy = *ev]() {780SDLInputSource* is =781static_cast<SDLInputSource*>(InputManager::GetInputSourceInterface(InputSourceType::SDL));782if (is)783is->ProcessSDLEvent(&event_copy);784});785}786}787break;788}789}790791void MiniHost::ProcessCoreThreadPlatformMessages()792{793// This is lame. On Win32, we need to pump messages, even though *we* don't have any windows794// on the CPU thread, because SDL creates a hidden window for raw input for some game controllers.795// If we don't do this, we don't get any controller events.796#ifdef _WIN32797MSG msg;798while (PeekMessageW(&msg, NULL, 0, 0, PM_REMOVE))799{800TranslateMessage(&msg);801DispatchMessageW(&msg);802}803#endif804}805806void MiniHost::ProcessCoreThreadEvents(bool block)807{808std::unique_lock lock(s_state.core_thread_events_mutex);809810for (;;)811{812if (s_state.core_thread_events.empty())813{814if (!block || !s_state.core_thread_running.load(std::memory_order_acquire))815return;816817// we still need to keep polling the controllers when we're paused818do819{820ProcessCoreThreadPlatformMessages();821InputManager::PollSources();822} while (!s_state.core_thread_event_posted.wait_for(lock, CORE_THREAD_POLL_INTERVAL,823[]() { return !s_state.core_thread_events.empty(); }));824}825826// return after processing all events if we had one827block = false;828829auto event = std::move(s_state.core_thread_events.front());830s_state.core_thread_events.pop_front();831lock.unlock();832event.first();833lock.lock();834835if (event.second)836{837s_state.blocking_core_events_pending--;838s_state.core_thread_event_done.notify_one();839}840}841}842843void MiniHost::StartCoreThread()844{845s_state.core_thread_running.store(true, std::memory_order_release);846s_state.core_thread.Start(CoreThreadEntryPoint);847}848849void MiniHost::StopCoreThread()850{851if (!s_state.core_thread.Joinable())852return;853854{855std::unique_lock lock(s_state.core_thread_events_mutex);856s_state.core_thread_running.store(false, std::memory_order_release);857s_state.core_thread_event_posted.notify_one();858}859860s_state.core_thread.Join();861}862863void MiniHost::CoreThreadEntryPoint()864{865Threading::SetNameOfCurrentThread("CPU Thread");866867// input source setup must happen on emu thread868Error error;869if (!System::CoreThreadInitialize(&error))870{871Host::ReportFatalError("CPU Thread Initialization Failed", error.GetDescription());872return;873}874875// startup worker threads876s_async_task_queue.SetWorkerCount(NUM_ASYNC_WORKER_THREADS);877878// start up GPU thread879s_state.video_thread.Start(&VideoThreadEntryPoint);880881// start the fullscreen UI and get it going882if (VideoThread::StartFullscreenUI(s_state.start_fullscreen_ui_fullscreen, &error))883{884WarnAboutInterface();885886// kick a game list refresh if we're not in batch mode887if (!s_state.batch_mode)888Host::RefreshGameListAsync(false);889890CoreThreadMainLoop();891892Host::CancelGameListRefresh();893}894else895{896Host::ReportFatalError("Error", fmt::format("Failed to start fullscreen UI: {}", error.GetDescription()));897}898899// finish any events off (e.g. shutdown system with save)900ProcessCoreThreadEvents(false);901902if (System::IsValid())903System::ShutdownSystem(false);904905VideoThread::StopFullscreenUI();906VideoThread::Internal::RequestShutdown();907s_state.video_thread.Join();908909// join worker threads910s_async_task_queue.SetWorkerCount(0);911912System::CoreThreadShutdown();913914// Tell the UI thread to shut down.915Host::RunOnUIThread([]() { s_state.ui_thread_running = false; });916}917918void MiniHost::CoreThreadMainLoop()919{920while (s_state.core_thread_running.load(std::memory_order_acquire))921{922if (System::IsRunning())923{924System::Execute();925continue;926}927else if (!VideoThread::IsUsingThread() && VideoThread::IsRunningIdle())928{929ProcessCoreThreadEvents(false);930if (!VideoThread::IsUsingThread() && VideoThread::IsRunningIdle())931VideoThread::Internal::DoRunIdle();932}933934ProcessCoreThreadEvents(true);935}936}937938void MiniHost::VideoThreadEntryPoint()939{940Threading::SetNameOfCurrentThread("Video Thread");941VideoThread::Internal::VideoThreadEntryPoint();942}943944void Host::OnSystemStarting()945{946MiniHost::s_state.was_paused_by_focus_loss = false;947}948949void Host::OnSystemStarted()950{951}952953void Host::OnSystemPaused()954{955}956957void Host::OnSystemResumed()958{959}960961void Host::OnSystemStopping()962{963}964965void Host::OnSystemDestroyed()966{967}968969void Host::OnSystemAbnormalShutdown(const std::string_view reason)970{971VideoThread::RunOnThread([reason = std::string(reason)]() {972FullscreenUI::OpenInfoMessageDialog(973ICON_EMOJI_NO_ENTRY_SIGN, "Abnormal System Shutdown",974fmt::format("Unfortunately, the virtual machine has abnormally shut down and cannot "975"be recovered. More information about the error is below:\n\n{}",976reason));977});978}979980void Host::OnVideoThreadRunIdleChanged(bool is_active)981{982}983984bool Host::SetScreensaverInhibit(bool inhibit, Error* error)985{986if (inhibit)987{988if (SDL_DisableScreenSaver())989{990Error::SetStringFmt(error, "SDL_DisableScreenSaver() failed: {}", SDL_GetError());991return false;992}993994return true;995}996else997{998if (SDL_EnableScreenSaver())999{1000Error::SetStringFmt(error, "SDL_EnableScreenSaver() failed: {}", SDL_GetError());1001return false;1002}10031004return true;1005}1006}10071008void Host::FrameDoneOnVideoThread(GPUBackend* gpu_backend, u32 frame_number)1009{1010}10111012void Host::OnPerformanceCountersUpdated(const GPUBackend* gpu_backend)1013{1014// noop1015}10161017void Host::OnAchievementsLoginRequested(Achievements::LoginRequestReason reason)1018{1019// noop1020}10211022void Host::OnAchievementsLoginSuccess(const char* username, u32 points, u32 sc_points, u32 unread_messages)1023{1024// noop1025}10261027void Host::OnAchievementsActiveChanged(bool active)1028{1029// noop1030}10311032void Host::OnAchievementsHardcoreModeChanged(bool enabled)1033{1034// noop1035}10361037#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION10381039void Host::OnRAIntegrationMenuChanged()1040{1041// noop1042}10431044#endif10451046void Host::SetMouseMode(bool relative, bool hide_cursor)1047{1048// noop1049}10501051void Host::OnMediaCaptureStarted()1052{1053// noop1054}10551056void Host::OnMediaCaptureStopped()1057{1058// noop1059}10601061void Host::PumpMessagesOnCoreThread()1062{1063MiniHost::ProcessCoreThreadEvents(false);1064}10651066std::string MiniHost::GetWindowTitle(const std::string& game_title)1067{1068#if defined(_DEBUGFAST)1069static constexpr std::string_view suffix = " [DebugFast]";1070#elif defined(_DEBUG)1071static constexpr std::string_view suffix = " [Debug]";1072#else1073static constexpr std::string_view suffix = std::string_view();1074#endif10751076if (System::IsShutdown() || game_title.empty())1077return fmt::format("DuckStation {}{}", g_scm_version_str, suffix);1078else1079return fmt::format("{}{}", game_title, suffix);1080}10811082void MiniHost::WarnAboutInterface()1083{1084Host::AddIconOSDMessage(1085OSDMessageType::Warning, "MiniWarning", "images/duck.png",1086"This is the \"mini\" interface for DuckStation, and is missing many features.",1087"We recommend using the Qt interface instead, which you can download from https://www.duckstation.org/.");1088}10891090void Host::OnSystemGameChanged(const std::string& disc_path, const std::string& game_serial,1091const std::string& game_name, GameHash game_hash)1092{1093using namespace MiniHost;10941095VERBOSE_LOG("Host::OnGameChanged(\"{}\", \"{}\", \"{}\")", disc_path, game_serial, game_name);1096if (s_state.sdl_window)1097SDL_SetWindowTitle(s_state.sdl_window, GetWindowTitle(game_name).c_str());1098}10991100void Host::OnSystemUndoStateAvailabilityChanged(bool available, u64 timestamp)1101{1102//1103}11041105void Host::RunOnCoreThread(std::function<void()> function, bool block /* = false */)1106{1107using namespace MiniHost;11081109std::unique_lock lock(s_state.core_thread_events_mutex);1110s_state.core_thread_events.emplace_back(std::move(function), block);1111s_state.blocking_core_events_pending += BoolToUInt32(block);1112s_state.core_thread_event_posted.notify_one();1113if (block)1114s_state.core_thread_event_done.wait(lock, []() { return s_state.blocking_core_events_pending == 0; });1115}11161117void Host::RunOnUIThread(std::function<void()> function, bool block /* = false */)1118{1119using namespace MiniHost;11201121std::function<void()>* pfunc = new std::function<void()>(std::move(function));11221123SDL_Event ev;1124ev.user = {};1125ev.type = s_state.func_event_id;1126ev.user.data1 = pfunc;1127SDL_PushEvent(&ev);1128}11291130void Host::QueueAsyncTask(std::function<void()> function)1131{1132MiniHost::s_async_task_queue.SubmitTask(std::move(function));1133}11341135void Host::WaitForAllAsyncTasks()1136{1137MiniHost::s_async_task_queue.WaitForAll();1138}11391140void Host::RefreshGameListAsync(bool invalidate_cache)1141{1142using namespace MiniHost;11431144std::unique_lock lock(s_state.state_mutex);11451146while (s_state.game_list_refresh_progress)1147{1148lock.unlock();1149CancelGameListRefresh();1150lock.lock();1151}11521153s_state.game_list_refresh_progress = new FullscreenUI::BackgroundProgressCallback("glrefresh");1154Host::QueueAsyncTask([invalidate_cache]() {1155GameList::Refresh(invalidate_cache, false, s_state.game_list_refresh_progress);11561157std::unique_lock lock(s_state.state_mutex);1158delete s_state.game_list_refresh_progress;1159s_state.game_list_refresh_progress = nullptr;1160});1161}11621163void Host::CancelGameListRefresh()1164{1165using namespace MiniHost;11661167{1168std::unique_lock lock(s_state.state_mutex);1169if (!s_state.game_list_refresh_progress)1170return;11711172s_state.game_list_refresh_progress->SetCancelled();1173}11741175Host::WaitForAllAsyncTasks();1176}11771178void Host::OnGameListEntriesChanged(std::span<const u32> changed_indices)1179{1180// constantly re-querying, don't need to do anything1181}11821183std::optional<WindowInfo> Host::GetTopLevelWindowInfo()1184{1185return MiniHost::TranslateSDLWindowInfo(MiniHost::s_state.sdl_window, nullptr);1186}11871188void Host::RequestExitApplication(bool allow_confirm)1189{1190Host::RunOnCoreThread([]() {1191System::ShutdownSystem(g_settings.save_state_on_exit);11921193// clear the running flag, this'll break out of the main CPU loop once the VM is shutdown.1194MiniHost::s_state.core_thread_running.store(false, std::memory_order_release);1195});1196}11971198void Host::RequestExitBigPicture()1199{1200// sorry dude1201}12021203void Host::RequestSystemShutdown(bool allow_confirm, bool save_state, bool check_memcard_busy)1204{1205// TODO: Confirm1206if (System::IsValid())1207{1208Host::RunOnCoreThread([save_state]() { System::ShutdownSystem(save_state); });1209}1210}12111212void Host::ReportFatalError(std::string_view title, std::string_view message)1213{1214// Depending on the platform, this may not be available.1215std::fputs(SmallString::from_format("Fatal error: {}: {}\n", title, message).c_str(), stderr);1216SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, TinyString(title).c_str(), SmallString(message).c_str(), nullptr);1217std::abort();1218}12191220void Host::ReportErrorAsync(std::string_view title, std::string_view message)1221{1222std::fputs(SmallString::from_format("Error: {}: {}\n", title, message).c_str(), stderr);1223SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, TinyString(title).c_str(), SmallString(message).c_str(), nullptr);1224}12251226void Host::ReportStatusMessage(std::string_view message)1227{1228Host::AddOSDMessage(OSDMessageType::Info, std::string(message));1229}12301231void Host::RequestResizeHostDisplay(s32 width, s32 height)1232{1233using namespace MiniHost;12341235if (!s_state.sdl_window || SDL_GetWindowFlags(s_state.sdl_window) & SDL_WINDOW_FULLSCREEN)1236return;12371238SDL_SetWindowSize(s_state.sdl_window, width, height);1239}12401241void Host::OpenURL(std::string_view url)1242{1243if (!SDL_OpenURL(SmallString(url).c_str()))1244ERROR_LOG("SDL_OpenURL({}) failed: {}", url, SDL_GetError());1245}12461247std::string Host::GetClipboardText()1248{1249std::string ret;12501251char* text = SDL_GetClipboardText();1252if (text)1253{1254ret = text;1255SDL_free(text);1256}12571258return ret;1259}12601261bool Host::CopyTextToClipboard(std::string_view text)1262{1263if (!SDL_SetClipboardText(SmallString(text).c_str()))1264{1265ERROR_LOG("SDL_SetClipboardText({}) failed: {}", text, SDL_GetError());1266return false;1267}12681269return true;1270}12711272std::string Host::FormatNumber(NumberFormatType type, s64 value)1273{1274std::string ret;12751276if (type >= NumberFormatType::ShortDate && type <= NumberFormatType::LongDateTime)1277{1278const char* format;1279switch (type)1280{1281case NumberFormatType::ShortDate:1282format = "%x";1283break;12841285case NumberFormatType::LongDate:1286format = "%A %B %e %Y";1287break;12881289case NumberFormatType::ShortTime:1290case NumberFormatType::LongTime:1291format = "%X";1292break;12931294case NumberFormatType::ShortDateTime:1295format = "%X %x";1296break;12971298case NumberFormatType::LongDateTime:1299format = "%c";1300break;13011302DefaultCaseIsUnreachable();1303}13041305ret.resize(128);13061307if (const std::optional<std::tm> ltime = Common::LocalTime(static_cast<std::time_t>(value)))1308ret.resize(std::strftime(ret.data(), ret.size(), format, <ime.value()));1309else1310ret = "Invalid";1311}1312else1313{1314ret = fmt::format("{}", value);1315}13161317return ret;1318}13191320std::string Host::FormatNumber(NumberFormatType type, double value)1321{1322return fmt::format("{}", value);1323}13241325void Host::ConfirmMessageAsync(std::string_view title, std::string_view message, ConfirmMessageAsyncCallback callback,1326std::string_view yes_text /* = std::string_view() */,1327std::string_view no_text /* = std::string_view() */)1328{1329Host::RunOnCoreThread([title = std::string(title), message = std::string(message), callback = std::move(callback),1330yes_text = std::string(yes_text), no_text = std::move(no_text)]() mutable {1331// in case we haven't started yet...1332if (!FullscreenUI::IsInitialized())1333{1334callback(false);1335return;1336}13371338// Pause system while dialog is up.1339const bool needs_pause = System::IsValid() && !System::IsPaused();1340if (needs_pause)1341System::PauseSystem(true);13421343VideoThread::RunOnThread([title = std::string(title), message = std::string(message),1344callback = std::move(callback), yes_text = std::string(yes_text),1345no_text = std::string(no_text), needs_pause]() mutable {1346FullscreenUI::Initialize();13471348// Need to reset run idle state _again_ after displaying.1349auto final_callback = [callback = std::move(callback), needs_pause](bool result) {1350FullscreenUI::UpdateRunIdleState();13511352if (needs_pause)1353{1354Host::RunOnCoreThread([]() {1355if (System::IsValid())1356System::PauseSystem(false);1357});1358}13591360callback(result);1361};13621363FullscreenUI::OpenConfirmMessageDialog(ICON_EMOJI_QUESTION_MARK, std::move(title), std::move(message),1364std::move(final_callback), fmt::format(ICON_FA_CHECK " {}", yes_text),1365fmt::format(ICON_FA_XMARK " {}", no_text));1366FullscreenUI::UpdateRunIdleState();1367});1368});1369}13701371const char* Host::GetDefaultFullscreenUITheme()1372{1373return "";1374}13751376static void SignalHandler(int signal)1377{1378// First try the normal (graceful) shutdown/exit.1379static bool graceful_shutdown_attempted = false;1380if (!graceful_shutdown_attempted)1381{1382std::fprintf(stderr, "Received CTRL+C, attempting graceful shutdown. Press CTRL+C again to force.\n");1383graceful_shutdown_attempted = true;1384Host::RequestExitApplication(false);1385return;1386}13871388std::signal(signal, SIG_DFL);13891390// MacOS is missing std::quick_exit() despite it being C++11...1391#ifndef __APPLE__1392std::quick_exit(1);1393#else1394_Exit(1);1395#endif1396}13971398void MiniHost::HookSignals()1399{1400std::signal(SIGINT, SignalHandler);1401std::signal(SIGTERM, SignalHandler);14021403#ifndef _WIN321404// Ignore SIGCHLD by default on Linux, since we kick off aplay asynchronously.1405struct sigaction sa_chld = {};1406sigemptyset(&sa_chld.sa_mask);1407sa_chld.sa_handler = SIG_IGN;1408sa_chld.sa_flags = SA_RESTART | SA_NOCLDSTOP | SA_NOCLDWAIT;1409sigaction(SIGCHLD, &sa_chld, nullptr);1410#endif1411}14121413void MiniHost::InitializeEarlyConsole()1414{1415const bool was_console_enabled = Log::IsConsoleOutputEnabled();1416if (!was_console_enabled)1417Log::SetConsoleOutputParams(true);1418}14191420void MiniHost::PrintCommandLineVersion()1421{1422InitializeEarlyConsole();14231424std::fprintf(stderr, "DuckStation Version %s (%s)\n", g_scm_tag_str, g_scm_branch_str);1425std::fprintf(stderr, "https://github.com/stenzek/duckstation\n");1426std::fprintf(stderr, "\n");1427}14281429void MiniHost::PrintCommandLineHelp(const char* progname)1430{1431InitializeEarlyConsole();14321433PrintCommandLineVersion();1434std::fprintf(stderr, "Usage: %s [parameters] [--] [boot filename]\n", progname);1435std::fprintf(stderr, "\n");1436std::fprintf(stderr, " -help: Displays this information and exits.\n");1437std::fprintf(stderr, " -version: Displays version information and exits.\n");1438std::fprintf(stderr, " -batch: Enables batch mode (exits after powering off).\n");1439std::fprintf(stderr, " -fastboot: Force fast boot for provided filename.\n");1440std::fprintf(stderr, " -slowboot: Force slow boot for provided filename.\n");1441std::fprintf(stderr, " -bios: Boot into the BIOS shell.\n");1442std::fprintf(stderr, " -resume: Load resume save state. If a boot filename is provided,\n"1443" that game's resume state will be loaded, otherwise the most\n"1444" recent resume save state will be loaded.\n");1445std::fprintf(stderr, " -state <index>: Loads specified save state by index. If a boot\n"1446" filename is provided, a per-game state will be loaded, otherwise\n"1447" a global state will be loaded.\n");1448std::fprintf(stderr, " -statefile <filename>: Loads state from the specified filename.\n"1449" No boot filename is required with this option.\n");1450std::fprintf(stderr, " -exe <filename>: Boot the specified exe instead of loading from disc.\n");1451std::fprintf(stderr, " -fullscreen: Enters fullscreen mode immediately after starting.\n");1452std::fprintf(stderr, " -nofullscreen: Prevents fullscreen mode from triggering if enabled.\n");1453std::fprintf(stderr, " -earlyconsole: Creates console as early as possible, for logging.\n");1454std::fprintf(stderr, " -prerotation <degrees>: Prerotates output by 90/180/270 degrees.\n");1455std::fprintf(stderr, " --: Signals that no more arguments will follow and the remaining\n"1456" parameters make up the filename. Use when the filename contains\n"1457" spaces or starts with a dash.\n");1458std::fprintf(stderr, "\n");1459}14601461std::optional<SystemBootParameters>& AutoBoot(std::optional<SystemBootParameters>& autoboot)1462{1463if (!autoboot)1464autoboot.emplace();14651466return autoboot;1467}14681469bool MiniHost::ParseCommandLineParametersAndInitializeConfig(int argc, char* argv[],1470std::optional<SystemBootParameters>& autoboot)1471{1472std::optional<s32> state_index;1473bool starting_bios = false;1474bool no_more_args = false;14751476for (int i = 1; i < argc; i++)1477{1478if (!no_more_args)1479{1480#define CHECK_ARG(str) (std::strcmp(argv[i], (str)) == 0)1481#define CHECK_ARG_PARAM(str) (std::strcmp(argv[i], (str)) == 0 && ((i + 1) < argc))14821483if (CHECK_ARG("-help"))1484{1485PrintCommandLineHelp(argv[0]);1486return false;1487}1488else if (CHECK_ARG("-version"))1489{1490PrintCommandLineVersion();1491return false;1492}1493else if (CHECK_ARG("-batch"))1494{1495INFO_LOG("Command Line: Using batch mode.");1496s_state.batch_mode = true;1497continue;1498}1499else if (CHECK_ARG("-bios"))1500{1501INFO_LOG("Command Line: Starting BIOS.");1502AutoBoot(autoboot);1503starting_bios = true;1504continue;1505}1506else if (CHECK_ARG("-fastboot"))1507{1508INFO_LOG("Command Line: Forcing fast boot.");1509AutoBoot(autoboot)->override_fast_boot = true;1510continue;1511}1512else if (CHECK_ARG("-slowboot"))1513{1514INFO_LOG("Command Line: Forcing slow boot.");1515AutoBoot(autoboot)->override_fast_boot = false;1516continue;1517}1518else if (CHECK_ARG("-resume"))1519{1520state_index = -1;1521INFO_LOG("Command Line: Loading resume state.");1522continue;1523}1524else if (CHECK_ARG_PARAM("-state"))1525{1526state_index = StringUtil::FromChars<s32>(argv[++i]);1527if (!state_index.has_value())1528{1529ERROR_LOG("Invalid state index");1530return false;1531}15321533INFO_LOG("Command Line: Loading state index: {}", state_index.value());1534continue;1535}1536else if (CHECK_ARG_PARAM("-statefile"))1537{1538AutoBoot(autoboot)->save_state = argv[++i];1539INFO_LOG("Command Line: Loading state file: '{}'", autoboot->save_state);1540continue;1541}1542else if (CHECK_ARG_PARAM("-exe"))1543{1544AutoBoot(autoboot)->override_exe = argv[++i];1545INFO_LOG("Command Line: Overriding EXE file: '{}'", autoboot->override_exe);1546continue;1547}1548else if (CHECK_ARG("-fullscreen"))1549{1550INFO_LOG("Command Line: Using fullscreen.");1551AutoBoot(autoboot)->override_fullscreen = true;1552s_state.start_fullscreen_ui_fullscreen = true;1553continue;1554}1555else if (CHECK_ARG("-nofullscreen"))1556{1557INFO_LOG("Command Line: Not using fullscreen.");1558AutoBoot(autoboot)->override_fullscreen = false;1559continue;1560}1561else if (CHECK_ARG("-earlyconsole"))1562{1563InitializeEarlyConsole();1564continue;1565}1566else if (CHECK_ARG_PARAM("-prerotation"))1567{1568const char* prerotation_str = argv[++i];1569if (std::strcmp(prerotation_str, "0") == 0 || StringUtil::EqualNoCase(prerotation_str, "identity"))1570{1571INFO_LOG("Command Line: Forcing surface pre-rotation to identity.");1572s_state.force_prerotation = WindowInfoPrerotation::Identity;1573}1574else if (std::strcmp(prerotation_str, "90") == 0)1575{1576INFO_LOG("Command Line: Forcing surface pre-rotation to 90 degrees clockwise.");1577s_state.force_prerotation = WindowInfoPrerotation::Rotate90Clockwise;1578}1579else if (std::strcmp(prerotation_str, "180") == 0)1580{1581INFO_LOG("Command Line: Forcing surface pre-rotation to 180 degrees clockwise.");1582s_state.force_prerotation = WindowInfoPrerotation::Rotate180Clockwise;1583}1584else if (std::strcmp(prerotation_str, "270") == 0)1585{1586INFO_LOG("Command Line: Forcing surface pre-rotation to 270 degrees clockwise.");1587s_state.force_prerotation = WindowInfoPrerotation::Rotate270Clockwise;1588}1589else1590{1591ERROR_LOG("Invalid prerotation value: {}", prerotation_str);1592return false;1593}15941595continue;1596}1597else if (CHECK_ARG("--"))1598{1599no_more_args = true;1600continue;1601}1602else if (argv[i][0] == '-')1603{1604Host::ReportFatalError("Error", fmt::format("Unknown parameter: {}", argv[i]));1605return false;1606}16071608#undef CHECK_ARG1609#undef CHECK_ARG_PARAM1610}16111612if (autoboot && !autoboot->path.empty())1613autoboot->path += ' ';1614AutoBoot(autoboot)->path += argv[i];1615}16161617// To do anything useful, we need the config initialized.1618Error error;1619if (!InitializeFoldersAndConfig(&error))1620{1621// NOTE: No point translating this, because no config means the language won't be loaded anyway.1622Host::ReportFatalError("DuckStation", error.GetDescription());1623return EXIT_FAILURE;1624}16251626// Check the file we're starting actually exists.16271628if (autoboot && !autoboot->path.empty() && !FileSystem::FileExists(autoboot->path.c_str()) &&1629!CDImage::IsDeviceName(autoboot->path.c_str()))1630{1631Host::ReportFatalError("DuckStation", fmt::format("File '{}' does not exist.", autoboot->path));1632return false;1633}16341635if (state_index.has_value())1636{1637AutoBoot(autoboot);16381639if (autoboot->path.empty())1640{1641// loading global state, -1 means resume the last game1642if (state_index.value() < 0)1643autoboot->save_state = System::GetMostRecentResumeSaveStatePath();1644else1645autoboot->save_state = System::GetGlobalSaveStatePath(state_index.value());1646}1647else1648{1649// loading game state1650const std::string game_serial(GameDatabase::GetSerialForPath(autoboot->path.c_str()));1651autoboot->save_state = System::GetGameSaveStatePath(game_serial, state_index.value());1652}16531654if (autoboot->save_state.empty() || !FileSystem::FileExists(autoboot->save_state.c_str()))1655{1656Host::ReportFatalError("DuckStation", "The specified save state does not exist.");1657return false;1658}1659}16601661// check autoboot parameters, if we set something like fullscreen without a bios1662// or disc, we don't want to actually start.1663if (autoboot && autoboot->path.empty() && autoboot->save_state.empty() && !starting_bios)1664autoboot.reset();16651666// if we don't have autoboot, we definitely don't want batch mode (because that'll skip1667// scanning the game list).1668if (s_state.batch_mode)1669{1670if (!autoboot)1671{1672Host::ReportFatalError("DuckStation", "Cannot use batch mode, because no boot filename was specified.");1673return false;1674}16751676// if using batch mode, immediately refresh the game list so the data is available1677GameList::Refresh(false, true);1678}16791680return true;1681}16821683#include <SDL3/SDL_main.h>16841685int main(int argc, char* argv[])1686{1687using namespace MiniHost;16881689CrashHandler::Install(&Bus::CleanupMemoryMap);16901691if (!PerformEarlyHardwareChecks())1692return EXIT_FAILURE;16931694if (!SDL_InitSubSystem(SDL_INIT_VIDEO | SDL_INIT_EVENTS))1695{1696Host::ReportFatalError("Error", TinyString::from_format("SDL_InitSubSystem() failed: {}", SDL_GetError()));1697return EXIT_FAILURE;1698}16991700s_state.func_event_id = SDL_RegisterEvents(1);1701if (s_state.func_event_id == static_cast<u32>(-1))1702{1703Host::ReportFatalError("Error", TinyString::from_format("SDL_RegisterEvents() failed: {}", SDL_GetError()));1704return EXIT_FAILURE;1705}17061707if (!EarlyProcessStartup())1708return EXIT_FAILURE;17091710std::optional<SystemBootParameters> autoboot;1711if (!ParseCommandLineParametersAndInitializeConfig(argc, argv, autoboot))1712return EXIT_FAILURE;17131714// the rest of initialization happens on the CPU thread.1715HookSignals();17161717// prevent input source polling on CPU thread...1718SDLInputSource::ALLOW_EVENT_POLLING = false;1719s_state.ui_thread_running = true;1720StartCoreThread();17211722// process autoboot early, that way we can set the fullscreen flag1723if (autoboot)1724{1725s_state.start_fullscreen_ui_fullscreen =1726s_state.start_fullscreen_ui_fullscreen || autoboot->override_fullscreen.value_or(false);1727Host::RunOnCoreThread([params = std::move(autoboot.value())]() mutable {1728Error error;1729if (!System::BootSystem(std::move(params), &error))1730Host::ReportErrorAsync("Failed to boot system", error.GetDescription());1731});1732}17331734UIThreadMainLoop();17351736StopCoreThread();17371738System::ProcessShutdown();17391740// Ensure log is flushed.1741Log::SetFileOutputParams(false, nullptr);17421743Core::SaveBaseSettingsLayer(nullptr);17441745SDL_QuitSubSystem(SDL_INIT_VIDEO | SDL_INIT_EVENTS);17461747return EXIT_SUCCESS;1748}174917501751