Files
psxrecomp/runtime/tests/test_dynamic_resolution_rt.c
Matthew Stanley 62d26bff82 Fix redraw policy, HD reload residency, AA ownership and FG budget publication
Address objective review blockers in #563, #567, #576 and #580 on the combined renderer stack. Preserve redraw/default policy, bounded same-pack learned uploads, GL context ownership, and coherent current-window budget accounting. Add focused CPU, real-GL, concurrent-publication and authority-transition regressions.
2026-10-08 11:16:22 -07:00

468 lines
20 KiB
C

/* The render-thread dynamic-resolution controller (dynamic_resolution.c,
* dynrt_*) against a synthetic two-stage pipeline: the guest takes E per
* frame, the render thread R(S) = a + b * S^2 (S the level), at most two
* frames in flight. A frame's interval is max(period, E, R); when R is the
* slowest stage the guest blocks on the queue for the difference
* (backpressure). Costs reach the controller `lag` frames late, at the level
* the frame was rendered at, like the GPU timestamp readback.
* Checks: overrun -> down one level at a time, headroom -> up only after the
* window, hysteresis (no oscillation), guest-bound -> no step, a down step
* that does not lower the cost -> undone and backed off, queue full ->
* fast down, floor and ceiling, holds; fast descent (armed at start and by
* dynrt_arm_descent): a sustained overrun jumps to the predicted level in
* one or two steps, then fine-tunes without oscillating, and a cost that
* does not scale still ends in the undo.
* Build/run: ctest -R dynamic_resolution_rt_test */
#include <math.h>
#include <stdio.h>
#include <string.h>
#include "dynamic_resolution.h"
static int failures, checks;
#define CHECK(cond, ...) do { checks++; if (!(cond)) { \
fprintf(stderr, "FAIL: "); fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n"); \
failures++; } } while (0)
static const double kPeriod = 1.0 / 59.94;
#define LAG 4
typedef struct Scene {
double a, b; /* render cost a + b*S^2 (seconds) */
double emu; /* guest time per frame */
double flat; /* > 0: render cost independent of S (seconds) */
double under; /* report cost * (1 - under): a meter that underreads */
double heavy_from, heavy_to, heavy_a; /* optional time-limited extra cost */
} Scene;
static double render_cost(const Scene *sc, int s, double t) {
double r = sc->flat > 0.0 ? sc->flat : sc->a + sc->b * (double)s * (double)s;
if (sc->heavy_to > sc->heavy_from && t >= sc->heavy_from && t < sc->heavy_to)
r += sc->heavy_a;
return r;
}
typedef struct Run {
double t;
int level, steps, downs, ups;
int min_level, max_level;
double first_down_t, first_up_t;
double time_at[DYNRES_MAX_LEVEL + 1];
double lagq[LAG];
int lagn;
double frames; /* guest frames */
} Run;
static void run_init(Run *r, int level) {
memset(r, 0, sizeof *r);
r->level = level;
r->min_level = r->max_level = level;
r->first_down_t = r->first_up_t = -1.0;
}
static void drive(DynrtController *c, Run *r, const Scene *sc, double seconds,
double held_until) {
const double end = r->t + seconds;
while (r->t < end) {
const double R = render_cost(sc, r->level, r->t);
double base = kPeriod > sc->emu ? kPeriod : sc->emu;
double interval = R > base ? R : base;
double bp = R > base ? R - base : 0.0;
r->t += interval;
r->frames += 1.0;
r->time_at[r->level] += interval;
/* the cost of the frame rendered LAG frames ago */
int frames = 0;
double cost = 0.0;
if (r->lagn == LAG) {
frames = 1;
cost = r->lagq[0] * (1.0 - sc->under);
memmove(r->lagq, r->lagq + 1, sizeof(double) * (LAG - 1));
r->lagn--;
}
r->lagq[r->lagn++] = R;
DynrtSample s = { kPeriod, interval, frames, cost, bp, r->t < held_until };
int l = dynrt_sample(c, r->t, &s);
if (l != r->level) {
r->steps++;
if (l < r->level) { r->downs++; if (r->first_down_t < 0) r->first_down_t = r->t; }
else { r->ups++; if (r->first_up_t < 0) r->first_up_t = r->t; }
r->level = l;
if (l < r->min_level) r->min_level = l;
if (l > r->max_level) r->max_level = l;
}
}
}
static void ctl(DynrtController *c, int floor_l, int ceil_l, int level) {
DynrtParams p;
dynrt_default_params(&p);
dynrt_init(c, &p, floor_l, ceil_l, level);
}
/* Fast descent off: the one-level rules alone. */
static void ctl_single(DynrtController *c, int floor_l, int ceil_l, int level) {
DynrtParams p;
dynrt_default_params(&p);
p.descent_s = 0.0;
dynrt_init(c, &p, floor_l, ceil_l, level);
}
/* Highest level whose cost fits the 15 % margin. */
static int fit_level(const Scene *sc, int floor_l, int ceil_l) {
int best = floor_l;
for (int s = floor_l; s <= ceil_l; s++)
if (render_cost(sc, s, 0.0) <= kPeriod * 0.85) best = s;
return best;
}
/* 1. Overrun at the ceiling, fast descent off: down one level at a time to
* the highest level that fits, then no oscillation. */
static void test_overrun_steps_down_and_settles(void) {
DynrtController c; Run r;
Scene sc = { .a = 0.003, .b = 0.00030 }; /* 9x: 27.3 ms, 6x: 13.8 ms, 7x: 17.7 ms */
ctl_single(&c, 3, 9, 9);
run_init(&r, 9);
drive(&c, &r, &sc, 20.0, 0.0);
int want = fit_level(&sc, 3, 9);
CHECK(r.level == want, "overrun: settled at %d, want %d", r.level, want);
CHECK(r.first_down_t >= 0.0 && r.first_down_t < 0.6,
"overrun: first down at %.2f s, want < 0.6 s", r.first_down_t);
CHECK(r.ups == 0 && r.min_level == want, "overrun: never below %d (min %d, ups %d)",
want, r.min_level, r.ups);
/* every step a single level */
CHECK(r.downs == 9 - want, "overrun: %d downs for %d levels", r.downs, 9 - want);
int steps = r.steps;
drive(&c, &r, &sc, 120.0, 0.0);
CHECK(r.steps == steps, "overrun: %d steps after settling (oscillation)", r.steps - steps);
CHECK(r.time_at[want] > 115.0, "overrun: %.1f s at the settled level", r.time_at[want]);
}
/* 2. Headroom: up one level, never before up_after_s, up to the ceiling. */
static void test_headroom_steps_up_after_window(void) {
DynrtController c; Run r;
Scene sc = { .a = 0.001, .b = 0.00005 }; /* 9x: 5 ms */
ctl(&c, 3, 9, 4);
run_init(&r, 4);
drive(&c, &r, &sc, 2.9, 0.0);
CHECK(r.ups == 0, "headroom: up within %.1f s (%d ups)", 2.9, r.ups);
drive(&c, &r, &sc, 60.0, 0.0);
CHECK(r.first_up_t >= 3.0, "headroom: first up at %.2f s", r.first_up_t);
CHECK(r.level == 9 && r.downs == 0, "headroom: level %d, downs %d", r.level, r.downs);
CHECK(r.ups == 5, "headroom: %d ups for 5 levels (one at a time)", r.ups);
}
/* 3. Hysteresis band: a level at 0.80 of the interval stays (under the 0.85
* budget) and the next one (predicted over up_load) is never tried. */
static void test_hysteresis(void) {
DynrtController c; Run r;
/* 5x: 0.80 * period */
double target = 0.80 * kPeriod;
Scene sc = { .a = 0.0, .b = target / 25.0 };
ctl(&c, 3, 9, 5);
run_init(&r, 5);
drive(&c, &r, &sc, 120.0, 0.0);
CHECK(r.steps == 0, "hysteresis: %d steps at a level inside the band", r.steps);
}
/* 4. Guest-bound: the guest takes 25 ms per frame, the render thread 15.5 ms
* (0.93 of the nominal interval, over the budget). The guest never waits
* on the queue and the render thread has slack in the 25 ms it is given:
* resolution cannot help, so no down step. */
static void test_guest_bound_no_step(void) {
DynrtController c; Run r;
Scene sc = { .flat = 0.0155, .emu = 0.025 };
ctl(&c, 3, 9, 9);
run_init(&r, 9);
drive(&c, &r, &sc, 60.0, 0.0);
CHECK(r.downs == 0, "guest-bound: %d down steps", r.downs);
CHECK(c.guest_bound_windows > 100, "guest-bound: %llu guest-bound windows",
c.guest_bound_windows);
/* The render cost scales with S here, the guest is still the limit. */
DynrtController c2; Run r2;
Scene sc2 = { .a = 0.004, .b = 0.00014, .emu = 0.026 }; /* 9x: 15.3 ms */
ctl(&c2, 3, 9, 9);
run_init(&r2, 9);
drive(&c2, &r2, &sc2, 60.0, 0.0);
CHECK(r2.downs == 0, "guest-bound (scaling): %d down steps", r2.downs);
}
/* 5. A down step that does not lower the cost (render-thread submission
* bound, not pixels): undone, and down steps back off (doubling). */
static void test_undo_and_backoff(void) {
DynrtController c; Run r;
Scene sc = { .flat = 0.0175 }; /* 1.05 of the period at every level */
ctl_single(&c, 3, 9, 9);
run_init(&r, 9);
drive(&c, &r, &sc, 3.0, 0.0);
CHECK(c.undos == 1 && r.level == 9, "undo: undos %llu, level %d after 3 s", c.undos, r.level);
drive(&c, &r, &sc, 117.0, 0.0);
/* two steps per try, blocks of 20, 40, 80 s: at most 3 tries in 120 s */
CHECK(r.downs <= 6, "back-off: %d down steps in 120 s", r.downs);
CHECK(r.time_at[9] > 112.0, "back-off: %.1f s at the ceiling", r.time_at[9]);
CHECK(r.min_level >= 7, "back-off: one level at a time (min %d)", r.min_level);
}
/* 6. The queue is full but the meter underreads (load 0.75 reported): the
* backpressure alone steps down, one window each. */
static void test_queue_full_steps_down(void) {
DynrtController c; Run r;
Scene sc = { .a = 0.003, .b = 0.00030, .under = 0.45 };
ctl(&c, 3, 9, 9);
run_init(&r, 9);
drive(&c, &r, &sc, 0.4, 0.0);
CHECK(r.downs >= 1, "queue full: no down step within 0.4 s");
drive(&c, &r, &sc, 20.0, 0.0);
CHECK(r.level <= fit_level(&sc, 3, 9) + 1 && r.level >= 3,
"queue full: level %d", r.level);
/* No backpressure left at the level it settled at. */
CHECK(render_cost(&sc, r.level, 0.0) <= kPeriod + 1e-9,
"queue full: settled at %d still behind", r.level);
}
/* 7. Floor and ceiling. */
static void test_floor_and_ceiling(void) {
DynrtController c; Run r;
Scene heavy = { .a = 0.010, .b = 0.0010 }; /* over budget at every level */
ctl(&c, 3, 9, 9);
run_init(&r, 9);
drive(&c, &r, &heavy, 60.0, 0.0);
CHECK(r.min_level == 3 && r.level == 3, "floor: min %d level %d", r.min_level, r.level);
DynrtController c2; Run r2;
Scene light = { .a = 0.001 };
ctl(&c2, 3, 9, 9);
run_init(&r2, 9);
drive(&c2, &r2, &light, 60.0, 0.0);
CHECK(r2.max_level == 9 && r2.steps == 0, "ceiling: max %d steps %d", r2.max_level, r2.steps);
}
/* 8. A heavy stretch: down, then back up once it is over (one level at a
* time, after the window), and no relapse churn. Its first window mixes
* light and heavy frames, so the first step looks like it removed
* nothing: one strike, not an undo. */
static void test_heavy_stretch_recovers(void) {
DynrtController c; Run r;
Scene sc = { .a = 0.002, .b = 0.00012, /* 9x: 11.7 ms (fits) */
.heavy_from = 10.0, .heavy_to = 30.0, .heavy_a = 0.008 };
ctl(&c, 3, 9, 9);
run_init(&r, 9);
drive(&c, &r, &sc, 30.0, 0.0);
CHECK(r.level < 9, "stretch: still at %d during the heavy part", r.level);
drive(&c, &r, &sc, 90.0, 0.0);
CHECK(r.level == 9, "stretch: level %d after recovery", r.level);
CHECK(r.downs <= 4, "stretch: %d downs", r.downs);
}
/* 9. Holds: nothing is sampled or decided while held. */
static void test_holds(void) {
DynrtController c; Run r;
Scene sc = { .a = 0.003, .b = 0.00030 };
ctl(&c, 3, 9, 9);
run_init(&r, 9);
drive(&c, &r, &sc, 5.0, 5.0);
CHECK(r.steps == 0, "holds: %d steps while held", r.steps);
dynrt_hold(&c, r.t, 1.0);
drive(&c, &r, &sc, 0.9, 0.0);
CHECK(r.steps == 0, "holds: %d steps in the hold tail", r.steps);
drive(&c, &r, &sc, 2.0, 0.0);
CHECK(r.downs >= 1, "holds: no step after the hold");
}
/* 10. Missing costs (frames drawn at sync points): thin windows, no step. */
static void test_thin_windows(void) {
DynrtController c;
ctl(&c, 3, 9, 9);
double t = 0.0;
for (int i = 0; i < 600; i++) {
t += 0.02;
DynrtSample s = { kPeriod, 0.02, 0, 0.0, 0.0, 0 };
(void)dynrt_sample(&c, t, &s);
}
CHECK(c.level == 9 && c.downs == 0 && c.thin_windows > 10,
"thin: level %d downs %llu thin %llu", c.level, c.downs, c.thin_windows);
}
/* 11. Force pins the level; release resumes. */
static void test_force(void) {
DynrtController c; Run r;
Scene sc = { .a = 0.003, .b = 0.00030 };
ctl(&c, 3, 9, 9);
CHECK(dynrt_force(&c, 7) == 7, "force: level");
run_init(&r, 7);
drive(&c, &r, &sc, 10.0, 0.0);
CHECK(r.steps == 0, "force: %d steps while pinned", r.steps);
(void)dynrt_force(&c, 0);
drive(&c, &r, &sc, 10.0, 0.0);
CHECK(r.level == fit_level(&sc, 3, 9), "force: released to %d", r.level);
}
/* 12. Fast descent from a 10x ceiling (R4 2P VS shape: a fixed part plus a
* part that scales with area; 5x fits): the first step jumps several
* levels, the fit level is reached far sooner than one level per judged
* window, never more than one level below it, and nothing moves after. */
static void test_fast_descent_from_ceiling(void) {
Scene sc = { .a = 0.0025, .b = 0.000420 }; /* 10x: 44.5 ms, 5x: 13.0 ms, 6x: 17.6 ms */
const int want = fit_level(&sc, 2, 10);
DynrtController s; Run rs;
ctl_single(&s, 2, 10, 10);
run_init(&rs, 10);
double t_single = -1.0;
while (rs.t < 20.0 && t_single < 0.0) {
drive(&s, &rs, &sc, 0.05, 0.0);
if (rs.level == want) t_single = rs.t;
}
DynrtController c; Run r;
ctl(&c, 2, 10, 10);
run_init(&r, 10);
double t_fast = -1.0;
int first_jump = 0;
while (r.t < 20.0) {
const int before = r.level;
drive(&c, &r, &sc, 0.05, 0.0);
if (!first_jump && r.level < before) first_jump = before - r.level;
if (t_fast < 0.0 && r.level <= want + 0 && r.level >= want - 1) t_fast = r.t;
}
printf("fast descent: 10x -> %dx in %.2f s, %d down step(s), first %d level(s); "
"one level at a time: %.2f s\n", want, t_fast, r.downs, first_jump, t_single);
CHECK(want == 5, "fast descent: scene fits at %d, want 5", want);
CHECK(first_jump >= 3, "fast descent: first step %d level(s)", first_jump);
CHECK(t_fast > 0.0 && t_fast < 1.6, "fast descent: near the fit level at %.2f s", t_fast);
CHECK(t_single > 2.5 && t_fast < t_single / 2.0,
"fast descent: %.2f s vs %.2f s one level at a time", t_fast, t_single);
CHECK(r.level == want, "fast descent: settled at %d, want %d", r.level, want);
CHECK(r.min_level >= want - 1, "fast descent: overshot to %d", r.min_level);
CHECK(r.downs <= 3, "fast descent: %d down steps", r.downs);
CHECK(c.fast_downs >= 1 && !c.descent_armed, "fast descent: %llu jumps, armed %d",
c.fast_downs, c.descent_armed);
const int steps = r.steps;
drive(&c, &r, &sc, 120.0, 0.0);
CHECK(r.steps == steps, "fast descent: %d steps after settling (oscillation)",
r.steps - steps);
}
/* 13. Re-armed (a savestate load into a heavier scene): jumps again. Not
* armed (the window expired, nothing re-armed): one level at a time. */
static void test_fast_descent_rearm(void) {
Scene light = { .a = 0.002, .b = 0.00012 }; /* 9x fits */
Scene heavy = { .a = 0.003, .b = 0.00030 }; /* 6x fits */
for (int arm = 0; arm < 2; arm++) {
DynrtController c; Run r;
ctl(&c, 3, 9, 9);
run_init(&r, 9);
drive(&c, &r, &light, 10.0, 0.0);
CHECK(r.steps == 0 && !c.descent_armed, "rearm: light start steps %d armed %d",
r.steps, c.descent_armed);
/* the load: a hold, then the heavy scene */
dynrt_hold(&c, r.t, 0.5);
if (arm) dynrt_arm_descent(&c);
const double t0 = r.t;
drive(&c, &r, &heavy, 20.0, 0.0);
const int want = fit_level(&heavy, 3, 9);
CHECK(r.level == want, "rearm %d: settled at %d, want %d", arm, r.level, want);
if (arm)
CHECK(c.fast_downs >= 1 && r.downs <= 2,
"rearm: %llu jumps, %d downs", c.fast_downs, r.downs);
else
CHECK(c.fast_downs == 0 && r.downs == 9 - want,
"not armed: %llu jumps, %d downs", c.fast_downs, r.downs);
(void)t0;
}
}
/* 14. Light windows first (game entry: menus, loading) do not use up the
* arming; the heavy part then jumps. */
static void test_fast_descent_light_first(void) {
Scene sc = { .a = 0.003, .b = 0.00030, .heavy_from = 0.0, .heavy_to = 0.0 };
Scene light = { .a = 0.002 };
DynrtController c; Run r;
ctl(&c, 3, 9, 9);
run_init(&r, 9);
drive(&c, &r, &light, 2.0, 0.0);
CHECK(c.descent_armed, "light first: disarmed by light windows");
drive(&c, &r, &sc, 10.0, 0.0);
CHECK(c.fast_downs >= 1 && r.level == fit_level(&sc, 3, 9),
"light first: %llu jumps, level %d", c.fast_downs, r.level);
}
/* 15. A cost that does not scale, fast descent on: the jump is judged, the
* model is dropped (disarmed) and the two-strike undo still happens. */
static void test_fast_descent_flat_cost(void) {
DynrtController c; Run r;
Scene sc = { .flat = 0.0175 };
ctl(&c, 3, 9, 9);
run_init(&r, 9);
drive(&c, &r, &sc, 4.0, 0.0);
CHECK(c.undos == 1 && r.level == 9 && !c.descent_armed,
"flat: undos %llu level %d armed %d", c.undos, r.level, c.descent_armed);
drive(&c, &r, &sc, 116.0, 0.0);
CHECK(r.time_at[9] > 105.0, "flat: %.1f s at the ceiling", r.time_at[9]);
}
/* 16. dynrt_descent_target: the highest level whose pure-area prediction is
* at or below descent_load; the floor when none is. */
static void test_descent_target(void) {
DynrtController c;
ctl(&c, 2, 10, 10);
c.f = 0.3; /* the learned share does not matter: pure area */
/* load 2.0 at 10x: L^2/100 * 2 <= 0.78 -> L <= 6.24 */
CHECK(dynrt_descent_target(&c, 2.0, 10) == 6, "target: %d", dynrt_descent_target(&c, 2.0, 10));
CHECK(dynrt_descent_target(&c, 50.0, 10) == 2, "target floor");
CHECK(dynrt_descent_target(&c, 0.80, 10) == 9, "target one level");
}
static void test_generation_window_mix(void) {
DynrtParams p; dynrt_default_params(&p);
p.window_s = 0.04;
DynrtController c; dynrt_init(&c, &p, 1, 1, 1);
double now = 0.0;
DynrtSample s = { .period_s = 0.02, .wall_s = 0.02, .frames = 1,
.cost_s = 0.02, .generated_cost_s = 0.008 };
for (int i = 0; i < 2; i++) dynrt_sample(&c, now += 0.02, &s);
CHECK(c.over_streak > 0 && fabs(c.last_load - 1.0) < 1e-9, "mixed window is over budget");
CHECK(fabs(c.last_real_load - 0.6) < 1e-9 && c.last_real_load < 1.0 - c.p.margin,
"first mixed window does not suppress generation when real work fits");
s.generated_cost_s = 0.002;
for (int i = 0; i < 2; i++) dynrt_sample(&c, now += 0.02, &s);
CHECK(fabs(c.last_real_load - 0.9) < 1e-9 && c.last_real_load >= 1.0 - c.p.margin,
"next window uses its current mix, not the previous generation-heavy mix");
s.generated_cost_s = 0.0;
for (int i = 0; i < 2; i++) dynrt_sample(&c, now += 0.02, &s);
CHECK(c.last_real_load == c.last_load, "without generation real and total load are identical");
/* Discarded partial work must not leak into the next window. */
s.generated_cost_s = 0.008;
dynrt_sample(&c, now += 0.02, &s);
dynrt_hold(&c, now, 0.0);
CHECK(c.win_generated_cost == 0.0, "hold discards the generation mix with its window");
s.generated_cost_s = 0.0;
for (int i = 0; i < 2; i++) dynrt_sample(&c, now += 0.02, &s);
CHECK(c.last_real_load == c.last_load, "discarded generation does not contaminate the next window");
/* Combined load still controls down steps, even when real work fits. */
p.down_windows = 1;
dynrt_init(&c, &p, 1, 4, 4);
s.generated_cost_s = 0.008; now = 0.0;
for (int i = 0; i < 2; i++) dynrt_sample(&c, now += 0.02, &s);
CHECK(c.level < 4 && c.last_real_load < 1.0 - c.p.margin,
"generation load can lower resolution without pretending real work alone is over budget");
}
int main(void) {
test_generation_window_mix();
test_fast_descent_from_ceiling();
test_fast_descent_rearm();
test_fast_descent_light_first();
test_fast_descent_flat_cost();
test_descent_target();
test_overrun_steps_down_and_settles();
test_headroom_steps_up_after_window();
test_hysteresis();
test_guest_bound_no_step();
test_undo_and_backoff();
test_queue_full_steps_down();
test_floor_and_ceiling();
test_heavy_stretch_recovers();
test_holds();
test_thin_windows();
test_force();
printf("dynamic_resolution_rt_test: %d checks, %d failures\n", checks, failures);
return failures ? 1 : 0;
}