ui: expose the current display refresh rate from the window layer

This commit is contained in:
mchughalex 2026-07-19 15:33:59 +01:00
parent fc70322f3d
commit 649c9e73a7
6 changed files with 74 additions and 0 deletions

View file

@ -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

View file

@ -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;

View file

@ -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;

View file

@ -10,6 +10,7 @@
*/
#include <algorithm>
#include <atomic>
#include <iterator>
#include <rex/assert.h>
@ -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<float> 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);

View file

@ -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;

View file

@ -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