From fa23d16c3c9586117f346b2e5441589efa7a5963 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Sun, 28 Dec 2025 01:33:44 +1000 Subject: [PATCH] Qt: Cache window handle for size updates Apparently calling winId() during window resizes ends up with a bad m_surface pointer inside the QWindowsWindow... I don't feel like debugging Qt any further, so this will be sufficient. Plus it's faster. --- src/duckstation-qt/displaywidget.cpp | 133 +++++++++++++++++++-------- src/duckstation-qt/displaywidget.h | 14 +-- src/duckstation-qt/mainwindow.cpp | 9 +- src/duckstation-qt/qtwindowinfo.cpp | 93 ++++++++++--------- src/duckstation-qt/qtwindowinfo.h | 8 +- 5 files changed, 163 insertions(+), 94 deletions(-) diff --git a/src/duckstation-qt/displaywidget.cpp b/src/duckstation-qt/displaywidget.cpp index b96524842..eab34a4eb 100644 --- a/src/duckstation-qt/displaywidget.cpp +++ b/src/duckstation-qt/displaywidget.cpp @@ -51,15 +51,38 @@ DisplayWidget::DisplayWidget(QWidget* parent) : QWidget(parent) DisplayWidget::~DisplayWidget() = default; -std::optional DisplayWidget::getWindowInfo(RenderAPI render_api, Error* error) +const std::optional& DisplayWidget::getWindowInfo(RenderAPI render_api, Error* error) { - std::optional ret = QtUtils::GetWindowInfoForWidget(this, render_api, error); - if (ret.has_value()) + if (!m_window_info.has_value()) + m_window_info = QtUtils::GetWindowInfoForWidget(this, render_api, error); + + return m_window_info; +} + +void DisplayWidget::clearWindowInfo() +{ + m_window_info.reset(); +} + +void DisplayWidget::checkForSizeChange() +{ + if (!m_window_info.has_value()) + return; + + // avoid spamming resize events for paint events (sent on move on windows) + const u16 prev_width = m_window_info->surface_width; + const u16 prev_height = m_window_info->surface_height; + const float prev_scale = m_window_info->surface_scale; + QtUtils::UpdateSurfaceSize(this, &m_window_info.value()); + if (prev_width != m_window_info->surface_width || prev_height != m_window_info->surface_height || + prev_scale != m_window_info->surface_scale) { - m_last_window_size = QSize(ret->surface_width, ret->surface_height); - m_last_window_scale = ret->surface_scale; + DEV_LOG("Display widget resized to {}x{} (Qt {}x{}) DPR={}", m_window_info->surface_width, + m_window_info->surface_height, width(), height(), m_window_info->surface_scale); + emit windowResizedEvent(m_window_info->surface_width, m_window_info->surface_height, m_window_info->surface_scale); } - return ret; + + updateCenterPos(); } void DisplayWidget::updateRelativeMode(bool enabled) @@ -165,22 +188,6 @@ bool DisplayWidget::isActuallyFullscreen() const return container ? container->isFullScreen() : isFullScreen(); } -void DisplayWidget::checkForSizeChange() -{ - // avoid spamming resize events for paint events (sent on move on windows) - const auto& [size, dpr] = QtUtils::GetPixelSizeForWidget(this); - if (m_last_window_size != size || m_last_window_scale != dpr) - { - DEV_LOG("Display widget resized to {}x{} (Qt {}x{}) DPR={}", size.width(), size.height(), width(), height(), dpr); - - m_last_window_size = size; - m_last_window_scale = dpr; - emit windowResizedEvent(size.width(), size.height(), static_cast(dpr)); - } - - updateCenterPos(); -} - void DisplayWidget::updateCenterPos() { #ifdef _WIN32 @@ -272,9 +279,11 @@ bool DisplayWidget::event(QEvent* event) { if (!m_relative_mouse_enabled) { + const float surface_scale = + m_window_info.has_value() ? m_window_info->surface_scale : static_cast(devicePixelRatio()); const QPoint mouse_pos = static_cast(event)->pos(); - const float scaled_x = static_cast(static_cast(mouse_pos.x()) * m_last_window_scale); - const float scaled_y = static_cast(static_cast(mouse_pos.y()) * m_last_window_scale); + const float scaled_x = static_cast(mouse_pos.x()) * surface_scale; + const float scaled_y = static_cast(mouse_pos.y()) * surface_scale; InputManager::UpdatePointerAbsolutePosition(0, scaled_x, scaled_y); } else @@ -411,6 +420,19 @@ bool DisplayWidget::event(QEvent* event) return true; } + case QEvent::WinIdChange: + { + QWidget::event(event); + + if (m_window_info.has_value()) + { + ERROR_LOG("Window ID changed while we had a valid WindowInfo. This is NOT expected, please report."); + clearWindowInfo(); + } + + return true; + } + default: return QWidget::event(event); } @@ -490,6 +512,38 @@ QPaintEngine* AuxiliaryDisplayWidget::paintEngine() const return nullptr; } +const std::optional& AuxiliaryDisplayWidget::getWindowInfo(RenderAPI render_api, Error* error) +{ + if (!m_window_info.has_value()) + m_window_info = QtUtils::GetWindowInfoForWidget(this, render_api, error); + + return m_window_info; +} + +void AuxiliaryDisplayWidget::checkForSizeChange() +{ + if (!m_window_info.has_value()) + return; + + // avoid spamming resize events for paint events (sent on move on windows) + const u16 prev_width = m_window_info->surface_width; + const u16 prev_height = m_window_info->surface_height; + const float prev_scale = m_window_info->surface_scale; + QtUtils::UpdateSurfaceSize(this, &m_window_info.value()); + if (prev_width != m_window_info->surface_width || prev_height != m_window_info->surface_height || + prev_scale != m_window_info->surface_scale) + { + DEV_LOG("Auxiliary display widget resized to {}x{} (Qt {}x{}) DPR={}", m_window_info->surface_width, + m_window_info->surface_height, width(), height(), m_window_info->surface_scale); + + g_core_thread->queueAuxiliaryRenderWindowInputEvent( + m_userdata, Host::AuxiliaryRenderWindowEvent::Resized, + Host::AuxiliaryRenderWindowEventParam{.uint_param = static_cast(m_window_info->surface_width)}, + Host::AuxiliaryRenderWindowEventParam{.uint_param = static_cast(m_window_info->surface_height)}, + Host::AuxiliaryRenderWindowEventParam{.float_param = static_cast(m_window_info->surface_scale)}); + } +} + bool AuxiliaryDisplayWidget::event(QEvent* event) { const QEvent::Type type = event->type(); @@ -531,8 +585,10 @@ bool AuxiliaryDisplayWidget::event(QEvent* event) case QEvent::MouseMove: { const QPoint mouse_pos = static_cast(event)->pos(); - const float scaled_x = static_cast(static_cast(mouse_pos.x()) * m_last_window_scale); - const float scaled_y = static_cast(static_cast(mouse_pos.y()) * m_last_window_scale); + const float surface_scale = + m_window_info.has_value() ? m_window_info->surface_scale : static_cast(devicePixelRatio()); + const float scaled_x = static_cast(mouse_pos.x()) * surface_scale; + const float scaled_y = static_cast(mouse_pos.y()) * surface_scale; g_core_thread->queueAuxiliaryRenderWindowInputEvent( m_userdata, Host::AuxiliaryRenderWindowEvent::MouseMoved, @@ -588,20 +644,19 @@ bool AuxiliaryDisplayWidget::event(QEvent* event) { QWidget::event(event); - // avoid spamming resize events for paint events (sent on move on windows) - const auto& [size, dpr] = QtUtils::GetPixelSizeForWidget(this); - if (m_last_window_size != size || m_last_window_scale != dpr) - { - DEV_LOG("Auxiliary widget resized to {}x{} (Qt {}x{}) DPR={}", size.width(), size.height(), width(), height(), - dpr); + checkForSizeChange(); + return true; + } - m_last_window_size = size; - m_last_window_scale = dpr; - g_core_thread->queueAuxiliaryRenderWindowInputEvent( - m_userdata, Host::AuxiliaryRenderWindowEvent::Resized, - Host::AuxiliaryRenderWindowEventParam{.uint_param = static_cast(size.width())}, - Host::AuxiliaryRenderWindowEventParam{.uint_param = static_cast(size.height())}, - Host::AuxiliaryRenderWindowEventParam{.float_param = static_cast(dpr)}); + case QEvent::WinIdChange: + { + QWidget::event(event); + + if (m_window_info.has_value()) + { + ERROR_LOG("Auxiliary display widget window ID changed while we had a valid WindowInfo. This is NOT expected, " + "please report."); + m_window_info.reset(); } return true; diff --git a/src/duckstation-qt/displaywidget.h b/src/duckstation-qt/displaywidget.h index d6ea2b295..ccc79df36 100644 --- a/src/duckstation-qt/displaywidget.h +++ b/src/duckstation-qt/displaywidget.h @@ -30,7 +30,8 @@ public: QPaintEngine* paintEngine() const override; - std::optional getWindowInfo(RenderAPI render_api, Error* error); + const std::optional& getWindowInfo(RenderAPI render_api, Error* error); + void clearWindowInfo(); void updateRelativeMode(bool enabled); void updateCursor(bool hidden); @@ -65,9 +66,7 @@ private: std::vector m_keys_pressed_with_modifiers; - QSize m_last_window_size; - u32 m_last_window_height = 0; - qreal m_last_window_scale = 1; + std::optional m_window_info; const char* m_window_position_key = nullptr; }; @@ -96,6 +95,8 @@ public: QPaintEngine* paintEngine() const override; + const std::optional& getWindowInfo(RenderAPI render_api, Error* error); + static AuxiliaryDisplayWidget* create(s32 pos_x, s32 pos_y, u32 width, u32 height, const QString& title, const QString& icon_name, void* userdata); void destroy(); @@ -104,8 +105,9 @@ protected: bool event(QEvent* event) override; private: + void checkForSizeChange(); + void* m_userdata = nullptr; - QSize m_last_window_size; - qreal m_last_window_scale = 1; + std::optional m_window_info; bool m_destroying = false; }; diff --git a/src/duckstation-qt/mainwindow.cpp b/src/duckstation-qt/mainwindow.cpp index d6a3772f3..3ef78b481 100644 --- a/src/duckstation-qt/mainwindow.cpp +++ b/src/duckstation-qt/mainwindow.cpp @@ -247,6 +247,9 @@ std::optional MainWindow::acquireRenderWindow(RenderAPI render_api, DEV_LOG("Toggling to {} without recreating surface", (fullscreen ? "fullscreen" : "windowed")); m_exclusive_fullscreen_requested = exclusive_fullscreen; + // in case it gets a new native handle + m_display_widget->clearWindowInfo(); + // ensure it's resizable when changing size, we'll fix it up later in updateWindowState() QtUtils::SetWindowResizeable(container, true); @@ -295,7 +298,7 @@ std::optional MainWindow::acquireRenderWindow(RenderAPI render_api, QGuiApplication::processEvents(QEventLoop::ExcludeUserInputEvents); #endif - const std::optional wi = m_display_widget->getWindowInfo(render_api, error); + const std::optional& wi = m_display_widget->getWindowInfo(render_api, error); if (!wi.has_value()) { destroyDisplayWidget(); @@ -438,6 +441,8 @@ void MainWindow::destroyDisplayWidget() { if (m_display_widget) { + m_display_widget->clearWindowInfo(); + if (!isRenderingFullscreen() && !isRenderingToMain()) saveDisplayWindowGeometryToConfig(); @@ -3267,7 +3272,7 @@ bool MainWindow::onCreateAuxiliaryRenderWindow(RenderAPI render_api, qint32 x, q PlatformMisc::SetWindowRoundedCornerState(reinterpret_cast(widget->winId()), false); #endif - const std::optional owi = QtUtils::GetWindowInfoForWidget(widget, render_api, error); + const std::optional& owi = widget->getWindowInfo(render_api, error); if (!owi.has_value()) { widget->destroy(); diff --git a/src/duckstation-qt/qtwindowinfo.cpp b/src/duckstation-qt/qtwindowinfo.cpp index c2a88c09b..342578bbe 100644 --- a/src/duckstation-qt/qtwindowinfo.cpp +++ b/src/duckstation-qt/qtwindowinfo.cpp @@ -25,44 +25,6 @@ LOG_CHANNEL(Host); -std::pair QtUtils::GetPixelSizeForWidget(const QWidget* widget) -{ - // Why this nonsense? Qt's device independent sizes are integer, and fractional scaling is lossy. - // We can't get back the "real" size of the window. So we have to platform natively query the actual client size. -#if defined(_WIN32) - if (RECT rc; GetClientRect(reinterpret_cast(widget->winId()), &rc)) - { - const qreal device_pixel_ratio = widget->devicePixelRatio(); - return std::make_pair(QSize(static_cast(rc.right - rc.left), static_cast(rc.bottom - rc.top)), - device_pixel_ratio); - } -#elif defined(__APPLE__) - if (Core::GetBaseBoolSettingValue("Main", "UseFractionalWindowScale", true)) - { - if (const std::optional real_device_pixel_ratio = - CocoaTools::GetViewRealScalingFactor(reinterpret_cast(widget->winId()))) - { - const qreal device_pixel_ratio = static_cast(real_device_pixel_ratio.value()); - return std::make_pair(ApplyDevicePixelRatioToSize(widget->size(), device_pixel_ratio), device_pixel_ratio); - } - } - else - { - if (std::optional> size = - CocoaTools::GetViewSizeInPixels(reinterpret_cast(widget->winId()))) - { - const qreal device_pixel_ratio = widget->devicePixelRatio(); - return std::make_pair(QSize(size->first, size->second), device_pixel_ratio); - } - } -#endif - - // On Linux, fuck you, enjoy round trip to the X server, and on Wayland you can't query it in the first place... - // I ain't dealing with this crap OS. Enjoy your mismatched sizes and shit experience. - const qreal device_pixel_ratio = widget->devicePixelRatio(); - return std::make_pair(ApplyDevicePixelRatioToSize(widget->size(), device_pixel_ratio), device_pixel_ratio); -} - WindowInfoType QtUtils::GetWindowInfoType() { // Windows and Apple are easy here since there's no display connection. @@ -91,7 +53,7 @@ WindowInfoType QtUtils::GetWindowInfoType() std::optional QtUtils::GetWindowInfoForWidget(QWidget* widget, RenderAPI render_api, Error* error) { - WindowInfo wi; + WindowInfo wi = {}; // Windows and Apple are easy here since there's no display connection. #if defined(_WIN32) @@ -134,10 +96,7 @@ std::optional QtUtils::GetWindowInfoForWidget(QWidget* widget, Rende } #endif - const auto& [size, dpr] = GetPixelSizeForWidget(widget); - wi.surface_width = static_cast(size.width()); - wi.surface_height = static_cast(size.height()); - wi.surface_scale = static_cast(dpr); + UpdateSurfaceSize(widget, &wi); // Query refresh rate, we need it for sync. Error refresh_rate_error; @@ -160,3 +119,51 @@ std::optional QtUtils::GetWindowInfoForWidget(QWidget* widget, Rende return wi; } + +void QtUtils::UpdateSurfaceSize(QWidget* widget, WindowInfo* wi) +{ + // Why this nonsense? Qt's device independent sizes are integer, and fractional scaling is lossy. + // We can't get back the "real" size of the window. So we have to platform natively query the actual client size. +#if defined(_WIN32) + if (RECT rc; GetClientRect(static_cast(wi->window_handle), &rc)) + { + const qreal device_pixel_ratio = widget->devicePixelRatio(); + wi->surface_width = static_cast(rc.right - rc.left); + wi->surface_height = static_cast(rc.bottom - rc.top); + wi->surface_scale = static_cast(device_pixel_ratio); + } +#elif defined(__APPLE__) + if (Core::GetBaseBoolSettingValue("Main", "UseFractionalWindowScale", true)) + { + if (const std::optional real_device_pixel_ratio = CocoaTools::GetViewRealScalingFactor(wi->window_handle)) + { + const qreal device_pixel_ratio = static_cast(real_device_pixel_ratio.value()); + const QSize scaled_size = ApplyDevicePixelRatioToSize(widget->size(), device_pixel_ratio); + wi->surface_width = static_cast(scaled_size.width()); + wi->surface_height = static_cast(scaled_size.height()); + wi->surface_scale = static_cast(device_pixel_ratio); + return; + } + } + else + { + if (std::optional> size = + CocoaTools::GetViewSizeInPixels(reinterpret_cast(widget->winId()))) + { + const qreal device_pixel_ratio = widget->devicePixelRatio(); + wi->surface_width = static_cast(size->first); + wi->surface_height = static_cast(size->second); + wi->surface_scale = static_cast(device_pixel_ratio); + return; + } + } +#endif + + // On Linux, fuck you, enjoy round trip to the X server, and on Wayland you can't query it in the first place... + // I ain't dealing with this crap OS. Enjoy your mismatched sizes and shit experience. + const qreal device_pixel_ratio = widget->devicePixelRatio(); + const QSize scaled_size = ApplyDevicePixelRatioToSize(widget->size(), device_pixel_ratio); + wi->surface_width = static_cast(scaled_size.width()); + wi->surface_height = static_cast(scaled_size.height()); + wi->surface_scale = static_cast(device_pixel_ratio); +} diff --git a/src/duckstation-qt/qtwindowinfo.h b/src/duckstation-qt/qtwindowinfo.h index 00baa38b2..d2f346bbe 100644 --- a/src/duckstation-qt/qtwindowinfo.h +++ b/src/duckstation-qt/qtwindowinfo.h @@ -13,14 +13,14 @@ class QWidget; namespace QtUtils { -/// Returns the pixel size (real geometry) for a widget. -/// Also returns the "real" DPR scale for the widget, ignoring any operating-system level downsampling. -std::pair GetPixelSizeForWidget(const QWidget* widget); - /// Returns the window type for the current Qt platform. WindowInfoType GetWindowInfoType(); /// Returns the common window info structure for a Qt widget. std::optional GetWindowInfoForWidget(QWidget* widget, RenderAPI render_api, Error* error = nullptr); +/// Calculates the pixel size (real geometry) for a widget. +/// Also sets the "real" DPR scale for the widget, ignoring any operating-system level downsampling. +void UpdateSurfaceSize(QWidget* widget, WindowInfo* wi); + } // namespace QtUtils