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 }