video: move Scanline Sync's display access behind the display server, and aim the target so the tear lands in blanking

Two things, both surfacing from the same test run.

The tear was visible on screen. The target was a fixed 15/16 of
height - line 2025 at 4K, with the visible area ending at 2160 - so it
sat 135 lines inside the picture. Worse, the wait only releases the
frame loop: the present happens core_run_time later, so the aim has to
subtract the core's own duration for the flip rather than the release
to land in blanking. c9260c895d took core_run_time as a parameter and
then discarded it, which dropped the "corelines" term the previous
implementation had for exactly this. Aim at the middle of the blanking
interval with core_run_time subtracted, which puts the present at line
2205 of 2250 whether the core takes 2 ms or 13 ms.

No smoothing on core_run_time, which is what the measurement said
rather than what was expected. The target moving is the correction and
not error - it moves because the core's duration moves, and that is
what holds the present in place; smoothing would lag a real load change
and put the tear on screen during the transitions this is meant to
absorb. The error is the frame-to-frame delta, since each frame is
aimed with the previous frame's measurement, and that measured 68-142
us on a PS2 core: 9 to 19 lines against a 90 line blanking interval.
Movement is clamped instead, because peaks reach 1268 us in steady
state and 4335 us across a geometry change - 171 and 585 lines - and
without a limit one anomalous frame spoils the next as well.

Second, the two platform primitives move behind
video_display_server_t as get_scanline and wait_vblank. The phase lock
itself is arithmetic and now compiles everywhere with no HAVE_D3DKMT in
video_driver.c; a server that implements neither reports nothing and
the feature disables itself.

Nothing to consolidate elsewhere, which is worth recording. Wayland's
presentation-time protocol reports after the fact rather than blocking;
X11 detects GLX_OML_sync_control but only uses it to pick a swap mode;
the KMS context has a page-flip handler and no drmWaitVBlank. The
equivalents exist in those platform APIs - drmWaitVBlank and
glXWaitForMscOML - but both are vblank waits with no live scanout
position, and Scanline Sync needs the position to calibrate the line
count and to target a line. The interface allows one without the other
so that a vblank-only server can still be wired up for something else.
This commit is contained in:
LibretroAdmin authored and libretroadmin committed 2026-09-02 03:27:33 +02:00
1 parent c9260c895d
commit 83762306a5
9 files changed
+128 -37

No files matched your search

+2
View File
@@ -563,5 +563,7 @@ const video_display_server_t dispserv_android = {
android_display_server_get_video_output_next,
android_display_get_metrics,
android_display_server_get_flags,
NULL, /* get_scanline */
NULL, /* wait_vblank */
"android"
};
+2
View File
@@ -694,5 +694,7 @@ const video_display_server_t dispserv_apple = {
NULL, /* get_video_output_next */
cocoa_get_metrics,
NULL, /* get_flags */
NULL, /* get_scanline */
NULL, /* wait_vblank */
"apple"
};
+2
View File
@@ -273,5 +273,7 @@ const video_display_server_t dispserv_kms = {
NULL, /* get_video_output_next */
NULL, /* get_metrics */
kms_display_server_get_flags,
NULL, /* get_scanline */
NULL, /* wait_vblank */
"kms"
};
+2
View File
@@ -99,5 +99,7 @@ const video_display_server_t dispserv_uwp = {
NULL, /* get_video_output_next */
uwp_display_server_get_metrics,
NULL, /* get_flags */
NULL, /* get_scanline */
NULL, /* wait_vblank */
"uwp"
};
+21
View File
@@ -925,6 +925,20 @@ static uint32_t win32_display_server_get_flags(void *data)
return flags;
}
#ifdef HAVE_D3DKMT
static int win32_display_server_get_scanline(void *data)
{
(void)data;
return d3dkmt_scanline_get();
}
static bool win32_display_server_wait_vblank(void *data)
{
(void)data;
return d3dkmt_wait_vblank();
}
#endif
const video_display_server_t dispserv_win32 = {
win32_display_server_init,
win32_display_server_destroy,
@@ -947,5 +961,12 @@ const video_display_server_t dispserv_win32 = {
win32_display_server_get_video_output_next,
win32_display_server_get_metrics,
win32_display_server_get_flags,
#ifdef HAVE_D3DKMT
win32_display_server_get_scanline,
win32_display_server_wait_vblank,
#else
NULL,
NULL,
#endif
"win32"
};
+2
View File
@@ -226,5 +226,7 @@ const video_display_server_t dispserv_wl = {
NULL, /* get_video_output_next */
wl_display_server_get_metrics,
NULL, /* get_flags */
NULL, /* get_scanline */
NULL, /* wait_vblank */
"wayland"
};
+2
View File
@@ -1194,5 +1194,7 @@ const video_display_server_t dispserv_x11 = {
#endif
x11_get_metrics,
x11_display_server_get_flags,
NULL, /* get_scanline */
NULL, /* wait_vblank */
"x11"
};
+21
View File
@@ -66,6 +66,24 @@ typedef struct video_display_server
bool (*get_metrics)(void *data, enum display_metric_types type,
float *value);
uint32_t (*get_flags)(void *data);
/* Display scanout timing, for Scanline Sync.
*
* get_scanline returns the current beam position in scanlines, or a
* negative value if unavailable. wait_vblank blocks until the next
* vertical blank and returns false if it cannot.
*
* Both are optional and a server may implement one without the
* other, but Scanline Sync needs get_scanline: it calibrates the
* total line count from the peak value and targets a specific line.
* A server offering only wait_vblank cannot drive it.
*
* Only win32 implements these today, through D3DKMT. The equivalents
* elsewhere are drmWaitVBlank on KMS and glXWaitForMscOML on X11 -
* both vblank waits, neither exposing a live scanout position -
* while Wayland's presentation-time protocol reports after the fact
* rather than blocking. None of them are wired up. */
int (*get_scanline)(void *data);
bool (*wait_vblank)(void *data);
const char *ident;
} video_display_server_t;
@@ -75,6 +93,9 @@ void video_display_server_destroy(void);
bool video_display_server_get_flags(gfx_ctx_flags_t *flags);
int video_display_server_get_scanline(void);
bool video_display_server_wait_vblank(void);
bool video_display_server_set_window_opacity(unsigned opacity);
bool video_display_server_set_window_progress(int progress, bool finished);
+74 -37
View File
@@ -137,6 +137,8 @@ static const video_display_server_t dispserv_null = {
NULL, /* get_video_output_next */
NULL, /* get_metrics */
NULL, /* get_flags */
NULL, /* get_scanline */
NULL, /* wait_vblank */
"null"
};
@@ -1702,6 +1704,24 @@ enum rotation video_display_server_get_screen_orientation(void)
}
int video_display_server_get_scanline(void)
{
video_driver_state_t *video_st = &video_driver_st;
if (current_display_server && current_display_server->get_scanline)
return current_display_server->get_scanline(
video_st->current_display_server_data);
return -1;
}
bool video_display_server_wait_vblank(void)
{
video_driver_state_t *video_st = &video_driver_st;
if (current_display_server && current_display_server->wait_vblank)
return current_display_server->wait_vblank(
video_st->current_display_server_data);
return false;
}
float video_display_server_get_refresh_rate(void)
{
video_driver_state_t *video_st = &video_driver_st;
@@ -6237,9 +6257,7 @@ void video_frame_delay(video_driver_state_t *video_st,
}
}
#ifdef HAVE_D3DKMT
static void scanline_pll_reset(void);
#endif
void video_driver_scanline_init(void)
{
@@ -6247,24 +6265,17 @@ void video_driver_scanline_init(void)
video_st->scanline[SCANLINE_NEXT] = 1;
video_st->scanline[SCANLINE_HOLD] = 1;
video_st->scanline[SCANLINE_TOTAL] = 0;
#ifdef HAVE_D3DKMT
/* Runs from d3dkmt_init(), so once per video (re)init. Drop the
* calibration: the line count, the period and the bias are all
* properties of the display mode and all three move when it
* changes. */
/* Once per video (re)init. Drop the calibration: the line count,
* the period and the bias are all properties of the display mode
* and all three move when it changes. */
scanline_pll_reset();
#endif
}
static INLINE int16_t video_driver_scanline_get(void)
{
#ifdef HAVE_D3DKMT
return d3dkmt_scanline_get();
#endif
return -1;
return (int16_t)video_display_server_get_scanline();
}
#ifdef HAVE_D3DKMT
/* --- Scanline Sync phase lock ------------------------------------
*
* D3DKMTGetScanLine costs ~223 us on a WDDM driver, during which the
@@ -6321,6 +6332,7 @@ typedef struct
int total; /* lines including blanking */
bool locked;
unsigned misses; /* frames that could not make their target */
int target; /* last aim point, clamped frame to frame */
} scanline_pll_t;
static scanline_pll_t scl_pll;
@@ -6328,6 +6340,7 @@ static scanline_pll_t scl_pll;
static void scanline_pll_reset(void)
{
scl_pll.locked = false;
scl_pll.target = 0;
}
static bool scanline_pll_calibrate(void)
@@ -6353,7 +6366,7 @@ static bool scanline_pll_calibrate(void)
for (i = 0; i < SCANLINE_CAL_SAMPLES; i++)
{
double t;
if (!d3dkmt_wait_vblank())
if (!video_display_server_wait_vblank())
return false;
t = (double)cpu_features_get_time_usec();
if (prev > 0.0)
@@ -6385,7 +6398,7 @@ static bool scanline_pll_calibrate(void)
{
double t, at, off, pred, d;
int16_t meas;
if (!d3dkmt_wait_vblank())
if (!video_display_server_wait_vblank())
return false;
t = (double)cpu_features_get_time_usec();
do { at = (double)cpu_features_get_time_usec(); }
@@ -6421,7 +6434,7 @@ static bool scanline_pll_calibrate(void)
/* Refresh the anchor without blocking.
*
* The obvious way to re-anchor is another d3dkmt_wait_vblank(), but
* The obvious way to re-anchor is another wait_vblank(), but
* that blocks for up to a full frame period on top of the frame's own
* wait - at SCANLINE_ANCHOR_MAXAGE of 30 that was four visible hitches
* a second, and it is what made the first version of this stutter.
@@ -6478,17 +6491,14 @@ static bool scanline_pll_wait(int target_line)
;
return true;
}
#endif /* HAVE_D3DKMT */
VIDEO_NOINLINE static void video_driver_scanline_before_frame(video_driver_state_t *video_st,
float refresh_rate,
uint16_t frame_time_target,
uint16_t core_run_time)
{
#ifdef HAVE_D3DKMT
(void)refresh_rate;
(void)frame_time_target;
(void)core_run_time;
/* Lock on first use. Calibration measures the line count, the period
* and the residual bias together, because a mode change moves all
@@ -6503,18 +6513,52 @@ VIDEO_NOINLINE static void video_driver_scanline_before_frame(video_driver_state
}
scanline_pll_anchor();
/* Target the far end of the visible area so the flip lands in
* blanking. A plain line number: no tuner state, nothing to
* converge, and therefore nothing to thrash. */
video_st->scanline[SCANLINE_NEXT] =
(int16_t)(video_st->height - (video_st->height / 16));
/* Aim so the flip lands in the middle of the blanking interval.
*
* The wait releases the frame loop; the present happens
* core_run_time later. Aiming at a fixed fraction of the visible
* area therefore puts the tear on screen - a fixed 15/16 of height
* is line 2025 with the visible area ending at 2160, which is 135
* lines inside the picture. Subtract the core's own duration so the
* present, not the release, is what lands in blanking.
*
* No smoothing on core_run_time. The target moving is the
* correction, not error: it moves because the core's duration moves,
* and that is what holds the present in place. Smoothing would lag a
* real load change and put the tear on screen during exactly the
* transitions this is meant to absorb. The error is the
* frame-to-frame delta, since this frame is aimed with last frame's
* measurement, and that was 68-142 us on a PS2 core - 9 to 19 lines
* against a 90 line blanking interval.
*
* Movement is clamped instead. Measured peaks reach 1268 us in
* steady state and 4335 us across a geometry change, which would
* throw the aim 171 and 585 lines; one anomalous frame then spoils
* the next as well. A sustained change still converges in a few
* frames. */
{
int blank = scl_pll.total - (int)video_st->height;
int centre = (int)video_st->height + (blank > 0 ? blank / 2 : 0);
int want = centre
- (int)((double)core_run_time / scl_pll.line_us);
int limit = (blank > 4) ? blank / 2 : 2;
if (!scl_pll.target)
scl_pll.target = want;
else if (want > scl_pll.target + limit)
scl_pll.target += limit;
else if (want < scl_pll.target - limit)
scl_pll.target -= limit;
else
scl_pll.target = want;
/* A core that cannot fit leaves nothing to aim at. */
if (scl_pll.target < 1)
scl_pll.target = 1;
video_st->scanline[SCANLINE_NEXT] = (int16_t)scl_pll.target;
}
video_st->scanline[SCANLINE_TOTAL] = (int16_t)scl_pll.total;
#else
(void)video_st;
(void)refresh_rate;
(void)frame_time_target;
(void)core_run_time;
#endif
}
VIDEO_NOINLINE static void video_driver_scanline_after_frame(video_driver_state_t *video_st,
@@ -6522,7 +6566,6 @@ VIDEO_NOINLINE static void video_driver_scanline_after_frame(video_driver_state_
uint16_t frame_time_target,
uint16_t core_run_time)
{
#ifdef HAVE_D3DKMT
int target;
(void)refresh_rate;
@@ -6531,10 +6574,4 @@ VIDEO_NOINLINE static void video_driver_scanline_after_frame(video_driver_state_
if ((target = (int)video_st->scanline[SCANLINE_NEXT]) > 0)
scanline_pll_wait(target);
#else
(void)video_st;
(void)refresh_rate;
(void)frame_time_target;
(void)core_run_time;
#endif
}