video: add a user-tunable Scanline Sync offset

The tear line persists after every CPU-side correction. The aim
arithmetic is right, the release-to-present interval is measured rather
than assumed, the timer sleep was removed and tested and made no
difference, and a two-image swapchain made no difference either. What
remains is a stable residual of roughly 950 scanlines - about 7 ms at
4K60 - between where the CPU believes the flip will land and where it
lands.

That residual is the part the CPU cannot observe. The wait releases,
the core runs, the frame is submitted, and the present call returns -
but the GPU has not finished rendering, and the flip happens when it
does. No timestamp taken on the CPU side sees that moment. Closing the
loop properly needs present-timing feedback from the driver
(VK_KHR_present_wait or VK_GOOGLE_display_timing on Vulkan), which is
the correct long-term fix and is not attempted here.

Expose the offset instead, in scanlines, signed, and dialled by hand
until the tear moves into blanking. This is how RTSS's scanline sync
handles the same limit, and it is honest about what the CPU can and
cannot know. The default is zero. The aim is wrapped to stay inside one
frame since the offset can push it out either side.

Plumbed through settings_def_video_sync.h so the config entry, the
enum, the menu descriptor and the strings come from one row.
This commit is contained in:
LibretroAdmin authored and libretroadmin committed 2026-09-02 03:45:22 +02:00
1 parent 6abbdb7095
commit 49f07a52ee
4 files changed
+22 -4

No files matched your search

+9
View File
@@ -400,6 +400,15 @@
#define DEFAULT_MAX_FRAME_LATENCY 1
#define MAXIMUM_MAX_FRAME_LATENCY 4
/* Scanline Sync aims the flip at the middle of the blanking interval
* using what it can measure from the CPU. It cannot see when the GPU
* finishes or when the flip actually reaches the panel, so a residual
* offset remains that depends on the driver, resolution and shader
* chain. This is dialled by hand until the tear sits in blanking, the
* same way RTSS's scanline sync exposes its offset. Signed scanlines. */
#define DEFAULT_SCANLINE_SYNC_OFFSET 0
#define MAXIMUM_SCANLINE_SYNC_OFFSET 3000
/* GL specific */
#define DEFAULT_ADAPTIVE_VSYNC false
+1
View File
@@ -408,6 +408,7 @@ typedef struct settings
int crt_switch_porch_adjust;
int crt_switch_vertical_adjust;
int video_max_frame_latency;
int video_scanline_sync_offset;
#ifdef HAVE_VULKAN
int vulkan_gpu_index;
#endif
+7 -4
View File
@@ -6548,7 +6548,8 @@ VIDEO_NOINLINE static void video_driver_scanline_before_frame(video_driver_state
int blank = scl_pll.total - (int)video_st->height;
int centre = (int)video_st->height + (blank > 0 ? blank / 2 : 0);
int want = centre
- (int)(scl_pll.work_us / scl_pll.line_us);
- (int)(scl_pll.work_us / scl_pll.line_us)
+ config_get_ptr()->ints.video_scanline_sync_offset;
int limit = (blank > 4) ? blank / 2 : 2;
if (!scl_pll.target)
@@ -6560,9 +6561,11 @@ VIDEO_NOINLINE static void video_driver_scanline_before_frame(video_driver_state
else
scl_pll.target = want;
/* A core that cannot fit leaves nothing to aim at. */
if (scl_pll.target < 1)
scl_pll.target = 1;
/* Keep the aim inside one frame; the offset can push it out. */
while (scl_pll.target < 1)
scl_pll.target += scl_pll.total;
while (scl_pll.target >= scl_pll.total)
scl_pll.target -= scl_pll.total;
video_st->scanline[SCANLINE_NEXT] = (int16_t)scl_pll.target;
}
+5
View File
@@ -62,6 +62,11 @@ S_INT(video_max_frame_latency, VIDEO_MAX_FRAME_LATENCY,
DEFAULT_MAX_FRAME_LATENCY, SD_FLAG_CMD_APPLY_AUTO, SDESC_RANGE_MINMAX, CMD_EVENT_REINIT, -1, MAXIMUM_MAX_FRAME_LATENCY, 1, -1, setting_action_ok_uint, NULL,
"Max Frame Latency",
"Tells the video driver to explicitly use a specified buffering mode.")
S_INT(video_scanline_sync_offset, VIDEO_SCANLINE_SYNC_OFFSET,
"video_scanline_sync_offset",
DEFAULT_SCANLINE_SYNC_OFFSET, SD_FLAG_LAKKA_ADVANCED, SDESC_RANGE_MINMAX, CMD_EVENT_NONE, -MAXIMUM_SCANLINE_SYNC_OFFSET, MAXIMUM_SCANLINE_SYNC_OFFSET, 1, 0, setting_action_ok_uint, NULL,
"Scanline Sync Offset",
"Shifts the Scanline Sync target by this many scanlines. The sync cannot see when the GPU finishes a frame or when the flip reaches the display, so adjust this until the tear line moves off screen. Negative aims earlier.")
S_BOOL(video_hard_sync, VIDEO_HARD_SYNC,
"video_hard_sync",
DEFAULT_HARD_SYNC, SD_FLAG_NONE, SDESC_FLG_REFRESH, CMD_EVENT_NONE,