Preserve stock-frame VSync on faster displays

This commit is contained in:
Matthew Stanley
2026-10-19 17:59:05 -07:00
parent 68250e202b
commit 03e635e86d
5 changed files with 79 additions and 19 deletions
+3
View File
@@ -1194,6 +1194,9 @@ if(BUILD_TESTING)
# Bug B: the pacing decision must take ONE counter sample, so the unsigned
# subtraction cannot underflow into a ~24.7-day SDL_Delay.
add_executable(frame_pacing_test tests/test_frame_pacing.c)
add_executable(present_sync_test tests/test_present_sync.c)
target_include_directories(present_sync_test PRIVATE include)
add_test(NAME present_sync_test COMMAND present_sync_test)
target_include_directories(frame_pacing_test PRIVATE include)
add_test(NAME frame_pacing_test COMMAND frame_pacing_test)
# Render thread core ([video] render_thread, docs/RENDER_THREAD.md):
+6 -6
View File
@@ -45,14 +45,14 @@ uint32_t frame_pacing_sleep_ms(uint64_t now, uint64_t deadline,
* period in milliseconds (e.g. 1000.0 / 59.94). */
void frame_pacer_wait(FramePacer *p, double period_ms);
/* 1 while the display driver's swap block — not the pacer above — is what holds
* the guest to its vblank cadence (present_vsync_owns_cadence() in main.cpp).
* The two are XOR: when this returns 1 the frontend does not call
* frame_pacer_wait(), so the ONLY thing throttling the guest is the swap.
/* 1 when the panel matches guest cadence and synchronized swaps can supply
* the guest's VBlank clock (present_vsync_owns_cadence() in main.cpp). The
* deadline pacer still caps speed if the driver does not actually block.
* Faster panels can also synchronize swaps while the pacer owns guest timing;
* this query then returns 0.
*
* A renderer that elides a redundant swap (unchanged front buffer) must
* therefore not do so while this is 1: the elided frame blocks on nothing, and
* a game that presents every other vblank runs at 2x. */
* therefore retain it while this is 1 to preserve display-clock pacing. */
int psx_present_vsync_owns_cadence(void);
#ifdef __cplusplus
+22
View File
@@ -0,0 +1,22 @@
#ifndef PSX_PRESENT_SYNC_H
#define PSX_PRESENT_SYNC_H
/* Swap synchronization prevents tearing; the wall-clock pacer limits guest
* speed. They need not have the same rate. A faster panel can synchronize
* stock frames without becoming the guest's clock. A slower/unknown panel
* cannot block every guest frame without slowing emulation.
*
* blocked covers interpolation's own scheduler, netplay, driver recovery,
* and hosts whose swap blocking is unreliable. Mode 2 is the existing VRR
* frame-generation mode, which uses immediate swaps. */
static inline int psx_present_sync_interval(int requested, int blocked,
double guest_hz, double host_hz) {
if (blocked || (requested != 1 && requested != -1))
return 0;
if (!(guest_hz > 0.0) || !(host_hz > 0.0))
return 0;
/* Same 2% tolerance as the frontend's host/guest cadence match. */
return host_hz >= guest_hz * 0.98 ? requested : 0;
}
#endif
+15 -13
View File
@@ -62,6 +62,7 @@ extern "C" void psx_event_step_conservative_env_init(void);
#include "../third_party/stb_image.h"
#include "gpu_vk_renderer.h"
#include "frame_pacing.h"
#include "present_sync.h"
#include "latency_ring.h"
#include "sio.h"
#ifndef PSX_MAX_PLAYERS
@@ -1459,10 +1460,10 @@ static int g_fmv_skip_no_xa_hold = 4;
* (1=vsync/tear-free, 0=immediate/lowest display latency+tearing, -1=adaptive);
* it trims the display-side scanout latency the CPU-side ring can't see.
*
* Driver vsync and the wall-clock pacer are XOR. Waiting on both (pacer then
* FIFO SwapBuffers) double-blocks on Linux compositors: ~16.7 ms + ~16.7 ms
* = 30 Hz / 0.50x with the CPU idle. ~60 Hz panels may use vsync as the clock;
* otherwise the pacer holds 59.94 Hz and present must not wait on the swap. */
* A deadline pacer caps guest speed even if the driver ignores swap blocking.
* A matching panel can supply the guest clock; a faster panel synchronizes
* scanout while the pacer retains the guest cadence. Do not equate clock
* ownership with the swap interval or high-refresh fullscreen can tear. */
static int g_low_latency_input = 1;
/* [video] render_thread (docs/RENDER_THREAD.md): the GL backend runs on its
* own thread. Started at the first vblank once the context and every startup
@@ -3611,15 +3612,12 @@ extern "C" int psx_present_vsync_owns_cadence(void) {
}
static int present_effective_swap_interval(void) {
if (g_netplay_vsync_forced_off || psx_netplay_active())
return 0;
if (g_frame_interpolation)
return 0;
if (g_frame_period_ms <= 0.0)
return 0;
if (present_vsync_owns_cadence())
return g_video_vsync;
return 0;
const int blocked = g_netplay_vsync_forced_off || psx_netplay_active() ||
g_frame_interpolation || g_present_vsync_disabled ||
host_driver_vsync_unreliable();
return psx_present_sync_interval(g_video_vsync, blocked,
g_frame_period_ms > 0.0 ? 1000.0 / g_frame_period_ms : 0.0,
g_host_refresh_hz);
}
static int present_should_wall_pace(void) {
@@ -3678,6 +3676,10 @@ static void log_present_cadence(void) {
std::printf("psxrecomp: present cadence: wall-clock pacer "
"(%.4f ms/frame); driver vsync off under WSL\n",
g_frame_period_ms);
} else if (present_effective_swap_interval() != 0) {
std::printf("psxrecomp: present cadence: wall-clock pacer "
"(%.4f ms/frame); synchronized swaps on %.0f Hz panel\n",
g_frame_period_ms, g_host_refresh_hz);
} else if (g_host_refresh_hz > 0.0) {
std::printf("psxrecomp: present cadence: wall-clock pacer "
"(%.4f ms/frame); driver vsync off on %.0f Hz panel\n",
+33
View File
@@ -0,0 +1,33 @@
#include "present_sync.h"
#include <stdio.h>
static int failures;
static void check(int requested, int blocked, double guest, double host,
int expected) {
const int actual = psx_present_sync_interval(requested, blocked, guest, host);
if (actual != expected) {
fprintf(stderr, "sync(%d,%d,%.3f,%.3f): got %d expected %d\n",
requested, blocked, guest, host, actual, expected);
++failures;
}
}
int main(void) {
const double panels[] = {60, 75, 120, 144, 165, 240};
for (unsigned i = 0; i < sizeof panels / sizeof panels[0]; ++i) {
check(1, 0, 59.94, panels[i], 1);
check(1, 0, 50, panels[i], 1); /* PAL on a faster panel */
check(-1, 0, 59.94, panels[i], -1); /* explicit adaptive */
check(0, 0, 59.94, panels[i], 0); /* explicit VSync off */
check(2, 0, 59.94, panels[i], 0); /* VRR frame generation */
check(1, 1, 59.94, panels[i], 0); /* scheduler/netplay/recovery */
}
check(1, 0, 50, 50, 1);
check(1, 0, 60, 59, 1);
check(1, 0, 59.94, 50, 0); /* do not slow the guest to the panel */
check(1, 0, 120, 60, 0);
check(1, 0, 59.94, 0, 0); /* unknown display */
check(1, 0, 0, 144, 0); /* uncapped guest */
puts(failures ? "present sync FAILED" : "present sync passed (42 cases)");
return failures ? 1 : 0;
}