mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
Frame generation: warm cost estimate, re-probe, guest-paced breaker
This commit is contained in:
@@ -89,6 +89,41 @@ void fg_breaker_init(FgBreaker *b, double base_hold, double max_hold, double rep
|
||||
void fg_breaker_trip(FgBreaker *b, double now, const char *reason);
|
||||
int fg_breaker_open(const FgBreaker *b, double now); /* 1 = generation allowed */
|
||||
|
||||
/* The cost of one generated frame, as the plan sees it.
|
||||
* - Cold samples are discarded: the first frames after the surfaces were
|
||||
* (re)allocated or generation was enabled pay first-touch costs (driver
|
||||
* allocation, page faults) that steady state never sees; fg_cost_cold()
|
||||
* marks the next `n` samples as such.
|
||||
* - A stale estimate is re-probed: when the estimate has kept the plan at
|
||||
* zero for `probe_s`, fg_cost_estimate() reports 0 (unknown) once so one
|
||||
* frame is generated and measured, and that probe's sample replaces the
|
||||
* estimate instead of blending into it. A probe that still does not fit
|
||||
* doubles the wait before the next one (up to max_probe_s); one that fits
|
||||
* resets it. */
|
||||
typedef struct FgCost {
|
||||
double ema; /* seconds, 0 = unknown */
|
||||
uint32_t samples, discarded, probes;
|
||||
int cold; /* samples still to discard */
|
||||
int probing; /* the next sample is a probe's */
|
||||
double blocked_since;/* when the estimate started keeping the plan at 0 */
|
||||
double probe_at; /* when the probe was granted */
|
||||
double probe_s, base_probe_s, max_probe_s;
|
||||
} FgCost;
|
||||
void fg_cost_init(FgCost *c, double probe_s, double max_probe_s);
|
||||
void fg_cost_cold(FgCost *c, int n);
|
||||
/* One measured frame; `fit_s` is what one generated frame may cost to fit. */
|
||||
void fg_cost_add(FgCost *c, double cost_s, double fit_s);
|
||||
/* The estimate to plan with at `now`: 0 when unknown or due for a probe.
|
||||
* `fit_s` as above: an estimate above it is what blocks the plan. */
|
||||
double fg_cost_estimate(FgCost *c, double now, double fit_s);
|
||||
|
||||
/* The guest's own pacing: frame boundaries against a schedule that advances
|
||||
* one interval per frame. Late only when the guest slipped more than `slack_s`
|
||||
* behind its schedule (jitter, a long frame followed by a short one, does not
|
||||
* count); the schedule then restarts from now. Early boundaries pull it in. */
|
||||
typedef struct FgPace { double next; int primed; } FgPace;
|
||||
int fg_pace_note(FgPace *p, double now, double period_s, double slack_s);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -330,13 +330,13 @@ void gl_renderer_dynres_stats(GlDynresStats *out);
|
||||
* in-between frames drawn by the render thread from the last two game
|
||||
* frames' draw lists when it has time to spare. Off by default; with it off
|
||||
* nothing changes. set: a sync point. configure: the display's refresh and
|
||||
* the guest's frame rate (any thread). hold: trip the breaker (generation
|
||||
* stays off for a few seconds), e.g. while dynamic resolution is over budget.
|
||||
* the guest's frame rate (any thread). hold: pause generation for `secs`
|
||||
* (not a breaker trip), e.g. while dynamic resolution is over budget.
|
||||
* json: the debug server's {"cmd":"frame_gen"} fields (no sync point). */
|
||||
void gl_renderer_set_frame_generation(int on);
|
||||
int gl_renderer_frame_generation(void);
|
||||
void gl_renderer_frame_gen_configure(double refresh_hz, double guest_hz);
|
||||
void gl_renderer_frame_gen_hold(const char *reason);
|
||||
void gl_renderer_frame_gen_hold(const char *reason, double secs);
|
||||
int gl_renderer_frame_gen_json(char *out, int cap);
|
||||
/* Host time the renderer spent, as running totals in performance-counter
|
||||
* ticks (only kept while dynamic resolution is on): waits for the frame
|
||||
|
||||
@@ -190,3 +190,66 @@ void fg_breaker_trip(FgBreaker *b, double now, const char *reason) {
|
||||
}
|
||||
|
||||
int fg_breaker_open(const FgBreaker *b, double now) { return now >= b->until; }
|
||||
|
||||
void fg_cost_init(FgCost *c, double probe_s, double max_probe_s) {
|
||||
memset(c, 0, sizeof *c);
|
||||
c->base_probe_s = c->probe_s = probe_s;
|
||||
c->max_probe_s = max_probe_s;
|
||||
c->blocked_since = -1.0;
|
||||
}
|
||||
|
||||
void fg_cost_cold(FgCost *c, int n) { if (n > c->cold) c->cold = n; }
|
||||
|
||||
void fg_cost_add(FgCost *c, double cost_s, double fit_s) {
|
||||
if (cost_s <= 0.0) return;
|
||||
if (c->cold > 0) { c->cold--; c->discarded++; return; }
|
||||
c->samples++;
|
||||
if (c->probing || c->ema <= 0.0) {
|
||||
c->ema = cost_s;
|
||||
if (c->probing) {
|
||||
c->probing = 0;
|
||||
if (fit_s > 0.0 && cost_s > fit_s) {
|
||||
c->probe_s *= 2.0;
|
||||
if (c->probe_s > c->max_probe_s) c->probe_s = c->max_probe_s;
|
||||
} else {
|
||||
c->probe_s = c->base_probe_s;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
c->ema = c->ema * 0.8 + cost_s * 0.2;
|
||||
}
|
||||
if (fit_s <= 0.0 || c->ema <= fit_s) c->blocked_since = -1.0;
|
||||
}
|
||||
|
||||
double fg_cost_estimate(FgCost *c, double now, double fit_s) {
|
||||
if (c->ema <= 0.0) return 0.0;
|
||||
if (c->probing) {
|
||||
/* A probe in flight; one never drawn (no room) is given up. */
|
||||
if (now - c->probe_at < c->probe_s) return c->ema;
|
||||
c->probing = 0;
|
||||
c->blocked_since = now;
|
||||
}
|
||||
if (fit_s > 0.0 && c->ema > fit_s) {
|
||||
if (c->blocked_since < 0.0) c->blocked_since = now;
|
||||
else if (now - c->blocked_since >= c->probe_s) {
|
||||
c->blocked_since = now;
|
||||
c->probing = 1;
|
||||
c->probe_at = now;
|
||||
c->probes++;
|
||||
return 0.0;
|
||||
}
|
||||
} else {
|
||||
c->blocked_since = -1.0;
|
||||
}
|
||||
return c->ema;
|
||||
}
|
||||
|
||||
int fg_pace_note(FgPace *p, double now, double period_s, double slack_s) {
|
||||
if (period_s <= 0.0) return 0;
|
||||
if (!p->primed) { p->primed = 1; p->next = now + period_s; return 0; }
|
||||
int late = 0;
|
||||
if (now > p->next + slack_s) { late = 1; p->next = now; }
|
||||
else if (now < p->next - period_s) p->next = now; /* early: pull in */
|
||||
p->next += period_s;
|
||||
return late;
|
||||
}
|
||||
|
||||
@@ -10023,7 +10023,6 @@ static _Atomic double s_fg_refresh_hz = 0.0, s_fg_guest_hz = 59.94;
|
||||
static _Atomic int s_fg_late = 0; /* emulation thread: a late frame */
|
||||
static const char *_Atomic s_fg_hold_reason = NULL;
|
||||
static _Atomic uint64_t s_fg_hold_until = 0; /* rt_now_ns: no generation before */
|
||||
static uint64_t s_fg_emu_last_ns = 0; /* emulation thread */
|
||||
static FgList s_fg_l[4]; /* older, newer, capturing, spare */
|
||||
static int s_fg_older = 0, s_fg_newer = 1, s_fg_cur = 2;
|
||||
static int s_fg_pa = -1, s_fg_pb = -1; /* the pair a schedule draws from */
|
||||
@@ -10041,7 +10040,15 @@ static FgMatchStats s_fg_mst;
|
||||
static FgBreaker s_fg_brk;
|
||||
static int s_fg_brk_init = 0;
|
||||
static uint64_t s_fg_bp_seen = 0;
|
||||
static double s_fg_real_ema = 0.0, s_fg_gen_ema = 0.0; /* seconds */
|
||||
static double s_fg_real_ema = 0.0; /* seconds */
|
||||
/* A generated frame's cost: cold samples discarded, a blocking estimate
|
||||
* re-probed (frame_gen.h FgCost). s_fg_fit_s: what one may cost to fit. */
|
||||
static FgCost s_fg_cost;
|
||||
static int s_fg_cost_init = 0;
|
||||
static double s_fg_fit_s = 0.0;
|
||||
static FgPace s_fg_pace; /* emulation thread */
|
||||
static _Atomic uint64_t s_fg_last_gen_ns = 0; /* the last generated frame (rt_now_ns) */
|
||||
static uint64_t s_fg_ignored_late = 0, s_fg_ignored_bp = 0;
|
||||
static const char *s_fg_hold_why = NULL;
|
||||
static double s_fg_gen_gpu_ms = 0.0, s_fg_gen_cpu_ms = 0.0; /* the last one measured */
|
||||
/* Schedule after a flip: n generated frames, then the real one. */
|
||||
@@ -10070,6 +10077,18 @@ static double s_fg_last_match_ms = 0.0;
|
||||
|
||||
static double fg_now_s(void) { return (double)rt_now_ns() * 1e-9; }
|
||||
|
||||
static FgCost *fg_cost(void) {
|
||||
if (!s_fg_cost_init) { fg_cost_init(&s_fg_cost, 2.0, 16.0); s_fg_cost_init = 1; }
|
||||
return &s_fg_cost;
|
||||
}
|
||||
|
||||
/* Breaker trips only count while generation is what could have caused
|
||||
* them: a generated frame in the last half second. */
|
||||
static int fg_recently_generated(void) {
|
||||
const uint64_t g = atomic_load(&s_fg_last_gen_ns);
|
||||
return g && rt_now_ns() - g < 500000000ull;
|
||||
}
|
||||
|
||||
/* Render thread: every swap's wall time. A swap waits for the compositor
|
||||
* (and on a busy WindowServer for its round trip); each generated frame adds
|
||||
* one, so the plan counts it. */
|
||||
@@ -10352,6 +10371,22 @@ static void fg_invalidate(void) {
|
||||
fg_list_open();
|
||||
}
|
||||
|
||||
/* A new surface's first touch (the driver backing its storage) happens here,
|
||||
* when it is allocated, not inside the first generated frame; that frame's
|
||||
* measured cost is discarded as well (cold). */
|
||||
static void fg_surface_warm(GLuint fbo) {
|
||||
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, fbo);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glClearColor(0.f, 0.f, 0.f, 0.f);
|
||||
glClearStencil(0);
|
||||
glClearDepth(1.0);
|
||||
glStencilMask(0xFF);
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
||||
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0);
|
||||
glFlush();
|
||||
fg_cost_cold(fg_cost(), 2);
|
||||
}
|
||||
|
||||
/* Surfaces for the generated image, at the current level. */
|
||||
static int fg_surfaces_ensure(int wide) {
|
||||
const int S = s_hr_scale;
|
||||
@@ -10364,6 +10399,7 @@ static int fg_surfaces_ensure(int wide) {
|
||||
s_fg_hr_S = 0;
|
||||
if (!hr_alloc_surface(S, &s_fg_hr_tex, &s_fg_hr_rb, &s_fg_hr_fbo)) return 0;
|
||||
s_fg_hr_S = S;
|
||||
fg_surface_warm(s_fg_hr_fbo);
|
||||
}
|
||||
if (wide && (s_fg_w_S != S || s_fg_w_w != g_wide_w)) {
|
||||
if (s_fg_w_fbo) {
|
||||
@@ -10374,6 +10410,7 @@ static int fg_surfaces_ensure(int wide) {
|
||||
s_fg_w_S = 0;
|
||||
if (!wide_alloc_surface(S, &s_fg_w_tex, &s_fg_w_rb, &s_fg_w_fbo)) return 0;
|
||||
s_fg_w_S = S; s_fg_w_w = g_wide_w;
|
||||
fg_surface_warm(s_fg_w_fbo);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
@@ -10662,8 +10699,11 @@ static int fg_generate(double t, int swap) {
|
||||
/* ---- frame generation: schedule (render thread) ---- */
|
||||
static void rth_present_payload(uint16_t op, const uint8_t *p); /* below */
|
||||
|
||||
/* Short first hold, doubling on repeats within 2 s of the last hold's end,
|
||||
* up to 8 s: one hiccup costs a fraction of a second of generation. */
|
||||
#define FG_BRK_INIT() do { if (!s_fg_brk_init) { fg_breaker_init(&s_fg_brk, 0.5, 8.0, 2.0); s_fg_brk_init = 1; } } while (0)
|
||||
static void fg_trip(const char *why) {
|
||||
if (!s_fg_brk_init) { fg_breaker_init(&s_fg_brk, 3.0, 24.0, 10.0); s_fg_brk_init = 1; }
|
||||
FG_BRK_INIT();
|
||||
fg_breaker_trip(&s_fg_brk, fg_now_s(), why);
|
||||
}
|
||||
|
||||
@@ -10675,7 +10715,7 @@ static void fg_gen_cost_poll(void) {
|
||||
if (!avail) break;
|
||||
p_glGetQueryObjectui64v(s_fg_q[i], GL_QUERY_RESULT, &ns);
|
||||
double c = (double)(ns > s_fg_q_cpu[i] ? ns : s_fg_q_cpu[i]) * 1e-9;
|
||||
s_fg_gen_ema = s_fg_gen_ema > 0.0 ? s_fg_gen_ema * 0.8 + c * 0.2 : c;
|
||||
fg_cost_add(fg_cost(), c, s_fg_fit_s);
|
||||
s_fg_gen_gpu_ms = (double)ns * 1e-6;
|
||||
s_fg_gen_cpu_ms = (double)s_fg_q_cpu[i] * 1e-6;
|
||||
s_fg_qt++;
|
||||
@@ -10724,7 +10764,7 @@ static void fg_generate_timed(double t) {
|
||||
s_fg_presenting = 0;
|
||||
}
|
||||
s_rth_ov.valid = 0;
|
||||
if (ok) s_fg_generated++;
|
||||
if (ok) { s_fg_generated++; atomic_store(&s_fg_last_gen_ns, rt_now_ns()); }
|
||||
rthf_resume();
|
||||
}
|
||||
|
||||
@@ -10791,8 +10831,9 @@ static uint64_t fg_tick(void *user, uint64_t now) {
|
||||
const uint64_t bp = rt_backpressure_events();
|
||||
const int ahead = rt_frames_ahead();
|
||||
if (!s_fg_force && (ahead >= 1 || bp != s_fg_bp_seen)) {
|
||||
if (bp != s_fg_bp_seen) fg_trip("queue backed up");
|
||||
else if (ahead >= 2) fg_trip("render thread behind");
|
||||
if (bp != s_fg_bp_seen) {
|
||||
if (fg_recently_generated()) fg_trip("queue backed up"); else s_fg_ignored_bp++;
|
||||
} else if (ahead >= 2) fg_trip("render thread behind");
|
||||
s_fg_bp_seen = bp;
|
||||
fg_flush();
|
||||
return 0;
|
||||
@@ -10864,26 +10905,40 @@ static int fg_on_present(uint16_t op, const uint8_t *p, uint32_t bytes, int stal
|
||||
if (B->valid && (B->disp[0] != disp[0] || B->disp[1] != disp[1])) B = A = NULL;
|
||||
if (B) { B->wide = wide; B->linear = wide ? a[3] : a[4]; B->force43 = wide ? 0 : a[5]; }
|
||||
const double now = fg_now_s();
|
||||
if (atomic_exchange(&s_fg_late, 0)) fg_trip("guest late");
|
||||
/* Guest lateness and backpressure trip the breaker only while generation
|
||||
* runs: otherwise they are not generation's doing. */
|
||||
const int gen_recent = fg_recently_generated();
|
||||
if (atomic_exchange(&s_fg_late, 0)) {
|
||||
if (gen_recent) fg_trip("guest late"); else s_fg_ignored_late++;
|
||||
}
|
||||
const char *why = atomic_exchange(&s_fg_hold_reason, NULL);
|
||||
if (why) s_fg_hold_why = why;
|
||||
const int held = rt_now_ns() < atomic_load(&s_fg_hold_until);
|
||||
const uint64_t bp = rt_backpressure_events();
|
||||
if (bp != s_fg_bp_seen) { s_fg_bp_seen = bp; fg_trip("queue backed up"); }
|
||||
if (bp != s_fg_bp_seen) {
|
||||
s_fg_bp_seen = bp;
|
||||
if (gen_recent) fg_trip("queue backed up"); else s_fg_ignored_bp++;
|
||||
}
|
||||
if (stale) fg_trip("render thread behind");
|
||||
if (!s_fg_brk_init) { fg_breaker_init(&s_fg_brk, 3.0, 24.0, 10.0); s_fg_brk_init = 1; }
|
||||
if (s_fg_qok > 0) fg_gen_cost_poll(); /* once per game frame: a poll flushes */
|
||||
FG_BRK_INIT();
|
||||
const double ghz = atomic_load(&s_fg_guest_hz), rhz = atomic_load(&s_fg_refresh_hz);
|
||||
const double flip_s = (double)s_fg_flip_vb / (ghz > 1.0 ? ghz : 59.94);
|
||||
const int slots = (int)floor(flip_s * rhz + 0.5);
|
||||
s_fg_last_slots = slots;
|
||||
/* What one generated frame may cost (its swap included) to fit. */
|
||||
const double real_s = s_fg_real_ema * (double)s_fg_flip_vb + s_fg_swap_ema;
|
||||
s_fg_fit_s = 0.85 * flip_s - real_s - s_fg_swap_ema;
|
||||
if (s_fg_qok > 0) fg_gen_cost_poll(); /* once per game frame: a poll flushes */
|
||||
int n = 0;
|
||||
if (A && B && A->valid && B->valid && A->disp[2] == B->disp[2] &&
|
||||
A->disp[3] == B->disp[3] && A->wide_w == B->wide_w && A->scale > 0) {
|
||||
/* The surfaces exist (and are warm) before a frame is planned on them. */
|
||||
if (!s_hiw) (void)fg_surfaces_ensure(B->wide);
|
||||
if (s_fg_force) n = slots > 1 ? slots - 1 : 1;
|
||||
else if (fg_breaker_open(&s_fg_brk, now) && !stale && !held)
|
||||
n = fg_plan(flip_s, rhz, s_fg_real_ema * (double)s_fg_flip_vb + s_fg_swap_ema,
|
||||
s_fg_gen_ema > 0.0 ? s_fg_gen_ema + s_fg_swap_ema : 0.0, 0.85, 7);
|
||||
else if (fg_breaker_open(&s_fg_brk, now) && !stale && !held) {
|
||||
const double est = fg_cost_estimate(fg_cost(), now, s_fg_fit_s);
|
||||
n = fg_plan(flip_s, rhz, real_s, est > 0.0 ? est + s_fg_swap_ema : 0.0, 0.85, 7);
|
||||
}
|
||||
if (n == 0) s_fg_skipped_plan++;
|
||||
}
|
||||
s_fg_last_n = n;
|
||||
@@ -10944,24 +10999,25 @@ void gl_renderer_frame_gen_configure(double refresh_hz, double guest_hz) {
|
||||
}
|
||||
|
||||
/* Not a breaker trip: generation pauses while the real frames are under
|
||||
* pressure (dynamic resolution over budget or stepping down) and for a second
|
||||
* after, without the breaker's escalating holds. */
|
||||
void gl_renderer_frame_gen_hold(const char *reason) {
|
||||
* pressure (dynamic resolution over budget or stepping down) for the caller's
|
||||
* short tail, without the breaker's escalating holds. */
|
||||
void gl_renderer_frame_gen_hold(const char *reason, double secs) {
|
||||
if (!s_fg_on) return;
|
||||
if (secs <= 0.0) secs = 0.1;
|
||||
atomic_store(&s_fg_hold_reason, reason ? reason : "hold");
|
||||
atomic_store(&s_fg_hold_until, rt_now_ns() + 1000000000ull);
|
||||
const uint64_t until = rt_now_ns() + (uint64_t)(secs * 1e9);
|
||||
if (until > atomic_load(&s_fg_hold_until)) atomic_store(&s_fg_hold_until, until);
|
||||
}
|
||||
|
||||
/* Emulation thread, once per frame boundary: a frame that took much longer
|
||||
* than the guest's interval trips the breaker. */
|
||||
/* Emulation thread, once per frame boundary: the guest is late when it has
|
||||
* slipped two intervals behind its own VBlank schedule. One long frame made
|
||||
* up by short ones (scheduling jitter, a burst after a wait) is not. */
|
||||
static void fg_note_guest_frame(void) {
|
||||
if (!s_fg_on) return;
|
||||
const uint64_t now = rt_now_ns();
|
||||
const double ghz = atomic_load(&s_fg_guest_hz);
|
||||
if (s_fg_emu_last_ns && ghz > 1.0 &&
|
||||
(double)(now - s_fg_emu_last_ns) > 1.5e9 / ghz)
|
||||
if (ghz <= 1.0) return;
|
||||
if (fg_pace_note(&s_fg_pace, (double)rt_now_ns() * 1e-9, 1.0 / ghz, 2.0 / ghz))
|
||||
atomic_store(&s_fg_late, 1);
|
||||
s_fg_emu_last_ns = now;
|
||||
}
|
||||
|
||||
int gl_renderer_frame_gen_json(char *out, int cap) {
|
||||
@@ -10975,13 +11031,14 @@ int gl_renderer_frame_gen_json(char *out, int cap) {
|
||||
"\"match_ms\":%.3f,"
|
||||
"\"prims\":%u,\"matched\":%u,\"moved\":%u,\"unmatched\":%u,"
|
||||
"\"breaker_s\":%.2f,\"trips\":%u,\"last_trip\":\"%s\",\"held_s\":%.2f,"
|
||||
"\"last_hold\":\"%s\",\"swaps\":%llu",
|
||||
"\"last_hold\":\"%s\",\"swaps\":%llu,\"gen_samples\":%u,\"gen_cold\":%u,"
|
||||
"\"gen_probes\":%u,\"ignored_late\":%llu,\"ignored_bp\":%llu",
|
||||
s_fg_on, s_fg_on && s_rth_on && open, s_fg_force,
|
||||
(unsigned long long)s_fg_generated, (unsigned long long)s_fg_real_presents,
|
||||
(unsigned long long)s_fg_flips, (unsigned long long)s_fg_dups,
|
||||
(unsigned long long)s_fg_flushed, (unsigned long long)s_fg_skipped_plan,
|
||||
s_fg_last_n, s_fg_last_slots, s_fg_flip_vb, atomic_load(&s_fg_refresh_hz),
|
||||
s_fg_real_ema * 1e3, s_fg_gen_ema * 1e3, s_fg_gen_cpu_ms, s_fg_gen_gpu_ms,
|
||||
s_fg_real_ema * 1e3, fg_cost()->ema * 1e3, s_fg_gen_cpu_ms, s_fg_gen_gpu_ms,
|
||||
s_fg_swap_ema * 1e3,
|
||||
s_fg_last_match_ms,
|
||||
s_fg_mst.prims, s_fg_mst.matched, s_fg_mst.moved, s_fg_mst.unmatched,
|
||||
@@ -10989,7 +11046,9 @@ int gl_renderer_frame_gen_json(char *out, int cap) {
|
||||
s_fg_brk.reason ? s_fg_brk.reason : "",
|
||||
(double)(atomic_load(&s_fg_hold_until) > rt_now_ns()
|
||||
? atomic_load(&s_fg_hold_until) - rt_now_ns() : 0) * 1e-9,
|
||||
s_fg_hold_why ? s_fg_hold_why : "", (unsigned long long)s_swaps_total);
|
||||
s_fg_hold_why ? s_fg_hold_why : "", (unsigned long long)s_swaps_total,
|
||||
fg_cost()->samples, fg_cost()->discarded, fg_cost()->probes,
|
||||
(unsigned long long)s_fg_ignored_late, (unsigned long long)s_fg_ignored_bp);
|
||||
}
|
||||
|
||||
/* ---- the recording vtable ------------------------------------------------ */
|
||||
|
||||
@@ -9361,10 +9361,12 @@ static void dynres_tick_rt(double now_s, double wall, double period, int held,
|
||||
const int prev_level = c.level;
|
||||
const int level = dynrt_sample(&c, now_s, &smp);
|
||||
/* Frame generation only spends surplus: not while the real frames are
|
||||
* over budget or the level is stepping down. */
|
||||
if (level < prev_level || c.over_streak > 0)
|
||||
gl_renderer_frame_gen_hold(level < prev_level ? "dynres stepped down"
|
||||
: "dynres over budget");
|
||||
* over budget (renewed every over-budget sample) and briefly after a
|
||||
* step down, while the new level's first frames settle. */
|
||||
if (level < prev_level)
|
||||
gl_renderer_frame_gen_hold("dynres stepped down", 0.25);
|
||||
else if (c.over_streak > 0)
|
||||
gl_renderer_frame_gen_hold("dynres over budget", 0.1);
|
||||
if (c.windows != g_dynres.rt_last_windows) {
|
||||
g_dynres.rt_last_windows = c.windows;
|
||||
g_dynres.rt_win_cpu_ms = g_dynres.rt_acc_frames
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* Frame generation's renderer-independent half (src/frame_gen.c,
|
||||
* docs/FRAME_GENERATION.md): matching triangles of two frames, the
|
||||
* interpolation endpoints, the plan of how many in-between frames fit, and
|
||||
* the breaker. */
|
||||
* the breaker, the generated-frame cost estimate and the guest pace. */
|
||||
#include "frame_gen.h"
|
||||
|
||||
#include <math.h>
|
||||
@@ -139,7 +139,80 @@ static void test_breaker(void) {
|
||||
check(b.hold <= 24.0, "the hold is capped");
|
||||
}
|
||||
|
||||
static void test_cost(void) {
|
||||
FgCost c;
|
||||
fg_cost_init(&c, 2.0, 16.0);
|
||||
const double fit = 0.020;
|
||||
check(fg_cost_estimate(&c, 0.0, fit) == 0.0, "unknown before any sample");
|
||||
/* Cold: the first frames after allocation are discarded. */
|
||||
fg_cost_cold(&c, 2);
|
||||
fg_cost_add(&c, 0.070, fit);
|
||||
fg_cost_add(&c, 0.040, fit);
|
||||
check(c.ema == 0.0 && c.discarded == 2, "cold samples discarded");
|
||||
fg_cost_add(&c, 0.006, fit);
|
||||
check(fabs(fg_cost_estimate(&c, 0.1, fit) - 0.006) < 1e-12, "first warm sample is the estimate");
|
||||
fg_cost_add(&c, 0.016, fit);
|
||||
check(fabs(c.ema - 0.008) < 1e-12, "warm samples blend");
|
||||
/* A spike that pushes the estimate past the fit blocks the plan... */
|
||||
for (int i = 0; i < 10; i++) fg_cost_add(&c, 0.060, fit);
|
||||
check(fg_cost_estimate(&c, 10.0, fit) > fit, "a high estimate blocks");
|
||||
check(fg_cost_estimate(&c, 11.9, fit) > fit, "... until the probe interval");
|
||||
/* ...for at most probe_s: then one probe, whose sample replaces it. */
|
||||
check(fg_cost_estimate(&c, 12.0, fit) == 0.0 && c.probes == 1, "stale estimate re-probed");
|
||||
check(fg_cost_estimate(&c, 12.03, fit) > fit, "one probe at a time");
|
||||
fg_cost_add(&c, 0.005, fit);
|
||||
check(fabs(fg_cost_estimate(&c, 12.1, fit) - 0.005) < 1e-12, "the probe replaces the estimate");
|
||||
/* A probe that still does not fit backs off. */
|
||||
for (int i = 0; i < 10; i++) fg_cost_add(&c, 0.060, fit);
|
||||
(void)fg_cost_estimate(&c, 20.0, fit);
|
||||
check(fg_cost_estimate(&c, 22.0, fit) == 0.0, "second probe");
|
||||
fg_cost_add(&c, 0.050, fit);
|
||||
check(c.probe_s == 4.0, "a probe that does not fit doubles the wait");
|
||||
check(fg_cost_estimate(&c, 25.9, fit) > fit && fg_cost_estimate(&c, 26.0, fit) == 0.0,
|
||||
"next probe after the doubled wait");
|
||||
fg_cost_add(&c, 0.004, fit);
|
||||
check(c.probe_s == 2.0, "a probe that fits resets the wait");
|
||||
/* A granted probe that was never drawn is given up, then re-granted. */
|
||||
for (int i = 0; i < 10; i++) fg_cost_add(&c, 0.060, fit);
|
||||
(void)fg_cost_estimate(&c, 40.0, fit);
|
||||
check(fg_cost_estimate(&c, 42.0, fit) == 0.0, "probe granted");
|
||||
check(fg_cost_estimate(&c, 44.0, fit) > fit, "an undrawn probe expires");
|
||||
check(fg_cost_estimate(&c, 46.0, fit) == 0.0, "and is granted again");
|
||||
/* Re-allocation makes the next samples cold again. */
|
||||
fg_cost_cold(&c, 2);
|
||||
fg_cost_add(&c, 0.5, fit); fg_cost_add(&c, 0.5, fit);
|
||||
check(c.cold == 0 && c.ema < 0.5, "cold after re-allocation");
|
||||
}
|
||||
|
||||
static void test_pace(void) {
|
||||
FgPace p;
|
||||
memset(&p, 0, sizeof p);
|
||||
const double T = 1.0 / 59.94, slack = 2.0 * T;
|
||||
double t = 0.0;
|
||||
int late = 0;
|
||||
for (int i = 0; i < 600; i++) { /* jitter: +-6 ms around the schedule */
|
||||
late += fg_pace_note(&p, t + ((i % 3) - 1) * 0.006, T, slack);
|
||||
t += T;
|
||||
}
|
||||
check(late == 0, "jitter is not late");
|
||||
late = 0; /* one 40 ms frame made up by a burst */
|
||||
late += fg_pace_note(&p, t + 0.024, T, slack); t += T;
|
||||
late += fg_pace_note(&p, t + 0.004, T, slack); t += T;
|
||||
late += fg_pace_note(&p, t, T, slack); t += T;
|
||||
check(late == 0, "a long frame made up is not late");
|
||||
/* A real slip: 60 ms gap. */
|
||||
check(fg_pace_note(&p, t + 0.045, T, slack) == 1, "a slip beyond two intervals is late");
|
||||
t += 0.045 + T;
|
||||
check(fg_pace_note(&p, t, T, slack) == 0, "the schedule restarts after a slip");
|
||||
/* Slow but steady (a 50 Hz guest told 59.94): one trip, then resync each time it slips. */
|
||||
int trips = 0;
|
||||
for (int i = 0; i < 60; i++) { t += 0.020; trips += fg_pace_note(&p, t, T, slack); }
|
||||
check(trips > 0 && trips < 20, "a steadily slow guest is late now and then");
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
test_cost();
|
||||
test_pace();
|
||||
test_identical_and_moved();
|
||||
test_keys_limits_and_duplicates();
|
||||
test_each_older_pairs_once();
|
||||
|
||||
Reference in New Issue
Block a user