mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
Dynamic resolution on the render thread: recorded steps, per-frame render cost, render-thread controller
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1202,6 +1202,15 @@ if(BUILD_TESTING)
|
||||
target_link_libraries(dynamic_resolution_test PRIVATE m)
|
||||
endif()
|
||||
add_test(NAME dynamic_resolution_test COMMAND dynamic_resolution_test)
|
||||
# The render-thread controller (dynrt_*) against a synthetic pipeline.
|
||||
add_executable(dynamic_resolution_rt_test
|
||||
tests/test_dynamic_resolution_rt.c
|
||||
src/dynamic_resolution.c)
|
||||
target_include_directories(dynamic_resolution_rt_test PRIVATE include)
|
||||
if(NOT MSVC)
|
||||
target_link_libraries(dynamic_resolution_rt_test PRIVATE m)
|
||||
endif()
|
||||
add_test(NAME dynamic_resolution_rt_test COMMAND dynamic_resolution_rt_test)
|
||||
|
||||
# Render-pass time freeze against mock devices (psx_cycles.c only).
|
||||
add_executable(render_pass_freeze_test
|
||||
|
||||
@@ -133,6 +133,121 @@ double dynres_predict(const DynresController *c, double load, int from, int to);
|
||||
/* Up steps into `level` blocked for this many seconds from now_s (0 = not). */
|
||||
double dynres_up_blocked_s(const DynresController *c, int level, double now_s);
|
||||
|
||||
|
||||
/* ---- Render-thread mode ([video] render_thread on) -------------------------
|
||||
* With the render thread the guest's frame no longer pays for GL, so the
|
||||
* emulation thread's wall time says nothing about whether the resolution
|
||||
* fits. This controller is fed what the render thread measured instead
|
||||
* (docs/RENDER_THREAD.md, "Dynamic resolution"):
|
||||
*
|
||||
* COST. Per replayed guest frame, max(GPU time, render-thread CPU time): GPU
|
||||
* time from a GL_TIMESTAMP pair (first record of the frame .. after its
|
||||
* present), CPU time the render thread spent replaying the frame minus its
|
||||
* idle waits for records and its block in the swap (the display's vsync).
|
||||
* load = mean cost / the guest's nominal interval (one display refresh at
|
||||
* 60 Hz); the budget is 1 - margin (15 %).
|
||||
*
|
||||
* BOUND. The render thread is the limit when the emulation thread blocked on
|
||||
* the queue bound (backpressure) or the cost is over budget. When the guest
|
||||
* runs below its nominal rate, never waits on the queue and the render
|
||||
* thread had slack in the actual interval, the guest is the limit: no down
|
||||
* step (resolution cannot help). Backpressure for bp_strong of a window is a
|
||||
* strong signal: one window suffices.
|
||||
*
|
||||
* RULES (DynrtParams; dynrt_default_params):
|
||||
* - down: ONE level, after down_windows consecutive over-budget windows (one
|
||||
* if strong). Never below the floor.
|
||||
* - judge: the window after a step settles (the cost lags the queue and the
|
||||
* GPU readback); the next one judges it. A down step that removed less
|
||||
* than verify_fraction of the predicted cost (and did not end the
|
||||
* backpressure) is a strike: the window it was decided on may have mixed
|
||||
* lighter frames in. A second such step in a row (within strike_s) means
|
||||
* the cost is not the pixels: both are undone and down steps are blocked
|
||||
* for verify_block_s, doubling per failure up to verify_block_max_s.
|
||||
* - up: one level, after up_after_s of clean windows (no backpressure, the
|
||||
* next level predicted at <= up_load), up_cooldown_s after the last step,
|
||||
* and the level not blocked. The prediction is load * ((1-f) + f*(S'/S)^2)
|
||||
* with f, the share of the cost that scales with pixels, learned from each
|
||||
* judged step. down_load > up_load is the hysteresis.
|
||||
* - relapse: a level reached by an up step and left by a down step within
|
||||
* relapse_s is blocked for up steps for relapse_block_s, doubling up to
|
||||
* relapse_block_max_s; holding it relapse_forget_s resets that.
|
||||
* - holds and gaps discard the window in progress, as in the controller
|
||||
* above; so does a window in which fewer than min_coverage of the guest
|
||||
* frames have a measured cost (frames the emulation thread drew itself
|
||||
* at a sync point). */
|
||||
typedef struct DynrtParams {
|
||||
double window_s;
|
||||
double margin; /* budget = 1 - margin of the interval */
|
||||
int down_windows; /* consecutive over-budget windows */
|
||||
double strong_load; /* load at or above: one window suffices */
|
||||
double bp_strong; /* backpressure share of wall: strong */
|
||||
double bp_weak; /* below: no backpressure */
|
||||
double guest_slow; /* interval > period * this: guest slow */
|
||||
double guest_slack; /* cost < interval * this: render slack */
|
||||
double up_load;
|
||||
double up_after_s;
|
||||
double up_cooldown_s;
|
||||
double verify_fraction;
|
||||
double verify_block_s, verify_block_max_s, verify_forget_s;
|
||||
double strike_s; /* a first failed judgement lapses after */
|
||||
double relapse_s;
|
||||
double relapse_block_s, relapse_block_max_s, relapse_forget_s;
|
||||
double prior_scaled;
|
||||
double learn_rate;
|
||||
double gap_factor, gap_hold_s;
|
||||
double min_coverage;
|
||||
} DynrtParams;
|
||||
|
||||
void dynrt_default_params(DynrtParams *p);
|
||||
|
||||
typedef struct DynrtSample {
|
||||
double period_s; /* the guest's nominal interval */
|
||||
double wall_s; /* this guest interval's wall time */
|
||||
int frames; /* render-thread frames whose cost arrived since the last sample */
|
||||
double cost_s; /* their summed cost (see COST) */
|
||||
double bp_s; /* emulation thread blocked on the queue bound */
|
||||
int held; /* the host holds: not a sample, the window restarts */
|
||||
} DynrtSample;
|
||||
|
||||
typedef struct DynrtController {
|
||||
DynrtParams p;
|
||||
int floor, ceiling, level, forced;
|
||||
double f;
|
||||
/* the window in progress */
|
||||
double win_period, win_wall, win_cost, win_bp;
|
||||
int win_n, win_frames;
|
||||
/* the last closed window */
|
||||
double last_load, last_bp_share, last_hz;
|
||||
int last_valid, last_guest_bound, last_over;
|
||||
int over_streak;
|
||||
double hold_until;
|
||||
double up_streak_s;
|
||||
double last_step_t, last_down_t;
|
||||
double up_reached_t[DYNRES_MAX_LEVEL + 1];
|
||||
double up_block_until[DYNRES_MAX_LEVEL + 1];
|
||||
double relapse_dur[DYNRES_MAX_LEVEL + 1];
|
||||
double down_block_until, verify_dur, verify_last_fail;
|
||||
int post_active, post_from, post_to, post_windows, post_down;
|
||||
double post_load_before, post_pred, post_bp_before;
|
||||
int verify_strikes, strike_from;
|
||||
double strike_t;
|
||||
/* telemetry */
|
||||
unsigned long long downs, ups, undos, relapses, windows, held_windows,
|
||||
guest_bound_windows, thin_windows;
|
||||
const char *last_reason;
|
||||
double last_decision_t;
|
||||
} DynrtController;
|
||||
|
||||
void dynrt_init(DynrtController *c, const DynrtParams *p, int floor_level,
|
||||
int ceiling, int level);
|
||||
void dynrt_hold(DynrtController *c, double now_s, double tail_s);
|
||||
/* One guest interval ending at now_s; returns the level to render at. */
|
||||
int dynrt_sample(DynrtController *c, double now_s, const DynrtSample *s);
|
||||
int dynrt_force(DynrtController *c, int level);
|
||||
double dynrt_predict(const DynrtController *c, double load, int from, int to);
|
||||
double dynrt_up_blocked_s(const DynrtController *c, int level, double now_s);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -335,6 +335,15 @@ typedef struct GlHostLedger {
|
||||
uint64_t interp_presents, swaps;
|
||||
} GlHostLedger;
|
||||
void gl_renderer_host_ledger(GlHostLedger *out);
|
||||
/* Render thread ([video] render_thread): the cost of each guest frame it
|
||||
* replayed, max(GPU time, its CPU time without idle waits and the swap), as
|
||||
* running totals the emulation thread differences. Measured only while
|
||||
* gl_renderer_render_thread_measure(1) (dynamic resolution on). */
|
||||
typedef struct GlRthCosts {
|
||||
uint64_t frames, cost_ns, cpu_ns, gpu_ns, gpu_frames, dropped;
|
||||
} GlRthCosts;
|
||||
void gl_renderer_render_thread_measure(int on);
|
||||
void gl_renderer_render_thread_costs(GlRthCosts *out);
|
||||
|
||||
/* Narrow a native-wide display aspect num:den to the widest whose surface
|
||||
* this context can allocate at its internal scale (psx_gl_fit_wide_aspect;
|
||||
|
||||
@@ -23,7 +23,8 @@
|
||||
* thread at all.
|
||||
* - No lock is held while exec() runs. Locks only guard sleeping/waking.
|
||||
*
|
||||
* Every function except the rt_on_render_thread() / rt_frames_ahead()
|
||||
* Every function except the rt_on_render_thread() / rt_frames_ahead() /
|
||||
* rt_render_idle_ns()
|
||||
* queries is emulation-thread only. */
|
||||
#ifndef PSX_RENDER_THREAD_H
|
||||
#define PSX_RENDER_THREAD_H
|
||||
@@ -85,6 +86,9 @@ void rt_frame_end(void);
|
||||
/* Render-thread query while executing: how many complete frames are queued
|
||||
* after the frame whose record is being executed (0 = it is the newest). */
|
||||
int rt_frames_ahead(void);
|
||||
/* Render-thread query: the consumer's running idle total (waiting for
|
||||
* records, or parked while the emulation thread holds the context). */
|
||||
uint64_t rt_render_idle_ns(void);
|
||||
|
||||
/* Sync point. Drain, then move the context to the calling thread (held).
|
||||
* reason is a static string kept in the acquire ring. No-op when already held
|
||||
|
||||
@@ -299,3 +299,294 @@ int dynres_sample(DynresController *c, double now_s, const DynresSample *s) {
|
||||
if (c->win_period >= c->p.window_s - 1e-9) return close_window(c, now_s);
|
||||
return c->level;
|
||||
}
|
||||
|
||||
/* ---- Render-thread mode (see dynamic_resolution.h) ----------------------- */
|
||||
|
||||
void dynrt_default_params(DynrtParams *p) {
|
||||
memset(p, 0, sizeof *p);
|
||||
p->window_s = 0.25;
|
||||
p->margin = 0.15;
|
||||
p->down_windows = 2;
|
||||
p->strong_load = 1.0;
|
||||
p->bp_strong = 0.05;
|
||||
p->bp_weak = 0.01;
|
||||
p->guest_slow = 1.03;
|
||||
p->guest_slack = 0.90;
|
||||
p->up_load = 0.70;
|
||||
p->up_after_s = 3.0;
|
||||
p->up_cooldown_s = 2.0;
|
||||
p->verify_fraction = 0.30;
|
||||
p->verify_block_s = 20.0;
|
||||
p->verify_block_max_s = 320.0;
|
||||
p->verify_forget_s = 300.0;
|
||||
p->strike_s = 2.0;
|
||||
p->relapse_s = 10.0;
|
||||
p->relapse_block_s = 10.0;
|
||||
p->relapse_block_max_s = 160.0;
|
||||
p->relapse_forget_s = 60.0;
|
||||
p->prior_scaled = 0.8;
|
||||
p->learn_rate = 0.3;
|
||||
p->gap_factor = 4.0;
|
||||
p->gap_hold_s = 1.0;
|
||||
p->min_coverage = 0.5;
|
||||
}
|
||||
|
||||
static int rt_clamp(const DynrtController *c, int l) {
|
||||
if (l < c->floor) l = c->floor;
|
||||
if (l > c->ceiling) l = c->ceiling;
|
||||
return l;
|
||||
}
|
||||
|
||||
void dynrt_init(DynrtController *c, const DynrtParams *p, int floor_level,
|
||||
int ceiling, int level) {
|
||||
memset(c, 0, sizeof *c);
|
||||
if (p) c->p = *p; else dynrt_default_params(&c->p);
|
||||
if (ceiling < 1) ceiling = 1;
|
||||
if (ceiling > DYNRES_MAX_LEVEL) ceiling = DYNRES_MAX_LEVEL;
|
||||
if (floor_level < 1) floor_level = 1;
|
||||
if (floor_level > ceiling) floor_level = ceiling;
|
||||
c->floor = floor_level;
|
||||
c->ceiling = ceiling;
|
||||
c->level = rt_clamp(c, level);
|
||||
c->f = c->p.prior_scaled;
|
||||
c->last_step_t = c->last_down_t = -1e9;
|
||||
c->verify_last_fail = -1e9;
|
||||
c->verify_dur = c->p.verify_block_s;
|
||||
for (int i = 0; i <= DYNRES_MAX_LEVEL; i++) {
|
||||
c->up_reached_t[i] = -1e9;
|
||||
c->relapse_dur[i] = c->p.relapse_block_s;
|
||||
}
|
||||
c->last_reason = "start";
|
||||
}
|
||||
|
||||
double dynrt_predict(const DynrtController *c, double load, int from, int to) {
|
||||
if (from < 1 || to < 1) return load;
|
||||
double q = ((double)to * (double)to) / ((double)from * (double)from);
|
||||
return load * ((1.0 - c->f) + c->f * q);
|
||||
}
|
||||
|
||||
double dynrt_up_blocked_s(const DynrtController *c, int level, double now_s) {
|
||||
if (level < 0 || level > DYNRES_MAX_LEVEL) return 0.0;
|
||||
double r = c->up_block_until[level] - now_s;
|
||||
return r > 0.0 ? r : 0.0;
|
||||
}
|
||||
|
||||
static void rt_window_reset(DynrtController *c) {
|
||||
c->win_period = c->win_wall = c->win_cost = c->win_bp = 0.0;
|
||||
c->win_n = c->win_frames = 0;
|
||||
}
|
||||
|
||||
static void rt_discard(DynrtController *c) {
|
||||
if (c->win_n) c->held_windows++;
|
||||
rt_window_reset(c);
|
||||
c->over_streak = 0;
|
||||
c->up_streak_s = 0.0;
|
||||
c->post_active = 0;
|
||||
}
|
||||
|
||||
void dynrt_hold(DynrtController *c, double now_s, double tail_s) {
|
||||
double until = now_s + (tail_s > 0.0 ? tail_s : 0.0);
|
||||
if (until > c->hold_until) c->hold_until = until;
|
||||
rt_discard(c);
|
||||
}
|
||||
|
||||
int dynrt_force(DynrtController *c, int level) {
|
||||
if (level <= 0) {
|
||||
c->forced = 0;
|
||||
c->last_reason = "force released";
|
||||
rt_discard(c);
|
||||
return c->level;
|
||||
}
|
||||
c->forced = rt_clamp(c, level);
|
||||
c->level = c->forced;
|
||||
c->last_reason = "forced";
|
||||
rt_discard(c);
|
||||
return c->level;
|
||||
}
|
||||
|
||||
static void rt_step(DynrtController *c, double now, int to, int down, double load,
|
||||
double bp_share, const char *why) {
|
||||
const int from = c->level;
|
||||
c->post_active = 1;
|
||||
c->post_from = from;
|
||||
c->post_to = to;
|
||||
c->post_windows = 0;
|
||||
c->post_down = down;
|
||||
c->post_load_before = load;
|
||||
c->post_bp_before = bp_share;
|
||||
c->post_pred = dynrt_predict(c, load, from, to);
|
||||
if (down) {
|
||||
if (now - c->up_reached_t[from] < c->p.relapse_s) {
|
||||
c->up_block_until[from] = now + c->relapse_dur[from];
|
||||
c->relapse_dur[from] *= 2.0;
|
||||
if (c->relapse_dur[from] > c->p.relapse_block_max_s)
|
||||
c->relapse_dur[from] = c->p.relapse_block_max_s;
|
||||
c->relapses++;
|
||||
}
|
||||
c->up_reached_t[from] = -1e9;
|
||||
c->last_down_t = now;
|
||||
c->downs++;
|
||||
} else {
|
||||
c->up_reached_t[to] = now;
|
||||
c->ups++;
|
||||
}
|
||||
c->level = to;
|
||||
c->last_step_t = now;
|
||||
c->over_streak = 0;
|
||||
c->up_streak_s = 0.0;
|
||||
c->last_reason = why;
|
||||
c->last_decision_t = now;
|
||||
}
|
||||
|
||||
static void rt_learn(DynrtController *c, double before, double after, int from, int to) {
|
||||
if (before <= 0.05 || from == to) return;
|
||||
double q = ((double)to * (double)to) / ((double)from * (double)from);
|
||||
double fo = (after / before - 1.0) / (q - 1.0);
|
||||
if (fo < 0.1) fo = 0.1;
|
||||
if (fo > 0.95) fo = 0.95;
|
||||
c->f += c->p.learn_rate * (fo - c->f);
|
||||
}
|
||||
|
||||
static int rt_close_window(DynrtController *c, double now) {
|
||||
const double dur = c->win_period;
|
||||
const int n = c->win_n, frames = c->win_frames;
|
||||
const double wall = c->win_wall, cost = c->win_cost, bp = c->win_bp;
|
||||
rt_window_reset(c);
|
||||
c->windows++;
|
||||
if (frames < 1 || (double)frames < c->p.min_coverage * (double)n) {
|
||||
/* Mostly frames the emulation thread drew at a sync point, or the
|
||||
* costs have not arrived yet: nothing to judge on. */
|
||||
c->thin_windows++;
|
||||
c->over_streak = 0;
|
||||
c->up_streak_s = 0.0;
|
||||
c->post_active = 0;
|
||||
return c->level;
|
||||
}
|
||||
const double period = dur / (double)n;
|
||||
const double load = (cost / (double)frames) / period;
|
||||
const double bp_share = wall > 0.0 ? bp / wall : 0.0;
|
||||
const double interval = wall / (double)n;
|
||||
const double budget = 1.0 - c->p.margin;
|
||||
c->last_load = load;
|
||||
c->last_bp_share = bp_share;
|
||||
c->last_hz = wall > 0.0 ? (double)n / wall : 0.0;
|
||||
c->last_valid = 1;
|
||||
const int strong_bp = bp_share >= c->p.bp_strong;
|
||||
const int no_bp = bp_share < c->p.bp_weak;
|
||||
/* The guest is the limit: slow, never held by the queue, and the render
|
||||
* thread had room in the interval it was actually given. */
|
||||
const int guest_bound = no_bp && interval > period * c->p.guest_slow &&
|
||||
(cost / (double)frames) < interval * c->p.guest_slack;
|
||||
c->last_guest_bound = guest_bound;
|
||||
if (guest_bound) c->guest_bound_windows++;
|
||||
|
||||
/* Judge a recent step: the first window settles, the second judges. */
|
||||
if (c->post_active && ++c->post_windows >= 2) {
|
||||
c->post_active = 0;
|
||||
const double want = c->post_load_before - c->post_pred;
|
||||
const double fell = c->post_load_before - load;
|
||||
const int bp_cleared = c->post_bp_before >= c->p.bp_strong && no_bp;
|
||||
const int failed = c->post_down && want > 0.0 &&
|
||||
fell < c->p.verify_fraction * want && !bp_cleared;
|
||||
if (failed && !c->verify_strikes) {
|
||||
/* One strike: the window before the step may have mixed lighter
|
||||
* frames in (a scene getting heavier, the cost's lag), so it
|
||||
* says little. Keep going; a second step that also removes
|
||||
* nothing is the evidence. */
|
||||
c->verify_strikes = 1;
|
||||
c->strike_from = c->post_from;
|
||||
c->strike_t = now;
|
||||
c->last_reason = "down step removed less than predicted (one strike)";
|
||||
c->last_decision_t = now;
|
||||
} else if (failed) {
|
||||
c->verify_strikes = 0;
|
||||
if (now - c->verify_last_fail > c->p.verify_forget_s)
|
||||
c->verify_dur = c->p.verify_block_s;
|
||||
c->down_block_until = now + c->verify_dur;
|
||||
c->verify_dur *= 2.0;
|
||||
if (c->verify_dur > c->p.verify_block_max_s)
|
||||
c->verify_dur = c->p.verify_block_max_s;
|
||||
c->verify_last_fail = now;
|
||||
const int to = rt_clamp(c, c->strike_from > c->post_from ? c->strike_from
|
||||
: c->post_from);
|
||||
if (to != c->level) {
|
||||
c->undos++;
|
||||
c->level = to;
|
||||
c->last_step_t = now;
|
||||
}
|
||||
c->over_streak = 0;
|
||||
c->up_streak_s = 0.0;
|
||||
c->last_reason = "two down steps did not lower the cost: undone, down steps blocked";
|
||||
c->last_decision_t = now;
|
||||
return c->level;
|
||||
} else {
|
||||
if (c->post_down) c->verify_strikes = 0;
|
||||
rt_learn(c, c->post_load_before, load, c->post_from, c->post_to);
|
||||
}
|
||||
}
|
||||
/* A strike is about the steps right after it. */
|
||||
if (c->verify_strikes && !c->post_active && now - c->strike_t > c->p.strike_s)
|
||||
c->verify_strikes = 0;
|
||||
if (now - c->up_reached_t[c->level] >= c->p.relapse_forget_s) {
|
||||
c->relapse_dur[c->level] = c->p.relapse_block_s;
|
||||
c->up_reached_t[c->level] = -1e9;
|
||||
}
|
||||
if (c->post_active) return c->level;
|
||||
|
||||
const int over = !guest_bound && (load >= budget || strong_bp);
|
||||
c->last_over = over;
|
||||
if (over) {
|
||||
c->up_streak_s = 0.0;
|
||||
c->over_streak++;
|
||||
const int strong = strong_bp || load >= c->p.strong_load;
|
||||
if (c->level > c->floor && now >= c->down_block_until &&
|
||||
(strong || c->over_streak >= c->p.down_windows))
|
||||
rt_step(c, now, c->level - 1, 1, load, bp_share,
|
||||
strong_bp ? "queue full (render thread behind)"
|
||||
: strong ? "render cost over the interval"
|
||||
: "render cost over budget");
|
||||
else if (c->level <= c->floor)
|
||||
c->last_reason = "over budget at the floor";
|
||||
return c->level;
|
||||
}
|
||||
c->over_streak = 0;
|
||||
if (guest_bound) c->last_reason = "guest-bound (no step)";
|
||||
if (c->level >= c->ceiling) { c->up_streak_s = 0.0; return c->level; }
|
||||
const int next = c->level + 1;
|
||||
if (no_bp && dynrt_predict(c, load, c->level, next) <= c->p.up_load)
|
||||
c->up_streak_s += dur;
|
||||
else
|
||||
c->up_streak_s = 0.0;
|
||||
if (c->up_streak_s >= c->p.up_after_s - 1e-9 &&
|
||||
now - c->last_step_t >= c->p.up_cooldown_s &&
|
||||
now >= c->up_block_until[next])
|
||||
rt_step(c, now, next, 0, load, bp_share, "headroom");
|
||||
return c->level;
|
||||
}
|
||||
|
||||
int dynrt_sample(DynrtController *c, double now_s, const DynrtSample *s) {
|
||||
if (c->forced) return c->level;
|
||||
if (!s || s->period_s <= 0.0) return c->level;
|
||||
if (s->held || now_s < c->hold_until) {
|
||||
if (s->held || c->win_n) rt_discard(c);
|
||||
return c->level;
|
||||
}
|
||||
/* A long interval the queue explains is the render thread being slow,
|
||||
* not a gap. */
|
||||
const double unexplained = s->wall_s - (s->bp_s > 0.0 ? s->bp_s : 0.0);
|
||||
if (unexplained > s->period_s * c->p.gap_factor) {
|
||||
dynrt_hold(c, now_s, c->p.gap_hold_s);
|
||||
c->last_reason = "gap";
|
||||
return c->level;
|
||||
}
|
||||
c->win_period += s->period_s;
|
||||
c->win_wall += s->wall_s;
|
||||
c->win_n++;
|
||||
if (s->frames > 0 && s->cost_s >= 0.0) {
|
||||
c->win_frames += s->frames;
|
||||
c->win_cost += s->cost_s;
|
||||
}
|
||||
if (s->bp_s > 0.0) c->win_bp += s->bp_s;
|
||||
if (c->win_period >= c->p.window_s - 1e-9) return rt_close_window(c, now_s);
|
||||
return c->level;
|
||||
}
|
||||
|
||||
@@ -108,6 +108,7 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdatomic.h>
|
||||
#include "png_write.h" /* png_write_rgb — present_shot readback */
|
||||
|
||||
#ifndef GL_BGRA
|
||||
@@ -407,9 +408,13 @@ enum {
|
||||
RTH_OFFSET, RTH_WIDE_CONFIGURE, RTH_WIDE_VIEW, RTH_WIDE_TARGET,
|
||||
RTH_WIDE_DISABLE, RTH_WIDE_CLEAR, RTH_WIDE_CLEAR_MARGINS, RTH_PROJ_TRI,
|
||||
RTH_WIDE_RECOVERY, RTH_INTERP_SUSPENDED, RTH_PRESENT_VRAM,
|
||||
RTH_PRESENT_WIDE, RTH_STATE, RTH_PEEK, RTH_RING_CAPTURE
|
||||
RTH_PRESENT_WIDE, RTH_STATE, RTH_PEEK, RTH_RING_CAPTURE, RTH_DYN_STEP,
|
||||
RTH_FRAME
|
||||
};
|
||||
static int rth_record_mode(void);
|
||||
/* Render-thread frame cost (dynamic resolution; defined with GL_RT_BACKEND). */
|
||||
static uint64_t s_rthf_swap_ns = 0; /* render thread: time in the swap */
|
||||
static uint64_t host_now_ns_rthf(void);
|
||||
static int rth_rec_ints(uint16_t op, uint16_t flags, int n, const int32_t *v);
|
||||
static uint16_t rth_prim_flags(void);
|
||||
static int rth_record_present(uint16_t op, int n, const int32_t *v);
|
||||
@@ -8184,7 +8189,9 @@ static void gl_swap_with_osd(void) {
|
||||
/* Dynamic resolution's ledger: time blocked in the swap is the driver's
|
||||
* vsync wait unless the frame was late (main.cpp). Keep one owned swap. */
|
||||
uint64_t t0 = s_dyn_on ? SDL_GetPerformanceCounter() : 0;
|
||||
const uint64_t rthf_t0 = rth_replaying() ? host_now_ns_rthf() : 0;
|
||||
SDL_GL_SwapWindow(s_win);
|
||||
if (rthf_t0) s_rthf_swap_ns += host_now_ns_rthf() - rthf_t0;
|
||||
if (s_dyn_on) {
|
||||
s_dyn_last_swap_ticks = SDL_GetPerformanceCounter() - t0;
|
||||
s_dyn_ledger.swap_ticks += s_dyn_last_swap_ticks;
|
||||
@@ -8803,6 +8810,7 @@ static void dyn_context_ready(void) {
|
||||
}
|
||||
|
||||
void gl_renderer_set_dynamic_resolution(int on) {
|
||||
if (s_ctx) GL_RT_SYNC("set_dynamic_resolution");
|
||||
s_dyn_on = on ? 1 : 0;
|
||||
if (!s_ctx || !s_raster_ok) return; /* dyn_context_ready decides */
|
||||
if (s_dyn_on) {
|
||||
@@ -8895,6 +8903,15 @@ static void dyn_rescale_in_place(GLuint fbo, int surf_w, const int (*cols)[2], i
|
||||
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0);
|
||||
}
|
||||
|
||||
/* Render thread: the displayed rect a recorded step keeps at full detail,
|
||||
* captured when the step was recorded (gpu.c's display state is the
|
||||
* emulation thread's). */
|
||||
static int s_dyn_rth_disp_valid = 0, s_dyn_rth_disp[5];
|
||||
static atomic_flag s_dyn_stats_lock = ATOMIC_FLAG_INIT; /* s_dyn_stats: render thread writes */
|
||||
static void dyn_stats_lock(void) { while (atomic_flag_test_and_set_explicit(&s_dyn_stats_lock, memory_order_acquire)) {} }
|
||||
static void dyn_stats_unlock(void) { atomic_flag_clear_explicit(&s_dyn_stats_lock, memory_order_release); }
|
||||
static int s_rthm_scale = 1; /* emulation-side level (recording) */
|
||||
|
||||
static int dyn_apply(int snew) {
|
||||
if (!s_alloc_scale || !s_raster_ok || !s_ctx) { s_dyn_stats.refused++; return 0; }
|
||||
if (snew < 1) snew = 1;
|
||||
@@ -8930,7 +8947,14 @@ static int dyn_apply(int snew) {
|
||||
int qn = 0, qr[2][4];
|
||||
{
|
||||
GpuDisplayInfo di;
|
||||
gpu_get_display_info(&di);
|
||||
if (rth_replaying()) {
|
||||
memset(&di, 0, sizeof di);
|
||||
di.disabled = !s_dyn_rth_disp_valid || s_dyn_rth_disp[0];
|
||||
di.display_x = (uint32_t)s_dyn_rth_disp[1]; di.display_y = (uint32_t)s_dyn_rth_disp[2];
|
||||
di.width = (uint32_t)s_dyn_rth_disp[3]; di.height = (uint32_t)s_dyn_rth_disp[4];
|
||||
} else {
|
||||
gpu_get_display_info(&di);
|
||||
}
|
||||
if (!di.disabled && di.width > 0 && di.height > 0) {
|
||||
qr[qn][0] = (int)di.display_x; qr[qn][1] = (int)di.display_y;
|
||||
qr[qn][2] = (int)di.width; qr[qn][3] = (int)di.height;
|
||||
@@ -9032,6 +9056,7 @@ static int dyn_apply(int snew) {
|
||||
pass_gens_invalidate();
|
||||
for (int r = 0; r < PRES_ROWS; r++) s_present_dirty[r] = ~0ull;
|
||||
const uint64_t t6 = SDL_GetPerformanceCounter();
|
||||
dyn_stats_lock();
|
||||
s_dyn_stats.steps++;
|
||||
s_dyn_stats.last_from = sold;
|
||||
s_dyn_stats.last_to = snew;
|
||||
@@ -9041,6 +9066,7 @@ static int dyn_apply(int snew) {
|
||||
s_dyn_stats.last_rects_ms = dyn_ms(t3, t4);
|
||||
s_dyn_stats.last_wide_ms = dyn_ms(t4, t5);
|
||||
s_dyn_stats.last_rects = qn;
|
||||
dyn_stats_unlock();
|
||||
if (s_dyn_timing)
|
||||
fprintf(stdout, "psxrecomp: dynamic resolution %dx -> %dx in %.2f ms (drain %.2f, "
|
||||
"1x+rects %.2f, reseed %.2f, rects back %.2f, wide %.2f)\n", sold, snew,
|
||||
@@ -9050,16 +9076,37 @@ static int dyn_apply(int snew) {
|
||||
}
|
||||
|
||||
static void dyn_after_present(void) {
|
||||
/* Recording: the present was queued, not run; the step it would follow
|
||||
* is recorded after it (gl_renderer_step_internal_scale_now). */
|
||||
if (s_rth_on && !rt_on_render_thread() && !rt_held()) return;
|
||||
if (!s_dyn_pending) return;
|
||||
int s = s_dyn_pending;
|
||||
s_dyn_pending = 0;
|
||||
(void)dyn_apply(s); /* inside a pass: re-queued */
|
||||
}
|
||||
|
||||
/* Render thread: record the step in stream order (after the frame just
|
||||
* closed, before the next one's first draw); the emulation side's level
|
||||
* moves at once (s_rthm_scale), the backend's when the step is replayed.
|
||||
* s_alloc_scale only changes under a sync point. 1 when recorded. */
|
||||
static int dyn_record_step(int scale) {
|
||||
if (!(s_rth_on && !rt_on_render_thread() && !rt_held())) return 0;
|
||||
if (gpu_display_is_depth24()) { gl_rth_acquire("depth24"); return 0; }
|
||||
if (scale == s_rthm_scale) return 1;
|
||||
GpuDisplayInfo di;
|
||||
gpu_get_display_info(&di);
|
||||
int32_t a[6] = { scale, di.disabled ? 1 : 0, (int32_t)di.display_x,
|
||||
(int32_t)di.display_y, (int32_t)di.width, (int32_t)di.height };
|
||||
if (!rth_rec_ints(RTH_DYN_STEP, 0, 6, a)) { gl_rth_acquire("dyn_step"); return 0; }
|
||||
s_rthm_scale = scale;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int gl_renderer_request_internal_scale(int scale) {
|
||||
if (!s_alloc_scale || !s_raster_ok) return 0;
|
||||
if (scale < 1) scale = 1;
|
||||
if (scale > s_alloc_scale) scale = s_alloc_scale;
|
||||
if (dyn_record_step(scale)) return 1;
|
||||
s_dyn_pending = scale == s_hr_scale ? 0 : scale;
|
||||
return 1;
|
||||
}
|
||||
@@ -9068,16 +9115,21 @@ int gl_renderer_step_internal_scale_now(int scale) {
|
||||
if (!s_alloc_scale || !s_raster_ok) return 0;
|
||||
if (scale > s_alloc_scale) scale = s_alloc_scale;
|
||||
if (scale < 1) scale = 1;
|
||||
if (dyn_record_step(scale)) return 1;
|
||||
s_dyn_pending = 0;
|
||||
return dyn_apply(scale) && s_hr_scale == scale;
|
||||
}
|
||||
|
||||
void gl_renderer_dynres_stats(GlDynresStats *out) {
|
||||
if (!out) return;
|
||||
dyn_stats_lock();
|
||||
*out = s_dyn_stats;
|
||||
out->level = s_raster_ok ? s_hr_scale : 0;
|
||||
dyn_stats_unlock();
|
||||
/* While recording, the level the emulation thread has asked for. */
|
||||
const int rec = s_rth_on && !rt_on_render_thread() && !rt_held();
|
||||
out->level = !s_raster_ok ? 0 : rec ? s_rthm_scale : s_hr_scale;
|
||||
out->ceiling = gl_renderer_dynamic_resolution_ceiling();
|
||||
out->pending = s_dyn_pending;
|
||||
out->pending = rec ? 0 : s_dyn_pending;
|
||||
}
|
||||
|
||||
void gl_renderer_host_ledger(GlHostLedger *out) {
|
||||
@@ -9145,6 +9197,7 @@ static uint64_t s_rth_presents = 0, s_rth_presents_stale = 0;
|
||||
#define RTHF_BD_SHIFT 2 /* bits 2-3: psx_ws_prim_in_backdrop() 0..3 */
|
||||
|
||||
static void rth_mirror_resync(void) {
|
||||
s_rthm_scale = s_out_scale;
|
||||
s_rthm_wide_w = g_wide_w;
|
||||
s_rthm_wide_off = g_wide_off;
|
||||
s_rthm_cur = g_wide_cur != 0;
|
||||
@@ -9161,8 +9214,8 @@ static int rth_mirror_wide_for(int base_x) {
|
||||
for (int i = 0; i < WIDE_MAX_SURF; i++)
|
||||
if (s_rthm_base[i] == base_x) return 1;
|
||||
if (s_gl_max_dim > 0 &&
|
||||
((int64_t)s_rthm_wide_w * s_out_scale > s_gl_max_dim ||
|
||||
(int64_t)VRAM_H * s_out_scale > s_gl_max_dim))
|
||||
((int64_t)s_rthm_wide_w * alloc_scale_for(s_rthm_scale) > s_gl_max_dim ||
|
||||
(int64_t)VRAM_H * alloc_scale_for(s_rthm_scale) > s_gl_max_dim))
|
||||
return 0;
|
||||
for (int i = 0; i < WIDE_MAX_SURF; i++)
|
||||
if (s_rthm_base[i] < 0) { s_rthm_base[i] = base_x; return 1; }
|
||||
@@ -9322,6 +9375,114 @@ static void rth_replay_present(const RtCmd *c, const uint8_t *p) {
|
||||
s_rth_ov.valid = 0;
|
||||
}
|
||||
|
||||
|
||||
/* ---- render-thread frame cost (dynamic resolution) -------------------------
|
||||
* docs/RENDER_THREAD.md, "Dynamic resolution". Measured on the render thread
|
||||
* per replayed guest frame, between its first record and its RTH_FRAME
|
||||
* marker (recorded at the frame boundary): CPU = wall time replaying it minus
|
||||
* the render thread's idle waits for records and its time in the swap (the
|
||||
* display's vsync); GPU = a GL_TIMESTAMP pair around the same span, read back
|
||||
* when available (never waited on). cost = max(CPU, GPU). A frame during
|
||||
* which the context went to the emulation thread (a sync point) is dropped:
|
||||
* its span holds the emulation thread's own drawing. Published as running
|
||||
* totals for the emulation thread (gl_renderer_render_thread_costs). */
|
||||
#define RTHF_Q 8
|
||||
static _Atomic int s_rthf_want = 0; /* host: measure (dynres active) */
|
||||
static int s_rthf_qok = -1; /* render thread: queries usable */
|
||||
static GLuint s_rthf_q[RTHF_Q][2];
|
||||
static uint64_t s_rthf_slot_cpu[RTHF_Q];
|
||||
static unsigned s_rthf_head = 0, s_rthf_tail = 0;
|
||||
static int s_rthf_open = 0, s_rthf_taint = 0, s_rthf_has_q = 0;
|
||||
static uint64_t s_rthf_t0 = 0, s_rthf_idle0 = 0, s_rthf_swap0 = 0;
|
||||
static _Atomic uint64_t s_rthf_frames, s_rthf_cost_ns, s_rthf_cpu_ns, s_rthf_gpu_ns,
|
||||
s_rthf_gpu_frames, s_rthf_dropped;
|
||||
|
||||
static uint64_t host_now_ns_rthf(void) {
|
||||
return (uint64_t)((double)SDL_GetPerformanceCounter() * 1.0e9 /
|
||||
(double)SDL_GetPerformanceFrequency());
|
||||
}
|
||||
|
||||
static void rthf_publish(uint64_t cpu, uint64_t gpu, int has_gpu) {
|
||||
uint64_t cost = has_gpu && gpu > cpu ? gpu : cpu;
|
||||
atomic_fetch_add(&s_rthf_cpu_ns, cpu);
|
||||
if (has_gpu) {
|
||||
atomic_fetch_add(&s_rthf_gpu_ns, gpu);
|
||||
atomic_fetch_add(&s_rthf_gpu_frames, 1);
|
||||
}
|
||||
atomic_fetch_add(&s_rthf_cost_ns, cost);
|
||||
atomic_fetch_add(&s_rthf_frames, 1); /* last: the totals above are in */
|
||||
}
|
||||
|
||||
/* Read every finished query pair, oldest first, without waiting. */
|
||||
static void rthf_poll(void) {
|
||||
while (s_rthf_tail != s_rthf_head) {
|
||||
const unsigned i = s_rthf_tail % RTHF_Q;
|
||||
GLuint64 avail = 0, a = 0, b = 0;
|
||||
p_glGetQueryObjectui64v(s_rthf_q[i][1], GL_QUERY_RESULT_AVAILABLE, &avail);
|
||||
if (!avail) break;
|
||||
p_glGetQueryObjectui64v(s_rthf_q[i][0], GL_QUERY_RESULT, &a);
|
||||
p_glGetQueryObjectui64v(s_rthf_q[i][1], GL_QUERY_RESULT, &b);
|
||||
rthf_publish(s_rthf_slot_cpu[i], b > a ? (uint64_t)(b - a) : 0, 1);
|
||||
s_rthf_tail++;
|
||||
}
|
||||
}
|
||||
|
||||
static void rthf_begin(void) {
|
||||
if (s_rthf_qok < 0) {
|
||||
s_rthf_qok = (p_glGenQueries && p_glQueryCounter && p_glGetQueryObjectui64v &&
|
||||
!(getenv("PSX_DYNRES_GPU_TIMER") && getenv("PSX_DYNRES_GPU_TIMER")[0] == '0')) ? 1 : 0;
|
||||
if (s_rthf_qok) p_glGenQueries(RTHF_Q * 2, &s_rthf_q[0][0]);
|
||||
}
|
||||
s_rthf_open = 1;
|
||||
s_rthf_taint = 0;
|
||||
s_rthf_t0 = host_now_ns_rthf();
|
||||
s_rthf_idle0 = rt_render_idle_ns();
|
||||
s_rthf_swap0 = s_rthf_swap_ns;
|
||||
s_rthf_has_q = s_rthf_qok && s_rthf_head - s_rthf_tail < RTHF_Q;
|
||||
if (s_rthf_has_q) p_glQueryCounter(s_rthf_q[s_rthf_head % RTHF_Q][0], GL_TIMESTAMP);
|
||||
}
|
||||
|
||||
static void rthf_end(void) {
|
||||
if (!s_rthf_open) return;
|
||||
s_rthf_open = 0;
|
||||
if (s_rthf_taint) {
|
||||
/* The query pair is still issued so the ring stays in order; the
|
||||
* frame is not published. */
|
||||
if (s_rthf_has_q) {
|
||||
p_glQueryCounter(s_rthf_q[s_rthf_head % RTHF_Q][1], GL_TIMESTAMP);
|
||||
s_rthf_tail = ++s_rthf_head; /* drop it and anything older */
|
||||
}
|
||||
atomic_fetch_add(&s_rthf_dropped, 1);
|
||||
return;
|
||||
}
|
||||
const uint64_t now = host_now_ns_rthf();
|
||||
const uint64_t idle = rt_render_idle_ns() - s_rthf_idle0;
|
||||
const uint64_t swap = s_rthf_swap_ns - s_rthf_swap0;
|
||||
const uint64_t span = now - s_rthf_t0;
|
||||
const uint64_t cpu = span > idle + swap ? span - idle - swap : 0;
|
||||
if (s_rthf_has_q) {
|
||||
const unsigned i = s_rthf_head % RTHF_Q;
|
||||
p_glQueryCounter(s_rthf_q[i][1], GL_TIMESTAMP);
|
||||
s_rthf_slot_cpu[i] = cpu;
|
||||
s_rthf_head++;
|
||||
} else {
|
||||
rthf_publish(cpu, 0, 0);
|
||||
}
|
||||
if (s_rthf_qok > 0) rthf_poll();
|
||||
}
|
||||
|
||||
void gl_renderer_render_thread_measure(int on) { atomic_store(&s_rthf_want, on ? 1 : 0); }
|
||||
|
||||
void gl_renderer_render_thread_costs(GlRthCosts *out) {
|
||||
if (!out) return;
|
||||
out->frames = atomic_load(&s_rthf_frames);
|
||||
out->cost_ns = atomic_load(&s_rthf_cost_ns);
|
||||
out->cpu_ns = atomic_load(&s_rthf_cpu_ns);
|
||||
out->gpu_ns = atomic_load(&s_rthf_gpu_ns);
|
||||
out->gpu_frames = atomic_load(&s_rthf_gpu_frames);
|
||||
out->dropped = atomic_load(&s_rthf_dropped);
|
||||
}
|
||||
|
||||
/* ---- the recording vtable ------------------------------------------------ */
|
||||
#define RTH_DIRECT_OR(rec) do { if (rth_record_mode()) { rec; return; } } while (0)
|
||||
static void rtb_init(uint16_t *vram) {
|
||||
@@ -9331,7 +9492,11 @@ static void rtb_init(uint16_t *vram) {
|
||||
if (s_rth_on && !rt_held()) sw_renderer_rebind_vram(s_rth_vram_priv);
|
||||
}
|
||||
static void rtb_set_scale(int sc) { GL_RT_SYNC("set_scale"); glb_set_scale(sc); }
|
||||
static int rtb_scale(void) { return s_out_scale; } /* changes only under a sync point */
|
||||
/* The scale changes under a sync point, or by a recorded dynamic-resolution
|
||||
* step: while recording, the level as of the recording position. */
|
||||
static int rtb_scale(void) {
|
||||
return (s_rth_on && !rt_on_render_thread() && !rt_held()) ? s_rthm_scale : s_out_scale;
|
||||
}
|
||||
static void rtb_set_texture_filter(int b) { GL_RT_SYNC("texture_filter"); glb_set_texture_filter(b); }
|
||||
static int rtb_texture_filter(void) { return s_tex_filter; }
|
||||
static void rtb_set_semi_transparency(int e, int m) {
|
||||
@@ -9541,6 +9706,8 @@ static void gl_rth_exec(void *user, const RtCmd *c, const void *payload) {
|
||||
s_rthx_prim = (c->flags & RTHF_PRIM) != 0;
|
||||
s_rthx_tagged = (c->flags & RTHF_TAGGED) != 0;
|
||||
s_rthx_backdrop = (c->flags >> RTHF_BD_SHIFT) & 3;
|
||||
if (c->op == RTH_FRAME) { rthf_end(); return; }
|
||||
if (!s_rthf_open && atomic_load_explicit(&s_rthf_want, memory_order_relaxed)) rthf_begin();
|
||||
switch (c->op) {
|
||||
case RTH_SEMI: glb_set_semi_transparency(v[0], v[1]); break;
|
||||
case RTH_MASK: glb_set_mask_bits(v[0], v[1]); break;
|
||||
@@ -9611,6 +9778,12 @@ static void gl_rth_exec(void *user, const RtCmd *c, const void *payload) {
|
||||
break;
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
case RTH_RING_CAPTURE: rth_ring_capture_now(v); break;
|
||||
case RTH_DYN_STEP:
|
||||
s_dyn_rth_disp_valid = 1;
|
||||
memcpy(s_dyn_rth_disp, v + 1, sizeof s_dyn_rth_disp);
|
||||
(void)dyn_apply(v[0]);
|
||||
s_dyn_rth_disp_valid = 0;
|
||||
break;
|
||||
#endif
|
||||
case RTH_PEEK: {
|
||||
uint64_t ptr = (uint64_t)(uint32_t)v[4] | ((uint64_t)(uint32_t)v[5] << 32);
|
||||
@@ -9629,6 +9802,7 @@ static void gl_rth_ctx(void *user, int current) {
|
||||
} else {
|
||||
glFlush();
|
||||
SDL_GL_MakeCurrent(s_win, NULL);
|
||||
if (rt_on_render_thread() && s_rthf_open) s_rthf_taint = 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9695,6 +9869,9 @@ void gl_renderer_render_thread_frame_boundary(void) {
|
||||
} else if (!gl_rth_eligible()) {
|
||||
gl_rth_acquire("ineligible");
|
||||
}
|
||||
/* The render thread's per-frame cost (dynamic resolution) ends here. */
|
||||
if (!rt_held() && atomic_load_explicit(&s_rthf_want, memory_order_relaxed))
|
||||
RTH_REC(RTH_FRAME, 0, 0);
|
||||
rt_frame_end();
|
||||
}
|
||||
|
||||
|
||||
+140
-1
@@ -12,6 +12,7 @@
|
||||
#include "window_size.h" /* default game-window size */
|
||||
#include "internal_resolution.h" /* Settings -> Display -> Internal resolution */
|
||||
#include "dynamic_resolution.h" /* [video] dynamic_resolution: the step controller */
|
||||
#include "render_thread.h" /* rt_get_stats: queue backpressure (dynres) */
|
||||
#include "psx_scheduler.h" /* psx_scheduler_run — deterministic TCB scheduler */
|
||||
#include "parity_trace.h" /* general two-process control-flow parity ring */
|
||||
#include "device_trace.h" /* general two-process device-event cycle ring */
|
||||
@@ -1319,6 +1320,17 @@ struct DynresHost {
|
||||
bool step_measure_next = false;
|
||||
FILE *trace = nullptr;
|
||||
double t0_s = 0.0;
|
||||
/* Render-thread mode (dynres_tick_rt): the render thread's per-frame
|
||||
* costs and the queue's backpressure replace the wall-time model. */
|
||||
bool rt_mode = false;
|
||||
DynrtController rt{};
|
||||
GlRthCosts last_costs{};
|
||||
uint64_t last_bp_ns = 0;
|
||||
unsigned long long rt_last_windows = 0;
|
||||
bool rt_trace_header = false;
|
||||
double rt_win_cpu_ms = 0.0, rt_win_gpu_ms = 0.0; /* last window's means */
|
||||
double rt_acc_cpu = 0.0, rt_acc_gpu = 0.0;
|
||||
uint64_t rt_acc_frames = 0, rt_acc_gpu_frames = 0;
|
||||
};
|
||||
static DynresHost g_dynres;
|
||||
/* A savestate load re-stages VRAM and re-anchors pacing: hold for a while. */
|
||||
@@ -9239,12 +9251,16 @@ static void dynres_setup(void) {
|
||||
DynresParams params;
|
||||
dynres_default_params(¶ms);
|
||||
dynres_init(&g_dynres.ctl, ¶ms, floor_s, ceiling, ceiling);
|
||||
DynrtParams rtp;
|
||||
dynrt_default_params(&rtp);
|
||||
dynrt_init(&g_dynres.rt, &rtp, floor_s, ceiling, ceiling);
|
||||
g_dynres.active = floor_s < ceiling;
|
||||
if (const char* e = std::getenv("PSX_DYNRES_FORCE")) {
|
||||
const int n = std::atoi(e);
|
||||
if (n > 0) {
|
||||
g_dynres.active = true;
|
||||
const int l = dynres_force(&g_dynres.ctl, n);
|
||||
(void)dynrt_force(&g_dynres.rt, n);
|
||||
(void)gl_renderer_step_internal_scale_now(l);
|
||||
}
|
||||
}
|
||||
@@ -9278,6 +9294,57 @@ static void dynres_apply_level(int level) {
|
||||
}
|
||||
}
|
||||
|
||||
/* Render-thread mode: one sample per guest VBlank from what the render
|
||||
* thread measured since the last one (its frames' costs arrive a few frames
|
||||
* late) and the time the emulation thread spent blocked on the queue. */
|
||||
static void dynres_tick_rt(double now_s, double wall, double period, int held,
|
||||
double tail, const char *why) {
|
||||
DynrtController &c = g_dynres.rt;
|
||||
if (tail > 0.0) dynrt_hold(&c, now_s, tail);
|
||||
if (why) g_dynres.hold_reason = why;
|
||||
else if (now_s >= c.hold_until) g_dynres.hold_reason = "";
|
||||
GlRthCosts co;
|
||||
gl_renderer_render_thread_costs(&co);
|
||||
RtStats rs;
|
||||
rt_get_stats(&rs);
|
||||
const GlRthCosts &c0 = g_dynres.last_costs;
|
||||
const int frames = (int)(co.frames - c0.frames);
|
||||
const double cost = (double)(co.cost_ns - c0.cost_ns) * 1e-9;
|
||||
const uint64_t bp_ns = rs.backpressure_ns + rs.ring_full_ns;
|
||||
const double bp = (double)(bp_ns - g_dynres.last_bp_ns) * 1e-9;
|
||||
g_dynres.rt_acc_cpu += (double)(co.cpu_ns - c0.cpu_ns) * 1e-6;
|
||||
g_dynres.rt_acc_gpu += (double)(co.gpu_ns - c0.gpu_ns) * 1e-6;
|
||||
g_dynres.rt_acc_frames += co.frames - c0.frames;
|
||||
g_dynres.rt_acc_gpu_frames += co.gpu_frames - c0.gpu_frames;
|
||||
g_dynres.last_costs = co;
|
||||
g_dynres.last_bp_ns = bp_ns;
|
||||
DynrtSample smp{ period, wall, frames, cost, bp, held };
|
||||
const int level = dynrt_sample(&c, now_s, &smp);
|
||||
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
|
||||
? g_dynres.rt_acc_cpu / (double)g_dynres.rt_acc_frames : 0.0;
|
||||
g_dynres.rt_win_gpu_ms = g_dynres.rt_acc_gpu_frames
|
||||
? g_dynres.rt_acc_gpu / (double)g_dynres.rt_acc_gpu_frames : 0.0;
|
||||
g_dynres.rt_acc_cpu = g_dynres.rt_acc_gpu = 0.0;
|
||||
g_dynres.rt_acc_frames = g_dynres.rt_acc_gpu_frames = 0;
|
||||
if (g_dynres.trace) {
|
||||
if (!g_dynres.rt_trace_header) {
|
||||
g_dynres.rt_trace_header = true;
|
||||
std::fprintf(g_dynres.trace, "# render thread: t_s,level,load,bp_share,"
|
||||
"guest_hz,cpu_ms,gpu_ms,guest_bound,decision\n");
|
||||
}
|
||||
std::fprintf(g_dynres.trace, "%.3f,%d,%.3f,%.3f,%.2f,%.2f,%.2f,%d,%s\n",
|
||||
now_s - g_dynres.t0_s, c.level, c.last_load, c.last_bp_share,
|
||||
c.last_hz, g_dynres.rt_win_cpu_ms, g_dynres.rt_win_gpu_ms,
|
||||
c.last_guest_bound,
|
||||
c.last_decision_t == now_s ? c.last_reason : "");
|
||||
std::fflush(g_dynres.trace);
|
||||
}
|
||||
}
|
||||
dynres_apply_level(level);
|
||||
}
|
||||
|
||||
/* One guest VBlank, after its present. */
|
||||
static void dynres_tick(void) {
|
||||
if (!g_dynres.active || !g_gl_active) return;
|
||||
@@ -9351,6 +9418,38 @@ static void dynres_tick(void) {
|
||||
why = "savestate load";
|
||||
g_dynres.hold_request_s = 0.0;
|
||||
}
|
||||
/* The render thread on: its own model (dynres_tick_rt). The level carries
|
||||
* over when the mode changes (the render thread starts at the first
|
||||
* VBlank, after dynres_setup). */
|
||||
const bool rt = gl_renderer_render_thread_active() != 0;
|
||||
if (rt != g_dynres.rt_mode) {
|
||||
g_dynres.rt_mode = rt;
|
||||
gl_renderer_render_thread_measure(rt ? 1 : 0);
|
||||
GlDynresStats st;
|
||||
gl_renderer_dynres_stats(&st);
|
||||
if (rt) {
|
||||
const DynrtParams rp = g_dynres.rt.p;
|
||||
const int forced = g_dynres.rt.forced;
|
||||
dynrt_init(&g_dynres.rt, &rp, g_dynres.ctl.floor, g_dynres.ctl.ceiling, st.level);
|
||||
if (forced) (void)dynrt_force(&g_dynres.rt, forced);
|
||||
gl_renderer_render_thread_costs(&g_dynres.last_costs);
|
||||
RtStats rs;
|
||||
rt_get_stats(&rs);
|
||||
g_dynres.last_bp_ns = rs.backpressure_ns + rs.ring_full_ns;
|
||||
} else {
|
||||
const DynresParams cp = g_dynres.ctl.p;
|
||||
const int forced = g_dynres.ctl.forced;
|
||||
const int fl = g_dynres.ctl.floor, ce = g_dynres.ctl.ceiling;
|
||||
dynres_init(&g_dynres.ctl, &cp, fl, ce, st.level);
|
||||
if (forced) (void)dynres_force(&g_dynres.ctl, forced);
|
||||
}
|
||||
if (tail < 1.0) tail = 1.0;
|
||||
if (!why) why = rt ? "render thread started" : "render thread stopped";
|
||||
}
|
||||
if (rt) {
|
||||
dynres_tick_rt(now_s, wall, period, held, tail, why);
|
||||
return;
|
||||
}
|
||||
if (tail > 0.0) dynres_hold(&g_dynres.ctl, now_s, tail);
|
||||
if (why) g_dynres.hold_reason = why;
|
||||
else if (now_s >= g_dynres.ctl.hold_until) g_dynres.hold_reason = "";
|
||||
@@ -9380,7 +9479,8 @@ extern "C" void psx_dynres_summary(int *enabled, int *level, int *floor_s, int *
|
||||
|
||||
extern "C" int psx_dynres_force(int scale) {
|
||||
if (!g_dynres.active) return -1;
|
||||
const int l = dynres_force(&g_dynres.ctl, scale);
|
||||
const int l = g_dynres.rt_mode ? dynrt_force(&g_dynres.rt, scale)
|
||||
: dynres_force(&g_dynres.ctl, scale);
|
||||
dynres_apply_level(l);
|
||||
return l;
|
||||
}
|
||||
@@ -9388,6 +9488,45 @@ extern "C" int psx_dynres_force(int scale) {
|
||||
extern "C" int psx_dynres_status_json(char *out, int cap) {
|
||||
GlDynresStats st;
|
||||
gl_renderer_dynres_stats(&st);
|
||||
if (g_dynres.rt_mode) { /* render-thread mode (dynres_tick_rt) */
|
||||
const DynrtController &r = g_dynres.rt;
|
||||
const double now_s = (double)SDL_GetPerformanceCounter() /
|
||||
(double)SDL_GetPerformanceFrequency();
|
||||
char blocked[256];
|
||||
int bp = 0;
|
||||
blocked[0] = 0;
|
||||
for (int l = r.floor; g_dynres.active && l <= r.ceiling && bp < (int)sizeof blocked - 24; l++) {
|
||||
double b = dynrt_up_blocked_s(&r, l, now_s);
|
||||
if (b > 0.0)
|
||||
bp += std::snprintf(blocked + bp, sizeof blocked - (size_t)bp, "%s\"%d\":%.1f",
|
||||
bp ? "," : "", l, b);
|
||||
}
|
||||
GlRthCosts co;
|
||||
gl_renderer_render_thread_costs(&co);
|
||||
const double hold = r.hold_until - now_s;
|
||||
return std::snprintf(out, (size_t)cap,
|
||||
"\"mode\":\"render_thread\",\"enabled\":%d,\"requested\":%d,\"ceiling\":%d,"
|
||||
"\"floor\":%d,\"level\":%d,\"internal_lines\":%d,\"forced\":%d,\"load\":%.3f,"
|
||||
"\"budget\":%.3f,\"bp_share\":%.3f,\"guest_hz\":%.2f,\"cpu_ms\":%.3f,"
|
||||
"\"gpu_ms\":%.3f,\"guest_bound\":%d,\"over\":%d,\"scaled_share\":%.3f,"
|
||||
"\"hold\":\"%s\",\"hold_s\":%.2f,\"last_reason\":\"%s\",\"downs\":%llu,"
|
||||
"\"ups\":%llu,\"undos\":%llu,\"relapses\":%llu,\"windows\":%llu,"
|
||||
"\"held_windows\":%llu,\"guest_bound_windows\":%llu,\"thin_windows\":%llu,"
|
||||
"\"steps\":%llu,\"last_from\":%d,\"last_to\":%d,\"last_ms\":%.3f,"
|
||||
"\"frames_measured\":%llu,\"gpu_frames\":%llu,\"frames_dropped\":%llu,"
|
||||
"\"down_blocked_s\":%.1f,\"up_blocked\":{%s}",
|
||||
g_dynres.active ? 1 : 0, dynres_requested(), st.ceiling,
|
||||
g_dynres.active ? r.floor : 0, st.level, st.level * g_video_ref_lines, r.forced,
|
||||
r.last_load, 1.0 - r.p.margin, r.last_bp_share, r.last_hz, g_dynres.rt_win_cpu_ms,
|
||||
g_dynres.rt_win_gpu_ms, r.last_guest_bound, r.last_over, r.f,
|
||||
g_dynres.hold_reason ? g_dynres.hold_reason : "", hold > 0.0 ? hold : 0.0,
|
||||
r.last_reason ? r.last_reason : "", r.downs, r.ups, r.undos, r.relapses,
|
||||
r.windows, r.held_windows, r.guest_bound_windows, r.thin_windows,
|
||||
(unsigned long long)st.steps, st.last_from, st.last_to, st.last_ms,
|
||||
(unsigned long long)co.frames, (unsigned long long)co.gpu_frames,
|
||||
(unsigned long long)co.dropped,
|
||||
r.down_block_until > now_s ? r.down_block_until - now_s : 0.0, blocked);
|
||||
}
|
||||
const DynresController &c = g_dynres.ctl;
|
||||
const double now_s = (double)SDL_GetPerformanceCounter() /
|
||||
(double)SDL_GetPerformanceFrequency();
|
||||
|
||||
@@ -126,6 +126,7 @@ static __thread int t_render;
|
||||
int rt_running(void) { return atomic_load(&R.running); }
|
||||
int rt_on_render_thread(void) { return t_render; }
|
||||
int rt_held(void) { return R.held; }
|
||||
uint64_t rt_render_idle_ns(void) { return atomic_load(&R.idle_ns); }
|
||||
|
||||
static inline uint64_t align_up(uint64_t n) {
|
||||
return (n + (RT_ALIGN - 1)) & ~(uint64_t)(RT_ALIGN - 1);
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
/* 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.
|
||||
* 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);
|
||||
}
|
||||
|
||||
/* 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: down one level at a time to the highest level
|
||||
* that fits, fast, 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(&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(&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);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user