video: size the Scanline Sync sleep from the distance left to travel

video_driver_scanline_after_frame() sleeps off the bulk of the wait and
polls the remainder, because D3DKMTGetScanLine is the only scanline
access the D3DKMT surface offers and every wait it exposes -
WaitForVerticalBlankEvent, SetSyncRefreshCountWaitTarget - is
frame-granular. A sub-frame wait is therefore a sleep or a spin, and
the spin is expensive: the query measures ~223 us on a WDDM driver, so
the beam covers ~60 lines per sample at 4K120 and whatever is not slept
is paid for at that rate.

The sleep was (frame_time_target - core_run_time) / 1000 - 4
milliseconds. At 120 Hz that is (8306 - core_run) / 1000 - 4, negative
for any core using more than about 4 ms, so it clamped to the 1 ms
floor. Measured on a 4K120 display with a PS2 core: 25 to 41 samples
per frame, 5.5 to 9.2 ms of busy-wait against an 8.31 ms frame period.
The spin was running longer than the frame lasted.

Compute the remaining time from the lines still to travel and the line
period that frame_time_target and the learned scanline_total already
give, and sleep that less a margin. Modelled on the observed case -
beam near vblank, target line 2156 - this is a 6 ms sleep and about 9
samples where it was a 1 ms sleep and 31.

The sleep is an efficiency estimate, not a timing authority. The loop
still exits on a measured scanline, so a wrong sleep costs extra
samples or an overshoot that the existing scanline_total learning
absorbs; it cannot put the beam somewhere it is not. That is the
distinction from predicting beam position off a CPU counter, which
would need the counter and the pixel clock to agree in rate and would
break outright under VRR.

The original sleep is kept for the case where no line count has been
learned yet, or the query failed.
This commit is contained in:
LibretroAdmin authored and libretroadmin committed 2026-09-02 01:06:40 +02:00
1 parent 0966f2f32d
commit 3a95d8f1c0
1 file changed
+50 -4
+50 -4
View File
@@ -6386,15 +6386,61 @@ VIDEO_NOINLINE static void video_driver_scanline_after_frame(video_driver_state_
/* Minimum usage is vblank */
core_run_time = (core_run_time < min_run_time) ? min_run_time : core_run_time;
/* Use CPU friendlier sleep as much as possible */
if (wait && frame_time_target > core_run_time)
/* Sleep off the bulk of the wait, then poll the remainder.
*
* The poll below is the only authority on beam position, and it is
* expensive: D3DKMTGetScanLine measures ~223 us on a WDDM driver, so
* the beam covers ~60 lines per sample at 4K120. Whatever is not
* slept is therefore paid for at that rate, and the D3DKMT surface
* offers no blocking wait on a scanline - every wait it exposes
* (WaitForVerticalBlankEvent, SetSyncRefreshCountWaitTarget) is
* frame-granular, so a sub-frame wait is either a sleep or a spin.
*
* The sleep length was (frame_time_target - core_run_time) / 1000 - 4
* milliseconds. At 120 Hz that is (8306 - core_run) / 1000 - 4, which
* is negative for any core using more than ~4 ms and clamps to the
* 1 ms floor - so ~7 ms of every frame went into the poll loop, 31
* samples of 223 us, a core pinned for most of the frame period.
*
* Size it from the distance actually left to travel instead. This is
* an efficiency estimate, not a timing authority: the loop still
* exits on a measured scanline, so an inaccurate sleep costs extra
* samples or an overshoot that scanline_total learning already
* absorbs. Deliberately short by SCANLINE_SLEEP_MARGIN_US, plus
* whatever the truncation to whole milliseconds adds, so the poll and
* not the sleep decides the moment.
*
* retro_sleep() is the right primitive for this on Windows: it is
* not Sleep() but the out-of-line rtime.c implementation over a high
* resolution waitable timer, added because "Frame Delay and Scanline
* Sync both depend on sub-frame sleeps". Where that timer is
* unavailable it falls back to Sleep() at ~15.6 ms granularity, which
* overshoots a frame either way - the old 1 ms sleep no less than
* this one. */
if (wait && scanline_total > 0)
{
#define SCANLINE_SLEEP_MARGIN_US 1500
int16_t here = video_driver_scanline_get();
if (here >= 0 && here < scanline_target)
{
/* us per scanline, from the frame period the caller already
* has and the total line count the loop has learned. */
int32_t togo_us = (int32_t)((scanline_target - here)
* ((double)frame_time_target / (double)scanline_total));
if (togo_us > SCANLINE_SLEEP_MARGIN_US)
retro_sleep((togo_us - SCANLINE_SLEEP_MARGIN_US) / 1000);
}
}
else if (wait && frame_time_target > core_run_time)
{
/* No line count learned yet (first frames, or the query failed).
* Fall back to the original conservative sleep. */
int8_t sleep = (frame_time_target - core_run_time) / 1000;
if (sleep > 1)
{
/* Sleeping too much causes problems */
sleep -= 4;
/* At least 1ms for balancing heavier loads */
sleep = (sleep < 1) ? 1 : sleep;
retro_sleep(sleep);
}