Qt: Refactor render window lifecycle

Remove multiple sources of truth, eliminate bugs in handling edge cases
when switching between modes.
This commit is contained in:
Stenzek 2025-07-16 16:38:29 +10:00
parent a5e3f163a5
commit b07998512e
No known key found for this signature in database
11 changed files with 151 additions and 95 deletions

View File

@ -696,15 +696,12 @@ void GPUThread::DestroyDeviceOnThread(bool clear_fsui_state)
// Presenter should be gone by this point
Assert(!s_state.gpu_presenter);
const bool has_window = g_gpu_device->HasMainSwapChain();
FullscreenUI::Shutdown(clear_fsui_state);
ImGuiManager::Shutdown();
INFO_LOG("Destroying {} GPU device...", GPUDevice::RenderAPIToString(g_gpu_device->GetRenderAPI()));
g_gpu_device->Destroy();
g_gpu_device.reset();
if (has_window)
Host::ReleaseRenderWindow();
UpdateRunIdle();

View File

@ -1746,6 +1746,7 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error)
{
Error::SetStringFmt(error, "File '{}' is not a valid executable to boot.",
Path::GetFileName(parameters.override_exe));
Host::OnSystemStopping();
DestroySystem();
return false;
}
@ -1786,6 +1787,7 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error)
if (cancelled)
{
// Technically a failure, but user-initiated. Returning false here would try to display a non-existent error.
Host::OnSystemStopping();
DestroySystem();
return true;
}
@ -1805,6 +1807,7 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error)
parameters.override_fullscreen.value_or(ShouldStartFullscreen()), error) ||
!CheckForRequiredSubQ(error))
{
Host::OnSystemStopping();
DestroySystem();
return false;
}
@ -1829,6 +1832,7 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error)
{
Error::AddPrefixFmt(error, "Failed to load save state file '{}' for booting:\n",
Path::GetFileName(parameters.save_state));
Host::OnSystemStopping();
DestroySystem();
return false;
}
@ -2020,6 +2024,7 @@ void System::AbnormalShutdown(const std::string_view reason)
// Immediately switch to destroying and exit execution to get out of here.
s_state.state = State::Stopping;
std::atomic_thread_fence(std::memory_order_release);
Host::OnSystemStopping();
if (s_state.system_executing)
InterruptExecution();
else
@ -5256,6 +5261,7 @@ void System::ShutdownSystem(bool save_resume_state)
}
}
Host::OnSystemStopping();
s_state.state = State::Stopping;
std::atomic_thread_fence(std::memory_order_release);
if (!s_state.system_executing)

View File

@ -99,6 +99,9 @@ void OnSystemStarting();
/// Called when the VM is created.
void OnSystemStarted();
/// Called when the VM is shutting down.
void OnSystemStopping();
/// Called when the VM is shut down or destroyed.
void OnSystemDestroyed();

View File

@ -1069,6 +1069,10 @@ void Host::OnSystemResumed()
{
}
void Host::OnSystemStopping()
{
}
void Host::OnSystemDestroyed()
{
}

View File

@ -142,6 +142,10 @@ void DisplayWidget::handleCloseEvent(QCloseEvent* event)
QMetaObject::invokeMethod(g_main_window, "requestShutdown", Qt::QueuedConnection, Q_ARG(bool, true),
Q_ARG(bool, true), Q_ARG(bool, false), Q_ARG(bool, true));
}
else if (QtHost::IsFullscreenUIStarted())
{
g_emu_thread->stopFullscreenUI();
}
else
{
QMetaObject::invokeMethod(g_main_window, "requestExit", Qt::QueuedConnection);

View File

@ -270,10 +270,11 @@ std::optional<WindowInfo> MainWindow::acquireRenderWindow(RenderAPI render_api,
// Skip recreating the surface if we're just transitioning between fullscreen and windowed with render-to-main off.
// .. except on Wayland, where everything tends to break if you don't recreate.
// Container can also be null if we're messing with settings while surfaceless.
const bool has_container = (m_display_container != nullptr);
const bool needs_container = DisplayContainer::isNeeded(fullscreen, render_to_main);
if (m_display_created && !is_rendering_to_main && !render_to_main && has_container == needs_container &&
!needs_container && !changing_surfaceless)
if (container && !is_rendering_to_main && !render_to_main && has_container == needs_container && !needs_container &&
!changing_surfaceless)
{
DEV_LOG("Toggling to {} without recreating surface", (fullscreen ? "fullscreen" : "windowed"));
m_exclusive_fullscreen_requested = exclusive_fullscreen;
@ -304,15 +305,13 @@ std::optional<WindowInfo> MainWindow::acquireRenderWindow(RenderAPI render_api,
}
destroyDisplayWidget(surfaceless);
m_display_created = true;
// if we're going to surfaceless, we're done here
if (surfaceless)
return WindowInfo();
std::optional<WindowInfo> wi;
if (!surfaceless)
{
createDisplayWidget(fullscreen, render_to_main, use_main_window_pos);
std::optional<WindowInfo> wi = m_display_widget->getWindowInfo(render_api, error);
wi = m_display_widget->getWindowInfo(render_api, error);
if (!wi.has_value())
{
QMessageBox::critical(this, tr("Error"), tr("Failed to get window info from widget"));
@ -321,18 +320,24 @@ std::optional<WindowInfo> MainWindow::acquireRenderWindow(RenderAPI render_api,
}
g_emu_thread->connectDisplaySignals(m_display_widget);
}
else
{
wi = WindowInfo();
}
updateWindowTitle();
updateWindowState();
updateDisplayWidgetCursor();
updateDisplayRelatedActions(true, render_to_main, fullscreen);
QtUtils::ShowOrRaiseWindow(QtUtils::GetRootWidget(m_display_widget));
m_display_widget->setFocus();
return wi;
}
bool MainWindow::wantsDisplayWidget() const
{
// big picture or system created
return (s_system_starting || s_system_valid || s_fullscreen_ui_started);
}
void MainWindow::createDisplayWidget(bool fullscreen, bool render_to_main, bool use_main_window_pos)
{
// If we're rendering to main and were hidden (e.g. coming back from fullscreen),
@ -393,8 +398,14 @@ void MainWindow::createDisplayWidget(bool fullscreen, bool render_to_main, bool
updateDisplayRelatedActions(true, render_to_main, fullscreen);
updateShortcutActions(false);
updateDisplayWidgetCursor();
// We need the surface visible.
QGuiApplication::sync();
if (!render_to_main)
QtUtils::ShowOrRaiseWindow(QtUtils::GetRootWidget(m_display_widget));
m_display_widget->setFocus();
}
void MainWindow::displayResizeRequested(qint32 width, qint32 height)
@ -423,21 +434,12 @@ void MainWindow::releaseRenderWindow()
{
// Now we can safely destroy the display window.
destroyDisplayWidget(true);
m_display_created = false;
m_exclusive_fullscreen_requested = false;
updateDisplayRelatedActions(false, false, false);
updateShortcutActions(false);
m_ui.actionViewSystemDisplay->setEnabled(false);
m_ui.actionFullscreen->setEnabled(false);
}
void MainWindow::destroyDisplayWidget(bool show_game_list)
{
if (!m_display_widget)
return;
if (m_display_widget)
{
if (!isRenderingFullscreen() && !isRenderingToMain())
saveDisplayWindowGeometryToConfig();
@ -466,6 +468,12 @@ void MainWindow::destroyDisplayWidget(bool show_game_list)
m_display_container->deleteLater();
m_display_container = nullptr;
}
}
m_exclusive_fullscreen_requested = false;
updateDisplayRelatedActions(false, false, false);
updateShortcutActions(false);
}
void MainWindow::updateDisplayWidgetCursor()
@ -481,7 +489,7 @@ void MainWindow::updateDisplayWidgetCursor()
void MainWindow::updateDisplayRelatedActions(bool has_surface, bool render_to_main, bool fullscreen)
{
// rendering to main, or switched to gamelist/grid
m_ui.actionViewSystemDisplay->setEnabled((has_surface && render_to_main) || (!has_surface && g_gpu_device));
m_ui.actionViewSystemDisplay->setEnabled(wantsDisplayWidget() && QtHost::CanRenderToMainWindow());
m_ui.menuWindowSize->setEnabled(has_surface && !fullscreen);
m_ui.actionFullscreen->setEnabled(has_surface);
@ -567,7 +575,7 @@ void MainWindow::onSystemResumed()
}
}
void MainWindow::onSystemDestroyed()
void MainWindow::onSystemStopping()
{
// update UI
{
@ -580,6 +588,14 @@ void MainWindow::onSystemDestroyed()
s_system_paused = false;
s_undo_state_timestamp.reset();
updateEmulationActions(false, false, s_achievements_hardcore_mode);
updateStatusBarWidgetVisibility();
}
void MainWindow::onSystemDestroyed()
{
Assert(!s_system_starting && !s_system_valid);
// If we're closing or in batch mode, quit the whole application now.
if (m_is_closing || QtHost::InBatchMode())
{
@ -588,8 +604,6 @@ void MainWindow::onSystemDestroyed()
return;
}
updateEmulationActions(false, false, s_achievements_hardcore_mode);
updateStatusBarWidgetVisibility();
if (m_display_widget)
updateDisplayWidgetCursor();
else
@ -718,7 +732,7 @@ void MainWindow::quit()
}
// Big picture might still be active.
if (m_display_created)
if (s_fullscreen_ui_started)
g_emu_thread->stopFullscreenUI();
// Ensure subwindows are removed before quitting. That way the log window cancelling
@ -748,7 +762,7 @@ void MainWindow::recreate()
// Remove subwindows before switching to surfaceless, because otherwise e.g. the debugger can cause funkyness.
destroySubWindows();
const bool was_display_created = m_display_created;
const bool was_display_created = wantsDisplayWidget();
const bool was_fullscreen = (was_display_created && g_emu_thread->isFullscreen());
if (was_display_created)
{
@ -759,7 +773,6 @@ void MainWindow::recreate()
g_emu_thread->setSurfaceless(true);
QtUtils::ProcessEventsWithSleep(QEventLoop::ExcludeUserInputEvents,
[this]() { return (m_display_widget || !g_emu_thread->isSurfaceless()); });
m_display_created = false;
}
// We need to close input sources, because e.g. DInput uses our window handle.
@ -811,6 +824,11 @@ void MainWindow::recreate()
notifyRAIntegrationOfWindowChange();
}
void MainWindow::ensureVisible()
{
updateWindowState(true);
}
void MainWindow::destroySubWindows()
{
QtUtils::CloseAndDeleteWindow(m_memory_scanner_window);
@ -1375,7 +1393,6 @@ void MainWindow::onViewGameGridActionTriggered()
void MainWindow::onViewSystemDisplayTriggered()
{
if (m_display_created)
switchToEmulationView();
}
@ -2033,17 +2050,16 @@ void MainWindow::updateWindowState(bool force_visible)
if (m_is_closing)
return;
const bool hide_window = !isRenderingToMain() && shouldHideMainWindow();
const bool hide_window = shouldHideMainWindow();
const bool disable_resize = Host::GetBoolSettingValue("Main", "DisableWindowResize", false);
const bool has_window = s_system_valid || m_display_widget;
// Need to test both valid and display widget because of startup (vm invalid while window is created).
const bool visible = force_visible || !hide_window || !has_window;
const bool visible = force_visible || !hide_window;
if (isVisible() != visible)
setVisible(visible);
// No point changing realizability if we're not visible.
const bool resizeable = force_visible || !disable_resize || !has_window;
const bool resizeable = force_visible || !disable_resize;
if (visible)
QtUtils::SetWindowResizeable(this, resizeable);
@ -2100,15 +2116,19 @@ bool MainWindow::shouldHideMouseCursor() const
bool MainWindow::shouldHideMainWindow() const
{
return Host::GetBoolSettingValue("Main", "HideMainWindowWhenRunning", false) || QtHost::CanRenderToMainWindow() ||
QtHost::InNoGUIMode();
// CanRenderToMain check is for temporary unfullscreens.
return !isRenderingToMain() && wantsDisplayWidget() &&
(Host::GetBoolSettingValue("Main", "HideMainWindowWhenRunning", false) ||
(QtHost::CanRenderToMainWindow() &&
(isRenderingFullscreen() || s_system_locked.load(std::memory_order_relaxed))) ||
QtHost::InNoGUIMode());
}
void MainWindow::switchToGameListView()
{
if (!isShowingGameList())
{
if (m_display_created)
if (wantsDisplayWidget())
{
m_was_paused_on_surface_loss = s_system_paused;
if (!s_system_paused)
@ -2128,7 +2148,7 @@ void MainWindow::switchToGameListView()
void MainWindow::switchToEmulationView()
{
if (!m_display_created || !isShowingGameList())
if (!wantsDisplayWidget() || !isShowingGameList())
return;
// we're no longer surfaceless! this will call back to UpdateDisplay(), which will swap the widget out.
@ -2141,7 +2161,11 @@ void MainWindow::switchToEmulationView()
updateShortcutActions(false);
if (m_display_widget)
{
if (!isRenderingToMain())
QtUtils::ShowOrRaiseWindow(QtUtils::GetRootWidget(m_display_widget));
m_display_widget->setFocus();
}
}
void MainWindow::connectSignals()
@ -2259,6 +2283,7 @@ void MainWindow::connectSignals()
connect(g_emu_thread, &EmuThread::focusDisplayWidgetRequested, this, &MainWindow::focusDisplayWidget);
connect(g_emu_thread, &EmuThread::systemStarting, this, &MainWindow::onSystemStarting);
connect(g_emu_thread, &EmuThread::systemStarted, this, &MainWindow::onSystemStarted);
connect(g_emu_thread, &EmuThread::systemStopping, this, &MainWindow::onSystemStopping);
connect(g_emu_thread, &EmuThread::systemDestroyed, this, &MainWindow::onSystemDestroyed);
connect(g_emu_thread, &EmuThread::systemPaused, this, &MainWindow::onSystemPaused);
connect(g_emu_thread, &EmuThread::systemResumed, this, &MainWindow::onSystemResumed);
@ -2596,10 +2621,10 @@ void MainWindow::showEvent(QShowEvent* event)
void MainWindow::closeEvent(QCloseEvent* event)
{
// If there's no VM, we can just exit as normal.
if (!s_system_valid || !m_display_created)
if (!s_system_valid)
{
saveStateToConfig();
if (m_display_created)
if (s_fullscreen_ui_started)
g_emu_thread->stopFullscreenUI();
destroySubWindows();
QMainWindow::closeEvent(event);
@ -3145,6 +3170,9 @@ MainWindow::SystemLock MainWindow::pauseAndLockSystem()
#endif
const bool was_paused = !s_system_valid || s_system_paused;
// Have to do this early to avoid making the main window visible.
s_system_locked.fetch_add(1, std::memory_order_release);
// We need to switch out of exclusive fullscreen before we can display our popup.
// However, we do not want to switch back to render-to-main, the window might have generated this event.
if (was_fullscreen)
@ -3171,7 +3199,10 @@ MainWindow::SystemLock MainWindow::pauseAndLockSystem()
}
// Now we'll either have a borderless window, or a regular window (if we were exclusive fullscreen).
QWidget* dialog_parent = (s_system_valid && was_fullscreen) ? getDisplayContainer() : this;
QWidget* dialog_parent = getDisplayContainer();
if (dialog_parent->parent())
dialog_parent = this;
return SystemLock(dialog_parent, was_paused, was_fullscreen);
}
@ -3179,13 +3210,11 @@ MainWindow::SystemLock MainWindow::pauseAndLockSystem()
MainWindow::SystemLock::SystemLock(QWidget* dialog_parent, bool was_paused, bool was_fullscreen)
: m_dialog_parent(dialog_parent), m_was_paused(was_paused), m_was_fullscreen(was_fullscreen)
{
s_system_locked.fetch_add(1, std::memory_order_release);
}
MainWindow::SystemLock::SystemLock(SystemLock&& lock)
: m_dialog_parent(lock.m_dialog_parent), m_was_paused(lock.m_was_paused), m_was_fullscreen(lock.m_was_fullscreen)
{
s_system_locked.fetch_add(1, std::memory_order_release);
lock.m_dialog_parent = nullptr;
lock.m_was_paused = true;
lock.m_was_fullscreen = false;

View File

@ -125,6 +125,7 @@ public Q_SLOTS:
void checkForUpdates(bool display_message);
void recreate();
void ensureVisible();
void* getNativeWindowId();
@ -144,6 +145,7 @@ private Q_SLOTS:
void onSettingsResetToDefault(bool system, bool controller);
void onSystemStarting();
void onSystemStarted();
void onSystemStopping();
void onSystemDestroyed();
void onSystemPaused();
void onSystemResumed();
@ -264,6 +266,7 @@ private:
void restoreStateFromConfig();
void saveDisplayWindowGeometryToConfig();
void restoreDisplayWindowGeometryFromConfig();
bool wantsDisplayWidget() const;
void createDisplayWidget(bool fullscreen, bool render_to_main, bool use_main_window_pos);
void destroyDisplayWidget(bool show_game_list);
void updateDisplayWidgetCursor();
@ -340,7 +343,6 @@ private:
bool m_relative_mouse_mode = false;
bool m_hide_mouse_cursor = false;
bool m_display_created = false;
bool m_exclusive_fullscreen_requested = false;
bool m_save_states_invalidated = false;
bool m_was_paused_on_surface_loss = false;

View File

@ -703,15 +703,17 @@ void EmuThread::startFullscreenUI()
const bool start_fullscreen =
(s_start_fullscreen_ui_fullscreen || Host::GetBaseBoolSettingValue("Main", "StartFullscreen", false));
m_is_fullscreen_ui_started = true;
emit fullscreenUIStartedOrStopped(true);
Error error;
if (!GPUThread::StartFullscreenUI(start_fullscreen, &error))
{
Host::ReportErrorAsync("Error", error.GetDescription());
m_is_fullscreen_ui_started = false;
emit fullscreenUIStartedOrStopped(false);
return;
}
m_is_fullscreen_ui_started = true;
emit fullscreenUIStartedOrStopped(true);
}
void EmuThread::stopFullscreenUI()
@ -722,7 +724,7 @@ void EmuThread::stopFullscreenUI()
// wait until the host display is gone
QtUtils::ProcessEventsWithSleep(QEventLoop::ExcludeUserInputEvents,
[]() { return (!QtHost::IsSystemValid() && g_gpu_device); });
[]() { return QtHost::IsFullscreenUIStarted(); });
return;
}
@ -749,13 +751,8 @@ void EmuThread::exitFullscreenUI()
const bool was_in_nogui_mode = std::exchange(s_nogui_mode, false);
// force a return to main window before exiting, otherwise qt will terminate the application
if (!m_is_rendering_to_main)
{
m_is_fullscreen = false;
m_is_rendering_to_main = true;
GPUThread::UpdateDisplayWindow(false);
}
// force the main window to be visible, otherwise qt will terminate the application
QMetaObject::invokeMethod(g_main_window, &MainWindow::ensureVisible, Qt::QueuedConnection);
// then stop as normal
stopFullscreenUI();
@ -974,6 +971,7 @@ void EmuThread::releaseRenderWindow()
{
emit onReleaseRenderWindowRequested();
m_is_fullscreen = false;
m_is_surfaceless = false;
}
void EmuThread::connectDisplaySignals(DisplayWidget* widget)
@ -1019,6 +1017,11 @@ void Host::OnSystemResumed()
g_emu_thread->stopBackgroundControllerPollTimer();
}
void Host::OnSystemStopping()
{
emit g_emu_thread->systemStopping();
}
void Host::OnSystemDestroyed()
{
g_emu_thread->resetPerformanceCounters();
@ -2757,7 +2760,6 @@ bool QtHost::ParseCommandLineParametersAndInitializeConfig(QApplication& app,
{
INFO_LOG("Command Line: Using NoGUI mode.");
s_nogui_mode = true;
s_batch_mode = true;
continue;
}
else if (CHECK_ARG("-bios"))
@ -2907,6 +2909,9 @@ bool QtHost::ParseCommandLineParametersAndInitializeConfig(QApplication& app,
if (autoboot && autoboot->path.empty() && autoboot->save_state.empty() && !starting_bios)
autoboot.reset();
// nogui implies batch mode if autobooting and not running big picture mode
s_batch_mode = (s_batch_mode || (autoboot && !s_start_fullscreen_ui));
// 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 && !autoboot && !s_start_fullscreen_ui)

View File

@ -137,6 +137,7 @@ Q_SIGNALS:
void settingsResetToDefault(bool system, bool controller);
void systemStarting();
void systemStarted();
void systemStopping();
void systemDestroyed();
void systemPaused();
void systemResumed();

View File

@ -286,6 +286,11 @@ void Host::OnSystemStarted()
//
}
void Host::OnSystemStopping()
{
//
}
void Host::OnSystemDestroyed()
{
//

View File

@ -321,7 +321,7 @@ void ImGuiManager::UpdateScale()
const float window_scale =
(g_gpu_device && g_gpu_device->HasMainSwapChain()) ? g_gpu_device->GetMainSwapChain()->GetScale() : 1.0f;
const float scale = std::max(window_scale * s_state.global_prescale, 1.0f);
const bool scale_changed = (scale == s_state.global_scale);
const bool scale_changed = (scale != s_state.global_scale);
if (!ImGuiFullscreen::UpdateLayoutScale() && !scale_changed)
return;