mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
Frame generation: plan ceiling for render-thread overload, stalls judged by wait
A render thread a frame behind lowers the plan's ceiling by one (recovering one per 2 s) instead of tripping the breaker; the guest waiting on the queue trips it only when the wait was a stall (a quarter of the game frame). The real-frame cost plans with CPU time; the GPU span is reported only. The generated-frame blit copies colour only and marks the stencil stale. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -117,6 +117,14 @@ void fg_cost_add(FgCost *c, double cost_s, double fit_s);
|
||||
* `fit_s` as above: an estimate above it is what blocks the plan. */
|
||||
double fg_cost_estimate(FgCost *c, double now, double fit_s);
|
||||
|
||||
/* A ceiling on the plan for what the cost estimate cannot see (the real
|
||||
* frames' GPU work): an overload trip while n frames were planned lowers
|
||||
* it to n - 1; each `recover_s` without one raises it by one, up to max. */
|
||||
typedef struct FgCeiling { int cap, max; double last; double recover_s; } FgCeiling;
|
||||
void fg_ceiling_init(FgCeiling *c, int max, double recover_s);
|
||||
void fg_ceiling_trip(FgCeiling *c, int n_planned, double now);
|
||||
int fg_ceiling_get(FgCeiling *c, double now);
|
||||
|
||||
/* The guest's own pacing: frame boundaries against a schedule that advances
|
||||
* one interval per frame. Late only when the guest slipped more than `slack_s`
|
||||
* behind its schedule (jitter, a long frame followed by a short one, does not
|
||||
|
||||
@@ -253,3 +253,24 @@ int fg_pace_note(FgPace *p, double now, double period_s, double slack_s) {
|
||||
p->next += period_s;
|
||||
return late;
|
||||
}
|
||||
|
||||
void fg_ceiling_init(FgCeiling *c, int max, double recover_s) {
|
||||
c->cap = c->max = max;
|
||||
c->last = -1e30;
|
||||
c->recover_s = recover_s;
|
||||
}
|
||||
|
||||
void fg_ceiling_trip(FgCeiling *c, int n_planned, double now) {
|
||||
if (n_planned <= 0) return;
|
||||
const int to = n_planned - 1 > 1 ? n_planned - 1 : 1;
|
||||
if (to < c->cap) c->cap = to;
|
||||
c->last = now;
|
||||
}
|
||||
|
||||
int fg_ceiling_get(FgCeiling *c, double now) {
|
||||
while (c->cap < c->max && now - c->last >= c->recover_s) {
|
||||
c->cap++;
|
||||
c->last = c->last < -1e29 ? now : c->last + c->recover_s;
|
||||
}
|
||||
return c->cap;
|
||||
}
|
||||
|
||||
@@ -9839,7 +9839,7 @@ static int s_rthf_nseg[RTHF_Q];
|
||||
static uint64_t s_rthf_slot_cpu[RTHF_Q];
|
||||
static int s_rthf_paused = 0; /* a pause ended this frame's query */
|
||||
static uint64_t s_rthf_pause_t0 = 0, s_rthf_pause_swap = 0, s_rthf_excl = 0;
|
||||
static void fg_note_real_cost(uint64_t cost_ns); /* frame generation */
|
||||
static void fg_note_real_cost(uint64_t cpu_ns, uint64_t gpu_ns, int has_gpu); /* frame generation */
|
||||
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;
|
||||
@@ -9860,11 +9860,9 @@ static void rthf_publish(uint64_t cpu, uint64_t gpu, int has_gpu) {
|
||||
}
|
||||
atomic_fetch_add(&s_rthf_cost_ns, cost);
|
||||
atomic_fetch_add(&s_rthf_frames, 1); /* last: the totals above are in */
|
||||
/* Frame generation budgets with the CPU time: on macOS a TIME_ELAPSED
|
||||
* span also counts the GPU waiting for a frame's records to arrive, so
|
||||
* it reads close to the whole interval whatever the work. GPU overload
|
||||
* shows up as a slow swap and a queue that backs up (the breaker). */
|
||||
fg_note_real_cost(cpu);
|
||||
/* Frame generation budgets with the CPU time (see
|
||||
* fg_note_real_cost for why only the CPU time plans). */
|
||||
fg_note_real_cost(cpu, gpu, has_gpu);
|
||||
}
|
||||
|
||||
/* Read every finished query pair, oldest first, without waiting. */
|
||||
@@ -10037,7 +10035,7 @@ static int32_t *s_fg_match = NULL; static uint32_t s_fg_match_cap = 0;
|
||||
static FgMatchStats s_fg_mst;
|
||||
static FgBreaker s_fg_brk;
|
||||
static int s_fg_brk_init = 0;
|
||||
static uint64_t s_fg_bp_seen = 0;
|
||||
static uint64_t s_fg_bp_seen = 0, s_fg_bp_ns_seen = 0;
|
||||
static double s_fg_real_ema = 0.0; /* seconds */
|
||||
/* A generated frame's cost: cold samples discarded, a blocking estimate
|
||||
* re-probed (frame_gen.h FgCost). s_fg_fit_s: what one may cost to fit. */
|
||||
@@ -10096,9 +10094,20 @@ static void fg_note_swap(uint64_t ns) {
|
||||
s_fg_swap_ema = s_fg_swap_ema > 0.0 ? s_fg_swap_ema * 0.9 + c * 0.1 : c;
|
||||
}
|
||||
|
||||
static void fg_note_real_cost(uint64_t cost_ns) {
|
||||
const double c = (double)cost_ns * 1e-9;
|
||||
/* A real VBlank frame's render-thread cost, for the plan: its CPU time. Its
|
||||
* GPU span is kept for the debug output only: on macOS it also counts the
|
||||
* GPU waiting for records, so with generation off it reads most of the
|
||||
* VBlank (R4 1P: 10 ms for ~3 ms of work) and would leave no room. GPU
|
||||
* overload shows as a queue that backs up instead: the breaker, and the
|
||||
* ceiling that lowers the plan by one per such trip (FgCeiling). */
|
||||
static double s_fg_real_gpu_ema = 0.0;
|
||||
static void fg_note_real_cost(uint64_t cpu_ns, uint64_t gpu_ns, int has_gpu) {
|
||||
const double c = (double)cpu_ns * 1e-9;
|
||||
s_fg_real_ema = s_fg_real_ema > 0.0 ? s_fg_real_ema * 0.9 + c * 0.1 : c;
|
||||
if (has_gpu) {
|
||||
const double g = (double)gpu_ns * 1e-9;
|
||||
s_fg_real_gpu_ema = s_fg_real_gpu_ema > 0.0 ? s_fg_real_gpu_ema * 0.9 + g * 0.1 : g;
|
||||
}
|
||||
}
|
||||
|
||||
static void fg_list_clear(FgList *l) {
|
||||
@@ -10424,13 +10433,16 @@ static void fg_surfaces_free(void) {
|
||||
s_fg_real_fbo = s_fg_real_tex = 0; s_fg_real_w = s_fg_real_h = 0; s_fg_real_kept = 0;
|
||||
}
|
||||
|
||||
/* Colour only: a depth-stencil blit costs the CPU milliseconds on macOS
|
||||
* (most of a generated frame at high levels). The stencil is the mask-bit
|
||||
* mirror of alpha; the caller marks it stale and it is rebuilt from the
|
||||
* copied alpha only if a generated draw checks the mask. */
|
||||
static void fg_blit(GLuint src, GLuint dst, int x, int y, int w, int h) {
|
||||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, src);
|
||||
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, dst);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glStencilMask(0xFF);
|
||||
p_glBlitFramebuffer(x, y, x + w, y + h, x, y, x + w, y + h,
|
||||
GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT, GL_NEAREST);
|
||||
GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0);
|
||||
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0);
|
||||
}
|
||||
@@ -10664,6 +10676,10 @@ static int fg_generate(double t, int swap) {
|
||||
s_wide_tex[iw] = s_fg_w_tex; s_wide_fbo[iw] = s_fg_w_fbo; s_wide_rb[iw] = s_fg_w_rb;
|
||||
s_wide_as[iw] = S;
|
||||
}
|
||||
/* The copied stencil is stale: rebuilt from alpha if a draw needs it. */
|
||||
s_stencil_valid = 0;
|
||||
rect_add(&s_stencil_stale, b->disp[0], dy, b->disp[0] + b->disp[2] - 1, dy + dh - 1);
|
||||
if (iw >= 0) wst_add(iw, 0, dy, g_wide_w, dy + dh);
|
||||
s_fg_drawing = 1;
|
||||
fg_replay(a, b, t, iw >= 0 ? s_fg_w_fbo : 0);
|
||||
fg_compose(b, iw >= 0 ? s_fg_w_fbo : 0, iw >= 0 ? s_fg_w_tex : 0, b->linear, b->force43);
|
||||
@@ -10700,9 +10716,29 @@ static void rth_present_payload(uint16_t op, const uint8_t *p); /* below */
|
||||
/* Short first hold, doubling on repeats within 2 s of the last hold's end,
|
||||
* up to 8 s: one hiccup costs a fraction of a second of generation. */
|
||||
#define FG_BRK_INIT() do { if (!s_fg_brk_init) { fg_breaker_init(&s_fg_brk, 0.5, 8.0, 2.0); s_fg_brk_init = 1; } } while (0)
|
||||
static FgCeiling s_fg_ceil;
|
||||
static int s_fg_ceil_init = 0;
|
||||
static FgCeiling *fg_ceil(void) {
|
||||
if (!s_fg_ceil_init) { fg_ceiling_init(&s_fg_ceil, 7, 2.0); s_fg_ceil_init = 1; }
|
||||
return &s_fg_ceil;
|
||||
}
|
||||
static uint32_t s_fg_trip_late = 0, s_fg_trip_bp = 0, s_fg_trip_behind = 0;
|
||||
static void fg_trip(const char *why) {
|
||||
FG_BRK_INIT();
|
||||
fg_breaker_trip(&s_fg_brk, fg_now_s(), why);
|
||||
if (why[0] == 'g') s_fg_trip_late++;
|
||||
else s_fg_trip_bp++;
|
||||
const double now = fg_now_s();
|
||||
fg_breaker_trip(&s_fg_brk, now, why);
|
||||
fg_ceiling_trip(fg_ceil(), s_fg_pending ? s_fg_n : s_fg_last_n, now);
|
||||
}
|
||||
/* The render thread a frame behind (a stale present, two frames queued):
|
||||
* proportional, not a stop. 2P races show stale presents with generation
|
||||
* off as well, so it lowers the plan's ceiling by one (recovering one per
|
||||
* 2 s) and the real frame shows at once; the breaker is for the guest
|
||||
* slipping or stalling on the queue. */
|
||||
static void fg_overload(void) {
|
||||
s_fg_trip_behind++;
|
||||
fg_ceiling_trip(fg_ceil(), s_fg_pending ? s_fg_n : s_fg_last_n, fg_now_s());
|
||||
}
|
||||
|
||||
static void fg_gen_cost_poll(void) {
|
||||
@@ -10829,9 +10865,9 @@ static uint64_t fg_tick(void *user, uint64_t now) {
|
||||
const uint64_t bp = rt_backpressure_events();
|
||||
const int ahead = rt_frames_ahead();
|
||||
if (!s_fg_force && (ahead >= 1 || bp != s_fg_bp_seen)) {
|
||||
if (bp != s_fg_bp_seen) {
|
||||
if (fg_recently_generated()) fg_trip("queue backed up"); else s_fg_ignored_bp++;
|
||||
} else if (ahead >= 2) fg_trip("render thread behind");
|
||||
/* Backpressure itself is judged at the flip, by how long the guest
|
||||
* waited (fg_on_present); here it only shows the real frame now. */
|
||||
if (bp == s_fg_bp_seen && ahead >= 2 && fg_recently_generated()) fg_overload();
|
||||
s_fg_bp_seen = bp;
|
||||
fg_flush();
|
||||
return 0;
|
||||
@@ -10912,12 +10948,25 @@ static int fg_on_present(uint16_t op, const uint8_t *p, uint32_t bytes, int stal
|
||||
const char *why = atomic_exchange(&s_fg_hold_reason, NULL);
|
||||
if (why) s_fg_hold_why = why;
|
||||
const int held = rt_now_ns() < atomic_load(&s_fg_hold_until);
|
||||
/* The guest waiting on the queue trips the breaker when the wait since
|
||||
* the last flip was a stall (a quarter of the game frame), not the short
|
||||
* waits a 2P race shows with generation off as well. */
|
||||
const uint64_t bp = rt_backpressure_events();
|
||||
if (bp != s_fg_bp_seen) {
|
||||
s_fg_bp_seen = bp;
|
||||
if (gen_recent) fg_trip("queue backed up"); else s_fg_ignored_bp++;
|
||||
s_fg_bp_seen = bp;
|
||||
{
|
||||
RtStats rs;
|
||||
rt_get_stats(&rs);
|
||||
const uint64_t bns = rs.backpressure_ns + rs.ring_full_ns;
|
||||
const double waited = (double)(bns - s_fg_bp_ns_seen) * 1e-9;
|
||||
const double ghz0 = atomic_load(&s_fg_guest_hz);
|
||||
s_fg_bp_ns_seen = bns;
|
||||
if (waited > 0.25 * (double)s_fg_flip_vb / (ghz0 > 1.0 ? ghz0 : 59.94)) {
|
||||
if (gen_recent) fg_trip("queue backed up"); else s_fg_ignored_bp++;
|
||||
} else if (waited > 0.0) {
|
||||
s_fg_ignored_bp++;
|
||||
}
|
||||
}
|
||||
if (stale) fg_trip("render thread behind");
|
||||
if (stale && gen_recent) fg_overload();
|
||||
FG_BRK_INIT();
|
||||
const double ghz = atomic_load(&s_fg_guest_hz), rhz = atomic_load(&s_fg_refresh_hz);
|
||||
const double flip_s = (double)s_fg_flip_vb / (ghz > 1.0 ? ghz : 59.94);
|
||||
@@ -10935,7 +10984,8 @@ static int fg_on_present(uint16_t op, const uint8_t *p, uint32_t bytes, int stal
|
||||
if (s_fg_force) n = slots > 1 ? slots - 1 : 1;
|
||||
else if (fg_breaker_open(&s_fg_brk, now) && !stale && !held) {
|
||||
const double est = fg_cost_estimate(fg_cost(), now, s_fg_fit_s);
|
||||
n = fg_plan(flip_s, rhz, real_s, est > 0.0 ? est + s_fg_swap_ema : 0.0, 0.85, 7);
|
||||
n = fg_plan(flip_s, rhz, real_s, est > 0.0 ? est + s_fg_swap_ema : 0.0, 0.85,
|
||||
fg_ceiling_get(fg_ceil(), now));
|
||||
}
|
||||
if (n == 0) s_fg_skipped_plan++;
|
||||
}
|
||||
@@ -11030,7 +11080,9 @@ int gl_renderer_frame_gen_json(char *out, int cap) {
|
||||
"\"prims\":%u,\"matched\":%u,\"moved\":%u,\"unmatched\":%u,"
|
||||
"\"breaker_s\":%.2f,\"trips\":%u,\"last_trip\":\"%s\",\"held_s\":%.2f,"
|
||||
"\"last_hold\":\"%s\",\"swaps\":%llu,\"gen_samples\":%u,\"gen_cold\":%u,"
|
||||
"\"gen_probes\":%u,\"ignored_late\":%llu,\"ignored_bp\":%llu",
|
||||
"\"gen_probes\":%u,\"ignored_late\":%llu,\"ignored_bp\":%llu,"
|
||||
"\"real_cpu_ms\":%.3f,\"real_gpu_ms\":%.3f,\"ceiling\":%d,"
|
||||
"\"trips_late\":%u,\"trips_backed_up\":%u,\"trips_behind\":%u",
|
||||
s_fg_on, s_fg_on && s_rth_on && open, s_fg_force,
|
||||
(unsigned long long)s_fg_generated, (unsigned long long)s_fg_real_presents,
|
||||
(unsigned long long)s_fg_flips, (unsigned long long)s_fg_dups,
|
||||
@@ -11046,7 +11098,9 @@ int gl_renderer_frame_gen_json(char *out, int cap) {
|
||||
? atomic_load(&s_fg_hold_until) - rt_now_ns() : 0) * 1e-9,
|
||||
s_fg_hold_why ? s_fg_hold_why : "", (unsigned long long)s_swaps_total,
|
||||
fg_cost()->samples, fg_cost()->discarded, fg_cost()->probes,
|
||||
(unsigned long long)s_fg_ignored_late, (unsigned long long)s_fg_ignored_bp);
|
||||
(unsigned long long)s_fg_ignored_late, (unsigned long long)s_fg_ignored_bp,
|
||||
s_fg_real_ema * 1e3, s_fg_real_gpu_ema * 1e3, fg_ceil()->cap,
|
||||
s_fg_trip_late, s_fg_trip_bp, s_fg_trip_behind);
|
||||
}
|
||||
|
||||
/* ---- the recording vtable ------------------------------------------------ */
|
||||
|
||||
@@ -210,7 +210,25 @@ static void test_pace(void) {
|
||||
check(trips > 0 && trips < 20, "a steadily slow guest is late now and then");
|
||||
}
|
||||
|
||||
static void test_ceiling(void) {
|
||||
FgCeiling c;
|
||||
fg_ceiling_init(&c, 7, 2.0);
|
||||
check(fg_ceiling_get(&c, 0.0) == 7, "no ceiling before a trip");
|
||||
fg_ceiling_trip(&c, 3, 1.0);
|
||||
check(fg_ceiling_get(&c, 1.5) == 2, "an overload at three plans two");
|
||||
fg_ceiling_trip(&c, 2, 1.6);
|
||||
check(fg_ceiling_get(&c, 1.7) == 1, "another lowers it again");
|
||||
fg_ceiling_trip(&c, 1, 1.8);
|
||||
check(fg_ceiling_get(&c, 1.9) == 1, "never below one (the breaker stops generation)");
|
||||
check(fg_ceiling_get(&c, 3.7) == 1 && fg_ceiling_get(&c, 3.9) == 2, "recovers one per 2 s");
|
||||
check(fg_ceiling_get(&c, 7.9) == 4, "and keeps recovering");
|
||||
fg_ceiling_trip(&c, 0, 8.0);
|
||||
check(fg_ceiling_get(&c, 8.0) == 4, "a trip with nothing planned is not an overload");
|
||||
check(fg_ceiling_get(&c, 100.0) == 7, "up to the maximum");
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
test_ceiling();
|
||||
test_cost();
|
||||
test_pace();
|
||||
test_identical_and_moved();
|
||||
|
||||
Reference in New Issue
Block a user