duckstation

duckstation, but archived from the revision just before upstream changed it to a proprietary software project, this version is the libre one
git clone https://git.neptards.moe/u3shit/duckstation.git
Log | Files | Refs | README | LICENSE

qthost.cpp (81731B)


      1 // SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
      2 // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
      3 
      4 #include "qthost.h"
      5 #include "autoupdaterdialog.h"
      6 #include "displaywidget.h"
      7 #include "logwindow.h"
      8 #include "mainwindow.h"
      9 #include "qtprogresscallback.h"
     10 #include "qtutils.h"
     11 #include "setupwizarddialog.h"
     12 
     13 #include "core/achievements.h"
     14 #include "core/cheats.h"
     15 #include "core/controller.h"
     16 #include "core/fullscreen_ui.h"
     17 #include "core/game_database.h"
     18 #include "core/game_list.h"
     19 #include "core/gdb_server.h"
     20 #include "core/gpu.h"
     21 #include "core/host.h"
     22 #include "core/imgui_overlays.h"
     23 #include "core/memory_card.h"
     24 #include "core/spu.h"
     25 #include "core/system.h"
     26 
     27 #include "common/assert.h"
     28 #include "common/crash_handler.h"
     29 #include "common/error.h"
     30 #include "common/file_system.h"
     31 #include "common/log.h"
     32 #include "common/minizip_helpers.h"
     33 #include "common/path.h"
     34 #include "common/scoped_guard.h"
     35 #include "common/string_util.h"
     36 
     37 #include "util/audio_stream.h"
     38 #include "util/http_downloader.h"
     39 #include "util/imgui_fullscreen.h"
     40 #include "util/imgui_manager.h"
     41 #include "util/ini_settings_interface.h"
     42 #include "util/input_manager.h"
     43 #include "util/platform_misc.h"
     44 #include "util/postprocessing.h"
     45 
     46 #include "scmversion/scmversion.h"
     47 
     48 #include "core/bus.h"
     49 #include "imgui.h"
     50 
     51 #include <QtCore/QCoreApplication>
     52 #include <QtCore/QDateTime>
     53 #include <QtCore/QDebug>
     54 #include <QtCore/QEventLoop>
     55 #include <QtCore/QFile>
     56 #include <QtCore/QTimer>
     57 #include <QtGui/QClipboard>
     58 #include <QtGui/QKeyEvent>
     59 #include <QtWidgets/QFileDialog>
     60 #include <QtWidgets/QMenu>
     61 #include <QtWidgets/QMessageBox>
     62 #include <algorithm>
     63 #include <cmath>
     64 #include <csignal>
     65 #include <cstdio>
     66 #include <cstdlib>
     67 #include <memory>
     68 
     69 Log_SetChannel(QtHost);
     70 
     71 #ifdef _WIN32
     72 #include "common/windows_headers.h"
     73 #include <ShlObj.h>
     74 #endif
     75 
     76 static constexpr u32 SETTINGS_VERSION = 3;
     77 static constexpr u32 SETTINGS_SAVE_DELAY = 1000;
     78 
     79 /// Interval at which the controllers are polled when the system is not active.
     80 static constexpr u32 BACKGROUND_CONTROLLER_POLLING_INTERVAL = 100;
     81 
     82 /// Poll at half the vsync rate for FSUI to reduce the chance of getting a press+release in the same frame.
     83 static constexpr u32 FULLSCREEN_UI_CONTROLLER_POLLING_INTERVAL = 8;
     84 
     85 /// Poll at 1ms when running GDB server. We can get rid of this once we move networking to its own thread.
     86 static constexpr u32 GDB_SERVER_POLLING_INTERVAL = 1;
     87 
     88 //////////////////////////////////////////////////////////////////////////
     89 // Local function declarations
     90 //////////////////////////////////////////////////////////////////////////
     91 namespace QtHost {
     92 static bool PerformEarlyHardwareChecks();
     93 static bool EarlyProcessStartup();
     94 static void RegisterTypes();
     95 static bool InitializeConfig(std::string settings_filename);
     96 static bool ShouldUsePortableMode();
     97 static void SetAppRoot();
     98 static void SetResourcesDirectory();
     99 static bool SetDataDirectory();
    100 static bool SetCriticalFolders();
    101 static void SetDefaultSettings(SettingsInterface& si, bool system, bool controller);
    102 static void MigrateSettings();
    103 static void SaveSettings();
    104 static bool RunSetupWizard();
    105 static std::string GetResourcePath(std::string_view name, bool allow_override);
    106 static std::optional<bool> DownloadFile(QWidget* parent, const QString& title, std::string url, std::vector<u8>* data);
    107 static void InitializeEarlyConsole();
    108 static void HookSignals();
    109 static void PrintCommandLineVersion();
    110 static void PrintCommandLineHelp(const char* progname);
    111 static bool ParseCommandLineParametersAndInitializeConfig(QApplication& app,
    112                                                           std::shared_ptr<SystemBootParameters>& boot_params);
    113 } // namespace QtHost
    114 
    115 static std::unique_ptr<INISettingsInterface> s_base_settings_interface;
    116 static std::unique_ptr<QTimer> s_settings_save_timer;
    117 static bool s_batch_mode = false;
    118 static bool s_nogui_mode = false;
    119 static bool s_start_fullscreen_ui = false;
    120 static bool s_start_fullscreen_ui_fullscreen = false;
    121 static bool s_run_setup_wizard = false;
    122 static bool s_cleanup_after_update = false;
    123 
    124 EmuThread* g_emu_thread = nullptr;
    125 
    126 EmuThread::EmuThread(QThread* ui_thread) : QThread(), m_ui_thread(ui_thread)
    127 {
    128 }
    129 
    130 EmuThread::~EmuThread() = default;
    131 
    132 void QtHost::RegisterTypes()
    133 {
    134   // Register any standard types we need elsewhere
    135   qRegisterMetaType<std::optional<WindowInfo>>("std::optional<WindowInfo>()");
    136   qRegisterMetaType<std::optional<bool>>();
    137   qRegisterMetaType<std::function<void()>>("std::function<void()>");
    138   qRegisterMetaType<std::shared_ptr<SystemBootParameters>>();
    139   qRegisterMetaType<const GameList::Entry*>();
    140   qRegisterMetaType<GPURenderer>("GPURenderer");
    141   qRegisterMetaType<InputBindingKey>("InputBindingKey");
    142   qRegisterMetaType<std::string>("std::string");
    143   qRegisterMetaType<std::vector<std::pair<std::string, std::string>>>(
    144     "std::vector<std::pair<std::string, std::string>>");
    145 }
    146 
    147 bool QtHost::PerformEarlyHardwareChecks()
    148 {
    149   Error error;
    150   const bool okay = System::Internal::PerformEarlyHardwareChecks(&error);
    151   if (okay && !error.IsValid()) [[likely]]
    152     return true;
    153 
    154   if (okay)
    155   {
    156     QMessageBox::warning(nullptr, QStringLiteral("Hardware Check Warning"),
    157                          QString::fromStdString(error.GetDescription()));
    158   }
    159   else
    160   {
    161     QMessageBox::critical(nullptr, QStringLiteral("Hardware Check Failed"),
    162                           QString::fromStdString(error.GetDescription()));
    163   }
    164 
    165   return okay;
    166 }
    167 
    168 bool QtHost::EarlyProcessStartup()
    169 {
    170   // Config-based RAIntegration switch must happen before the main window is displayed.
    171 #ifdef ENABLE_RAINTEGRATION
    172   if (!Achievements::IsUsingRAIntegration() && Host::GetBaseBoolSettingValue("Cheevos", "UseRAIntegration", false))
    173     Achievements::SwitchToRAIntegration();
    174 #endif
    175 
    176   Error error;
    177   if (System::Internal::ProcessStartup(&error)) [[likely]]
    178     return true;
    179 
    180   QMessageBox::critical(nullptr, QStringLiteral("Process Startup Failed"),
    181                         QString::fromStdString(error.GetDescription()));
    182   return false;
    183 }
    184 
    185 bool QtHost::InBatchMode()
    186 {
    187   return s_batch_mode;
    188 }
    189 
    190 bool QtHost::InNoGUIMode()
    191 {
    192   return s_nogui_mode;
    193 }
    194 
    195 QString QtHost::GetAppNameAndVersion()
    196 {
    197   return QStringLiteral("DuckStation %1").arg(QLatin1StringView(g_scm_tag_str));
    198 }
    199 
    200 QString QtHost::GetAppConfigSuffix()
    201 {
    202 #if defined(_DEBUGFAST)
    203   return QStringLiteral(" [DebugFast]");
    204 #elif defined(_DEBUG)
    205   return QStringLiteral(" [Debug]");
    206 #else
    207   return QString();
    208 #endif
    209 }
    210 
    211 QString QtHost::GetResourcesBasePath()
    212 {
    213   return QString::fromStdString(EmuFolders::Resources);
    214 }
    215 
    216 INISettingsInterface* QtHost::GetBaseSettingsInterface()
    217 {
    218   return s_base_settings_interface.get();
    219 }
    220 
    221 bool QtHost::SaveGameSettings(SettingsInterface* sif, bool delete_if_empty)
    222 {
    223   INISettingsInterface* ini = static_cast<INISettingsInterface*>(sif);
    224   Error error;
    225 
    226   // if there's no keys, just toss the whole thing out
    227   if (delete_if_empty && ini->IsEmpty())
    228   {
    229     INFO_LOG("Removing empty gamesettings ini {}", Path::GetFileName(ini->GetFileName()));
    230     if (FileSystem::FileExists(ini->GetFileName().c_str()) &&
    231         !FileSystem::DeleteFile(ini->GetFileName().c_str(), &error))
    232     {
    233       Host::ReportErrorAsync(
    234         TRANSLATE_SV("QtHost", "Error"),
    235         fmt::format(TRANSLATE_FS("QtHost", "An error occurred while deleting empty game settings:\n{}"),
    236                     error.GetDescription()));
    237       return false;
    238     }
    239 
    240     return true;
    241   }
    242 
    243   // clean unused sections, stops the file being bloated
    244   sif->RemoveEmptySections();
    245 
    246   if (!sif->Save(&error))
    247   {
    248     Host::ReportErrorAsync(
    249       TRANSLATE_SV("QtHost", "Error"),
    250       fmt::format(TRANSLATE_FS("QtHost", "An error occurred while saving game settings:\n{}"), error.GetDescription()));
    251     return false;
    252   }
    253 
    254   return true;
    255 }
    256 
    257 const QIcon& QtHost::GetAppIcon()
    258 {
    259   static QIcon icon = QIcon(QStringLiteral(":/icons/duck.png"));
    260   return icon;
    261 }
    262 
    263 std::optional<bool> QtHost::DownloadFile(QWidget* parent, const QString& title, std::string url, std::vector<u8>* data)
    264 {
    265   static constexpr u32 HTTP_POLL_INTERVAL = 10;
    266 
    267   std::unique_ptr<HTTPDownloader> http = HTTPDownloader::Create(Host::GetHTTPUserAgent());
    268   if (!http)
    269   {
    270     QMessageBox::critical(parent, qApp->translate("QtHost", "Error"),
    271                           qApp->translate("QtHost", "Failed to create HTTPDownloader."));
    272     return false;
    273   }
    274 
    275   std::optional<bool> download_result;
    276   const std::string::size_type url_file_part_pos = url.rfind('/');
    277   QtModalProgressCallback progress(parent);
    278   progress.GetDialog().setLabelText(qApp->translate("QtHost", "Downloading %1...")
    279                                       .arg(QtUtils::StringViewToQString(std::string_view(url).substr(
    280                                         (url_file_part_pos >= 0) ? (url_file_part_pos + 1) : 0))));
    281   progress.GetDialog().setWindowTitle(title);
    282   progress.GetDialog().setWindowIcon(GetAppIcon());
    283   progress.SetCancellable(true);
    284 
    285   http->CreateRequest(
    286     std::move(url),
    287     [parent, data, &download_result](s32 status_code, const std::string&, std::vector<u8> hdata) {
    288       if (status_code == HTTPDownloader::HTTP_STATUS_CANCELLED)
    289         return;
    290 
    291       if (status_code != HTTPDownloader::HTTP_STATUS_OK)
    292       {
    293         QMessageBox::critical(parent, qApp->translate("QtHost", "Error"),
    294                               qApp->translate("QtHost", "Download failed with HTTP status code %1.").arg(status_code));
    295         download_result = false;
    296         return;
    297       }
    298 
    299       if (hdata.empty())
    300       {
    301         QMessageBox::critical(parent, qApp->translate("QtHost", "Error"),
    302                               qApp->translate("QtHost", "Download failed: Data is empty.").arg(status_code));
    303 
    304         download_result = false;
    305         return;
    306       }
    307 
    308       *data = std::move(hdata);
    309       download_result = true;
    310     },
    311     &progress);
    312 
    313   // Block until completion.
    314   while (http->HasAnyRequests())
    315   {
    316     QApplication::processEvents(QEventLoop::AllEvents, HTTP_POLL_INTERVAL);
    317     http->PollRequests();
    318   }
    319 
    320   return download_result;
    321 }
    322 
    323 bool QtHost::DownloadFile(QWidget* parent, const QString& title, std::string url, const char* path)
    324 {
    325   INFO_LOG("Download from {}, saving to {}.", url, path);
    326 
    327   std::vector<u8> data;
    328   if (!DownloadFile(parent, title, std::move(url), &data).value_or(false) || data.empty())
    329     return false;
    330 
    331   // Directory may not exist. Create it.
    332   const std::string directory(Path::GetDirectory(path));
    333   if ((!directory.empty() && !FileSystem::DirectoryExists(directory.c_str()) &&
    334        !FileSystem::CreateDirectory(directory.c_str(), true)) ||
    335       !FileSystem::WriteBinaryFile(path, data.data(), data.size()))
    336   {
    337     QMessageBox::critical(parent, qApp->translate("QtHost", "Error"),
    338                           qApp->translate("QtHost", "Failed to write '%1'.").arg(QString::fromUtf8(path)));
    339     return false;
    340   }
    341 
    342   return true;
    343 }
    344 
    345 bool QtHost::DownloadFileFromZip(QWidget* parent, const QString& title, std::string url, const char* zip_filename,
    346                                  const char* output_path)
    347 {
    348   INFO_LOG("Download {} from {}, saving to {}.", zip_filename, url, output_path);
    349 
    350   std::vector<u8> data;
    351   if (!DownloadFile(parent, title, std::move(url), &data).value_or(false) || data.empty())
    352     return false;
    353 
    354   const unzFile zf = MinizipHelpers::OpenUnzMemoryFile(data.data(), data.size());
    355   if (!zf)
    356   {
    357     QMessageBox::critical(parent, qApp->translate("QtHost", "Error"),
    358                           qApp->translate("QtHost", "Failed to open downloaded zip file."));
    359     return false;
    360   }
    361 
    362   const ScopedGuard zf_guard = [&zf]() { unzClose(zf); };
    363 
    364   if (unzLocateFile(zf, zip_filename, 0) != UNZ_OK || unzOpenCurrentFile(zf) != UNZ_OK)
    365   {
    366     QMessageBox::critical(
    367       parent, qApp->translate("QtHost", "Error"),
    368       qApp->translate("QtHost", "Failed to locate '%1' in zip.").arg(QString::fromUtf8(zip_filename)));
    369     return false;
    370   }
    371 
    372   // Directory may not exist. Create it.
    373   Error error;
    374   FileSystem::ManagedCFilePtr output_file;
    375   const std::string directory(Path::GetDirectory(output_path));
    376   if ((!directory.empty() && !FileSystem::DirectoryExists(directory.c_str()) &&
    377        !FileSystem::CreateDirectory(directory.c_str(), true)) ||
    378       !(output_file = FileSystem::OpenManagedCFile(output_path, "wb", &error)))
    379   {
    380     QMessageBox::critical(parent, qApp->translate("QtHost", "Error"),
    381                           qApp->translate("QtHost", "Failed to open '%1': %2.")
    382                             .arg(QString::fromUtf8(output_path))
    383                             .arg(QString::fromStdString(error.GetDescription())));
    384     return false;
    385   }
    386 
    387   static constexpr size_t CHUNK_SIZE = 4096;
    388   char chunk[CHUNK_SIZE];
    389   for (;;)
    390   {
    391     int size = unzReadCurrentFile(zf, chunk, CHUNK_SIZE);
    392     if (size < 0)
    393     {
    394       QMessageBox::critical(
    395         parent, qApp->translate("QtHost", "Error"),
    396         qApp->translate("QtHost", "Failed to read '%1' from zip.").arg(QString::fromUtf8(zip_filename)));
    397       output_file.reset();
    398       FileSystem::DeleteFile(output_path);
    399       return false;
    400     }
    401     else if (size == 0)
    402     {
    403       break;
    404     }
    405 
    406     if (std::fwrite(chunk, size, 1, output_file.get()) != 1)
    407     {
    408       QMessageBox::critical(parent, qApp->translate("QtHost", "Error"),
    409                             qApp->translate("QtHost", "Failed to write to '%1'.").arg(QString::fromUtf8(output_path)));
    410 
    411       output_file.reset();
    412       FileSystem::DeleteFile(output_path);
    413       return false;
    414     }
    415   }
    416 
    417   return true;
    418 }
    419 
    420 bool QtHost::InitializeConfig(std::string settings_filename)
    421 {
    422   if (!SetCriticalFolders())
    423     return false;
    424 
    425   if (settings_filename.empty())
    426     settings_filename = Path::Combine(EmuFolders::DataRoot, "settings.ini");
    427 
    428   const bool settings_exists = FileSystem::FileExists(settings_filename.c_str());
    429   INFO_LOG("Loading config from {}.", settings_filename);
    430   s_base_settings_interface = std::make_unique<INISettingsInterface>(std::move(settings_filename));
    431   Host::Internal::SetBaseSettingsLayer(s_base_settings_interface.get());
    432 
    433   uint settings_version;
    434   if (!settings_exists || !s_base_settings_interface->Load() ||
    435       !s_base_settings_interface->GetUIntValue("Main", "SettingsVersion", &settings_version) ||
    436       settings_version != SETTINGS_VERSION)
    437   {
    438     if (s_base_settings_interface->ContainsValue("Main", "SettingsVersion"))
    439     {
    440       // NOTE: No point translating this, because there's no config loaded, so no language loaded.
    441       Host::ReportErrorAsync("Error", fmt::format("Settings version {} does not match expected version {}, resetting.",
    442                                                   settings_version, SETTINGS_VERSION));
    443     }
    444 
    445     s_base_settings_interface->SetUIntValue("Main", "SettingsVersion", SETTINGS_VERSION);
    446     SetDefaultSettings(*s_base_settings_interface, true, true);
    447 
    448     // Flag for running the setup wizard if this is our first run. We want to run it next time if they don't finish it.
    449     s_base_settings_interface->SetBoolValue("Main", "SetupWizardIncomplete", true);
    450 
    451     // Make sure we can actually save the config, and the user doesn't have some permission issue.
    452     Error error;
    453     if (!s_base_settings_interface->Save(&error))
    454     {
    455       QMessageBox::critical(
    456         nullptr, QStringLiteral("DuckStation"),
    457         QStringLiteral(
    458           "Failed to save configuration to\n\n%1\n\nThe error was: %2\n\nPlease ensure this directory is writable. You "
    459           "can also try portable mode by creating portable.txt in the same directory you installed DuckStation into.")
    460           .arg(QString::fromStdString(s_base_settings_interface->GetFileName()))
    461           .arg(QString::fromStdString(error.GetDescription())));
    462       return false;
    463     }
    464   }
    465 
    466   // Setup wizard was incomplete last time?
    467   s_run_setup_wizard =
    468     s_run_setup_wizard || s_base_settings_interface->GetBoolValue("Main", "SetupWizardIncomplete", false);
    469 
    470   EmuFolders::LoadConfig(*s_base_settings_interface.get());
    471   EmuFolders::EnsureFoldersExist();
    472   MigrateSettings();
    473 
    474   // We need to create the console window early, otherwise it appears in front of the main window.
    475   if (!Log::IsConsoleOutputEnabled() &&
    476       s_base_settings_interface->GetBoolValue("Logging", "LogToConsole", Settings::DEFAULT_LOG_TO_CONSOLE))
    477   {
    478     Log::SetConsoleOutputParams(true, s_base_settings_interface->GetBoolValue("Logging", "LogTimestamps", true));
    479   }
    480 
    481   UpdateApplicationLanguage(nullptr);
    482   return true;
    483 }
    484 
    485 bool QtHost::SetCriticalFolders()
    486 {
    487   SetAppRoot();
    488   SetResourcesDirectory();
    489   if (!SetDataDirectory())
    490     return false;
    491 
    492   // logging of directories in case something goes wrong super early
    493   DEV_LOG("AppRoot Directory: {}", EmuFolders::AppRoot);
    494   DEV_LOG("DataRoot Directory: {}", EmuFolders::DataRoot);
    495   DEV_LOG("Resources Directory: {}", EmuFolders::Resources);
    496 
    497   // Write crash dumps to the data directory, since that'll be accessible for certain.
    498   CrashHandler::SetWriteDirectory(EmuFolders::DataRoot);
    499 
    500   // the resources directory should exist, bail out if not
    501   const std::string rcc_path = Path::Combine(EmuFolders::Resources, "duckstation-qt.rcc");
    502   if (!FileSystem::FileExists(rcc_path.c_str()) || !QResource::registerResource(QString::fromStdString(rcc_path)) ||
    503 #ifdef _WIN32
    504       !FileSystem::DirectoryExists(EmuFolders::Resources.c_str())
    505 #else
    506       !FileSystem::IsRealDirectory(EmuFolders::Resources.c_str())
    507 #endif
    508   )
    509   {
    510     QMessageBox::critical(nullptr, QStringLiteral("Error"),
    511                           QStringLiteral("Resources are missing, your installation is incomplete."));
    512     return false;
    513   }
    514 
    515   return true;
    516 }
    517 
    518 bool QtHost::ShouldUsePortableMode()
    519 {
    520   // Check whether portable.ini exists in the program directory.
    521   return (FileSystem::FileExists(Path::Combine(EmuFolders::AppRoot, "portable.txt").c_str()) ||
    522           FileSystem::FileExists(Path::Combine(EmuFolders::AppRoot, "settings.ini").c_str()));
    523 }
    524 
    525 void QtHost::SetAppRoot()
    526 {
    527   const std::string program_path = FileSystem::GetProgramPath();
    528   INFO_LOG("Program Path: {}", program_path.c_str());
    529 
    530   EmuFolders::AppRoot = Path::Canonicalize(Path::GetDirectory(program_path));
    531 }
    532 
    533 void QtHost::SetResourcesDirectory()
    534 {
    535 #ifndef __APPLE__
    536   // On Windows/Linux, these are in the binary directory.
    537   EmuFolders::Resources = Path::Combine(EmuFolders::AppRoot, "resources");
    538 #else
    539   // On macOS, this is in the bundle resources directory.
    540   EmuFolders::Resources = Path::Canonicalize(Path::Combine(EmuFolders::AppRoot, "../Resources"));
    541 #endif
    542 }
    543 
    544 bool QtHost::SetDataDirectory()
    545 {
    546   // Already set, e.g. by -portable.
    547   if (!EmuFolders::DataRoot.empty())
    548     return true;
    549 
    550   if (ShouldUsePortableMode())
    551   {
    552     EmuFolders::DataRoot = EmuFolders::AppRoot;
    553     return true;
    554   }
    555 
    556 #if defined(_WIN32)
    557   // On Windows, use My Documents\DuckStation.
    558   PWSTR documents_directory;
    559   if (SUCCEEDED(SHGetKnownFolderPath(FOLDERID_Documents, 0, NULL, &documents_directory)))
    560   {
    561     if (std::wcslen(documents_directory) > 0)
    562       EmuFolders::DataRoot = Path::Combine(StringUtil::WideStringToUTF8String(documents_directory), "DuckStation");
    563     CoTaskMemFree(documents_directory);
    564   }
    565 #elif defined(__linux__) || defined(__FreeBSD__)
    566   // Use $XDG_CONFIG_HOME/duckstation if it exists.
    567   const char* xdg_config_home = getenv("XDG_CONFIG_HOME");
    568   if (xdg_config_home && Path::IsAbsolute(xdg_config_home))
    569   {
    570     EmuFolders::DataRoot = Path::RealPath(Path::Combine(xdg_config_home, "duckstation"));
    571   }
    572   else
    573   {
    574     // Use ~/.local/share/duckstation otherwise.
    575     const char* home_dir = getenv("HOME");
    576     if (home_dir)
    577     {
    578       // ~/.local/share should exist, but just in case it doesn't and this is a fresh profile..
    579       const std::string local_dir(Path::Combine(home_dir, ".local"));
    580       const std::string share_dir(Path::Combine(local_dir, "share"));
    581       FileSystem::EnsureDirectoryExists(local_dir.c_str(), false);
    582       FileSystem::EnsureDirectoryExists(share_dir.c_str(), false);
    583       EmuFolders::DataRoot = Path::RealPath(Path::Combine(share_dir, "duckstation"));
    584     }
    585   }
    586 #elif defined(__APPLE__)
    587   static constexpr char MAC_DATA_DIR[] = "Library/Application Support/DuckStation";
    588   const char* home_dir = getenv("HOME");
    589   if (home_dir)
    590     EmuFolders::DataRoot = Path::RealPath(Path::Combine(home_dir, MAC_DATA_DIR));
    591 #endif
    592 
    593   // make sure it exists
    594   if (!EmuFolders::DataRoot.empty() && !FileSystem::DirectoryExists(EmuFolders::DataRoot.c_str()))
    595   {
    596     // we're in trouble if we fail to create this directory... but try to hobble on with portable
    597     Error error;
    598     if (!FileSystem::EnsureDirectoryExists(EmuFolders::DataRoot.c_str(), false, &error))
    599     {
    600       // no point translating, config isn't loaded
    601       QMessageBox::critical(
    602         nullptr, QStringLiteral("DuckStation"),
    603         QStringLiteral("Failed to create data directory at path\n\n%1\n\nThe error was: %2\nPlease ensure this "
    604                        "directory is writable. You can also try portable mode by creating portable.txt in the same "
    605                        "directory you installed DuckStation into.")
    606           .arg(QString::fromStdString(EmuFolders::DataRoot))
    607           .arg(QString::fromStdString(error.GetDescription())));
    608       return false;
    609     }
    610   }
    611 
    612   // couldn't determine the data directory? fallback to portable.
    613   if (EmuFolders::DataRoot.empty())
    614     EmuFolders::DataRoot = EmuFolders::AppRoot;
    615 
    616   return true;
    617 }
    618 
    619 void Host::LoadSettings(SettingsInterface& si, std::unique_lock<std::mutex>& lock)
    620 {
    621   g_emu_thread->loadSettings(si);
    622 }
    623 
    624 void EmuThread::loadSettings(SettingsInterface& si)
    625 {
    626   //
    627 }
    628 
    629 void EmuThread::setInitialState(std::optional<bool> override_fullscreen)
    630 {
    631   m_is_fullscreen = override_fullscreen.value_or(Host::GetBaseBoolSettingValue("Main", "StartFullscreen", false));
    632   m_is_rendering_to_main = shouldRenderToMain();
    633   m_is_surfaceless = false;
    634 }
    635 
    636 void EmuThread::checkForSettingsChanges(const Settings& old_settings)
    637 {
    638   if (g_main_window)
    639   {
    640     QMetaObject::invokeMethod(g_main_window, &MainWindow::checkForSettingChanges, Qt::QueuedConnection);
    641 
    642     if (System::IsValid())
    643       updatePerformanceCounters();
    644   }
    645 
    646   const bool render_to_main = shouldRenderToMain();
    647   if (m_is_rendering_to_main != render_to_main)
    648   {
    649     m_is_rendering_to_main = render_to_main;
    650     if (g_gpu_device)
    651       g_gpu_device->UpdateWindow();
    652   }
    653 }
    654 
    655 void Host::CheckForSettingsChanges(const Settings& old_settings)
    656 {
    657   g_emu_thread->checkForSettingsChanges(old_settings);
    658 }
    659 
    660 void EmuThread::setDefaultSettings(bool system /* = true */, bool controller /* = true */)
    661 {
    662   if (isOnThread())
    663   {
    664     QMetaObject::invokeMethod(this, "setDefaultSettings", Qt::QueuedConnection);
    665     return;
    666   }
    667 
    668   auto lock = Host::GetSettingsLock();
    669   QtHost::SetDefaultSettings(*s_base_settings_interface, system, controller);
    670   QtHost::QueueSettingsSave();
    671 
    672   applySettings(false);
    673 
    674   if (system)
    675     emit settingsResetToDefault(system, controller);
    676 }
    677 
    678 void QtHost::SetDefaultSettings(SettingsInterface& si, bool system, bool controller)
    679 {
    680   if (system)
    681   {
    682     System::SetDefaultSettings(si);
    683     EmuFolders::SetDefaults();
    684     EmuFolders::Save(si);
    685   }
    686 
    687   if (controller)
    688   {
    689     InputManager::SetDefaultSourceConfig(si);
    690     Settings::SetDefaultControllerConfig(si);
    691     Settings::SetDefaultHotkeyConfig(si);
    692   }
    693 }
    694 
    695 void QtHost::MigrateSettings()
    696 {
    697   SmallString value;
    698   if (s_base_settings_interface->GetStringValue("Display", "SyncMode", &value))
    699   {
    700     s_base_settings_interface->SetBoolValue("Display", "VSync", (value == "VSync" || value == "VSyncRelaxed"));
    701     s_base_settings_interface->SetBoolValue(
    702       "Display", "OptimalFramePacing",
    703       (value == "VRR" || s_base_settings_interface->GetBoolValue("Display", "DisplayAllFrames", false)));
    704     s_base_settings_interface->DeleteValue("Display", "SyncMode");
    705     s_base_settings_interface->DeleteValue("Display", "DisplayAllFrames");
    706     s_base_settings_interface->Save();
    707   }
    708 }
    709 
    710 bool EmuThread::shouldRenderToMain() const
    711 {
    712   return !Host::GetBoolSettingValue("Main", "RenderToSeparateWindow", false) && !QtHost::InNoGUIMode();
    713 }
    714 
    715 void Host::RequestResizeHostDisplay(s32 new_window_width, s32 new_window_height)
    716 {
    717   if (g_emu_thread->isFullscreen())
    718     return;
    719 
    720   emit g_emu_thread->onResizeRenderWindowRequested(new_window_width, new_window_height);
    721 }
    722 
    723 void EmuThread::applySettings(bool display_osd_messages /* = false */)
    724 {
    725   if (!isOnThread())
    726   {
    727     QMetaObject::invokeMethod(this, "applySettings", Qt::QueuedConnection, Q_ARG(bool, display_osd_messages));
    728     return;
    729   }
    730 
    731   System::ApplySettings(display_osd_messages);
    732 }
    733 
    734 void EmuThread::reloadGameSettings(bool display_osd_messages /* = false */)
    735 {
    736   if (!isOnThread())
    737   {
    738     QMetaObject::invokeMethod(this, "reloadGameSettings", Qt::QueuedConnection, Q_ARG(bool, display_osd_messages));
    739     return;
    740   }
    741 
    742   System::ReloadGameSettings(display_osd_messages);
    743 }
    744 
    745 void EmuThread::updateEmuFolders()
    746 {
    747   if (!isOnThread())
    748   {
    749     QMetaObject::invokeMethod(this, &EmuThread::updateEmuFolders, Qt::QueuedConnection);
    750     return;
    751   }
    752 
    753   EmuFolders::Update();
    754 }
    755 
    756 void EmuThread::updateControllerSettings()
    757 {
    758   if (!isOnThread())
    759   {
    760     QMetaObject::invokeMethod(this, &EmuThread::updateControllerSettings, Qt::QueuedConnection);
    761     return;
    762   }
    763 
    764   if (!System::IsValid())
    765     return;
    766 
    767   System::UpdateControllerSettings();
    768 }
    769 
    770 void EmuThread::startFullscreenUI()
    771 {
    772   if (!isOnThread())
    773   {
    774     QMetaObject::invokeMethod(this, &EmuThread::startFullscreenUI, Qt::QueuedConnection);
    775     return;
    776   }
    777 
    778   if (System::IsValid())
    779     return;
    780 
    781   // we want settings loaded so we choose the correct renderer
    782   // this also sorts out input sources.
    783   System::LoadSettings(false);
    784   setInitialState(s_start_fullscreen_ui_fullscreen ? std::optional<bool>(true) : std::optional<bool>());
    785   m_run_fullscreen_ui = true;
    786 
    787   Error error;
    788   if (!Host::CreateGPUDevice(Settings::GetRenderAPIForRenderer(g_settings.gpu_renderer), &error) ||
    789       !FullscreenUI::Initialize())
    790   {
    791     Host::ReportErrorAsync("Error", error.GetDescription());
    792     Host::ReleaseGPUDevice();
    793     Host::ReleaseRenderWindow();
    794     m_run_fullscreen_ui = false;
    795     return;
    796   }
    797 
    798   emit fullscreenUIStateChange(true);
    799 
    800   // poll more frequently so we don't lose events
    801   stopBackgroundControllerPollTimer();
    802   startBackgroundControllerPollTimer();
    803 }
    804 
    805 void EmuThread::stopFullscreenUI()
    806 {
    807   if (!isOnThread())
    808   {
    809     QMetaObject::invokeMethod(this, &EmuThread::stopFullscreenUI, Qt::QueuedConnection);
    810 
    811     // wait until the host display is gone
    812     while (!QtHost::IsSystemValid() && g_gpu_device)
    813       QApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 1);
    814 
    815     return;
    816   }
    817 
    818   setFullscreen(false, true);
    819 
    820   if (m_run_fullscreen_ui)
    821   {
    822     m_run_fullscreen_ui = false;
    823     emit fullscreenUIStateChange(false);
    824   }
    825 
    826   if (!g_gpu_device)
    827     return;
    828 
    829   Host::ReleaseGPUDevice();
    830   Host::ReleaseRenderWindow();
    831 }
    832 
    833 void EmuThread::bootSystem(std::shared_ptr<SystemBootParameters> params)
    834 {
    835   if (!isOnThread())
    836   {
    837     QMetaObject::invokeMethod(this, "bootSystem", Qt::QueuedConnection,
    838                               Q_ARG(std::shared_ptr<SystemBootParameters>, std::move(params)));
    839     return;
    840   }
    841 
    842   // Just in case of rapid clicking games before it gets the chance to start.
    843   if (System::IsValidOrInitializing())
    844     return;
    845 
    846   setInitialState(params->override_fullscreen);
    847 
    848   Error error;
    849   if (!System::BootSystem(std::move(*params), &error))
    850   {
    851     emit errorReported(tr("Error"),
    852                        tr("Failed to boot system: %1").arg(QString::fromStdString(error.GetDescription())));
    853   }
    854 }
    855 
    856 void EmuThread::bootOrLoadState(std::string path)
    857 {
    858   DebugAssert(isOnThread());
    859 
    860   if (System::IsValid())
    861   {
    862     Error error;
    863     if (!System::LoadState(path.c_str(), &error, true))
    864     {
    865       emit errorReported(tr("Error"),
    866                          tr("Failed to load state: %1").arg(QString::fromStdString(error.GetDescription())));
    867     }
    868   }
    869   else
    870   {
    871     std::shared_ptr<SystemBootParameters> params = std::make_shared<SystemBootParameters>();
    872     params->save_state = std::move(path);
    873     bootSystem(std::move(params));
    874   }
    875 }
    876 
    877 void EmuThread::resumeSystemFromMostRecentState()
    878 {
    879   if (!isOnThread())
    880   {
    881     QMetaObject::invokeMethod(this, &EmuThread::resumeSystemFromMostRecentState, Qt::QueuedConnection);
    882     return;
    883   }
    884 
    885   // shouldn't be doing this with a system running
    886   if (System::IsValid())
    887     return;
    888 
    889   std::string state_filename(System::GetMostRecentResumeSaveStatePath());
    890   if (state_filename.empty())
    891   {
    892     emit errorReported(tr("Error"), tr("No resume save state found."));
    893     return;
    894   }
    895 
    896   bootOrLoadState(std::move(state_filename));
    897 }
    898 
    899 void EmuThread::onDisplayWindowKeyEvent(int key, bool pressed)
    900 {
    901   DebugAssert(isOnThread());
    902 
    903   InputManager::InvokeEvents(InputManager::MakeHostKeyboardKey(key), static_cast<float>(pressed),
    904                              GenericInputBinding::Unknown);
    905 }
    906 
    907 void EmuThread::onDisplayWindowTextEntered(const QString& text)
    908 {
    909   DebugAssert(isOnThread());
    910 
    911   ImGuiManager::AddTextInput(text.toStdString());
    912 }
    913 
    914 void EmuThread::onDisplayWindowMouseButtonEvent(int button, bool pressed)
    915 {
    916   DebugAssert(isOnThread());
    917 
    918   InputManager::InvokeEvents(InputManager::MakePointerButtonKey(0, button), static_cast<float>(pressed),
    919                              GenericInputBinding::Unknown);
    920 }
    921 
    922 void EmuThread::onDisplayWindowMouseWheelEvent(const QPoint& delta_angle)
    923 {
    924   DebugAssert(isOnThread());
    925 
    926   const float dx = std::clamp(static_cast<float>(delta_angle.x()) / QtUtils::MOUSE_WHEEL_DELTA, -1.0f, 1.0f);
    927   if (dx != 0.0f)
    928     InputManager::UpdatePointerRelativeDelta(0, InputPointerAxis::WheelX, dx);
    929 
    930   const float dy = std::clamp(static_cast<float>(delta_angle.y()) / QtUtils::MOUSE_WHEEL_DELTA, -1.0f, 1.0f);
    931   if (dy != 0.0f)
    932     InputManager::UpdatePointerRelativeDelta(0, InputPointerAxis::WheelY, dy);
    933 }
    934 
    935 void EmuThread::onDisplayWindowResized(int width, int height, float scale)
    936 {
    937   Host::ResizeDisplayWindow(width, height, scale);
    938 }
    939 
    940 void EmuThread::redrawDisplayWindow()
    941 {
    942   if (!isOnThread())
    943   {
    944     QMetaObject::invokeMethod(this, "redrawDisplayWindow", Qt::QueuedConnection);
    945     return;
    946   }
    947 
    948   if (!g_gpu_device || System::IsShutdown())
    949     return;
    950 
    951   System::InvalidateDisplay();
    952 }
    953 
    954 void EmuThread::toggleFullscreen()
    955 {
    956   if (!isOnThread())
    957   {
    958     QMetaObject::invokeMethod(this, "toggleFullscreen", Qt::QueuedConnection);
    959     return;
    960   }
    961 
    962   setFullscreen(!m_is_fullscreen, true);
    963 }
    964 
    965 void EmuThread::setFullscreen(bool fullscreen, bool allow_render_to_main)
    966 {
    967   if (!isOnThread())
    968   {
    969     QMetaObject::invokeMethod(this, "setFullscreen", Qt::QueuedConnection, Q_ARG(bool, fullscreen),
    970                               Q_ARG(bool, allow_render_to_main));
    971     return;
    972   }
    973 
    974   if (!g_gpu_device || m_is_fullscreen == fullscreen)
    975     return;
    976 
    977   m_is_fullscreen = fullscreen;
    978   m_is_rendering_to_main = allow_render_to_main && shouldRenderToMain();
    979   Host::UpdateDisplayWindow();
    980 }
    981 
    982 bool Host::IsFullscreen()
    983 {
    984   return g_emu_thread->isFullscreen();
    985 }
    986 
    987 void Host::SetFullscreen(bool enabled)
    988 {
    989   g_emu_thread->setFullscreen(enabled, true);
    990 }
    991 
    992 void EmuThread::setSurfaceless(bool surfaceless)
    993 {
    994   if (!isOnThread())
    995   {
    996     QMetaObject::invokeMethod(this, "setSurfaceless", Qt::QueuedConnection, Q_ARG(bool, surfaceless));
    997     return;
    998   }
    999 
   1000   if (!g_gpu_device || m_is_surfaceless == surfaceless)
   1001     return;
   1002 
   1003   m_is_surfaceless = surfaceless;
   1004   Host::UpdateDisplayWindow();
   1005 }
   1006 
   1007 void EmuThread::requestDisplaySize(float scale)
   1008 {
   1009   if (!isOnThread())
   1010   {
   1011     QMetaObject::invokeMethod(this, "requestDisplaySize", Qt::QueuedConnection, Q_ARG(float, scale));
   1012     return;
   1013   }
   1014 
   1015   if (!System::IsValid())
   1016     return;
   1017 
   1018   System::RequestDisplaySize(scale);
   1019 }
   1020 
   1021 std::optional<WindowInfo> EmuThread::acquireRenderWindow(bool recreate_window)
   1022 {
   1023   DebugAssert(g_gpu_device);
   1024   u32 fs_width, fs_height;
   1025   float fs_refresh_rate;
   1026   m_is_exclusive_fullscreen = (m_is_fullscreen && g_gpu_device->SupportsExclusiveFullscreen() &&
   1027                                GPUDevice::GetRequestedExclusiveFullscreenMode(&fs_width, &fs_height, &fs_refresh_rate));
   1028 
   1029   const bool window_fullscreen = m_is_fullscreen && !m_is_exclusive_fullscreen;
   1030   const bool render_to_main = !m_is_exclusive_fullscreen && !window_fullscreen && m_is_rendering_to_main;
   1031   const bool use_main_window_pos = shouldRenderToMain();
   1032 
   1033   return emit onAcquireRenderWindowRequested(recreate_window, window_fullscreen, render_to_main, m_is_surfaceless,
   1034                                              use_main_window_pos);
   1035 }
   1036 
   1037 void EmuThread::releaseRenderWindow()
   1038 {
   1039   emit onReleaseRenderWindowRequested();
   1040 }
   1041 
   1042 void EmuThread::connectDisplaySignals(DisplayWidget* widget)
   1043 {
   1044   widget->disconnect(this);
   1045 
   1046   connect(widget, &DisplayWidget::windowResizedEvent, this, &EmuThread::onDisplayWindowResized);
   1047   connect(widget, &DisplayWidget::windowRestoredEvent, this, &EmuThread::redrawDisplayWindow);
   1048   connect(widget, &DisplayWidget::windowKeyEvent, this, &EmuThread::onDisplayWindowKeyEvent);
   1049   connect(widget, &DisplayWidget::windowTextEntered, this, &EmuThread::onDisplayWindowTextEntered);
   1050   connect(widget, &DisplayWidget::windowMouseButtonEvent, this, &EmuThread::onDisplayWindowMouseButtonEvent);
   1051   connect(widget, &DisplayWidget::windowMouseWheelEvent, this, &EmuThread::onDisplayWindowMouseWheelEvent);
   1052 }
   1053 
   1054 void Host::OnSystemStarting()
   1055 {
   1056   emit g_emu_thread->systemStarting();
   1057 }
   1058 
   1059 void Host::OnSystemStarted()
   1060 {
   1061   g_emu_thread->stopBackgroundControllerPollTimer();
   1062 
   1063   emit g_emu_thread->systemStarted();
   1064 }
   1065 
   1066 void Host::OnSystemPaused()
   1067 {
   1068   emit g_emu_thread->systemPaused();
   1069   g_emu_thread->startBackgroundControllerPollTimer();
   1070 }
   1071 
   1072 void Host::OnSystemResumed()
   1073 {
   1074   // if we were surfaceless (view->game list, system->unpause), get our display widget back
   1075   if (g_emu_thread->isSurfaceless())
   1076     g_emu_thread->setSurfaceless(false);
   1077 
   1078   emit g_emu_thread->systemResumed();
   1079 
   1080   g_emu_thread->stopBackgroundControllerPollTimer();
   1081 }
   1082 
   1083 void Host::OnSystemDestroyed()
   1084 {
   1085   g_emu_thread->resetPerformanceCounters();
   1086   g_emu_thread->startBackgroundControllerPollTimer();
   1087   emit g_emu_thread->systemDestroyed();
   1088 }
   1089 
   1090 void Host::OnIdleStateChanged()
   1091 {
   1092   g_emu_thread->wakeThread();
   1093 }
   1094 
   1095 void EmuThread::reloadInputSources()
   1096 {
   1097   if (!isOnThread())
   1098   {
   1099     QMetaObject::invokeMethod(this, &EmuThread::reloadInputSources, Qt::QueuedConnection);
   1100     return;
   1101   }
   1102 
   1103   System::ReloadInputSources();
   1104 }
   1105 
   1106 void EmuThread::reloadInputBindings()
   1107 {
   1108   if (!isOnThread())
   1109   {
   1110     QMetaObject::invokeMethod(this, &EmuThread::reloadInputBindings, Qt::QueuedConnection);
   1111     return;
   1112   }
   1113 
   1114   System::ReloadInputBindings();
   1115 }
   1116 
   1117 void EmuThread::reloadInputDevices()
   1118 {
   1119   if (!isOnThread())
   1120   {
   1121     QMetaObject::invokeMethod(this, &EmuThread::reloadInputDevices, Qt::QueuedConnection);
   1122     return;
   1123   }
   1124 
   1125   InputManager::ReloadDevices();
   1126 }
   1127 
   1128 void EmuThread::closeInputSources()
   1129 {
   1130   if (!isOnThread())
   1131   {
   1132     QMetaObject::invokeMethod(this, &EmuThread::reloadInputDevices, Qt::BlockingQueuedConnection);
   1133     return;
   1134   }
   1135 
   1136   InputManager::CloseSources();
   1137 }
   1138 
   1139 void EmuThread::enumerateInputDevices()
   1140 {
   1141   if (!isOnThread())
   1142   {
   1143     QMetaObject::invokeMethod(this, &EmuThread::enumerateInputDevices, Qt::QueuedConnection);
   1144     return;
   1145   }
   1146 
   1147   onInputDevicesEnumerated(InputManager::EnumerateDevices());
   1148 }
   1149 
   1150 void EmuThread::enumerateVibrationMotors()
   1151 {
   1152   if (!isOnThread())
   1153   {
   1154     QMetaObject::invokeMethod(this, &EmuThread::enumerateVibrationMotors, Qt::QueuedConnection);
   1155     return;
   1156   }
   1157 
   1158   const std::vector<InputBindingKey> motors(InputManager::EnumerateMotors());
   1159   QList<InputBindingKey> qmotors;
   1160   qmotors.reserve(motors.size());
   1161   for (InputBindingKey key : motors)
   1162     qmotors.push_back(key);
   1163 
   1164   onVibrationMotorsEnumerated(qmotors);
   1165 }
   1166 
   1167 void EmuThread::confirmActionIfMemoryCardBusy(const QString& action, bool cancel_resume_on_accept,
   1168                                               std::function<void(bool)> callback) const
   1169 {
   1170   DebugAssert(isOnThread());
   1171 
   1172   if (!System::IsValid() || !System::IsSavingMemoryCards())
   1173   {
   1174     callback(true);
   1175     return;
   1176   }
   1177 
   1178   QtHost::RunOnUIThread([action, cancel_resume_on_accept, callback = std::move(callback)]() mutable {
   1179     auto lock = g_main_window->pauseAndLockSystem();
   1180 
   1181     const bool result =
   1182       (QMessageBox::question(lock.getDialogParent(), tr("Memory Card Busy"),
   1183                              tr("WARNING: Your game is still saving to the memory card. Continuing to %1 may "
   1184                                 "IRREVERSIBLY DESTROY YOUR MEMORY CARD. We recommend resuming your game and waiting 5 "
   1185                                 "seconds for it to finish saving.\n\nDo you want to %1 anyway?")
   1186                                .arg(action)) != QMessageBox::No);
   1187 
   1188     if (cancel_resume_on_accept)
   1189       lock.cancelResume();
   1190 
   1191     Host::RunOnCPUThread([result, callback = std::move(callback)]() { callback(result); });
   1192   });
   1193 }
   1194 
   1195 void EmuThread::shutdownSystem(bool save_state, bool check_memcard_busy)
   1196 {
   1197   if (!isOnThread())
   1198   {
   1199     System::CancelPendingStartup();
   1200     QMetaObject::invokeMethod(this, "shutdownSystem", Qt::QueuedConnection, Q_ARG(bool, save_state),
   1201                               Q_ARG(bool, check_memcard_busy));
   1202     return;
   1203   }
   1204 
   1205   if (check_memcard_busy && System::IsSavingMemoryCards())
   1206   {
   1207     confirmActionIfMemoryCardBusy(tr("shut down"), true, [save_state](bool result) {
   1208       if (result)
   1209         g_emu_thread->shutdownSystem(save_state, false);
   1210       else
   1211         g_emu_thread->setSystemPaused(false);
   1212     });
   1213     return;
   1214   }
   1215 
   1216   System::ShutdownSystem(save_state);
   1217 }
   1218 
   1219 void EmuThread::resetSystem(bool check_memcard_busy)
   1220 {
   1221   if (!isOnThread())
   1222   {
   1223     QMetaObject::invokeMethod(this, "resetSystem", Qt::QueuedConnection, Q_ARG(bool, check_memcard_busy));
   1224     return;
   1225   }
   1226 
   1227   if (check_memcard_busy && System::IsSavingMemoryCards())
   1228   {
   1229     confirmActionIfMemoryCardBusy(tr("reset"), false, [](bool result) {
   1230       if (result)
   1231         g_emu_thread->resetSystem(false);
   1232     });
   1233     return;
   1234   }
   1235 
   1236   System::ResetSystem();
   1237 }
   1238 
   1239 void EmuThread::setSystemPaused(bool paused, bool wait_until_paused /* = false */)
   1240 {
   1241   if (!isOnThread())
   1242   {
   1243     QMetaObject::invokeMethod(this, "setSystemPaused",
   1244                               wait_until_paused ? Qt::BlockingQueuedConnection : Qt::QueuedConnection,
   1245                               Q_ARG(bool, paused), Q_ARG(bool, wait_until_paused));
   1246     return;
   1247   }
   1248 
   1249   System::PauseSystem(paused);
   1250 }
   1251 
   1252 void EmuThread::changeDisc(const QString& new_disc_filename, bool reset_system, bool check_memcard_busy)
   1253 {
   1254   if (!isOnThread())
   1255   {
   1256     QMetaObject::invokeMethod(this, "changeDisc", Qt::QueuedConnection, Q_ARG(const QString&, new_disc_filename),
   1257                               Q_ARG(bool, reset_system), Q_ARG(bool, check_memcard_busy));
   1258     return;
   1259   }
   1260 
   1261   if (check_memcard_busy && System::IsSavingMemoryCards())
   1262   {
   1263     confirmActionIfMemoryCardBusy(tr("change disc"), false, [new_disc_filename, reset_system](bool result) {
   1264       if (result)
   1265         g_emu_thread->changeDisc(new_disc_filename, reset_system, false);
   1266     });
   1267     return;
   1268   }
   1269 
   1270   if (System::IsShutdown())
   1271     return;
   1272 
   1273   if (!new_disc_filename.isEmpty())
   1274     System::InsertMedia(new_disc_filename.toStdString().c_str());
   1275   else
   1276     System::RemoveMedia();
   1277 
   1278   if (reset_system)
   1279     System::ResetSystem();
   1280 }
   1281 
   1282 void EmuThread::changeDiscFromPlaylist(quint32 index)
   1283 {
   1284   if (!isOnThread())
   1285   {
   1286     QMetaObject::invokeMethod(this, "changeDiscFromPlaylist", Qt::QueuedConnection, Q_ARG(quint32, index));
   1287     return;
   1288   }
   1289 
   1290   if (System::IsShutdown())
   1291     return;
   1292 
   1293   if (!System::SwitchMediaSubImage(index))
   1294     errorReported(tr("Error"), tr("Failed to switch to subimage %1").arg(index));
   1295 }
   1296 
   1297 void EmuThread::setCheatEnabled(quint32 index, bool enabled)
   1298 {
   1299   if (!isOnThread())
   1300   {
   1301     QMetaObject::invokeMethod(this, "setCheatEnabled", Qt::QueuedConnection, Q_ARG(quint32, index),
   1302                               Q_ARG(bool, enabled));
   1303     return;
   1304   }
   1305 
   1306   System::SetCheatCodeState(index, enabled);
   1307   emit cheatEnabled(index, enabled);
   1308 }
   1309 
   1310 void EmuThread::applyCheat(quint32 index)
   1311 {
   1312   if (!isOnThread())
   1313   {
   1314     QMetaObject::invokeMethod(this, "applyCheat", Qt::QueuedConnection, Q_ARG(quint32, index));
   1315     return;
   1316   }
   1317 
   1318   System::ApplyCheatCode(index);
   1319 }
   1320 
   1321 void EmuThread::reloadPostProcessingShaders()
   1322 {
   1323   if (!isOnThread())
   1324   {
   1325     QMetaObject::invokeMethod(this, "reloadPostProcessingShaders", Qt::QueuedConnection);
   1326     return;
   1327   }
   1328 
   1329   if (System::IsValid())
   1330     PostProcessing::ReloadShaders();
   1331 }
   1332 
   1333 void EmuThread::updatePostProcessingSettings()
   1334 {
   1335   if (!isOnThread())
   1336   {
   1337     QMetaObject::invokeMethod(this, "updatePostProcessingSettings", Qt::QueuedConnection);
   1338     return;
   1339   }
   1340 
   1341   if (System::IsValid())
   1342     PostProcessing::UpdateSettings();
   1343 }
   1344 
   1345 void EmuThread::clearInputBindStateFromSource(InputBindingKey key)
   1346 {
   1347   if (!isOnThread())
   1348   {
   1349     QMetaObject::invokeMethod(this, "clearInputBindStateFromSource", Qt::QueuedConnection, Q_ARG(InputBindingKey, key));
   1350     return;
   1351   }
   1352 
   1353   InputManager::ClearBindStateFromSource(key);
   1354 }
   1355 
   1356 void EmuThread::runOnEmuThread(std::function<void()> callback)
   1357 {
   1358   callback();
   1359 }
   1360 
   1361 void Host::RunOnCPUThread(std::function<void()> function, bool block /* = false */)
   1362 {
   1363   const bool self = g_emu_thread->isOnThread();
   1364 
   1365   QMetaObject::invokeMethod(g_emu_thread, "runOnEmuThread",
   1366                             (block && !self) ? Qt::BlockingQueuedConnection : Qt::QueuedConnection,
   1367                             Q_ARG(std::function<void()>, std::move(function)));
   1368 }
   1369 
   1370 void QtHost::RunOnUIThread(const std::function<void()>& func, bool block /*= false*/)
   1371 {
   1372   // main window always exists, so it's fine to attach it to that.
   1373   QMetaObject::invokeMethod(g_main_window, "runOnUIThread", block ? Qt::BlockingQueuedConnection : Qt::QueuedConnection,
   1374                             Q_ARG(const std::function<void()>&, func));
   1375 }
   1376 
   1377 void Host::RefreshGameListAsync(bool invalidate_cache)
   1378 {
   1379   QMetaObject::invokeMethod(g_main_window, "refreshGameList", Qt::QueuedConnection, Q_ARG(bool, invalidate_cache));
   1380 }
   1381 
   1382 void Host::CancelGameListRefresh()
   1383 {
   1384   QMetaObject::invokeMethod(g_main_window, "cancelGameListRefresh", Qt::BlockingQueuedConnection);
   1385 }
   1386 
   1387 void EmuThread::loadState(const QString& filename)
   1388 {
   1389   if (!isOnThread())
   1390   {
   1391     QMetaObject::invokeMethod(this, "loadState", Qt::QueuedConnection, Q_ARG(const QString&, filename));
   1392     return;
   1393   }
   1394 
   1395   bootOrLoadState(filename.toStdString());
   1396 }
   1397 
   1398 void EmuThread::loadState(bool global, qint32 slot)
   1399 {
   1400   if (!isOnThread())
   1401   {
   1402     QMetaObject::invokeMethod(this, "loadState", Qt::QueuedConnection, Q_ARG(bool, global), Q_ARG(qint32, slot));
   1403     return;
   1404   }
   1405 
   1406   // shouldn't even get here if we don't have a running game
   1407   if (!global && System::GetGameSerial().empty())
   1408     return;
   1409 
   1410   bootOrLoadState(global ? System::GetGlobalSaveStateFileName(slot) :
   1411                            System::GetGameSaveStateFileName(System::GetGameSerial(), slot));
   1412 }
   1413 
   1414 void EmuThread::saveState(const QString& filename, bool block_until_done /* = false */)
   1415 {
   1416   if (!isOnThread())
   1417   {
   1418     QMetaObject::invokeMethod(this, "saveState", block_until_done ? Qt::BlockingQueuedConnection : Qt::QueuedConnection,
   1419                               Q_ARG(const QString&, filename), Q_ARG(bool, block_until_done));
   1420     return;
   1421   }
   1422 
   1423   if (!System::IsValid())
   1424     return;
   1425 
   1426   Error error;
   1427   if (!System::SaveState(filename.toUtf8().data(), &error, g_settings.create_save_state_backups))
   1428     emit errorReported(tr("Error"), tr("Failed to save state: %1").arg(QString::fromStdString(error.GetDescription())));
   1429 }
   1430 
   1431 void EmuThread::saveState(bool global, qint32 slot, bool block_until_done /* = false */)
   1432 {
   1433   if (!isOnThread())
   1434   {
   1435     QMetaObject::invokeMethod(this, "saveState", block_until_done ? Qt::BlockingQueuedConnection : Qt::QueuedConnection,
   1436                               Q_ARG(bool, global), Q_ARG(qint32, slot), Q_ARG(bool, block_until_done));
   1437     return;
   1438   }
   1439 
   1440   if (!global && System::GetGameSerial().empty())
   1441     return;
   1442 
   1443   Error error;
   1444   if (!System::SaveState((global ? System::GetGlobalSaveStateFileName(slot) :
   1445                                    System::GetGameSaveStateFileName(System::GetGameSerial(), slot))
   1446                            .c_str(),
   1447                          &error, g_settings.create_save_state_backups))
   1448   {
   1449     emit errorReported(tr("Error"), tr("Failed to save state: %1").arg(QString::fromStdString(error.GetDescription())));
   1450   }
   1451 }
   1452 
   1453 void EmuThread::undoLoadState()
   1454 {
   1455   if (!isOnThread())
   1456   {
   1457     QMetaObject::invokeMethod(this, "undoLoadState", Qt::QueuedConnection);
   1458     return;
   1459   }
   1460 
   1461   System::UndoLoadState();
   1462 }
   1463 
   1464 void EmuThread::setAudioOutputVolume(int volume, int fast_forward_volume)
   1465 {
   1466   if (!isOnThread())
   1467   {
   1468     QMetaObject::invokeMethod(this, "setAudioOutputVolume", Qt::QueuedConnection, Q_ARG(int, volume),
   1469                               Q_ARG(int, fast_forward_volume));
   1470     return;
   1471   }
   1472 
   1473   g_settings.audio_output_volume = volume;
   1474   g_settings.audio_fast_forward_volume = fast_forward_volume;
   1475   System::UpdateVolume();
   1476 }
   1477 
   1478 void EmuThread::setAudioOutputMuted(bool muted)
   1479 {
   1480   if (!isOnThread())
   1481   {
   1482     QMetaObject::invokeMethod(this, "setAudioOutputMuted", Qt::QueuedConnection, Q_ARG(bool, muted));
   1483     return;
   1484   }
   1485 
   1486   g_settings.audio_output_muted = muted;
   1487   System::UpdateVolume();
   1488 }
   1489 
   1490 void EmuThread::startDumpingAudio()
   1491 {
   1492   if (!isOnThread())
   1493   {
   1494     QMetaObject::invokeMethod(this, "startDumpingAudio", Qt::QueuedConnection);
   1495     return;
   1496   }
   1497 
   1498   //System::StartDumpingAudio();
   1499 }
   1500 
   1501 void EmuThread::stopDumpingAudio()
   1502 {
   1503   if (!isOnThread())
   1504   {
   1505     QMetaObject::invokeMethod(this, "stopDumpingAudio", Qt::QueuedConnection);
   1506     return;
   1507   }
   1508 
   1509   //System::StopDumpingAudio();
   1510 }
   1511 
   1512 void EmuThread::singleStepCPU()
   1513 {
   1514   if (!isOnThread())
   1515   {
   1516     QMetaObject::invokeMethod(this, "singleStepCPU", Qt::BlockingQueuedConnection);
   1517     return;
   1518   }
   1519 
   1520   if (!System::IsValid())
   1521     return;
   1522 
   1523   System::SingleStepCPU();
   1524 }
   1525 
   1526 void EmuThread::dumpRAM(const QString& filename)
   1527 {
   1528   if (!isOnThread())
   1529   {
   1530     QMetaObject::invokeMethod(this, "dumpRAM", Qt::QueuedConnection, Q_ARG(const QString&, filename));
   1531     return;
   1532   }
   1533 
   1534   const std::string filename_str = filename.toStdString();
   1535   if (System::DumpRAM(filename_str.c_str()))
   1536     Host::AddOSDMessage(fmt::format("RAM dumped to '{}'", filename_str), 10.0f);
   1537   else
   1538     Host::ReportErrorAsync("Error", fmt::format("Failed to dump RAM to '{}'", filename_str));
   1539 }
   1540 
   1541 void EmuThread::dumpVRAM(const QString& filename)
   1542 {
   1543   if (!isOnThread())
   1544   {
   1545     QMetaObject::invokeMethod(this, "dumpVRAM", Qt::QueuedConnection, Q_ARG(const QString&, filename));
   1546     return;
   1547   }
   1548 
   1549   const std::string filename_str = filename.toStdString();
   1550   if (System::DumpVRAM(filename_str.c_str()))
   1551     Host::AddOSDMessage(fmt::format("VRAM dumped to '{}'", filename_str), 10.0f);
   1552   else
   1553     Host::ReportErrorAsync("Error", fmt::format("Failed to dump VRAM to '{}'", filename_str));
   1554 }
   1555 
   1556 void EmuThread::dumpSPURAM(const QString& filename)
   1557 {
   1558   if (!isOnThread())
   1559   {
   1560     QMetaObject::invokeMethod(this, "dumpSPURAM", Qt::QueuedConnection, Q_ARG(const QString&, filename));
   1561     return;
   1562   }
   1563 
   1564   const std::string filename_str = filename.toStdString();
   1565   if (System::DumpSPURAM(filename_str.c_str()))
   1566     Host::AddOSDMessage(fmt::format("SPU RAM dumped to '{}'", filename_str), 10.0f);
   1567   else
   1568     Host::ReportErrorAsync("Error", fmt::format("Failed to dump SPU RAM to '{}'", filename_str));
   1569 }
   1570 
   1571 void EmuThread::saveScreenshot()
   1572 {
   1573   if (!isOnThread())
   1574   {
   1575     QMetaObject::invokeMethod(this, "saveScreenshot", Qt::QueuedConnection);
   1576     return;
   1577   }
   1578 
   1579   System::SaveScreenshot();
   1580 }
   1581 
   1582 void Host::OnAchievementsLoginRequested(Achievements::LoginRequestReason reason)
   1583 {
   1584   emit g_emu_thread->achievementsLoginRequested(reason);
   1585 }
   1586 
   1587 void Host::OnAchievementsLoginSuccess(const char* username, u32 points, u32 sc_points, u32 unread_messages)
   1588 {
   1589   const QString message = qApp->translate("QtHost", "RA: Logged in as %1 (%2, %3 softcore). %4 unread messages.")
   1590                             .arg(QString::fromUtf8(username))
   1591                             .arg(points)
   1592                             .arg(sc_points)
   1593                             .arg(unread_messages);
   1594 
   1595   emit g_emu_thread->statusMessage(message);
   1596 }
   1597 
   1598 void Host::OnAchievementsRefreshed()
   1599 {
   1600   u32 game_id = 0;
   1601 
   1602   QString game_info;
   1603 
   1604   if (Achievements::HasActiveGame())
   1605   {
   1606     game_id = Achievements::GetGameID();
   1607 
   1608     game_info = qApp->translate("EmuThread", "Game: %1 (%2)\n")
   1609                   .arg(QString::fromStdString(Achievements::GetGameTitle()))
   1610                   .arg(game_id);
   1611 
   1612     const std::string& rich_presence_string = Achievements::GetRichPresenceString();
   1613     if (Achievements::HasRichPresence() && !rich_presence_string.empty())
   1614       game_info.append(QString::fromStdString(StringUtil::Ellipsise(rich_presence_string, 128)));
   1615     else
   1616       game_info.append(qApp->translate("EmuThread", "Rich presence inactive or unsupported."));
   1617   }
   1618   else
   1619   {
   1620     game_info = qApp->translate("EmuThread", "Game not loaded or no RetroAchievements available.");
   1621   }
   1622 
   1623   emit g_emu_thread->achievementsRefreshed(game_id, game_info);
   1624 }
   1625 
   1626 void Host::OnAchievementsHardcoreModeChanged(bool enabled)
   1627 {
   1628   emit g_emu_thread->achievementsChallengeModeChanged(enabled);
   1629 }
   1630 
   1631 void Host::OnCoverDownloaderOpenRequested()
   1632 {
   1633   emit g_emu_thread->onCoverDownloaderOpenRequested();
   1634 }
   1635 
   1636 bool Host::ShouldPreferHostFileSelector()
   1637 {
   1638 #ifdef __linux__
   1639   // If running inside a flatpak, we want to use native selectors/portals.
   1640   return (std::getenv("container") != nullptr);
   1641 #else
   1642   return false;
   1643 #endif
   1644 }
   1645 
   1646 void Host::OpenHostFileSelectorAsync(std::string_view title, bool select_directory, FileSelectorCallback callback,
   1647                                      FileSelectorFilters filters /* = FileSelectorFilters() */,
   1648                                      std::string_view initial_directory /* = std::string_view() */)
   1649 {
   1650   const bool from_cpu_thread = g_emu_thread->isOnThread();
   1651 
   1652   QString filters_str;
   1653   if (!filters.empty())
   1654   {
   1655     filters_str.append(QStringLiteral("All File Types (%1)")
   1656                          .arg(QString::fromStdString(StringUtil::JoinString(filters.begin(), filters.end(), " "))));
   1657     for (const std::string& filter : filters)
   1658     {
   1659       filters_str.append(
   1660         QStringLiteral(";;%1 Files (%2)")
   1661           .arg(
   1662             QtUtils::StringViewToQString(std::string_view(filter).substr(filter.starts_with("*.") ? 2 : 0)).toUpper())
   1663           .arg(QString::fromStdString(filter)));
   1664     }
   1665   }
   1666 
   1667   QtHost::RunOnUIThread([title = QtUtils::StringViewToQString(title), select_directory, callback = std::move(callback),
   1668                          filters_str = std::move(filters_str),
   1669                          initial_directory = QtUtils::StringViewToQString(initial_directory),
   1670                          from_cpu_thread]() mutable {
   1671     auto lock = g_main_window->pauseAndLockSystem();
   1672 
   1673     QString path;
   1674 
   1675     if (select_directory)
   1676       path = QFileDialog::getExistingDirectory(lock.getDialogParent(), title, initial_directory);
   1677     else
   1678       path = QFileDialog::getOpenFileName(lock.getDialogParent(), title, initial_directory, filters_str);
   1679 
   1680     if (!path.isEmpty())
   1681       path = QDir::toNativeSeparators(path);
   1682 
   1683     if (from_cpu_thread)
   1684       Host::RunOnCPUThread([callback = std::move(callback), path = path.toStdString()]() { callback(path); });
   1685     else
   1686       callback(path.toStdString());
   1687   });
   1688 }
   1689 
   1690 void EmuThread::doBackgroundControllerPoll()
   1691 {
   1692   System::Internal::IdlePollUpdate();
   1693 }
   1694 
   1695 void EmuThread::createBackgroundControllerPollTimer()
   1696 {
   1697   DebugAssert(!m_background_controller_polling_timer);
   1698   m_background_controller_polling_timer = new QTimer(this);
   1699   m_background_controller_polling_timer->setSingleShot(false);
   1700   m_background_controller_polling_timer->setTimerType(Qt::CoarseTimer);
   1701   connect(m_background_controller_polling_timer, &QTimer::timeout, this, &EmuThread::doBackgroundControllerPoll);
   1702 }
   1703 
   1704 void EmuThread::destroyBackgroundControllerPollTimer()
   1705 {
   1706   delete m_background_controller_polling_timer;
   1707   m_background_controller_polling_timer = nullptr;
   1708 }
   1709 
   1710 void EmuThread::startBackgroundControllerPollTimer()
   1711 {
   1712   if (m_background_controller_polling_timer->isActive())
   1713     return;
   1714 
   1715   u32 poll_interval = BACKGROUND_CONTROLLER_POLLING_INTERVAL;
   1716   if (FullscreenUI::IsInitialized())
   1717     poll_interval = FULLSCREEN_UI_CONTROLLER_POLLING_INTERVAL;
   1718   if (GDBServer::HasAnyClients())
   1719     poll_interval = GDB_SERVER_POLLING_INTERVAL;
   1720 
   1721   m_background_controller_polling_timer->start(poll_interval);
   1722 }
   1723 
   1724 void EmuThread::stopBackgroundControllerPollTimer()
   1725 {
   1726   if (!m_background_controller_polling_timer->isActive())
   1727     return;
   1728 
   1729   m_background_controller_polling_timer->stop();
   1730 }
   1731 
   1732 void EmuThread::start()
   1733 {
   1734   AssertMsg(!g_emu_thread, "Emu thread does not exist");
   1735 
   1736   g_emu_thread = new EmuThread(QThread::currentThread());
   1737   g_emu_thread->QThread::start();
   1738   g_emu_thread->m_started_semaphore.acquire();
   1739   g_emu_thread->moveToThread(g_emu_thread);
   1740 }
   1741 
   1742 void EmuThread::stop()
   1743 {
   1744   AssertMsg(g_emu_thread, "Emu thread exists");
   1745   AssertMsg(!g_emu_thread->isOnThread(), "Not called on the emu thread");
   1746 
   1747   QMetaObject::invokeMethod(g_emu_thread, &EmuThread::stopInThread, Qt::QueuedConnection);
   1748   while (g_emu_thread->isRunning())
   1749     QApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 1);
   1750 }
   1751 
   1752 void EmuThread::stopInThread()
   1753 {
   1754   stopFullscreenUI();
   1755 
   1756   m_shutdown_flag = true;
   1757   m_event_loop->quit();
   1758 }
   1759 
   1760 void EmuThread::run()
   1761 {
   1762   m_event_loop = new QEventLoop();
   1763   m_started_semaphore.release();
   1764 
   1765   // input source setup must happen on emu thread
   1766   {
   1767     Error startup_error;
   1768     if (!System::Internal::CPUThreadInitialize(&startup_error))
   1769     {
   1770       moveToThread(m_ui_thread);
   1771       Host::ReportFatalError("Fatal Startup Error", startup_error.GetDescription());
   1772       return;
   1773     }
   1774   }
   1775 
   1776   // bind buttons/axises
   1777   createBackgroundControllerPollTimer();
   1778   startBackgroundControllerPollTimer();
   1779 
   1780   // main loop
   1781   while (!m_shutdown_flag)
   1782   {
   1783     if (System::IsRunning())
   1784     {
   1785       System::Execute();
   1786     }
   1787     else
   1788     {
   1789       // we want to keep rendering the UI when paused and fullscreen UI is enabled
   1790       if (!FullscreenUI::HasActiveWindow() && !System::IsRunning())
   1791       {
   1792         // wait until we have a system before running
   1793         m_event_loop->exec();
   1794         continue;
   1795       }
   1796 
   1797       m_event_loop->processEvents(QEventLoop::AllEvents);
   1798       System::Internal::IdlePollUpdate();
   1799       if (g_gpu_device)
   1800       {
   1801         System::PresentDisplay(false, false);
   1802         if (!g_gpu_device->IsVSyncModeBlocking())
   1803           g_gpu_device->ThrottlePresentation();
   1804       }
   1805     }
   1806   }
   1807 
   1808   if (System::IsValid())
   1809     System::ShutdownSystem(false);
   1810 
   1811   destroyBackgroundControllerPollTimer();
   1812   System::Internal::CPUThreadShutdown();
   1813 
   1814   // move back to UI thread
   1815   moveToThread(m_ui_thread);
   1816 }
   1817 
   1818 void Host::FrameDone()
   1819 {
   1820 }
   1821 
   1822 void EmuThread::wakeThread()
   1823 {
   1824   if (isOnThread())
   1825     m_event_loop->quit();
   1826   else
   1827     QMetaObject::invokeMethod(m_event_loop, "quit", Qt::QueuedConnection);
   1828 }
   1829 
   1830 void Host::ReportFatalError(std::string_view title, std::string_view message)
   1831 {
   1832   auto cb = [title = QtUtils::StringViewToQString(title), message = QtUtils::StringViewToQString(message)]() {
   1833     QMessageBox::critical(g_main_window && g_main_window->isVisible() ? g_main_window : nullptr, title, message);
   1834 #ifndef __APPLE__
   1835     std::quick_exit(EXIT_FAILURE);
   1836 #else
   1837     _exit(EXIT_FAILURE);
   1838 #endif
   1839   };
   1840 
   1841   // https://stackoverflow.com/questions/34135624/how-to-properly-execute-gui-operations-in-qt-main-thread
   1842   QTimer* timer = new QTimer();
   1843   QThread* ui_thread = qApp->thread();
   1844   if (QThread::currentThread() == ui_thread)
   1845   {
   1846     // On UI thread, we can do it straight away.
   1847     cb();
   1848   }
   1849   else
   1850   {
   1851     timer->moveToThread(ui_thread);
   1852     timer->setSingleShot(true);
   1853     QObject::connect(timer, &QTimer::timeout, std::move(cb));
   1854     QMetaObject::invokeMethod(timer, "start", Qt::QueuedConnection, Q_ARG(int, 0));
   1855   }
   1856 }
   1857 
   1858 void Host::ReportErrorAsync(std::string_view title, std::string_view message)
   1859 {
   1860   if (!title.empty() && !message.empty())
   1861     ERROR_LOG("ReportErrorAsync: {}: {}", title, message);
   1862   else if (!message.empty())
   1863     ERROR_LOG("ReportErrorAsync: {}", message);
   1864 
   1865   QMetaObject::invokeMethod(
   1866     g_main_window, "reportError", Qt::QueuedConnection,
   1867     Q_ARG(const QString&, title.empty() ? QString() : QString::fromUtf8(title.data(), title.size())),
   1868     Q_ARG(const QString&, message.empty() ? QString() : QString::fromUtf8(message.data(), message.size())));
   1869 }
   1870 
   1871 bool Host::ConfirmMessage(std::string_view title, std::string_view message)
   1872 {
   1873   auto lock = g_emu_thread->pauseAndLockSystem();
   1874 
   1875   return emit g_emu_thread->messageConfirmed(QString::fromUtf8(title.data(), title.size()),
   1876                                              QString::fromUtf8(message.data(), message.size()));
   1877 }
   1878 
   1879 void Host::OpenURL(std::string_view url)
   1880 {
   1881   QtHost::RunOnUIThread([url = QtUtils::StringViewToQString(url)]() { QtUtils::OpenURL(g_main_window, QUrl(url)); });
   1882 }
   1883 
   1884 bool Host::CopyTextToClipboard(std::string_view text)
   1885 {
   1886   QtHost::RunOnUIThread([text = QtUtils::StringViewToQString(text)]() {
   1887     QClipboard* clipboard = QGuiApplication::clipboard();
   1888     if (clipboard)
   1889       clipboard->setText(text);
   1890   });
   1891   return true;
   1892 }
   1893 
   1894 void Host::ReportDebuggerMessage(std::string_view message)
   1895 {
   1896   emit g_emu_thread->debuggerMessageReported(QString::fromUtf8(message));
   1897 }
   1898 
   1899 void Host::AddFixedInputBindings(SettingsInterface& si)
   1900 {
   1901 }
   1902 
   1903 void Host::OnInputDeviceConnected(std::string_view identifier, std::string_view device_name)
   1904 {
   1905   emit g_emu_thread->onInputDeviceConnected(std::string(identifier), std::string(device_name));
   1906 
   1907   if (System::IsValid() || g_emu_thread->isRunningFullscreenUI())
   1908   {
   1909     Host::AddIconOSDMessage(fmt::format("controller_connected_{}", identifier), ICON_FA_GAMEPAD,
   1910                             fmt::format(TRANSLATE_FS("QtHost", "Controller {} connected."), identifier),
   1911                             Host::OSD_INFO_DURATION);
   1912   }
   1913 }
   1914 
   1915 void Host::OnInputDeviceDisconnected(InputBindingKey key, std::string_view identifier)
   1916 {
   1917   emit g_emu_thread->onInputDeviceDisconnected(std::string(identifier));
   1918 
   1919   if (g_settings.pause_on_controller_disconnection && System::GetState() == System::State::Running &&
   1920       InputManager::HasAnyBindingsForSource(key))
   1921   {
   1922     std::string message =
   1923       fmt::format(TRANSLATE_FS("QtHost", "System paused because controller {} was disconnected."), identifier);
   1924     Host::RunOnCPUThread([message = QString::fromStdString(message)]() {
   1925       System::PauseSystem(true);
   1926 
   1927       // has to be done after pause, otherwise pause message takes precedence
   1928       emit g_emu_thread->statusMessage(message);
   1929     });
   1930     Host::AddIconOSDMessage(fmt::format("controller_connected_{}", identifier), ICON_FA_GAMEPAD, std::move(message),
   1931                             Host::OSD_WARNING_DURATION);
   1932   }
   1933   else if (System::IsValid() || g_emu_thread->isRunningFullscreenUI())
   1934   {
   1935     Host::AddIconOSDMessage(fmt::format("controller_connected_{}", identifier), ICON_FA_GAMEPAD,
   1936                             fmt::format(TRANSLATE_FS("QtHost", "Controller {} disconnected."), identifier),
   1937                             Host::OSD_INFO_DURATION);
   1938   }
   1939 }
   1940 
   1941 ALWAYS_INLINE std::string QtHost::GetResourcePath(std::string_view filename, bool allow_override)
   1942 {
   1943   return allow_override ? EmuFolders::GetOverridableResourcePath(filename) :
   1944                           Path::Combine(EmuFolders::Resources, filename);
   1945 }
   1946 
   1947 bool Host::ResourceFileExists(std::string_view filename, bool allow_override)
   1948 {
   1949   const std::string path = QtHost::GetResourcePath(filename, allow_override);
   1950   return FileSystem::FileExists(path.c_str());
   1951 }
   1952 
   1953 std::optional<DynamicHeapArray<u8>> Host::ReadResourceFile(std::string_view filename, bool allow_override)
   1954 {
   1955   const std::string path = QtHost::GetResourcePath(filename, allow_override);
   1956   std::optional<DynamicHeapArray<u8>> ret(FileSystem::ReadBinaryFile(path.c_str()));
   1957   if (!ret.has_value())
   1958     ERROR_LOG("Failed to read resource file '{}'", filename);
   1959   return ret;
   1960 }
   1961 
   1962 std::optional<std::string> Host::ReadResourceFileToString(std::string_view filename, bool allow_override)
   1963 {
   1964   const std::string path = QtHost::GetResourcePath(filename, allow_override);
   1965   std::optional<std::string> ret(FileSystem::ReadFileToString(path.c_str()));
   1966   if (!ret.has_value())
   1967     ERROR_LOG("Failed to read resource file to string '{}'", filename);
   1968   return ret;
   1969 }
   1970 
   1971 std::optional<std::time_t> Host::GetResourceFileTimestamp(std::string_view filename, bool allow_override)
   1972 {
   1973   const std::string path = QtHost::GetResourcePath(filename, allow_override);
   1974 
   1975   FILESYSTEM_STAT_DATA sd;
   1976   if (!FileSystem::StatFile(path.c_str(), &sd))
   1977   {
   1978     ERROR_LOG("Failed to stat resource file '{}'", filename);
   1979     return std::nullopt;
   1980   }
   1981 
   1982   return sd.ModificationTime;
   1983 }
   1984 
   1985 void Host::CommitBaseSettingChanges()
   1986 {
   1987   if (g_emu_thread->isOnThread())
   1988     QtHost::RunOnUIThread([]() { QtHost::QueueSettingsSave(); });
   1989   else
   1990     QtHost::QueueSettingsSave();
   1991 }
   1992 
   1993 std::optional<WindowInfo> Host::AcquireRenderWindow(bool recreate_window)
   1994 {
   1995   return g_emu_thread->acquireRenderWindow(recreate_window);
   1996 }
   1997 
   1998 void Host::ReleaseRenderWindow()
   1999 {
   2000   g_emu_thread->releaseRenderWindow();
   2001 }
   2002 
   2003 void EmuThread::updatePerformanceCounters()
   2004 {
   2005   const RenderAPI render_api = g_gpu_device ? g_gpu_device->GetRenderAPI() : RenderAPI::None;
   2006   const bool hardware_renderer = g_gpu && g_gpu->IsHardwareRenderer();
   2007   u32 render_width = 0;
   2008   u32 render_height = 0;
   2009 
   2010   if (g_gpu)
   2011     std::tie(render_width, render_height) = g_gpu->GetEffectiveDisplayResolution();
   2012 
   2013   if (render_api != m_last_render_api || hardware_renderer != m_last_hardware_renderer)
   2014   {
   2015     const QString renderer_str = hardware_renderer ? QString::fromUtf8(GPUDevice::RenderAPIToString(render_api)) :
   2016                                                      qApp->translate("GPURenderer", "Software");
   2017     QMetaObject::invokeMethod(g_main_window->getStatusRendererWidget(), "setText", Qt::QueuedConnection,
   2018                               Q_ARG(const QString&, renderer_str));
   2019     m_last_render_api = render_api;
   2020     m_last_hardware_renderer = hardware_renderer;
   2021   }
   2022   if (render_width != m_last_render_width || render_height != m_last_render_height)
   2023   {
   2024     QMetaObject::invokeMethod(g_main_window->getStatusResolutionWidget(), "setText", Qt::QueuedConnection,
   2025                               Q_ARG(const QString&, tr("%1x%2").arg(render_width).arg(render_height)));
   2026     m_last_render_width = render_width;
   2027     m_last_render_height = render_height;
   2028   }
   2029 
   2030   const float gfps = System::GetFPS();
   2031   if (gfps != m_last_game_fps)
   2032   {
   2033     QMetaObject::invokeMethod(g_main_window->getStatusFPSWidget(), "setText", Qt::QueuedConnection,
   2034                               Q_ARG(const QString&, tr("Game: %1 FPS").arg(gfps, 0, 'f', 0)));
   2035     m_last_game_fps = gfps;
   2036   }
   2037 
   2038   const float speed = System::GetEmulationSpeed();
   2039   const float vfps = System::GetVPS();
   2040   if (speed != m_last_speed || vfps != m_last_video_fps)
   2041   {
   2042     QMetaObject::invokeMethod(
   2043       g_main_window->getStatusVPSWidget(), "setText", Qt::QueuedConnection,
   2044       Q_ARG(const QString&, tr("Video: %1 FPS (%2%)").arg(vfps, 0, 'f', 0).arg(speed, 0, 'f', 0)));
   2045     m_last_speed = speed;
   2046     m_last_video_fps = vfps;
   2047   }
   2048 }
   2049 
   2050 void EmuThread::resetPerformanceCounters()
   2051 {
   2052   m_last_speed = std::numeric_limits<float>::infinity();
   2053   m_last_game_fps = std::numeric_limits<float>::infinity();
   2054   m_last_video_fps = std::numeric_limits<float>::infinity();
   2055   m_last_render_width = std::numeric_limits<u32>::max();
   2056   m_last_render_height = std::numeric_limits<u32>::max();
   2057   m_last_render_api = RenderAPI::None;
   2058   m_last_hardware_renderer = false;
   2059 
   2060   QString blank;
   2061   QMetaObject::invokeMethod(g_main_window->getStatusRendererWidget(), "setText", Qt::QueuedConnection,
   2062                             Q_ARG(const QString&, blank));
   2063   QMetaObject::invokeMethod(g_main_window->getStatusResolutionWidget(), "setText", Qt::QueuedConnection,
   2064                             Q_ARG(const QString&, blank));
   2065   QMetaObject::invokeMethod(g_main_window->getStatusFPSWidget(), "setText", Qt::QueuedConnection,
   2066                             Q_ARG(const QString&, blank));
   2067   QMetaObject::invokeMethod(g_main_window->getStatusVPSWidget(), "setText", Qt::QueuedConnection,
   2068                             Q_ARG(const QString&, blank));
   2069 }
   2070 
   2071 void Host::OnPerformanceCountersUpdated()
   2072 {
   2073   g_emu_thread->updatePerformanceCounters();
   2074 }
   2075 
   2076 void Host::OnGameChanged(const std::string& disc_path, const std::string& game_serial, const std::string& game_name)
   2077 {
   2078   emit g_emu_thread->runningGameChanged(QString::fromStdString(disc_path), QString::fromStdString(game_serial),
   2079                                         QString::fromStdString(game_name));
   2080 }
   2081 
   2082 void Host::OnMediaCaptureStarted()
   2083 {
   2084   emit g_emu_thread->mediaCaptureStarted();
   2085 }
   2086 
   2087 void Host::OnMediaCaptureStopped()
   2088 {
   2089   emit g_emu_thread->mediaCaptureStopped();
   2090 }
   2091 
   2092 void Host::SetMouseMode(bool relative, bool hide_cursor)
   2093 {
   2094   emit g_emu_thread->mouseModeRequested(relative, hide_cursor);
   2095 }
   2096 
   2097 void Host::PumpMessagesOnCPUThread()
   2098 {
   2099   g_emu_thread->getEventLoop()->processEvents(QEventLoop::AllEvents);
   2100 }
   2101 
   2102 void QtHost::SaveSettings()
   2103 {
   2104   AssertMsg(!g_emu_thread->isOnThread(), "Saving should happen on the UI thread.");
   2105 
   2106   {
   2107     Error error;
   2108     auto lock = Host::GetSettingsLock();
   2109     if (!s_base_settings_interface->Save(&error))
   2110       ERROR_LOG("Failed to save settings: {}", error.GetDescription());
   2111   }
   2112 
   2113   if (s_settings_save_timer)
   2114   {
   2115     s_settings_save_timer->deleteLater();
   2116     s_settings_save_timer.release();
   2117   }
   2118 }
   2119 
   2120 void QtHost::QueueSettingsSave()
   2121 {
   2122   if (g_emu_thread->isOnThread())
   2123   {
   2124     QtHost::RunOnUIThread(QueueSettingsSave);
   2125     return;
   2126   }
   2127 
   2128   if (s_settings_save_timer)
   2129     return;
   2130 
   2131   s_settings_save_timer = std::make_unique<QTimer>();
   2132   s_settings_save_timer->connect(s_settings_save_timer.get(), &QTimer::timeout, SaveSettings);
   2133   s_settings_save_timer->setSingleShot(true);
   2134   s_settings_save_timer->start(SETTINGS_SAVE_DELAY);
   2135 }
   2136 
   2137 bool QtHost::ShouldShowDebugOptions()
   2138 {
   2139   return Host::GetBaseBoolSettingValue("Main", "ShowDebugMenu", false);
   2140 }
   2141 
   2142 void Host::RequestSystemShutdown(bool allow_confirm, bool save_state)
   2143 {
   2144   if (!System::IsValid())
   2145     return;
   2146 
   2147   QMetaObject::invokeMethod(g_main_window, "requestShutdown", Qt::QueuedConnection, Q_ARG(bool, allow_confirm),
   2148                             Q_ARG(bool, true), Q_ARG(bool, save_state));
   2149 }
   2150 
   2151 void Host::RequestExitApplication(bool allow_confirm)
   2152 {
   2153   QMetaObject::invokeMethod(g_main_window, "requestExit", Qt::QueuedConnection, Q_ARG(bool, allow_confirm));
   2154 }
   2155 
   2156 void Host::RequestExitBigPicture()
   2157 {
   2158   g_emu_thread->stopFullscreenUI();
   2159 }
   2160 
   2161 std::optional<WindowInfo> Host::GetTopLevelWindowInfo()
   2162 {
   2163   std::optional<WindowInfo> ret;
   2164   QMetaObject::invokeMethod(g_main_window, &MainWindow::getWindowInfo, Qt::BlockingQueuedConnection, &ret);
   2165   return ret;
   2166 }
   2167 
   2168 EmuThread::SystemLock EmuThread::pauseAndLockSystem()
   2169 {
   2170   const bool was_fullscreen = System::IsValid() && isFullscreen();
   2171   const bool was_paused = System::IsPaused();
   2172 
   2173   // We use surfaceless rather than switching out of fullscreen, because
   2174   // we're paused, so we're not going to be rendering anyway.
   2175   if (was_fullscreen)
   2176     setSurfaceless(true);
   2177   if (!was_paused)
   2178     setSystemPaused(true);
   2179 
   2180   return SystemLock(was_paused, was_fullscreen);
   2181 }
   2182 
   2183 EmuThread::SystemLock::SystemLock(bool was_paused, bool was_fullscreen)
   2184   : m_was_paused(was_paused), m_was_fullscreen(was_fullscreen)
   2185 {
   2186 }
   2187 
   2188 EmuThread::SystemLock::SystemLock(SystemLock&& lock)
   2189   : m_was_paused(lock.m_was_paused), m_was_fullscreen(lock.m_was_fullscreen)
   2190 {
   2191   lock.m_was_paused = true;
   2192   lock.m_was_fullscreen = false;
   2193 }
   2194 
   2195 EmuThread::SystemLock::~SystemLock()
   2196 {
   2197   if (m_was_fullscreen)
   2198     g_emu_thread->setSurfaceless(false);
   2199   if (!m_was_paused)
   2200     g_emu_thread->setSystemPaused(false);
   2201 }
   2202 
   2203 void EmuThread::SystemLock::cancelResume()
   2204 {
   2205   m_was_paused = true;
   2206   m_was_fullscreen = false;
   2207 }
   2208 
   2209 BEGIN_HOTKEY_LIST(g_host_hotkeys)
   2210 END_HOTKEY_LIST()
   2211 
   2212 static void SignalHandler(int signal)
   2213 {
   2214   // First try the normal (graceful) shutdown/exit.
   2215   static bool graceful_shutdown_attempted = false;
   2216   if (!graceful_shutdown_attempted && g_main_window)
   2217   {
   2218     std::fprintf(stderr, "Received CTRL+C, attempting graceful shutdown. Press CTRL+C again to force.\n");
   2219     graceful_shutdown_attempted = true;
   2220 
   2221     // This could be a bit risky invoking from a signal handler... hopefully it's okay.
   2222     QMetaObject::invokeMethod(g_main_window, "requestExit", Qt::QueuedConnection, Q_ARG(bool, true));
   2223     return;
   2224   }
   2225 
   2226   std::signal(signal, SIG_DFL);
   2227 
   2228   // MacOS is missing std::quick_exit() despite it being C++11...
   2229 #ifndef __APPLE__
   2230   std::quick_exit(1);
   2231 #else
   2232   _Exit(1);
   2233 #endif
   2234 }
   2235 
   2236 void QtHost::HookSignals()
   2237 {
   2238   std::signal(SIGINT, SignalHandler);
   2239   std::signal(SIGTERM, SignalHandler);
   2240 
   2241 #ifdef __linux__
   2242   // Ignore SIGCHLD by default on Linux, since we kick off aplay asynchronously.
   2243   struct sigaction sa_chld = {};
   2244   sigemptyset(&sa_chld.sa_mask);
   2245   sa_chld.sa_flags = SA_SIGINFO | SA_RESTART | SA_NOCLDSTOP | SA_NOCLDWAIT;
   2246   sigaction(SIGCHLD, &sa_chld, nullptr);
   2247 #endif
   2248 }
   2249 
   2250 void QtHost::InitializeEarlyConsole()
   2251 {
   2252   const bool was_console_enabled = Log::IsConsoleOutputEnabled();
   2253   if (!was_console_enabled)
   2254     Log::SetConsoleOutputParams(true);
   2255 }
   2256 
   2257 void QtHost::PrintCommandLineVersion()
   2258 {
   2259   InitializeEarlyConsole();
   2260 
   2261   std::fprintf(stderr, "DuckStation Version %s (%s)\n", g_scm_tag_str, g_scm_branch_str);
   2262   std::fprintf(stderr, "https://github.com/stenzek/duckstation\n");
   2263   std::fprintf(stderr, "\n");
   2264 }
   2265 
   2266 void QtHost::PrintCommandLineHelp(const char* progname)
   2267 {
   2268   InitializeEarlyConsole();
   2269 
   2270   PrintCommandLineVersion();
   2271   std::fprintf(stderr, "Usage: %s [parameters] [--] [boot filename]\n", progname);
   2272   std::fprintf(stderr, "\n");
   2273   std::fprintf(stderr, "  -help: Displays this information and exits.\n");
   2274   std::fprintf(stderr, "  -version: Displays version information and exits.\n");
   2275   std::fprintf(stderr, "  -batch: Enables batch mode (exits after powering off).\n");
   2276   std::fprintf(stderr, "  -fastboot: Force fast boot for provided filename.\n");
   2277   std::fprintf(stderr, "  -slowboot: Force slow boot for provided filename.\n");
   2278   std::fprintf(stderr, "  -bios: Boot into the BIOS shell.\n");
   2279   std::fprintf(stderr, "  -resume: Load resume save state. If a boot filename is provided,\n"
   2280                        "    that game's resume state will be loaded, otherwise the most\n"
   2281                        "    recent resume save state will be loaded.\n");
   2282   std::fprintf(stderr, "  -state <index>: Loads specified save state by index. If a boot\n"
   2283                        "    filename is provided, a per-game state will be loaded, otherwise\n"
   2284                        "    a global state will be loaded.\n");
   2285   std::fprintf(stderr, "  -statefile <filename>: Loads state from the specified filename.\n"
   2286                        "    No boot filename is required with this option.\n");
   2287   std::fprintf(stderr, "  -exe <filename>: Boot the specified exe instead of loading from disc.\n");
   2288   std::fprintf(stderr, "  -fullscreen: Enters fullscreen mode immediately after starting.\n");
   2289   std::fprintf(stderr, "  -nofullscreen: Prevents fullscreen mode from triggering if enabled.\n");
   2290   std::fprintf(stderr, "  -nogui: Disables main window from being shown, exits on shutdown.\n");
   2291   std::fprintf(stderr, "  -bigpicture: Automatically starts big picture UI.\n");
   2292   std::fprintf(stderr, "  -portable: Forces \"portable mode\", data in same directory.\n");
   2293   std::fprintf(stderr, "  -settings <filename>: Loads a custom settings configuration from the\n"
   2294                        "    specified filename. Default settings applied if file not found.\n");
   2295   std::fprintf(stderr, "  -earlyconsole: Creates console as early as possible, for logging.\n");
   2296 #ifdef ENABLE_RAINTEGRATION
   2297   std::fprintf(stderr, "  -raintegration: Use RAIntegration instead of built-in achievement support.\n");
   2298 #endif
   2299   std::fprintf(stderr, "  --: Signals that no more arguments will follow and the remaining\n"
   2300                        "    parameters make up the filename. Use when the filename contains\n"
   2301                        "    spaces or starts with a dash.\n");
   2302   std::fprintf(stderr, "\n");
   2303 }
   2304 
   2305 std::shared_ptr<SystemBootParameters>& AutoBoot(std::shared_ptr<SystemBootParameters>& autoboot)
   2306 {
   2307   if (!autoboot)
   2308     autoboot = std::make_shared<SystemBootParameters>();
   2309 
   2310   return autoboot;
   2311 }
   2312 
   2313 bool QtHost::ParseCommandLineParametersAndInitializeConfig(QApplication& app,
   2314                                                            std::shared_ptr<SystemBootParameters>& autoboot)
   2315 {
   2316   const QStringList args(app.arguments());
   2317   std::optional<s32> state_index;
   2318   std::string settings_filename;
   2319   bool starting_bios = false;
   2320 
   2321   bool no_more_args = false;
   2322 
   2323   for (qsizetype i = 1; i < args.size(); i++)
   2324   {
   2325     if (!no_more_args)
   2326     {
   2327 #define CHECK_ARG(str) (args[i] == QStringLiteral(str))
   2328 #define CHECK_ARG_PARAM(str) (args[i] == QStringLiteral(str) && ((i + 1) < args.size()))
   2329 
   2330       if (CHECK_ARG("-help"))
   2331       {
   2332         PrintCommandLineHelp(args[0].toUtf8().constData());
   2333         return false;
   2334       }
   2335       else if (CHECK_ARG("-version"))
   2336       {
   2337         PrintCommandLineVersion();
   2338         return false;
   2339       }
   2340       else if (CHECK_ARG("-batch"))
   2341       {
   2342         INFO_LOG("Command Line: Using batch mode.");
   2343         s_batch_mode = true;
   2344         continue;
   2345       }
   2346       else if (CHECK_ARG("-nogui"))
   2347       {
   2348         INFO_LOG("Command Line: Using NoGUI mode.");
   2349         s_nogui_mode = true;
   2350         s_batch_mode = true;
   2351         continue;
   2352       }
   2353       else if (CHECK_ARG("-bios"))
   2354       {
   2355         INFO_LOG("Command Line: Starting BIOS.");
   2356         AutoBoot(autoboot);
   2357         starting_bios = true;
   2358         continue;
   2359       }
   2360       else if (CHECK_ARG("-fastboot"))
   2361       {
   2362         INFO_LOG("Command Line: Forcing fast boot.");
   2363         AutoBoot(autoboot)->override_fast_boot = true;
   2364         continue;
   2365       }
   2366       else if (CHECK_ARG("-slowboot"))
   2367       {
   2368         INFO_LOG("Command Line: Forcing slow boot.");
   2369         AutoBoot(autoboot)->override_fast_boot = false;
   2370         continue;
   2371       }
   2372       else if (CHECK_ARG("-resume"))
   2373       {
   2374         state_index = -1;
   2375         INFO_LOG("Command Line: Loading resume state.");
   2376         continue;
   2377       }
   2378       else if (CHECK_ARG_PARAM("-state"))
   2379       {
   2380         state_index = args[++i].toInt();
   2381         INFO_LOG("Command Line: Loading state index: {}", state_index.value());
   2382         continue;
   2383       }
   2384       else if (CHECK_ARG_PARAM("-statefile"))
   2385       {
   2386         AutoBoot(autoboot)->save_state = args[++i].toStdString();
   2387         INFO_LOG("Command Line: Loading state file: '{}'", autoboot->save_state);
   2388         continue;
   2389       }
   2390       else if (CHECK_ARG_PARAM("-exe"))
   2391       {
   2392         AutoBoot(autoboot)->override_exe = args[++i].toStdString();
   2393         INFO_LOG("Command Line: Overriding EXE file: '{}'", autoboot->override_exe);
   2394         continue;
   2395       }
   2396       else if (CHECK_ARG("-fullscreen"))
   2397       {
   2398         INFO_LOG("Command Line: Using fullscreen.");
   2399         AutoBoot(autoboot)->override_fullscreen = true;
   2400         s_start_fullscreen_ui_fullscreen = true;
   2401         continue;
   2402       }
   2403       else if (CHECK_ARG("-nofullscreen"))
   2404       {
   2405         INFO_LOG("Command Line: Not using fullscreen.");
   2406         AutoBoot(autoboot)->override_fullscreen = false;
   2407         continue;
   2408       }
   2409       else if (CHECK_ARG("-portable"))
   2410       {
   2411         INFO_LOG("Command Line: Using portable mode.");
   2412         EmuFolders::DataRoot = EmuFolders::AppRoot;
   2413         continue;
   2414       }
   2415       else if (CHECK_ARG_PARAM("-settings"))
   2416       {
   2417         settings_filename = args[++i].toStdString();
   2418         INFO_LOG("Command Line: Overriding settings filename: {}", settings_filename);
   2419         continue;
   2420       }
   2421       else if (CHECK_ARG("-bigpicture"))
   2422       {
   2423         INFO_LOG("Command Line: Starting big picture mode.");
   2424         s_start_fullscreen_ui = true;
   2425         continue;
   2426       }
   2427       else if (CHECK_ARG("-setupwizard"))
   2428       {
   2429         s_run_setup_wizard = true;
   2430         continue;
   2431       }
   2432       else if (CHECK_ARG("-earlyconsole"))
   2433       {
   2434         InitializeEarlyConsole();
   2435         continue;
   2436       }
   2437       else if (CHECK_ARG("-updatecleanup"))
   2438       {
   2439         s_cleanup_after_update = AutoUpdaterDialog::isSupported();
   2440         continue;
   2441       }
   2442 #ifdef ENABLE_RAINTEGRATION
   2443       else if (CHECK_ARG("-raintegration"))
   2444       {
   2445         Achievements::SwitchToRAIntegration();
   2446         continue;
   2447       }
   2448 #endif
   2449       else if (CHECK_ARG("--"))
   2450       {
   2451         no_more_args = true;
   2452         continue;
   2453       }
   2454       else if (args[i][0] == QChar('-'))
   2455       {
   2456         QMessageBox::critical(nullptr, QStringLiteral("Error"), QStringLiteral("Unknown parameter: %1").arg(args[i]));
   2457         return false;
   2458       }
   2459 
   2460 #undef CHECK_ARG
   2461 #undef CHECK_ARG_PARAM
   2462     }
   2463 
   2464     if (autoboot && !autoboot->filename.empty())
   2465       autoboot->filename += ' ';
   2466     AutoBoot(autoboot)->filename += args[i].toStdString();
   2467   }
   2468 
   2469   // To do anything useful, we need the config initialized.
   2470   if (!InitializeConfig(std::move(settings_filename)))
   2471   {
   2472     // NOTE: No point translating this, because no config means the language won't be loaded anyway.
   2473     QMessageBox::critical(nullptr, QStringLiteral("Error"), QStringLiteral("Failed to initialize config."));
   2474     return false;
   2475   }
   2476 
   2477   // Check the file we're starting actually exists.
   2478 
   2479   if (autoboot && !autoboot->filename.empty() && !FileSystem::FileExists(autoboot->filename.c_str()))
   2480   {
   2481     QMessageBox::critical(
   2482       nullptr, qApp->translate("QtHost", "Error"),
   2483       qApp->translate("QtHost", "File '%1' does not exist.").arg(QString::fromStdString(autoboot->filename)));
   2484     return false;
   2485   }
   2486 
   2487   if (state_index.has_value())
   2488   {
   2489     AutoBoot(autoboot);
   2490 
   2491     if (autoboot->filename.empty())
   2492     {
   2493       // loading global state, -1 means resume the last game
   2494       if (state_index.value() < 0)
   2495         autoboot->save_state = System::GetMostRecentResumeSaveStatePath();
   2496       else
   2497         autoboot->save_state = System::GetGlobalSaveStateFileName(state_index.value());
   2498     }
   2499     else
   2500     {
   2501       // loading game state
   2502       const std::string game_serial(GameDatabase::GetSerialForPath(autoboot->filename.c_str()));
   2503       autoboot->save_state = System::GetGameSaveStateFileName(game_serial, state_index.value());
   2504     }
   2505 
   2506     if (autoboot->save_state.empty() || !FileSystem::FileExists(autoboot->save_state.c_str()))
   2507     {
   2508       QMessageBox::critical(nullptr, qApp->translate("QtHost", "Error"),
   2509                             qApp->translate("QtHost", "The specified save state does not exist."));
   2510       return false;
   2511     }
   2512   }
   2513 
   2514   // check autoboot parameters, if we set something like fullscreen without a bios
   2515   // or disc, we don't want to actually start.
   2516   if (autoboot && autoboot->filename.empty() && autoboot->save_state.empty() && !starting_bios)
   2517     autoboot.reset();
   2518 
   2519   // if we don't have autoboot, we definitely don't want batch mode (because that'll skip
   2520   // scanning the game list).
   2521   if (s_batch_mode && !autoboot && !s_start_fullscreen_ui)
   2522   {
   2523     QMessageBox::critical(
   2524       nullptr, qApp->translate("QtHost", "Error"),
   2525       s_nogui_mode ? qApp->translate("QtHost", "Cannot use no-gui mode, because no boot filename was specified.") :
   2526                      qApp->translate("QtHost", "Cannot use batch mode, because no boot filename was specified."));
   2527     return false;
   2528   }
   2529 
   2530   return true;
   2531 }
   2532 
   2533 bool QtHost::RunSetupWizard()
   2534 {
   2535   SetupWizardDialog dialog;
   2536   if (dialog.exec() == QDialog::Rejected)
   2537     return false;
   2538 
   2539   // Remove the flag.
   2540   Host::SetBaseBoolSettingValue("Main", "SetupWizardIncomplete", false);
   2541   Host::CommitBaseSettingChanges();
   2542   return true;
   2543 }
   2544 
   2545 int main(int argc, char* argv[])
   2546 {
   2547   CrashHandler::Install(&Bus::CleanupMemoryMap);
   2548 
   2549   QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
   2550   QtHost::RegisterTypes();
   2551 
   2552   QApplication app(argc, argv);
   2553 
   2554   if (!QtHost::PerformEarlyHardwareChecks())
   2555     return EXIT_FAILURE;
   2556 
   2557   std::shared_ptr<SystemBootParameters> autoboot;
   2558   if (!QtHost::ParseCommandLineParametersAndInitializeConfig(app, autoboot))
   2559     return EXIT_FAILURE;
   2560 
   2561   if (!AutoUpdaterDialog::warnAboutUnofficialBuild())
   2562     return EXIT_FAILURE;
   2563 
   2564   if (!QtHost::EarlyProcessStartup())
   2565     return EXIT_FAILURE;
   2566 
   2567   // Remove any previous-version remanants.
   2568   if (s_cleanup_after_update)
   2569     AutoUpdaterDialog::cleanupAfterUpdate();
   2570 
   2571   // Set theme before creating any windows.
   2572   QtHost::UpdateApplicationTheme();
   2573 
   2574   // Start logging early.
   2575   LogWindow::updateSettings();
   2576 
   2577   // Start up the CPU thread.
   2578   QtHost::HookSignals();
   2579   EmuThread::start();
   2580 
   2581   // Optionally run setup wizard.
   2582   MainWindow* main_window;
   2583   int result;
   2584   if (s_run_setup_wizard && !QtHost::RunSetupWizard())
   2585   {
   2586     result = EXIT_FAILURE;
   2587     goto shutdown_and_exit;
   2588   }
   2589 
   2590   // Create all window objects, the emuthread might still be starting up at this point.
   2591   main_window = new MainWindow();
   2592 
   2593   // When running in batch mode, ensure game list is loaded, but don't scan for any new files.
   2594   if (!s_batch_mode)
   2595     main_window->refreshGameList(false);
   2596   else
   2597     GameList::Refresh(false, true);
   2598 
   2599   // Don't bother showing the window in no-gui mode.
   2600   if (!s_nogui_mode)
   2601     main_window->show();
   2602 
   2603   // Initialize big picture mode if requested.
   2604   if (s_start_fullscreen_ui)
   2605     g_emu_thread->startFullscreenUI();
   2606   else
   2607     s_start_fullscreen_ui_fullscreen = false;
   2608 
   2609   // Skip the update check if we're booting a game directly.
   2610   if (autoboot)
   2611     g_emu_thread->bootSystem(std::move(autoboot));
   2612   else if (!s_nogui_mode)
   2613     main_window->startupUpdateCheck();
   2614 
   2615   // This doesn't return until we exit.
   2616   result = app.exec();
   2617 
   2618 shutdown_and_exit:
   2619   // Shutting down.
   2620   EmuThread::stop();
   2621 
   2622   // Close main window.
   2623   if (g_main_window)
   2624   {
   2625     g_main_window->close();
   2626     delete g_main_window;
   2627     Assert(!g_main_window);
   2628   }
   2629 
   2630   // Ensure settings are saved.
   2631   if (s_settings_save_timer)
   2632   {
   2633     s_settings_save_timer.reset();
   2634     QtHost::SaveSettings();
   2635   }
   2636 
   2637   // Ensure log is flushed.
   2638   Log::SetFileOutputParams(false, nullptr);
   2639 
   2640   System::Internal::ProcessShutdown();
   2641 
   2642   return result;
   2643 }