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:
Shokunin
2026-10-06 02:14:38 -07:00
parent 1665507fe4
commit 2a5e53ffbf
9 changed files with 1056 additions and 9 deletions
+9
View File
@@ -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
+115
View File
@@ -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
+9
View File
@@ -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;
+5 -1
View File
@@ -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
+291
View File
@@ -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;
}
+184 -7
View File
@@ -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) {
/* Never in the windowed high-resolution mode (s_hiw): dyn_eligible keeps
* s_alloc_scale 0 there; refuse here too rather than step its tiles. */
@@ -8932,7 +8949,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;
@@ -9034,6 +9058,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;
@@ -9043,6 +9068,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,
@@ -9052,16 +9078,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;
}
@@ -9070,16 +9117,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) {
@@ -9147,6 +9199,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;
@@ -9163,8 +9216,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; }
@@ -9324,6 +9377,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) {
@@ -9333,7 +9494,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) {
@@ -9543,6 +9708,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;
@@ -9613,6 +9780,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);
@@ -9631,6 +9804,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;
}
}
@@ -9697,6 +9871,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
View File
@@ -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(&params);
dynres_init(&g_dynres.ctl, &params, 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();
+1
View File
@@ -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);
+302
View File
@@ -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;
}