/* PCSX2 - PS2 Emulator for PCs * Copyright (C) 2002-2022 PCSX2 Dev Team * * PCSX2 is free software: you can redistribute it and/or modify it under the terms * of the GNU Lesser General Public License as published by the Free Software Found- * ation, either version 3 of the License, or (at your option) any later version. * * PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with PCSX2. * If not, see . */ #include "PrecompiledHeader.h" #include #include #include "common/Assertions.h" #include "common/Console.h" #include "common/CrashHandler.h" #include "common/Exceptions.h" #include "common/FileSystem.h" #include "common/Path.h" #include "common/SettingsWrapper.h" #include "common/StringUtil.h" #include "common/Timer.h" #include "pcsx2/CDVD/CDVD.h" #include "pcsx2/CDVD/CDVDcommon.h" #include "pcsx2/Counters.h" #include "pcsx2/DebugTools/Debug.h" #include "pcsx2/Frontend/CommonHost.h" #include "pcsx2/Frontend/FullscreenUI.h" #include "pcsx2/Frontend/GameList.h" #include "pcsx2/Frontend/InputManager.h" #include "pcsx2/Frontend/ImGuiManager.h" #include "pcsx2/Frontend/INISettingsInterface.h" #include "pcsx2/Frontend/LogSink.h" #include "pcsx2/GS.h" #include "pcsx2/GS/GS.h" #include "pcsx2/GSDumpReplayer.h" #include "pcsx2/HostDisplay.h" #include "pcsx2/HostSettings.h" #include "pcsx2/PAD/Host/PAD.h" #include "pcsx2/PerformanceMetrics.h" #include "pcsx2/VMManager.h" #include #include #include #include #include "DisplayWidget.h" #include "GameList/GameListWidget.h" #include "MainWindow.h" #include "QtHost.h" #include "QtUtils.h" #include "svnrev.h" static constexpr u32 SETTINGS_SAVE_DELAY = 1000; EmuThread* g_emu_thread = nullptr; ////////////////////////////////////////////////////////////////////////// // Local function declarations ////////////////////////////////////////////////////////////////////////// namespace QtHost { static void PrintCommandLineVersion(); static void PrintCommandLineHelp(const char* progname); static std::shared_ptr& AutoBoot(std::shared_ptr& autoboot); static bool ParseCommandLineOptions(int argc, char* argv[], std::shared_ptr& autoboot); static bool InitializeConfig(); static void SaveSettings(); static void HookSignals(); } ////////////////////////////////////////////////////////////////////////// // Local variable declarations ////////////////////////////////////////////////////////////////////////// const IConsoleWriter* PatchesCon = &Console; static std::unique_ptr s_settings_save_timer; static std::unique_ptr s_base_settings_interface; static std::unique_ptr s_host_display; static bool s_batch_mode = false; static bool s_nogui_mode = false; static bool s_start_fullscreen_ui = false; static bool s_start_fullscreen_ui_fullscreen = false; ////////////////////////////////////////////////////////////////////////// // CPU Thread ////////////////////////////////////////////////////////////////////////// EmuThread::EmuThread(QThread* ui_thread) : QThread() , m_ui_thread(ui_thread) { } EmuThread::~EmuThread() = default; bool EmuThread::isOnEmuThread() const { return QThread::currentThread() == this; } void EmuThread::start() { pxAssertRel(!g_emu_thread, "Emu thread does not exist"); g_emu_thread = new EmuThread(QThread::currentThread()); g_emu_thread->QThread::start(); g_emu_thread->m_started_semaphore.acquire(); g_emu_thread->moveToThread(g_emu_thread); } void EmuThread::stop() { pxAssertRel(g_emu_thread, "Emu thread exists"); pxAssertRel(!g_emu_thread->isOnEmuThread(), "Not called on the emu thread"); QMetaObject::invokeMethod(g_emu_thread, &EmuThread::stopInThread, Qt::QueuedConnection); while (g_emu_thread->isRunning()) QApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 1); } void EmuThread::stopInThread() { if (VMManager::HasValidVM()) destroyVM(); if (m_run_fullscreen_ui) stopFullscreenUI(); m_event_loop->quit(); m_shutdown_flag.store(true); } bool EmuThread::confirmMessage(const QString& title, const QString& message) { if (!isOnEmuThread()) { // This is definitely deadlock risky, but unlikely to happen (why would GS be confirming?). bool result = false; QMetaObject::invokeMethod(g_emu_thread, "confirmMessage", Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, result), Q_ARG(const QString&, title), Q_ARG(const QString&, message)); return result; } // Easy if there's no VM. if (!VMManager::HasValidVM()) return emit messageConfirmed(title, message); // Preemptively pause/set surfaceless on the emu thread, because it can't run while the popup is open. const bool was_paused = (VMManager::GetState() == VMState::Paused); const bool was_fullscreen = isFullscreen(); if (!was_paused) VMManager::SetPaused(true); if (was_fullscreen) setSurfaceless(true); // This won't return until the user confirms one way or another. const bool result = emit messageConfirmed(title, message); // Resume VM after confirming. if (was_fullscreen) setSurfaceless(false); if (!was_paused) VMManager::SetPaused(false); return result; } void EmuThread::startFullscreenUI(bool fullscreen) { if (!isOnEmuThread()) { QMetaObject::invokeMethod(this, "startFullscreenUI", Qt::QueuedConnection, Q_ARG(bool, fullscreen)); return; } if (VMManager::HasValidVM()) return; m_run_fullscreen_ui = true; if (fullscreen) m_is_fullscreen = true; if (!GetMTGS().WaitForOpen()) { m_run_fullscreen_ui = false; return; } // poll more frequently so we don't lose events stopBackgroundControllerPollTimer(); startBackgroundControllerPollTimer(); } void EmuThread::stopFullscreenUI() { if (!isOnEmuThread()) { QMetaObject::invokeMethod(this, &EmuThread::stopFullscreenUI, Qt::QueuedConnection); // wait until the host display is gone while (s_host_display) QApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 1); return; } if (!s_host_display) return; pxAssertRel(!VMManager::HasValidVM(), "VM is not valid at FSUI shutdown time"); m_run_fullscreen_ui = false; GetMTGS().WaitForClose(); } void EmuThread::startVM(std::shared_ptr boot_params) { if (!isOnEmuThread()) { QMetaObject::invokeMethod(this, "startVM", Qt::QueuedConnection, Q_ARG(std::shared_ptr, boot_params)); return; } pxAssertRel(!VMManager::HasValidVM(), "VM is shut down"); // Only initialize fullscreen/render-to-main when we're not running big picture. if (!m_run_fullscreen_ui) loadOurInitialSettings(); if (boot_params->fullscreen.has_value()) m_is_fullscreen = boot_params->fullscreen.value(); emit onVMStarting(); if (!VMManager::Initialize(*boot_params)) return; if (!Host::GetBoolSettingValue("UI", "StartPaused", false)) { // This will come back and call OnVMResumed(). VMManager::SetState(VMState::Running); } else { // When starting paused, redraw the window, so there's at least something there. redrawDisplayWindow(); Host::OnVMPaused(); } m_event_loop->quit(); } void EmuThread::resetVM() { if (!isOnEmuThread()) { QMetaObject::invokeMethod(this, &EmuThread::resetVM, Qt::QueuedConnection); return; } VMManager::Reset(); } void EmuThread::setVMPaused(bool paused) { if (!isOnEmuThread()) { QMetaObject::invokeMethod(this, "setVMPaused", Qt::QueuedConnection, Q_ARG(bool, paused)); return; } VMManager::SetPaused(paused); } void EmuThread::shutdownVM(bool save_state /* = true */) { if (!isOnEmuThread()) { QMetaObject::invokeMethod(this, "shutdownVM", Qt::QueuedConnection, Q_ARG(bool, save_state)); return; } const VMState state = VMManager::GetState(); if (state == VMState::Paused) m_event_loop->quit(); else if (state != VMState::Running) return; m_save_state_on_shutdown = save_state; VMManager::SetState(VMState::Stopping); } void EmuThread::loadState(const QString& filename) { if (!isOnEmuThread()) { QMetaObject::invokeMethod(this, "loadState", Qt::QueuedConnection, Q_ARG(const QString&, filename)); return; } if (!VMManager::HasValidVM()) return; VMManager::LoadState(filename.toUtf8().constData()); } void EmuThread::loadStateFromSlot(qint32 slot) { if (!isOnEmuThread()) { QMetaObject::invokeMethod(this, "loadStateFromSlot", Qt::QueuedConnection, Q_ARG(qint32, slot)); return; } if (!VMManager::HasValidVM()) return; VMManager::LoadStateFromSlot(slot); } void EmuThread::saveState(const QString& filename) { if (!isOnEmuThread()) { QMetaObject::invokeMethod(this, "saveState", Qt::QueuedConnection, Q_ARG(const QString&, filename)); return; } if (!VMManager::HasValidVM()) return; if (!VMManager::SaveState(filename.toUtf8().constData())) { // this one is usually the result of a user-chosen path, so we can display a message box safely here Console.Error("Failed to save state"); } } void EmuThread::saveStateToSlot(qint32 slot) { if (!isOnEmuThread()) { QMetaObject::invokeMethod(this, "saveStateToSlot", Qt::QueuedConnection, Q_ARG(qint32, slot)); return; } if (!VMManager::HasValidVM()) return; VMManager::SaveStateToSlot(slot); } void EmuThread::run() { Threading::SetNameOfCurrentThread("EmuThread"); PerformanceMetrics::SetCPUThread(Threading::ThreadHandle::GetForCallingThread()); m_event_loop = new QEventLoop(); m_started_semaphore.release(); // neither of these should ever fail. if (!VMManager::Internal::InitializeGlobals() || !VMManager::Internal::InitializeMemory()) pxFailRel("Failed to allocate memory map"); // We want settings loaded so we choose the correct renderer for big picture mode. // This also sorts out input sources. loadOurSettings(); loadOurInitialSettings(); VMManager::LoadSettings(); // Start background polling because the VM won't do it for us. createBackgroundControllerPollTimer(); startBackgroundControllerPollTimer(); connectSignals(); while (!m_shutdown_flag.load()) { if (!VMManager::HasValidVM()) { m_event_loop->exec(); continue; } executeVM(); } stopBackgroundControllerPollTimer(); destroyBackgroundControllerPollTimer(); InputManager::CloseSources(); VMManager::WaitForSaveStateFlush(); VMManager::Internal::ReleaseMemory(); VMManager::Internal::ReleaseGlobals(); PerformanceMetrics::SetCPUThread(Threading::ThreadHandle()); moveToThread(m_ui_thread); deleteLater(); } void EmuThread::destroyVM() { m_last_speed = 0.0f; m_last_game_fps = 0.0f; m_last_video_fps = 0.0f; m_last_internal_width = 0; m_last_internal_height = 0; m_was_paused_by_focus_loss = false; VMManager::Shutdown(m_save_state_on_shutdown); } void EmuThread::executeVM() { for (;;) { switch (VMManager::GetState()) { case VMState::Initializing: pxFailRel("Shouldn't be in the starting state state"); continue; case VMState::Paused: m_event_loop->exec(); continue; case VMState::Running: m_event_loop->processEvents(QEventLoop::AllEvents); VMManager::Execute(); continue; case VMState::Stopping: destroyVM(); m_event_loop->processEvents(QEventLoop::AllEvents); return; default: continue; } } } void EmuThread::createBackgroundControllerPollTimer() { pxAssert(!m_background_controller_polling_timer); m_background_controller_polling_timer = new QTimer(this); m_background_controller_polling_timer->setSingleShot(false); m_background_controller_polling_timer->setTimerType(Qt::CoarseTimer); connect(m_background_controller_polling_timer, &QTimer::timeout, this, &EmuThread::doBackgroundControllerPoll); } void EmuThread::destroyBackgroundControllerPollTimer() { delete m_background_controller_polling_timer; m_background_controller_polling_timer = nullptr; } void EmuThread::startBackgroundControllerPollTimer() { if (m_background_controller_polling_timer->isActive()) return; m_background_controller_polling_timer->start(FullscreenUI::IsInitialized() ? FULLSCREEN_UI_CONTROLLER_POLLING_INTERVAL : BACKGROUND_CONTROLLER_POLLING_INTERVAL); } void EmuThread::stopBackgroundControllerPollTimer() { if (!m_background_controller_polling_timer->isActive()) return; m_background_controller_polling_timer->stop(); } void EmuThread::doBackgroundControllerPoll() { InputManager::PollSources(); } void EmuThread::toggleFullscreen() { if (!isOnEmuThread()) { QMetaObject::invokeMethod(this, &EmuThread::toggleFullscreen, Qt::QueuedConnection); return; } setFullscreen(!m_is_fullscreen); } void EmuThread::setFullscreen(bool fullscreen) { if (!isOnEmuThread()) { QMetaObject::invokeMethod(this, "setFullscreen", Qt::QueuedConnection, Q_ARG(bool, fullscreen)); return; } if (!GetMTGS().IsOpen() || m_is_fullscreen == fullscreen) return; // This will call back to us on the MTGS thread. m_is_fullscreen = fullscreen; GetMTGS().UpdateDisplayWindow(); GetMTGS().WaitGS(); // If we're using exclusive fullscreen, the refresh rate may have changed. UpdateVSyncRate(); } void EmuThread::setSurfaceless(bool surfaceless) { if (!isOnEmuThread()) { QMetaObject::invokeMethod(this, "setSurfaceless", Qt::QueuedConnection, Q_ARG(bool, surfaceless)); return; } if (!GetMTGS().IsOpen() || m_is_surfaceless == surfaceless) return; // If we went surfaceless and were running the fullscreen UI, stop MTGS running idle. // Otherwise, we'll keep trying to present to nothing. GetMTGS().SetRunIdle(!surfaceless && m_run_fullscreen_ui); // This will call back to us on the MTGS thread. m_is_surfaceless = surfaceless; GetMTGS().UpdateDisplayWindow(); GetMTGS().WaitGS(); } void EmuThread::applySettings() { if (!isOnEmuThread()) { QMetaObject::invokeMethod(this, &EmuThread::applySettings, Qt::QueuedConnection); return; } checkForSettingChanges(); VMManager::ApplySettings(); } void EmuThread::reloadGameSettings() { if (!isOnEmuThread()) { QMetaObject::invokeMethod(this, &EmuThread::reloadGameSettings, Qt::QueuedConnection); return; } // this will skip applying settings when they're not active if (VMManager::ReloadGameSettings()) { // none of these settings below are per-game.. for now. but in case they are in the future. checkForSettingChanges(); } } void EmuThread::updateEmuFolders() { if (!isOnEmuThread()) { QMetaObject::invokeMethod(this, &EmuThread::updateEmuFolders, Qt::QueuedConnection); return; } Host::Internal::UpdateEmuFolders(); } void EmuThread::loadOurSettings() { m_verbose_status = Host::GetBaseBoolSettingValue("UI", "VerboseStatusBar", false); m_pause_on_focus_loss = Host::GetBaseBoolSettingValue("UI", "PauseOnFocusLoss", false); } void EmuThread::connectSignals() { connect(qApp, &QGuiApplication::applicationStateChanged, this, &EmuThread::onApplicationStateChanged); } void EmuThread::loadOurInitialSettings() { m_is_fullscreen = Host::GetBaseBoolSettingValue("UI", "StartFullscreen", false); m_is_rendering_to_main = shouldRenderToMain(); m_is_surfaceless = false; m_save_state_on_shutdown = false; } void EmuThread::checkForSettingChanges() { QMetaObject::invokeMethod(g_main_window, &MainWindow::checkForSettingChanges, Qt::QueuedConnection); if (s_host_display) { const bool render_to_main = shouldRenderToMain(); if (!m_is_fullscreen && m_is_rendering_to_main != render_to_main) { m_is_rendering_to_main = render_to_main; GetMTGS().UpdateDisplayWindow(); GetMTGS().WaitGS(); } } const bool last_verbose_status = m_verbose_status; loadOurSettings(); if (m_verbose_status != last_verbose_status) updatePerformanceMetrics(true); } bool EmuThread::shouldRenderToMain() const { return !Host::GetBaseBoolSettingValue("UI", "RenderToSeparateWindow", false) && !QtHost::InNoGUIMode(); } void EmuThread::toggleSoftwareRendering() { if (!isOnEmuThread()) { QMetaObject::invokeMethod(this, &EmuThread::toggleSoftwareRendering, Qt::QueuedConnection); return; } if (!VMManager::HasValidVM()) return; GetMTGS().ToggleSoftwareRendering(); } void EmuThread::switchRenderer(GSRendererType renderer) { if (!isOnEmuThread()) { QMetaObject::invokeMethod(this, "switchRenderer", Qt::QueuedConnection, Q_ARG(GSRendererType, renderer)); return; } if (!VMManager::HasValidVM()) return; GetMTGS().SwitchRenderer(renderer); } void EmuThread::changeDisc(CDVD_SourceType source, const QString& path) { if (!isOnEmuThread()) { QMetaObject::invokeMethod(this, "changeDisc", Qt::QueuedConnection, Q_ARG(CDVD_SourceType, source), Q_ARG(const QString&, path)); return; } if (!VMManager::HasValidVM()) return; VMManager::ChangeDisc(source, path.toStdString()); } void EmuThread::reloadPatches() { if (!isOnEmuThread()) { QMetaObject::invokeMethod(this, &EmuThread::reloadPatches, Qt::QueuedConnection); return; } if (!VMManager::HasValidVM()) return; VMManager::ReloadPatches(true, true); } void EmuThread::reloadInputSources() { if (!isOnEmuThread()) { QMetaObject::invokeMethod(this, &EmuThread::reloadInputSources, Qt::QueuedConnection); return; } std::unique_lock lock = Host::GetSettingsLock(); SettingsInterface* si = Host::GetSettingsInterface(); SettingsInterface* bindings_si = Host::GetSettingsInterfaceForBindings(); InputManager::ReloadSources(*si, lock); // skip loading bindings if we're not running, since it'll get done on startup anyway if (VMManager::HasValidVM()) InputManager::ReloadBindings(*si, *bindings_si); } void EmuThread::reloadInputBindings() { if (!isOnEmuThread()) { QMetaObject::invokeMethod(this, &EmuThread::reloadInputBindings, Qt::QueuedConnection); return; } // skip loading bindings if we're not running, since it'll get done on startup anyway if (!VMManager::HasValidVM()) return; auto lock = Host::GetSettingsLock(); SettingsInterface* si = Host::GetSettingsInterface(); SettingsInterface* bindings_si = Host::GetSettingsInterfaceForBindings(); InputManager::ReloadBindings(*si, *bindings_si); } void EmuThread::requestDisplaySize(float scale) { if (!isOnEmuThread()) { QMetaObject::invokeMethod(this, "requestDisplaySize", Qt::QueuedConnection, Q_ARG(float, scale)); return; } if (!VMManager::HasValidVM()) return; VMManager::RequestDisplaySize(scale); } void EmuThread::enumerateInputDevices() { if (!isOnEmuThread()) { QMetaObject::invokeMethod(this, &EmuThread::enumerateInputDevices, Qt::QueuedConnection); return; } const std::vector> devs(InputManager::EnumerateDevices()); QList> qdevs; qdevs.reserve(devs.size()); for (const std::pair& dev : devs) qdevs.emplace_back(QString::fromStdString(dev.first), QString::fromStdString(dev.second)); onInputDevicesEnumerated(qdevs); } void EmuThread::enumerateVibrationMotors() { if (!isOnEmuThread()) { QMetaObject::invokeMethod(this, &EmuThread::enumerateVibrationMotors, Qt::QueuedConnection); return; } const std::vector motors(InputManager::EnumerateMotors()); QList qmotors; qmotors.reserve(motors.size()); for (InputBindingKey key : motors) qmotors.push_back(key); onVibrationMotorsEnumerated(qmotors); } void EmuThread::connectDisplaySignals(DisplayWidget* widget) { widget->disconnect(this); connect(widget, &DisplayWidget::windowResizedEvent, this, &EmuThread::onDisplayWindowResized); connect(widget, &DisplayWidget::windowRestoredEvent, this, &EmuThread::redrawDisplayWindow); } void EmuThread::onDisplayWindowResized(int width, int height, float scale) { if (!s_host_display) return; GetMTGS().ResizeDisplayWindow(width, height, scale); } void EmuThread::onApplicationStateChanged(Qt::ApplicationState state) { // NOTE: This is executed on the emu thread, not UI thread. if (!m_pause_on_focus_loss || !VMManager::HasValidVM()) return; const bool focus_loss = (state != Qt::ApplicationActive); if (focus_loss) { if (!m_was_paused_by_focus_loss && VMManager::GetState() == VMState::Running) { m_was_paused_by_focus_loss = true; VMManager::SetPaused(true); } } else { if (m_was_paused_by_focus_loss) { m_was_paused_by_focus_loss = false; if (VMManager::GetState() == VMState::Paused) VMManager::SetPaused(false); } } } void EmuThread::redrawDisplayWindow() { if (!isOnEmuThread()) { QMetaObject::invokeMethod(this, &EmuThread::redrawDisplayWindow, Qt::QueuedConnection); return; } // If we're running, we're going to re-present anyway. if (!VMManager::HasValidVM() || VMManager::GetState() == VMState::Running) return; GetMTGS().RunOnGSThread([]() { GetMTGS().PresentCurrentFrame(); }); } void EmuThread::runOnCPUThread(const std::function& func) { func(); } void EmuThread::queueSnapshot(quint32 gsdump_frames) { if (!isOnEmuThread()) { QMetaObject::invokeMethod(this, "queueSnapshot", Qt::QueuedConnection, Q_ARG(quint32, gsdump_frames)); return; } if (!VMManager::HasValidVM()) return; GetMTGS().RunOnGSThread([gsdump_frames]() { GSQueueSnapshot(std::string(), gsdump_frames); }); } void EmuThread::updateDisplay() { pxAssertRel(!isOnEmuThread(), "Not on emu thread"); // finished with the display for now HostDisplay* display = Host::GetHostDisplay(); display->DoneRenderContextCurrent(); // but we should get it back after this call onUpdateDisplayRequested(m_is_fullscreen, !m_is_fullscreen && m_is_rendering_to_main, m_is_surfaceless); if (!display->MakeRenderContextCurrent()) { pxFailRel("Failed to recreate context after updating"); return; } } HostDisplay* EmuThread::acquireHostDisplay(HostDisplay::RenderAPI api) { s_host_display = HostDisplay::CreateDisplayForAPI(api); if (!s_host_display) return nullptr; DisplayWidget* widget = emit onCreateDisplayRequested(m_is_fullscreen, m_is_rendering_to_main); if (!widget) { s_host_display.reset(); return nullptr; } connectDisplaySignals(widget); if (!s_host_display->MakeRenderContextCurrent()) { Console.Error("Failed to make render context current"); releaseHostDisplay(); return nullptr; } if (!s_host_display->InitializeRenderDevice(EmuFolders::Cache, false) || !ImGuiManager::Initialize()) { Console.Error("Failed to initialize device/imgui"); releaseHostDisplay(); return nullptr; } Console.WriteLn(Color_StrongGreen, "%s Graphics Driver Info:", HostDisplay::RenderAPIToString(s_host_display->GetRenderAPI())); Console.Indent().WriteLn(s_host_display->GetDriverInfo()); if (m_run_fullscreen_ui && !FullscreenUI::Initialize()) { Console.Error("Failed to initialize fullscreen UI"); releaseHostDisplay(); m_run_fullscreen_ui = false; return nullptr; } return s_host_display.get(); } void EmuThread::releaseHostDisplay() { ImGuiManager::Shutdown(); s_host_display.reset(); emit onDestroyDisplayRequested(); } HostDisplay* Host::GetHostDisplay() { return s_host_display.get(); } HostDisplay* Host::AcquireHostDisplay(HostDisplay::RenderAPI api) { return g_emu_thread->acquireHostDisplay(api); } void Host::ReleaseHostDisplay() { g_emu_thread->releaseHostDisplay(); } bool Host::BeginPresentFrame(bool frame_skip) { if (!s_host_display->BeginPresent(frame_skip)) { // if we're skipping a frame, we need to reset imgui's state, since // we won't be calling EndPresentFrame(). ImGuiManager::NewFrame(); return false; } return true; } void Host::EndPresentFrame() { if (GSDumpReplayer::IsReplayingDump()) GSDumpReplayer::RenderUI(); FullscreenUI::Render(); ImGuiManager::RenderOSD(); s_host_display->EndPresent(); ImGuiManager::NewFrame(); } void Host::ResizeHostDisplay(u32 new_window_width, u32 new_window_height, float new_window_scale) { s_host_display->ResizeRenderWindow(new_window_width, new_window_height, new_window_scale); ImGuiManager::WindowResized(); // if we're paused, re-present the current frame at the new window size. if (VMManager::GetState() == VMState::Paused) GetMTGS().PresentCurrentFrame(); } void Host::RequestResizeHostDisplay(s32 width, s32 height) { g_emu_thread->onResizeDisplayRequested(width, height); } void Host::UpdateHostDisplay() { g_emu_thread->updateDisplay(); ImGuiManager::WindowResized(); // if we're paused, re-present the current frame at the new window size. if (VMManager::GetState() == VMState::Paused) GetMTGS().PresentCurrentFrame(); } void Host::OnVMStarting() { g_emu_thread->stopBackgroundControllerPollTimer(); emit g_emu_thread->onVMStarting(); } void Host::OnVMStarted() { emit g_emu_thread->onVMStarted(); } void Host::OnVMDestroyed() { emit g_emu_thread->onVMStopped(); g_emu_thread->startBackgroundControllerPollTimer(); } void Host::OnVMPaused() { g_emu_thread->startBackgroundControllerPollTimer(); emit g_emu_thread->onVMPaused(); } void Host::OnVMResumed() { // exit the event loop when we eventually return g_emu_thread->getEventLoop()->quit(); g_emu_thread->stopBackgroundControllerPollTimer(); // if we were surfaceless (view->game list, system->unpause), get our display widget back if (g_emu_thread->isSurfaceless()) g_emu_thread->setSurfaceless(false); emit g_emu_thread->onVMResumed(); } void Host::OnGameChanged(const std::string& disc_path, const std::string& game_serial, const std::string& game_name, u32 game_crc) { emit g_emu_thread->onGameChanged(QString::fromStdString(disc_path), QString::fromStdString(game_serial), QString::fromStdString(game_name), game_crc); } void EmuThread::updatePerformanceMetrics(bool force) { if (m_verbose_status && VMManager::HasValidVM()) { std::string gs_stat_str; GSgetTitleStats(gs_stat_str); QString gs_stat; if (THREAD_VU1) { gs_stat = QStringLiteral("%1 | EE: %2% | VU: %3% | GS: %4%") .arg(gs_stat_str.c_str()) .arg(PerformanceMetrics::GetCPUThreadUsage(), 0, 'f', 0) .arg(PerformanceMetrics::GetVUThreadUsage(), 0, 'f', 0) .arg(PerformanceMetrics::GetGSThreadUsage(), 0, 'f', 0); } else { gs_stat = QStringLiteral("%1 | EE: %2% | GS: %3%") .arg(gs_stat_str.c_str()) .arg(PerformanceMetrics::GetCPUThreadUsage(), 0, 'f', 0) .arg(PerformanceMetrics::GetGSThreadUsage(), 0, 'f', 0); } QMetaObject::invokeMethod(g_main_window->getStatusVerboseWidget(), "setText", Qt::QueuedConnection, Q_ARG(const QString&, gs_stat)); } const GSRendererType renderer = GSConfig.Renderer; const float speed = std::round(PerformanceMetrics::GetSpeed()); const float gfps = std::round(PerformanceMetrics::GetInternalFPS()); const float vfps = std::round(PerformanceMetrics::GetFPS()); int iwidth, iheight; GSgetInternalResolution(&iwidth, &iheight); if (iwidth != m_last_internal_width || iheight != m_last_internal_height || speed != m_last_speed || gfps != m_last_game_fps || vfps != m_last_video_fps || renderer != m_last_renderer || force) { if (iwidth == 0 && iheight == 0) { // if we don't have width/height yet, we're not going to have fps either. // and we'll probably be <100% due to compiling. so just leave it blank for now. QString blank; QMetaObject::invokeMethod(g_main_window->getStatusRendererWidget(), "setText", Qt::QueuedConnection, Q_ARG(const QString&, blank)); QMetaObject::invokeMethod(g_main_window->getStatusResolutionWidget(), "setText", Qt::QueuedConnection, Q_ARG(const QString&, blank)); QMetaObject::invokeMethod(g_main_window->getStatusFPSWidget(), "setText", Qt::QueuedConnection, Q_ARG(const QString&, blank)); QMetaObject::invokeMethod(g_main_window->getStatusVPSWidget(), "setText", Qt::QueuedConnection, Q_ARG(const QString&, blank)); return; } else { if (renderer != m_last_renderer || force) { QMetaObject::invokeMethod(g_main_window->getStatusRendererWidget(), "setText", Qt::QueuedConnection, Q_ARG(const QString&, QString::fromUtf8(Pcsx2Config::GSOptions::GetRendererName(renderer)))); m_last_renderer = renderer; } if (iwidth != m_last_internal_width || iheight != m_last_internal_height || force) { QMetaObject::invokeMethod(g_main_window->getStatusResolutionWidget(), "setText", Qt::QueuedConnection, Q_ARG(const QString&, tr("%1x%2") .arg(iwidth) .arg(iheight))); m_last_internal_width = iwidth; m_last_internal_height = iheight; } if (gfps != m_last_game_fps || force) { QMetaObject::invokeMethod(g_main_window->getStatusFPSWidget(), "setText", Qt::QueuedConnection, Q_ARG(const QString&, tr("Game: %1 FPS") .arg(gfps, 0, 'f', 0))); m_last_game_fps = gfps; } if (speed != m_last_speed || vfps != m_last_video_fps || force) { QMetaObject::invokeMethod(g_main_window->getStatusVPSWidget(), "setText", Qt::QueuedConnection, Q_ARG(const QString&, tr("Video: %1 FPS (%2%)") .arg(vfps, 0, 'f', 0) .arg(speed, 0, 'f', 0))); m_last_speed = speed; m_last_video_fps = vfps; } } } } void Host::OnPerformanceMetricsUpdated() { g_emu_thread->updatePerformanceMetrics(false); } void Host::OnSaveStateLoading(const std::string_view& filename) { emit g_emu_thread->onSaveStateLoading(QtUtils::StringViewToQString(filename)); } void Host::OnSaveStateLoaded(const std::string_view& filename, bool was_successful) { emit g_emu_thread->onSaveStateLoaded(QtUtils::StringViewToQString(filename), was_successful); } void Host::OnSaveStateSaved(const std::string_view& filename) { emit g_emu_thread->onSaveStateSaved(QtUtils::StringViewToQString(filename)); } void Host::PumpMessagesOnCPUThread() { g_emu_thread->getEventLoop()->processEvents(QEventLoop::AllEvents); } void Host::RunOnCPUThread(std::function function, bool block /* = false */) { if (g_emu_thread->isOnEmuThread()) { // probably shouldn't ever happen, but just in case.. function(); return; } QMetaObject::invokeMethod(g_emu_thread, "runOnCPUThread", block ? Qt::BlockingQueuedConnection : Qt::QueuedConnection, Q_ARG(const std::function&, std::move(function))); } void Host::RefreshGameListAsync(bool invalidate_cache) { QMetaObject::invokeMethod(g_main_window, "refreshGameList", Qt::QueuedConnection, Q_ARG(bool, invalidate_cache)); } void Host::CancelGameListRefresh() { QMetaObject::invokeMethod(g_main_window, "cancelGameListRefresh", Qt::BlockingQueuedConnection); } void Host::RequestExit(bool save_state_if_running) { if (VMManager::HasValidVM()) g_emu_thread->shutdownVM(save_state_if_running); QMetaObject::invokeMethod(g_main_window, "requestExit", Qt::QueuedConnection); } void Host::RequestVMShutdown(bool allow_confirm, bool allow_save_state, bool default_save_state) { if (!VMManager::HasValidVM()) return; // Run it on the host thread, that way we get the confirm prompt (if enabled). QMetaObject::invokeMethod(g_main_window, "requestShutdown", Qt::QueuedConnection, Q_ARG(bool, allow_confirm), Q_ARG(bool, allow_save_state), Q_ARG(bool, default_save_state), Q_ARG(bool, false)); } bool Host::IsFullscreen() { return g_emu_thread->isFullscreen(); } void Host::SetFullscreen(bool enabled) { g_emu_thread->setFullscreen(enabled); } alignas(16) static SysMtgsThread s_mtgs_thread; SysMtgsThread& GetMTGS() { return s_mtgs_thread; } bool QtHost::InitializeConfig() { if (!CommonHost::InitializeCriticalFolders()) { QMessageBox::critical(nullptr, QStringLiteral("PCSX2"), QStringLiteral("One or more critical directories are missing, your installation may be incomplete.")); return false; } const std::string path(Path::Combine(EmuFolders::Settings, "PCSX2.ini")); Console.WriteLn("Loading config from %s.", path.c_str()); s_base_settings_interface = std::make_unique(std::move(path)); Host::Internal::SetBaseSettingsLayer(s_base_settings_interface.get()); if (!s_base_settings_interface->Load() || !CommonHost::CheckSettingsVersion()) { // If the config file doesn't exist, assume this is a new install and don't prompt to overwrite. if (FileSystem::FileExists(s_base_settings_interface->GetFileName().c_str()) && QMessageBox::question(nullptr, QStringLiteral("PCSX2"), QStringLiteral("Settings failed to load, or are the incorrect version. Clicking Yes will reset all settings to defaults. Do you want to continue?")) != QMessageBox::Yes) { return false; } CommonHost::SetDefaultSettings(*s_base_settings_interface, true, true, true, true, true); SaveSettings(); } CommonHost::LoadStartupSettings(); Host::UpdateLogging(QtHost::InNoGUIMode()); return true; } void Host::SetDefaultUISettings(SettingsInterface& si) { Host::SetDefaultLoggingSettings(si); si.SetBoolValue("UI", "InhibitScreensaver", true); si.SetBoolValue("UI", "ConfirmShutdown", true); si.SetBoolValue("UI", "StartPaused", false); si.SetBoolValue("UI", "PauseOnFocusLoss", false); si.SetBoolValue("UI", "StartFullscreen", false); si.SetBoolValue("UI", "DoubleClickTogglesFullscreen", true); si.SetBoolValue("UI", "HideMouseCursor", false); si.SetBoolValue("UI", "RenderToSeparateWindow", false); si.SetBoolValue("UI", "HideMainWindowWhenRunning", false); si.SetBoolValue("UI", "DisableWindowResize", false); si.SetStringValue("UI", "Theme", MainWindow::DEFAULT_THEME_NAME); } void QtHost::SaveSettings() { pxAssertRel(!g_emu_thread->isOnEmuThread(), "Saving should happen on the UI thread."); { auto lock = Host::GetSettingsLock(); if (!s_base_settings_interface->Save()) Console.Error("Failed to save settings."); } s_settings_save_timer->deleteLater(); s_settings_save_timer.release(); } void Host::CommitBaseSettingChanges() { if (!QtHost::IsOnUIThread()) { QtHost::RunOnUIThread(&Host::CommitBaseSettingChanges); return; } auto lock = Host::GetSettingsLock(); if (s_settings_save_timer) return; s_settings_save_timer = std::make_unique(); s_settings_save_timer->connect(s_settings_save_timer.get(), &QTimer::timeout, &QtHost::SaveSettings); s_settings_save_timer->setSingleShot(true); s_settings_save_timer->start(SETTINGS_SAVE_DELAY); } bool QtHost::InBatchMode() { return s_batch_mode; } bool QtHost::InNoGUIMode() { return s_nogui_mode; } bool QtHost::IsOnUIThread() { QThread* ui_thread = qApp->thread(); return (QThread::currentThread() == ui_thread); } void QtHost::RunOnUIThread(const std::function& func, bool block /*= false*/) { // main window always exists, so it's fine to attach it to that. QMetaObject::invokeMethod(g_main_window, "runOnUIThread", block ? Qt::BlockingQueuedConnection : Qt::QueuedConnection, Q_ARG(const std::function&, func)); } bool Host::RequestResetSettings(bool folders, bool core, bool controllers, bool hotkeys, bool ui) { { auto lock = Host::GetSettingsLock(); CommonHost::SetDefaultSettings(*s_base_settings_interface.get(), folders, core, controllers, hotkeys, ui); } Host::CommitBaseSettingChanges(); g_emu_thread->applySettings(); if (folders) g_emu_thread->updateEmuFolders(); return true; } QString QtHost::GetAppNameAndVersion() { QString ret; if constexpr (!PCSX2_isReleaseVersion && GIT_TAGGED_COMMIT) { ret = QStringLiteral("PCSX2 Nightly - " GIT_TAG); } else if constexpr (PCSX2_isReleaseVersion) { #define APPNAME_STRINGIZE(x) #x ret = QStringLiteral("PCSX2 " APPNAME_STRINGIZE(PCSX2_VersionHi) "." APPNAME_STRINGIZE(PCSX2_VersionMid) "." APPNAME_STRINGIZE(PCSX2_VersionLo)); #undef APPNAME_STRINGIZE } else { return QStringLiteral("PCSX2 " GIT_REV); } return ret; } QString QtHost::GetAppConfigSuffix() { #if defined(PCSX2_DEBUG) return QStringLiteral(" [Debug]"); #elif defined(PCSX2_DEVBUILD) return QStringLiteral(" [Devel]"); #else return QString(); #endif } QString QtHost::GetResourcesBasePath() { return QString::fromStdString(EmuFolders::Resources); } std::optional> Host::ReadResourceFile(const char* filename) { const std::string path(Path::Combine(EmuFolders::Resources, filename)); std::optional> ret(FileSystem::ReadBinaryFile(path.c_str())); if (!ret.has_value()) Console.Error("Failed to read resource file '%s'", filename); return ret; } std::optional Host::ReadResourceFileToString(const char* filename) { const std::string path(Path::Combine(EmuFolders::Resources, filename)); std::optional ret(FileSystem::ReadFileToString(path.c_str())); if (!ret.has_value()) Console.Error("Failed to read resource file to string '%s'", filename); return ret; } void Host::ReportErrorAsync(const std::string_view& title, const std::string_view& message) { if (!title.empty() && !message.empty()) { Console.Error("ReportErrorAsync: %.*s: %.*s", static_cast(title.size()), title.data(), static_cast(message.size()), message.data()); } else if (!message.empty()) { Console.Error("ReportErrorAsync: %.*s", static_cast(message.size()), message.data()); } QMetaObject::invokeMethod(g_main_window, "reportError", Qt::QueuedConnection, Q_ARG(const QString&, title.empty() ? QString() : QString::fromUtf8(title.data(), title.size())), Q_ARG(const QString&, message.empty() ? QString() : QString::fromUtf8(message.data(), message.size()))); } bool Host::ConfirmMessage(const std::string_view& title, const std::string_view& message) { const QString qtitle(QString::fromUtf8(title.data(), title.size())); const QString qmessage(QString::fromUtf8(message.data(), message.size())); return g_emu_thread->confirmMessage(qtitle, qmessage); } void Host::OpenURL(const std::string_view& url) { QtHost::RunOnUIThread([url = QtUtils::StringViewToQString(url)]() { QtUtils::OpenURL(g_main_window, QUrl(url)); }); } bool Host::CopyTextToClipboard(const std::string_view& text) { QtHost::RunOnUIThread([text = QtUtils::StringViewToQString(text)]() { QClipboard* clipboard = QGuiApplication::clipboard(); if (clipboard) clipboard->setText(text); }); return true; } void Host::OnInputDeviceConnected(const std::string_view& identifier, const std::string_view& device_name) { emit g_emu_thread->onInputDeviceConnected( identifier.empty() ? QString() : QString::fromUtf8(identifier.data(), identifier.size()), device_name.empty() ? QString() : QString::fromUtf8(device_name.data(), device_name.size())); } void Host::OnInputDeviceDisconnected(const std::string_view& identifier) { emit g_emu_thread->onInputDeviceDisconnected( identifier.empty() ? QString() : QString::fromUtf8(identifier.data(), identifier.size())); } ////////////////////////////////////////////////////////////////////////// // Hotkeys ////////////////////////////////////////////////////////////////////////// BEGIN_HOTKEY_LIST(g_host_hotkeys) END_HOTKEY_LIST() ////////////////////////////////////////////////////////////////////////// // Interface Stuff ////////////////////////////////////////////////////////////////////////// static void SignalHandler(int signal) { // First try the normal (graceful) shutdown/exit. static bool graceful_shutdown_attempted = false; if (!graceful_shutdown_attempted && g_main_window) { std::fprintf(stderr, "Received CTRL+C, attempting graceful shutdown. Press CTRL+C again to force.\n"); graceful_shutdown_attempted = true; // This could be a bit risky invoking from a signal handler... hopefully it's okay. QMetaObject::invokeMethod(g_main_window, &MainWindow::requestExit, Qt::QueuedConnection); return; } std::signal(signal, SIG_DFL); // MacOS is missing std::quick_exit() despite it being C++11... #ifndef __APPLE__ std::quick_exit(1); #else _Exit(1); #endif } void QtHost::HookSignals() { std::signal(SIGINT, SignalHandler); std::signal(SIGTERM, SignalHandler); } void QtHost::PrintCommandLineVersion() { Host::InitializeEarlyConsole(); std::fprintf(stderr, "%s\n", (GetAppNameAndVersion() + GetAppConfigSuffix()).toUtf8().constData()); std::fprintf(stderr, "https://pcsx2.net/\n"); std::fprintf(stderr, "\n"); } void QtHost::PrintCommandLineHelp(const char* progname) { PrintCommandLineVersion(); std::fprintf(stderr, "Usage: %s [parameters] [--] [boot filename]\n", progname); std::fprintf(stderr, "\n"); std::fprintf(stderr, " -help: Displays this information and exits.\n"); std::fprintf(stderr, " -version: Displays version information and exits.\n"); std::fprintf(stderr, " -batch: Enables batch mode (exits after shutting down).\n"); std::fprintf(stderr, " -nogui: Hides main window while running (implies batch mode).\n"); std::fprintf(stderr, " -elf : Overrides the boot ELF with the specified filename.\n"); std::fprintf(stderr, " -disc : Uses the specified host DVD drive as a source.\n"); std::fprintf(stderr, " -bios: Starts the BIOS (System Menu/OSDSYS).\n"); std::fprintf(stderr, " -fastboot: Force fast boot for provided filename.\n"); std::fprintf(stderr, " -slowboot: Force slow boot for provided filename.\n"); std::fprintf(stderr, " -state : Loads specified save state by index.\n"); std::fprintf(stderr, " -statefile : Loads state from the specified filename.\n"); std::fprintf(stderr, " -fullscreen: Enters fullscreen mode immediately after starting.\n"); std::fprintf(stderr, " -nofullscreen: Prevents fullscreen mode from triggering if enabled.\n"); std::fprintf(stderr, " -earlyconsolelog: Forces logging of early console messages to console.\n"); std::fprintf(stderr, " --: Signals that no more arguments will follow and the remaining\n" " parameters make up the filename. Use when the filename contains\n" " spaces or starts with a dash.\n"); std::fprintf(stderr, "\n"); } std::shared_ptr& QtHost::AutoBoot(std::shared_ptr& autoboot) { if (!autoboot) autoboot = std::make_shared(); return autoboot; } bool QtHost::ParseCommandLineOptions(int argc, char* argv[], std::shared_ptr& autoboot) { bool no_more_args = false; for (int i = 1; i < argc; i++) { if (!no_more_args) { #define CHECK_ARG(str) !std::strcmp(argv[i], str) #define CHECK_ARG_PARAM(str) (!std::strcmp(argv[i], str) && ((i + 1) < argc)) if (CHECK_ARG("-help")) { PrintCommandLineHelp(argv[0]); return false; } else if (CHECK_ARG("-version")) { PrintCommandLineVersion(); return false; } else if (CHECK_ARG("-batch")) { s_batch_mode = true; continue; } else if (CHECK_ARG("-nogui")) { s_batch_mode = true; s_nogui_mode = true; continue; } else if (CHECK_ARG("-fastboot")) { AutoBoot(autoboot)->fast_boot = true; continue; } else if (CHECK_ARG("-slowboot")) { AutoBoot(autoboot)->fast_boot = false; continue; } else if (CHECK_ARG_PARAM("-state")) { AutoBoot(autoboot)->state_index = std::atoi(argv[++i]); continue; } else if (CHECK_ARG_PARAM("-statefile")) { AutoBoot(autoboot)->save_state = argv[++i]; continue; } else if (CHECK_ARG_PARAM("-elf")) { AutoBoot(autoboot)->elf_override = argv[++i]; continue; } else if (CHECK_ARG_PARAM("-disc")) { AutoBoot(autoboot)->source_type = CDVD_SourceType::Disc; AutoBoot(autoboot)->filename = argv[++i]; continue; } else if (CHECK_ARG("-bios")) { AutoBoot(autoboot)->source_type = CDVD_SourceType::NoDisc; continue; } else if (CHECK_ARG("-fullscreen")) { AutoBoot(autoboot)->fullscreen = true; s_start_fullscreen_ui_fullscreen = true; continue; } else if (CHECK_ARG("-nofullscreen")) { AutoBoot(autoboot)->fullscreen = false; continue; } else if (CHECK_ARG("-earlyconsolelog")) { Host::InitializeEarlyConsole(); continue; } else if (CHECK_ARG("-bigpicture")) { s_start_fullscreen_ui = true; continue; } else if (CHECK_ARG("--")) { no_more_args = true; continue; } else if (argv[i][0] == '-') { Host::InitializeEarlyConsole(); std::fprintf(stderr, "Unknown parameter: '%s'", argv[i]); return false; } #undef CHECK_ARG #undef CHECK_ARG_PARAM } if (!AutoBoot(autoboot)->filename.empty()) AutoBoot(autoboot)->filename += ' '; AutoBoot(autoboot)->filename += argv[i]; } // check autoboot parameters, if we set something like fullscreen without a bios // or disc, we don't want to actually start. if (autoboot && !autoboot->source_type.has_value() && autoboot->filename.empty() && autoboot->elf_override.empty()) { Host::InitializeEarlyConsole(); Console.Warning("Skipping autoboot due to no boot parameters."); autoboot.reset(); } // if we don't have autoboot, we definitely don't want batch mode (because that'll skip // scanning the game list). if (s_batch_mode && !s_start_fullscreen_ui && !autoboot) { QMessageBox::critical(nullptr, QStringLiteral("Error"), s_nogui_mode ? QStringLiteral("Cannot use no-gui mode, because no boot filename was specified.") : QStringLiteral("Cannot use batch mode, because no boot filename was specified.")); return false; } return true; } #ifndef _WIN32 // See note in EarlyHardwareChecks.cpp as to why we don't do this on Windows. static bool PerformEarlyHardwareChecks() { // NOTE: No point translating this message, because the configuration isn't loaded yet, so we // won't know which language to use, and loading the configuration uses float instructions. const char* error; if (VMManager::PerformEarlyHardwareChecks(&error)) return true; QMessageBox::critical(nullptr, QStringLiteral("Hardware Check Failed"), QString::fromUtf8(error)); return false; } #endif static void RegisterTypes() { qRegisterMetaType>(); qRegisterMetaType>("std::function"); qRegisterMetaType>(); qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); } int main(int argc, char* argv[]) { CrashHandler::Install(); QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); RegisterTypes(); QApplication app(argc, argv); #ifndef _WIN32 if (!PerformEarlyHardwareChecks()) return EXIT_FAILURE; #endif std::shared_ptr autoboot; if (!QtHost::ParseCommandLineOptions(argc, argv, autoboot)) return EXIT_FAILURE; // Bail out if we can't find any config. if (!QtHost::InitializeConfig()) return EXIT_FAILURE; // Set theme before creating any windows. MainWindow::updateApplicationTheme(); MainWindow* main_window = new MainWindow(QApplication::style()->objectName()); // Start up the CPU thread. QtHost::HookSignals(); EmuThread::start(); // Create all window objects, the emuthread might still be starting up at this point. main_window->initialize(); // When running in batch mode, ensure game list is loaded, but don't scan for any new files. if (!s_batch_mode) main_window->refreshGameList(false); else GameList::Refresh(false, true); // Don't bother showing the window in no-gui mode. if (!s_nogui_mode) main_window->show(); // Initialize big picture mode if requested. if (s_start_fullscreen_ui) g_emu_thread->startFullscreenUI(s_start_fullscreen_ui_fullscreen); // Skip the update check if we're booting a game directly. if (autoboot) g_emu_thread->startVM(std::move(autoboot)); else if (!s_nogui_mode) main_window->startupUpdateCheck(); // This doesn't return until we exit. const int result = app.exec(); // Shutting down. EmuThread::stop(); if (g_main_window) { g_main_window->close(); delete g_main_window; } // Ensure emulog is flushed. if (emuLog) { std::fclose(emuLog); emuLog = nullptr; } return result; }