diff --git a/include/rex/ui/window.h b/include/rex/ui/window.h index 90c49b2..7810986 100644 --- a/include/rex/ui/window.h +++ b/include/rex/ui/window.h @@ -222,6 +222,15 @@ class Window { // readable from any thread (frame pacers, settings UI). static float CachedDisplayRefreshHz(); + // Refresh-derived automatic frame cap for the given display refresh rate, + // 0 while the refresh is unknown. Presents run with tearing allowed, so on + // a VRR display any present arriving faster than the panel's minimum + // refresh period tears; the cap must sit far enough below the refresh rate + // that swap-to-present jitter cannot cross it. A fixed margin that is + // ample at 60 Hz is too thin at 144 Hz, so the margin scales with the + // refresh rate. Single source of the policy for pacers and settings UI. + static float AutoFrameCapHz(float refresh_hz); + // 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 diff --git a/src/ui/overlay/simple_settings_overlay.cpp b/src/ui/overlay/simple_settings_overlay.cpp index 1327c6c..c22345f 100644 --- a/src/ui/overlay/simple_settings_overlay.cpp +++ b/src/ui/overlay/simple_settings_overlay.cpp @@ -1351,10 +1351,12 @@ void SimpleSettingsDialog::BuildRows(std::vector& rows, int category) { "Auto tracks the current display and does exactly that. Frames above the " "refresh rate cannot be shown and only make steady motion judder."; if (FrameCapHasAuto()) { - const float hz = rex::ui::Window::CachedDisplayRefreshHz(); + // Same policy the guest pacer applies (see Window::AutoFrameCapHz). + const float auto_cap = + rex::ui::Window::AutoFrameCapHz(rex::ui::Window::CachedDisplayRefreshHz()); row.options.push_back( - hz >= 30.0f - ? "Auto (" + std::to_string(int(hz) - 4) + " FPS)" + auto_cap > 0.0f + ? "Auto (" + std::to_string(int(auto_cap)) + " FPS)" : std::string("Auto")); } for (size_t i = 1; i < kFrameCapLabels.size(); ++i) { diff --git a/src/ui/window.cpp b/src/ui/window.cpp index 1a66d42..11a6e15 100644 --- a/src/ui/window.cpp +++ b/src/ui/window.cpp @@ -11,6 +11,7 @@ #include #include +#include #include #include @@ -78,6 +79,17 @@ void Window::UpdateCachedDisplayRefresh() { } } +float Window::AutoFrameCapHz(float refresh_hz) { + if (refresh_hz < 30.0f) { + return 0.0f; + } + // 4 FPS of margin or 5% of the refresh rate, whichever is larger (see the + // declaration): 4 FPS is 6.7% of headroom at 60 Hz but only 0.2 ms of + // present slack at 144 Hz, within routine thread-scheduling jitter. + const float margin = std::max(4.0f, refresh_hz * 0.05f); + return std::floor(refresh_hz - margin); +} + Window::Window(WindowedAppContext& app_context, const std::string_view title, uint32_t desired_logical_width, uint32_t desired_logical_height) : app_context_(app_context),