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.
This commit is contained in:
Stenzek
2025-12-28 01:33:44 +10:00
parent a2d95a1ba5
commit fa23d16c3c
5 changed files with 163 additions and 94 deletions

View File

@@ -51,15 +51,38 @@ DisplayWidget::DisplayWidget(QWidget* parent) : QWidget(parent)
DisplayWidget::~DisplayWidget() = default;
std::optional<WindowInfo> DisplayWidget::getWindowInfo(RenderAPI render_api, Error* error)
const std::optional<WindowInfo>& DisplayWidget::getWindowInfo(RenderAPI render_api, Error* error)
{
std::optional<WindowInfo> 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<float>(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<float>(devicePixelRatio());
const QPoint mouse_pos = static_cast<QMouseEvent*>(event)->pos();
const float scaled_x = static_cast<float>(static_cast<qreal>(mouse_pos.x()) * m_last_window_scale);
const float scaled_y = static_cast<float>(static_cast<qreal>(mouse_pos.y()) * m_last_window_scale);
const float scaled_x = static_cast<float>(mouse_pos.x()) * surface_scale;
const float scaled_y = static_cast<float>(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<WindowInfo>& 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<u32>(m_window_info->surface_width)},
Host::AuxiliaryRenderWindowEventParam{.uint_param = static_cast<u32>(m_window_info->surface_height)},
Host::AuxiliaryRenderWindowEventParam{.float_param = static_cast<float>(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<QMouseEvent*>(event)->pos();
const float scaled_x = static_cast<float>(static_cast<qreal>(mouse_pos.x()) * m_last_window_scale);
const float scaled_y = static_cast<float>(static_cast<qreal>(mouse_pos.y()) * m_last_window_scale);
const float surface_scale =
m_window_info.has_value() ? m_window_info->surface_scale : static_cast<float>(devicePixelRatio());
const float scaled_x = static_cast<float>(mouse_pos.x()) * surface_scale;
const float scaled_y = static_cast<float>(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<u32>(size.width())},
Host::AuxiliaryRenderWindowEventParam{.uint_param = static_cast<u32>(size.height())},
Host::AuxiliaryRenderWindowEventParam{.float_param = static_cast<float>(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;

View File

@@ -30,7 +30,8 @@ public:
QPaintEngine* paintEngine() const override;
std::optional<WindowInfo> getWindowInfo(RenderAPI render_api, Error* error);
const std::optional<WindowInfo>& getWindowInfo(RenderAPI render_api, Error* error);
void clearWindowInfo();
void updateRelativeMode(bool enabled);
void updateCursor(bool hidden);
@@ -65,9 +66,7 @@ private:
std::vector<int> m_keys_pressed_with_modifiers;
QSize m_last_window_size;
u32 m_last_window_height = 0;
qreal m_last_window_scale = 1;
std::optional<WindowInfo> m_window_info;
const char* m_window_position_key = nullptr;
};
@@ -96,6 +95,8 @@ public:
QPaintEngine* paintEngine() const override;
const std::optional<WindowInfo>& 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<WindowInfo> m_window_info;
bool m_destroying = false;
};

View File

@@ -247,6 +247,9 @@ std::optional<WindowInfo> 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<WindowInfo> MainWindow::acquireRenderWindow(RenderAPI render_api,
QGuiApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
#endif
const std::optional<WindowInfo> wi = m_display_widget->getWindowInfo(render_api, error);
const std::optional<WindowInfo>& 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<void*>(widget->winId()), false);
#endif
const std::optional<WindowInfo> owi = QtUtils::GetWindowInfoForWidget(widget, render_api, error);
const std::optional<WindowInfo>& owi = widget->getWindowInfo(render_api, error);
if (!owi.has_value())
{
widget->destroy();

View File

@@ -25,44 +25,6 @@
LOG_CHANNEL(Host);
std::pair<QSize, qreal> 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<HWND>(widget->winId()), &rc))
{
const qreal device_pixel_ratio = widget->devicePixelRatio();
return std::make_pair(QSize(static_cast<int>(rc.right - rc.left), static_cast<int>(rc.bottom - rc.top)),
device_pixel_ratio);
}
#elif defined(__APPLE__)
if (Core::GetBaseBoolSettingValue("Main", "UseFractionalWindowScale", true))
{
if (const std::optional<double> real_device_pixel_ratio =
CocoaTools::GetViewRealScalingFactor(reinterpret_cast<void*>(widget->winId())))
{
const qreal device_pixel_ratio = static_cast<qreal>(real_device_pixel_ratio.value());
return std::make_pair(ApplyDevicePixelRatioToSize(widget->size(), device_pixel_ratio), device_pixel_ratio);
}
}
else
{
if (std::optional<std::pair<int, int>> size =
CocoaTools::GetViewSizeInPixels(reinterpret_cast<void*>(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<WindowInfo> 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<WindowInfo> QtUtils::GetWindowInfoForWidget(QWidget* widget, Rende
}
#endif
const auto& [size, dpr] = GetPixelSizeForWidget(widget);
wi.surface_width = static_cast<u16>(size.width());
wi.surface_height = static_cast<u16>(size.height());
wi.surface_scale = static_cast<float>(dpr);
UpdateSurfaceSize(widget, &wi);
// Query refresh rate, we need it for sync.
Error refresh_rate_error;
@@ -160,3 +119,51 @@ std::optional<WindowInfo> 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<HWND>(wi->window_handle), &rc))
{
const qreal device_pixel_ratio = widget->devicePixelRatio();
wi->surface_width = static_cast<u16>(rc.right - rc.left);
wi->surface_height = static_cast<u16>(rc.bottom - rc.top);
wi->surface_scale = static_cast<float>(device_pixel_ratio);
}
#elif defined(__APPLE__)
if (Core::GetBaseBoolSettingValue("Main", "UseFractionalWindowScale", true))
{
if (const std::optional<double> real_device_pixel_ratio = CocoaTools::GetViewRealScalingFactor(wi->window_handle))
{
const qreal device_pixel_ratio = static_cast<qreal>(real_device_pixel_ratio.value());
const QSize scaled_size = ApplyDevicePixelRatioToSize(widget->size(), device_pixel_ratio);
wi->surface_width = static_cast<u16>(scaled_size.width());
wi->surface_height = static_cast<u16>(scaled_size.height());
wi->surface_scale = static_cast<float>(device_pixel_ratio);
return;
}
}
else
{
if (std::optional<std::pair<int, int>> size =
CocoaTools::GetViewSizeInPixels(reinterpret_cast<void*>(widget->winId())))
{
const qreal device_pixel_ratio = widget->devicePixelRatio();
wi->surface_width = static_cast<u16>(size->first);
wi->surface_height = static_cast<u16>(size->second);
wi->surface_scale = static_cast<float>(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<u16>(scaled_size.width());
wi->surface_height = static_cast<u16>(scaled_size.height());
wi->surface_scale = static_cast<float>(device_pixel_ratio);
}

View File

@@ -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<QSize, qreal> 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<WindowInfo> 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