From 2b7f1db676447962c466ca209a1aad85510434cf Mon Sep 17 00:00:00 2001 From: Shokunin <30949000+tetrisgm@users.noreply.github.com> Date: Wed, 7 Oct 2026 13:09:20 -0700 Subject: [PATCH] VRR present mode: [video] vsync = "vrr" Variable refresh (G-Sync/FreeSync, ProMotion): present each frame when it is ready (swap interval 0). Smooth motion targets the panel's maximum refresh at the current size (psx_sdl_display_max_refresh) and never exceeds it. PSX_HOST_REFRESH_HZ / PSX_HOST_REFRESH_MAX_HZ simulate a display for headless tests. Co-Authored-By: Claude Opus 5.5 --- recompiler/src/config_loader.cpp | 6 +++++- runtime/include/psx_sdl.h | 32 ++++++++++++++++++++++++++++++++ runtime/src/main.cpp | 22 +++++++++++++++++++--- 3 files changed, 56 insertions(+), 4 deletions(-) diff --git a/recompiler/src/config_loader.cpp b/recompiler/src/config_loader.cpp index f6214cfc..defabaa7 100644 --- a/recompiler/src/config_loader.cpp +++ b/recompiler/src/config_loader.cpp @@ -867,8 +867,12 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path& if (mode == "on" || mode == "vsync") rt.video_vsync = 1; else if (mode == "off" || mode == "immediate") rt.video_vsync = 0; else if (mode == "adaptive") rt.video_vsync = -1; + // vrr: variable refresh (G-Sync/FreeSync, ProMotion): present + // each frame when it is ready (swap interval 0), Smooth motion + // targets the panel's maximum refresh and never exceeds it. + else if (mode == "vrr") rt.video_vsync = 2; else throw std::runtime_error(fmt::format( - "[video] vsync must be \"on\"|\"off\"|\"immediate\"|\"adaptive\": {}", mode)); + "[video] vsync must be \"on\"|\"off\"|\"immediate\"|\"adaptive\"|\"vrr\": {}", mode)); } if (video.contains("frame_interpolation")) { rt.video_frame_interpolation = diff --git a/runtime/include/psx_sdl.h b/runtime/include/psx_sdl.h index 343ff113..74a7cb6d 100644 --- a/runtime/include/psx_sdl.h +++ b/runtime/include/psx_sdl.h @@ -137,6 +137,25 @@ static inline int psx_sdl_get_current_display_mode( return 0; } +/* The highest refresh the display offers at the current mode's size (a VRR + * or ProMotion panel's maximum); 0 when unknown. */ +static inline double psx_sdl_display_max_refresh(SDL_DisplayID display) +{ + if (!display) display = SDL_GetPrimaryDisplay(); + const SDL_DisplayMode *cur = SDL_GetCurrentDisplayMode(display); + double best = cur ? (double)cur->refresh_rate : 0.0; + int count = 0; + SDL_DisplayMode **modes = SDL_GetFullscreenDisplayModes(display, &count); + if (modes) { + for (int i = 0; i < count; ++i) + if (modes[i] && cur && modes[i]->w == cur->w && modes[i]->h == cur->h && + (double)modes[i]->refresh_rate > best) + best = (double)modes[i]->refresh_rate; + SDL_free(modes); + } + return best; +} + /* Opt-in high-pixel-density game window. Without it, SDL3 on a Retina Mac * gives the GL context a drawable in POINTS (half the panel resolution) and * the compositor stretches it, so any internal resolution above that is @@ -269,4 +288,17 @@ static inline int psx_sdl_display_pixel_height(SDL_Window *window) return mode.h; } +static inline double psx_sdl_display_max_refresh(int display) +{ + SDL_DisplayMode cur, m; + if (display < 0) display = 0; + if (SDL_GetCurrentDisplayMode(display, &cur) != 0) return 0.0; + double best = (double)cur.refresh_rate; + const int n = SDL_GetNumDisplayModes(display); + for (int i = 0; i < n; ++i) + if (SDL_GetDisplayMode(display, i, &m) == 0 && m.w == cur.w && m.h == cur.h && + (double)m.refresh_rate > best) + best = (double)m.refresh_rate; + return best; +} #endif diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index d3c2326c..c86e95f7 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -1505,6 +1505,7 @@ static_assert((int)PSX_MOD_CONTROLLER_ANALOG == static_assert((int)PSX_MOD_CONTROLLER_DIGITAL == (int)PSXRecompV4::PAD_MODE_DIGITAL); static double g_host_refresh_hz = 0.0; +static double g_host_refresh_max_hz = 0.0; /* panel maximum (VRR) */ static constexpr double PSX_FRAME_PERIOD_MS = 1000.0 / 59.94; static double g_guest_frame_period_ms = PSX_FRAME_PERIOD_MS; static double g_frame_period_ms = PSX_FRAME_PERIOD_MS; @@ -3455,14 +3456,28 @@ static void refresh_host_display_cadence(int force_log, int force_probe) { } g_host_refresh_last_probe_ms = now_ms ? now_ms : 1ull; - double host_hz = 0.0; + double host_hz = 0.0, max_hz = 0.0; if (disp_idx >= 0) { SDL_DisplayMode dm; if (SDL_GetCurrentDisplayMode(disp_idx, &dm) == 0 && dm.refresh_rate > 0) { host_hz = (double)dm.refresh_rate; + /* The panel's maximum refresh at this size (VRR targets it). */ + max_hz = psx_sdl_display_max_refresh(disp_idx); } } + /* Simulated displays (headless tests): PSX_HOST_REFRESH_HZ and + * PSX_HOST_REFRESH_MAX_HZ override what SDL reports. */ + if (const char* e = std::getenv("PSX_HOST_REFRESH_HZ")) { + const double v = std::atof(e); + if (v > 0.0) host_hz = v; + } + if (const char* e = std::getenv("PSX_HOST_REFRESH_MAX_HZ")) { + const double v = std::atof(e); + if (v > 0.0) max_hz = v; + } + if (max_hz < host_hz) max_hz = host_hz; + g_host_refresh_max_hz = max_hz; const int display_changed = (disp_idx != g_host_refresh_display_idx); const int refresh_changed = @@ -3564,7 +3579,7 @@ static int host_driver_vsync_unreliable(void) { } static int present_vsync_owns_cadence(void) { - if (g_video_vsync == 0 || g_present_vsync_disabled) + if (g_video_vsync == 0 || g_video_vsync == 2 || g_present_vsync_disabled) return 0; if (host_driver_vsync_unreliable()) return 0; @@ -9290,7 +9305,8 @@ static void render_thread_vblank(void) { std::fflush(stdout); } if (g_frame_generation) - gl_renderer_frame_gen_configure(g_host_refresh_hz, + gl_renderer_frame_gen_configure(g_video_vsync == 2 && g_host_refresh_max_hz > 0.0 + ? g_host_refresh_max_hz : g_host_refresh_hz, g_guest_frame_period_ms > 0.0 ? 1000.0 / g_guest_frame_period_ms : 0.0); gl_renderer_render_thread_frame_boundary();