From 649c9e73a707371a10f909dd7740f4e1b35e2edc Mon Sep 17 00:00:00 2001 From: mchughalex <25671999+mchughalex@users.noreply.github.com> Date: Sun, 19 Jul 2026 15:33:59 +0100 Subject: [PATCH] ui: expose the current display refresh rate from the window layer --- include/rex/ui/window.h | 13 +++++++++++++ include/rex/ui/window_sdl.h | 1 + include/rex/ui/window_win.h | 1 + src/ui/window.cpp | 24 ++++++++++++++++++++++++ src/ui/window_sdl.cpp | 12 ++++++++++++ src/ui/window_win.cpp | 23 +++++++++++++++++++++++ 6 files changed, 74 insertions(+) diff --git a/include/rex/ui/window.h b/include/rex/ui/window.h index 36ef5ae..90c49b2 100644 --- a/include/rex/ui/window.h +++ b/include/rex/ui/window.h @@ -217,6 +217,11 @@ class Window { return dpi ? dpi : GetMediumDpi(); } + // Last known refresh rate (Hz) of the display a window occupies, 0 while + // unknown. Published by the common code on window open and focus gain; + // readable from any thread (frame pacers, settings UI). + static float CachedDisplayRefreshHz(); + // Round trips are not guaranteed to return the same results. static constexpr uint32_t ConvertSizeDpi(uint32_t size, uint32_t new_dpi, uint32_t old_dpi) { // Always rounding up to prevent zero sizes (unless the input is zero) as @@ -443,6 +448,14 @@ class Window { // medium DPI (0 returned from it will also be treated as medium DPI). virtual uint32_t GetLatestDpiImpl() const { return GetMediumDpi(); } + // Refresh rate (Hz) of the display the window currently occupies, or 0 when + // the platform can't report it. UI thread only (native display queries); + // consumers on other threads read the cache below. + virtual float QueryDisplayRefreshHzImpl() const { return 0.0f; } + // Re-query the display refresh rate and publish it to the process-wide + // cache (UI thread; called by the common code on open and focus gain). + void UpdateCachedDisplayRefresh(); + // Deletion of the window may (and must) not happen in OpenImpl, the listeners // are deferred, so there's no need to use WindowDestructionReceiver in it. // In case of failure, the implementation must not leave itself in an diff --git a/include/rex/ui/window_sdl.h b/include/rex/ui/window_sdl.h index 2e8c734..644fb47 100644 --- a/include/rex/ui/window_sdl.h +++ b/include/rex/ui/window_sdl.h @@ -29,6 +29,7 @@ class SDLWindow final : public Window { protected: uint32_t GetLatestDpiImpl() const override; + float QueryDisplayRefreshHzImpl() const override; bool OpenImpl() override; void RequestCloseImpl() override; diff --git a/include/rex/ui/window_win.h b/include/rex/ui/window_win.h index 0b0bb85..c429a97 100644 --- a/include/rex/ui/window_win.h +++ b/include/rex/ui/window_win.h @@ -51,6 +51,7 @@ class Win32Window : public Window { void RequestCloseImpl() override; uint32_t GetLatestDpiImpl() const override; + float QueryDisplayRefreshHzImpl() const override; void ApplyNewFullscreen() override; void ApplyNewTitle() override; diff --git a/src/ui/window.cpp b/src/ui/window.cpp index 784a604..1a66d42 100644 --- a/src/ui/window.cpp +++ b/src/ui/window.cpp @@ -10,6 +10,7 @@ */ #include +#include #include #include @@ -60,6 +61,23 @@ REXCVAR_DEFINE_DOUBLE(video_mode_refresh_rate, 60.0, "GPU", "Guest video mode re namespace rex { namespace ui { +namespace { +// Process-wide display refresh cache (see Window::CachedDisplayRefreshHz): +// written on the UI thread, read from any thread (frame pacers, settings UI). +std::atomic g_display_refresh_hz{0.0f}; +} // namespace + +float Window::CachedDisplayRefreshHz() { + return g_display_refresh_hz.load(std::memory_order_relaxed); +} + +void Window::UpdateCachedDisplayRefresh() { + const float hz = QueryDisplayRefreshHzImpl(); + if (hz > 0.0f) { + g_display_refresh_hz.store(hz, std::memory_order_relaxed); + } +} + Window::Window(WindowedAppContext& app_context, const std::string_view title, uint32_t desired_logical_width, uint32_t desired_logical_height) : app_context_(app_context), @@ -221,6 +239,7 @@ bool Window::Open() { return true; } phase_ = Phase::kOpen; + UpdateCachedDisplayRefresh(); // Call the listeners (OnOpened with all the new state so the listeners are // aware that they can start interacting with the open Window, and after that, @@ -541,6 +560,11 @@ void Window::OnFocusUpdate(bool new_has_focus, WindowDestructionReceiver& destru return; } has_focus_ = new_has_focus; + if (has_focus_) { + // Focus gain is the cheapest reliable moment to notice the window moved + // to a display with a different refresh rate. + UpdateCachedDisplayRefresh(); + } UISetupEvent e(this); if (has_focus_) { SendEventToListeners([&e](auto listener) { listener->OnGotFocus(e); }, destruction_receiver); diff --git a/src/ui/window_sdl.cpp b/src/ui/window_sdl.cpp index 4190bb7..5a0f46f 100644 --- a/src/ui/window_sdl.cpp +++ b/src/ui/window_sdl.cpp @@ -406,6 +406,18 @@ void SDLWindow::HandleSDLEvent(const SDL_Event& event) { uint32_t SDLWindow::GetLatestDpiImpl() const { return dpi_ ? dpi_ : kDefaultDpi; } +float SDLWindow::QueryDisplayRefreshHzImpl() const { + if (!window_) { + return 0.0f; + } + const SDL_DisplayID display = SDL_GetDisplayForWindow(window_); + if (!display) { + return 0.0f; + } + const SDL_DisplayMode* mode = SDL_GetCurrentDisplayMode(display); + return mode != nullptr && mode->refresh_rate > 0.0f ? mode->refresh_rate : 0.0f; +} + bool SDLWindow::OpenImpl() { SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1"); SDL_WindowFlags flags = SDL_WINDOW_RESIZABLE; diff --git a/src/ui/window_win.cpp b/src/ui/window_win.cpp index dd9c243..f67ae6a 100644 --- a/src/ui/window_win.cpp +++ b/src/ui/window_win.cpp @@ -403,6 +403,29 @@ uint32_t Win32Window::GetLatestDpiImpl() const { return dpi_; } +float Win32Window::QueryDisplayRefreshHzImpl() const { + if (!hwnd_) { + return 0.0f; + } + HMONITOR monitor = MonitorFromWindow(hwnd_, MONITOR_DEFAULTTONEAREST); + if (!monitor) { + return 0.0f; + } + MONITORINFOEXW mi = {}; + mi.cbSize = sizeof(mi); + if (!GetMonitorInfoW(monitor, &mi)) { + return 0.0f; + } + DEVMODEW dm = {}; + dm.dmSize = sizeof(dm); + if (!EnumDisplaySettingsW(mi.szDevice, ENUM_CURRENT_SETTINGS, &dm) || + !(dm.dmFields & DM_DISPLAYFREQUENCY) || dm.dmDisplayFrequency <= 1) { + // 0 and 1 are the documented "hardware default" sentinels. + return 0.0f; + } + return float(dm.dmDisplayFrequency); +} + void Win32Window::ApplyNewFullscreen() { // Various functions here may send messages that may result in the // listeners being invoked, and potentially cause the destruction of the