mirror of
https://github.com/libretro/RetroArch.git
synced 2026-11-07 08:36:02 -10:00
video_thread: the paced frame goes out on the vblank nearest its due time
The hold picks the vblank nearest the frame's due time on a grid from the display, and aims at the due time itself when the grid is only the video thread's clock (vsync off, no present timestamps). A 120 fps core on a 120 Hz display holds 120. The threaded-video harness gains a pacing-rate lane: every shipped pairing of content and display rate, with and without timestamps, with a core that takes 1.3 ms and a render that varies, must hold within 2% of its rate.
This commit is contained in:
1 parent
66939756ee
commit
3298947b8a
3 files changed
+218
-2
No files matched your search
@@ -2576,9 +2576,19 @@ static VIDEO_NOINLINE void video_thread_pace_hold(thread_video_t *thr,
|
||||
}
|
||||
if (thr->content_due < next)
|
||||
thr->content_due = next;
|
||||
/* The vblank nearest the due time, not the first at or after it:
|
||||
* the due time accumulates in the content's period and the grid
|
||||
* in the display's, and a due time a hair past a vblank belongs
|
||||
* to that vblank, not the next. A grid the video thread laid
|
||||
* from its own clock - vsync off, no timestamps - is no scanout
|
||||
* at all, just its last completion plus a period: it sits a
|
||||
* margin ahead of the due time every frame, and the frame is
|
||||
* aimed at the due time itself. */
|
||||
vblank = next;
|
||||
if (period > 0)
|
||||
while (vblank < thr->content_due)
|
||||
if (!(snap.flags & VIDEO_THREAD_STAT_F_PHASE_DISPLAY))
|
||||
vblank = thr->content_due;
|
||||
else if (period > 0)
|
||||
while (vblank + period / 2 < thr->content_due)
|
||||
vblank += period;
|
||||
|
||||
target = vblank - reserve - margin;
|
||||
|
||||
@@ -459,6 +459,26 @@ static unsigned harness_fb_granted;
|
||||
/* RETRO_API, like the core's own entry points: a Windows DLL exports
|
||||
* only what is marked, and the harness looks these two up by name. */
|
||||
RETRO_API void harness_core_use_framebuffer(int on) { harness_use_fb = on; }
|
||||
|
||||
/* The time each retro_run takes, spun on the frontend's clock: a real
|
||||
* core's cost, which the pacer reserves against. 0 runs as fast as
|
||||
* the frame can be written. */
|
||||
static unsigned harness_run_us;
|
||||
static retro_perf_get_time_usec_t harness_time_usec;
|
||||
RETRO_API void harness_core_set_run_us(unsigned us)
|
||||
{
|
||||
struct retro_perf_callback perf;
|
||||
harness_run_us = 0;
|
||||
if (!us || !environ_cb)
|
||||
return;
|
||||
memset(&perf, 0, sizeof(perf));
|
||||
if ( environ_cb(RETRO_ENVIRONMENT_GET_PERF_INTERFACE, &perf)
|
||||
&& perf.get_time_usec)
|
||||
{
|
||||
harness_time_usec = perf.get_time_usec;
|
||||
harness_run_us = us;
|
||||
}
|
||||
}
|
||||
RETRO_API unsigned harness_core_fb_granted(void) { return harness_fb_granted; }
|
||||
|
||||
void retro_run(void)
|
||||
@@ -471,6 +491,12 @@ void retro_run(void)
|
||||
const uint8_t *push = NULL;
|
||||
runs++;
|
||||
|
||||
if (harness_run_us)
|
||||
{
|
||||
retro_time_t until = harness_time_usec() + (retro_time_t)harness_run_us;
|
||||
while (harness_time_usec() < until) { }
|
||||
}
|
||||
|
||||
if (hw_mode && hw_vk)
|
||||
{
|
||||
/* The same image every frame, and every third frame a dupe, as
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include <stdlib.h>
|
||||
#include <stddef.h>
|
||||
#include <time.h>
|
||||
#include <math.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <file/file_path.h>
|
||||
@@ -2369,6 +2370,184 @@ static void lane_pacing_after_stall(void)
|
||||
}
|
||||
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Lane: the paced loop keeps the content's rate */
|
||||
/* Frames counted over two seconds under display pacing, for every */
|
||||
/* pairing of content and display rate that ships, with and without */
|
||||
/* the driver reporting when its presents reach the display (vsync */
|
||||
/* off reports nothing, and the hold lays its own grid from the */
|
||||
/* clock). The render takes 0.6-2.1 ms, varying frame to frame. */
|
||||
/* The loop must hold within 2% of the rate it is due at: a hold */
|
||||
/* that picks a vblank one past the frame's due time sleeps out a */
|
||||
/* whole period, and a 120 fps core on a 120 Hz display then runs */
|
||||
/* at 103. The core takes 1.3 ms a frame: the hold's cap counts */
|
||||
/* from the push, so a core that takes nothing hides a hold that */
|
||||
/* sleeps out a whole period. Rate is the count over the window, */
|
||||
/* not per-frame timing, and each case gets three tries: a busy */
|
||||
/* runner costs a try, a hold that misses its slot fails all three. */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static video_driver_t prlane_driver;
|
||||
static const video_driver_t *prlane_inner;
|
||||
static video_poke_interface_t prlane_poke;
|
||||
static const video_poke_interface_t *prlane_inner_poke;
|
||||
static retro_time_t prlane_period; /* the panel's real period */
|
||||
static retro_time_t prlane_base;
|
||||
static retro_time_t prlane_last_out;
|
||||
static unsigned prlane_seed;
|
||||
|
||||
static bool prlane_frame(void *data, const void *frame,
|
||||
unsigned dims, uint64_t frame_count,
|
||||
unsigned pitch, const char *msg, video_frame_info_t *video_info)
|
||||
{
|
||||
retro_time_t now = cpu_features_get_time_usec();
|
||||
retro_time_t done;
|
||||
prlane_seed = prlane_seed * 1103515245u + 12345u;
|
||||
/* 0.6 ms, plus up to 1.5 ms more on some frames */
|
||||
done = now + 600 + ((prlane_seed >> 16) % 4 == 0
|
||||
? (retro_time_t)((prlane_seed >> 8) % 1500) : 0);
|
||||
while (cpu_features_get_time_usec() < done) { }
|
||||
/* Vsync off: out on the next scanout, the present does not wait */
|
||||
now = cpu_features_get_time_usec();
|
||||
prlane_last_out = prlane_base
|
||||
+ ((now - prlane_base) / prlane_period + 1) * prlane_period;
|
||||
return prlane_inner->frame(data, frame, dims, frame_count,
|
||||
pitch, msg, video_info);
|
||||
}
|
||||
|
||||
static retro_time_t prlane_last_present(void *data)
|
||||
{
|
||||
retro_time_t now = cpu_features_get_time_usec();
|
||||
(void)data;
|
||||
return prlane_last_out <= now ? prlane_last_out
|
||||
: prlane_last_out - prlane_period;
|
||||
}
|
||||
|
||||
static double pacing_rate_measure(thread_video_t *thr, float display_hz,
|
||||
double panel_hz, double content_fps, bool timestamps)
|
||||
{
|
||||
settings_t *settings = config_get_ptr();
|
||||
video_driver_state_t *video_st = video_state_get_ptr();
|
||||
unsigned n = (unsigned)(content_fps * 2.0);
|
||||
uint64_t f0;
|
||||
retro_time_t t0, took;
|
||||
|
||||
settings->floats.video_refresh_rate = display_hz;
|
||||
settings->bools.video_present_timing_from_display = timestamps;
|
||||
video_st->av_info.timing.fps = content_fps;
|
||||
prlane_period = (retro_time_t)(1000000.0 / panel_hz);
|
||||
prlane_base = cpu_features_get_time_usec();
|
||||
prlane_last_out = prlane_base;
|
||||
prlane_poke.get_last_present_time = timestamps ? prlane_last_present : NULL;
|
||||
set_poke(thr, &prlane_poke);
|
||||
/* Settle: the period, render and core estimates */
|
||||
run_frames(n / 4);
|
||||
video_thread_wait_idle();
|
||||
f0 = core_frames();
|
||||
t0 = cpu_features_get_time_usec();
|
||||
run_frames(n);
|
||||
took = cpu_features_get_time_usec() - t0;
|
||||
return took > 0
|
||||
? (double)(core_frames() - f0) * 1000000.0 / (double)took : 0.0;
|
||||
}
|
||||
|
||||
static void lane_pacing_rate(void)
|
||||
{
|
||||
static const struct
|
||||
{
|
||||
float display_hz; /* what the driver reports */
|
||||
double panel_hz; /* what the panel runs at */
|
||||
double content_fps;
|
||||
double expect;
|
||||
} cases[] = {
|
||||
{ 120.0f, 120.0, 120.0, 120.0 },
|
||||
{ 120.0f, 119.982, 120.0, 120.0 }, /* a real panel, a hair slow */
|
||||
{ 120.0f, 120.018, 120.0, 120.0 }, /* and a hair fast */
|
||||
{ 120.0f, 120.0, 60.0, 60.0 },
|
||||
{ 60.0f, 60.0, 60.0, 60.0 },
|
||||
{ 60.0f, 59.94, 60.0, 60.0 },
|
||||
{ 144.0f, 144.0, 60.0, 60.0 },
|
||||
{ 144.0f, 144.0, 120.0, 120.0 },
|
||||
};
|
||||
unsigned had = failures;
|
||||
settings_t *settings = config_get_ptr();
|
||||
video_driver_state_t *video_st = video_state_get_ptr();
|
||||
thread_video_t *thr;
|
||||
bool saved_pacing = settings->bools.video_threaded_display_pacing;
|
||||
bool saved_ask = settings->bools.video_present_timing_from_display;
|
||||
float saved_refresh = settings->floats.video_refresh_rate;
|
||||
double saved_fps = video_st->av_info.timing.fps;
|
||||
dylib_t lib = runloop_state_get_ptr()->lib_handle;
|
||||
void (*run_us)(unsigned) = lib
|
||||
? (void (*)(unsigned))dylib_proc(lib, "harness_core_set_run_us") : NULL;
|
||||
unsigned c, ts;
|
||||
|
||||
CHECK(run_us != NULL, "pacing-rate lane: harness core has no run time");
|
||||
if (!run_us)
|
||||
return;
|
||||
settings->bools.video_threaded_display_pacing = true;
|
||||
set_threaded_via_setting(true);
|
||||
run_frames(3);
|
||||
expect_wrapper(true, "pacing-rate lane");
|
||||
/* The core takes 1.3 ms, as prboom does at 2560x1600: the loop has
|
||||
* that to spend inside every period on top of the hold. */
|
||||
run_us(1300);
|
||||
if (menu_is_up())
|
||||
command_event(CMD_EVENT_MENU_TOGGLE, NULL);
|
||||
CHECK(!menu_is_up(), "pacing-rate lane: menu still up");
|
||||
run_frames(3);
|
||||
video_thread_wait_idle();
|
||||
|
||||
thr = (thread_video_t*)video_state_get_ptr()->data;
|
||||
prlane_seed = 1;
|
||||
prlane_inner = thr->driver;
|
||||
prlane_driver = *thr->driver;
|
||||
prlane_driver.frame = prlane_frame;
|
||||
prlane_inner_poke = thr->poke;
|
||||
prlane_poke = *thr->poke;
|
||||
set_driver(thr, &prlane_driver);
|
||||
|
||||
for (c = 0; c < ARRAY_SIZE(cases); c++)
|
||||
for (ts = 0; ts < 2; ts++)
|
||||
{
|
||||
double best = 0.0, fps;
|
||||
unsigned t;
|
||||
for (t = 0; t < 3; t++)
|
||||
{
|
||||
fps = pacing_rate_measure(thr, cases[c].display_hz,
|
||||
cases[c].panel_hz, cases[c].content_fps, ts != 0);
|
||||
if (fabs(fps - cases[c].expect) < fabs(best - cases[c].expect))
|
||||
best = fps;
|
||||
if (fabs(best - cases[c].expect) <= cases[c].expect * 0.02)
|
||||
break;
|
||||
}
|
||||
CHECK(fabs(best - cases[c].expect) <= cases[c].expect * 0.02,
|
||||
"pacing-rate lane: %.0f fps content on a %.3f Hz panel "
|
||||
"(reported %.0f Hz), %s: ran at %.2f fps, not %.0f",
|
||||
cases[c].content_fps, cases[c].panel_hz,
|
||||
cases[c].display_hz,
|
||||
ts ? "present timestamps" : "no timestamps (vsync off)",
|
||||
best, cases[c].expect);
|
||||
fprintf(stderr, " pacing rate: %5.1f fps on %8.3f Hz, %-13s %.2f fps\n",
|
||||
cases[c].content_fps, cases[c].panel_hz,
|
||||
ts ? "timestamps:" : "clock grid:", best);
|
||||
}
|
||||
|
||||
run_us(0);
|
||||
set_driver(thr, prlane_inner);
|
||||
set_poke(thr, prlane_inner_poke);
|
||||
video_st->av_info.timing.fps = saved_fps;
|
||||
settings->bools.video_threaded_display_pacing = saved_pacing;
|
||||
settings->bools.video_present_timing_from_display = saved_ask;
|
||||
settings->floats.video_refresh_rate = saved_refresh;
|
||||
if (!menu_is_up())
|
||||
command_event(CMD_EVENT_MENU_TOGGLE, NULL);
|
||||
set_threaded_via_setting(false);
|
||||
|
||||
if (failures == had)
|
||||
fprintf(stderr, "[pass] pacing-rate lane\n");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* The window's answers reach the main thread, each as itself */
|
||||
/* */
|
||||
@@ -5308,6 +5487,7 @@ int main(int argc, char *argv[])
|
||||
lane_pacing_queue_drain();
|
||||
lane_pacing_fast_display();
|
||||
lane_pacing_after_stall();
|
||||
lane_pacing_rate();
|
||||
}
|
||||
lane_zero_copy();
|
||||
lane_zero_copy_ring_full();
|
||||
|
||||
Reference in new issue
Block a user