WIP: queue temporal deadlines and release refused render plans

This commit is contained in:
Shokunin
2026-10-21 14:57:04 -07:00
parent d0d97a7434
commit af942084ea
10 changed files with 120 additions and 22 deletions
+12
View File
@@ -213,6 +213,18 @@ on a fixed region -> next.
## 5. Status / Log (update every session)
- 2026-10-21 BRIEF-4 Stage 3 WIP: restore asynchronous temporal eligibility,
queue presentation deadlines on the presenter, release rejected plans, and
credit frontend deadline sleep without adding overlapping renderer idle.
A bounded size-aware image budget admits two images per generation at 8K
(1 GiB ceiling). Title tests pass Windows/Mac 25/25; schedule/guard checks
pass. R4 guest off/on identity matches 4,501 frames with 735 temporal passes.
Acceptance still FAILS: full-8K transactional redraw quiet 58.528 Hz,
311 >25 ms completion gaps, p99 busy 65.0%; 33.8 s context-acquire waits.
GPU recorded redraw experiment also failed timing/distinct-frame coverage
and was removed. Queued GPU transactions remain necessary; no Stage 3 push.
R4 `analysis/STATUS.md` links local receipts. Stage 2 remains pushed/accepted.
- 2026-10-21 BRIEF-4 Stage 2: match Windows emulation priority to render/present
workers and reuse packed vertex uploads across queued wide/window draws
(from `910eef11`, retaining current depth/coverage behavior). R4 at the
+3
View File
@@ -17,6 +17,9 @@ typedef struct FrameInterpolationSchedule {
* plans the whole 0..1 window, which is the historical behaviour. */
double phase_lo;
double phase_hi;
/* Bounded asynchronous producer: preparing several frames ahead is normal,
* so an early arrival must not reset the presentation clock. */
int queued;
} FrameInterpolationSchedule;
/* Plan one stock guest-frame interval. Presentation deadlines remain anchored
+4
View File
@@ -95,7 +95,11 @@ void gl_renderer_pass_set_flip_shown(int shown);
void gl_renderer_pass_end(uint32_t alpha_q16, int keep);
uint32_t gl_renderer_pass_leaks(void);
int gl_renderer_pass_verify_vram(void);
/* Relinquish the synchronous context after refusing a temporal plan. */
void gl_renderer_pass_cancel_plan(void);
void gl_renderer_pass_note_cost(uint64_t ticks);
/* Actual frontend deadline sleep; admission uses max with renderer idle. */
void gl_renderer_pass_note_frontend_idle(uint64_t ticks);
/* Present output deadlines that fell due while guest code ran. */
void gl_renderer_pass_service_presents(void);
/* promotions, presents, blended presents, expired, unmatched flips, presents
+1 -1
View File
@@ -45,7 +45,7 @@ int frame_interpolation_schedule_begin_phase(FrameInterpolationSchedule *schedul
* Normal guest work lands after the prior deadline but before the next one;
* retaining the old anchor makes that work consume the pacing budget. */
if (schedule->source_deadline <= 0.0 ||
now_d + source_period < schedule->source_deadline ||
(!schedule->queued && now_d + source_period < schedule->source_deadline) ||
now_d > schedule->source_deadline + source_period * 4.0) {
schedule->frame_start = now_d;
schedule->source_deadline = now_d + source_period;
+53 -13
View File
@@ -510,6 +510,7 @@ static int ctx_bg_full(void) {
* sync point (the emulation thread draws those frames itself). */
static int ctx_depth24(void) { return rth_replaying() ? 0 : gpu_display_is_depth24(); }
static void gl_rth_acquire(const char *reason);
static void gl_rth_release(void);
#define GL_RT_SYNC(reason) \
do { if (s_rth_on && !rt_on_render_thread()) gl_rth_acquire(reason); } while (0)
/* Recording (defined with GL_RT_BACKEND at the end of the file). */
@@ -522,7 +523,7 @@ enum {
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_DYN_STEP,
RTH_FRAME, RTH_FG_SRC, RTH_DEPTH, RTH_HD_NOTE, RTH_DITHER, RTH_WIDE_FEEDBACK
RTH_FRAME, RTH_FG_SRC, RTH_DEPTH, RTH_HD_NOTE, RTH_DITHER, RTH_WIDE_FEEDBACK, RTH_PASS_SERVICE
};
/* GP0(A0) whose payload gpu.c is still streaming into its array (the
* facade's vram_upload_open, emulation thread), tracked whatever the HD
@@ -714,6 +715,8 @@ static double s_interp_host_hz = 0.0;
static double s_interp_target_hz = 0.0;
static double s_interp_source_hz = 0.0;
static FrameInterpolationSchedule s_interp_schedule;
static uint64_t s_interp_pending_deadline;
static _Atomic uint64_t s_pass_frontend_idle;
/* Blend source (psx_mod_set_frame_interpolation_source). VBLANK (0, the
* default) treats every guest VBlank present as a new frame, exactly as
* before. FLIP (1) rotates history only on a real flip -- the display origin
@@ -8069,7 +8072,7 @@ static int interp_present(float alpha) {
s_interp_blend_mode);
}
static void interp_wait_until(uint64_t deadline, uint64_t frequency) {
static void interp_wait_until_ex(uint64_t deadline, uint64_t frequency, int render_idle) {
uint64_t now, start;
if (!deadline || !frequency) return;
start = SDL_GetPerformanceCounter();
@@ -8077,8 +8080,10 @@ static void interp_wait_until(uint64_t deadline, uint64_t frequency) {
now = SDL_GetPerformanceCounter();
if (now >= deadline) {
/* Idle host time feeds the render-pass budget. */
(void)s_idle_ticks_accum_fwd(now - start);
if (s_dyn_on) s_dyn_ledger.idle_ticks += now - start;
if (render_idle) {
(void)s_idle_ticks_accum_fwd(now - start);
if (s_dyn_on) s_dyn_ledger.idle_ticks += now - start;
}
return;
}
uint64_t remain = deadline - now;
@@ -8091,8 +8096,17 @@ static void interp_wait_until(uint64_t deadline, uint64_t frequency) {
}
}
static void interp_wait_until(uint64_t deadline, uint64_t frequency) {
interp_wait_until_ex(deadline, frequency, 1);
}
void gl_renderer_pass_note_frontend_idle(uint64_t ticks) {
atomic_fetch_add_explicit(&s_pass_frontend_idle, ticks, memory_order_relaxed);
}
static void interp_present_source_interval(void) {
static uint64_t diag_start, diag_swaps, diag_captures;
const int asynchronous = s_interp_hold && s_pt_on && s_rth_on && rt_on_render_thread();
s_interp_schedule.queued = asynchronous;
uint64_t frequency = SDL_GetPerformanceFrequency();
uint64_t now = SDL_GetPerformanceCounter();
uint64_t deadline;
@@ -8108,14 +8122,16 @@ static void interp_present_source_interval(void) {
while (frame_interpolation_schedule_next(
&s_interp_schedule, SDL_GetPerformanceCounter(),
&deadline, &alpha)) {
interp_wait_until(deadline, frequency);
if (!asynchronous) interp_wait_until(deadline, frequency);
s_interp_pending_deadline = asynchronous ? deadline : 0;
latency_ring_mark(LAT_SWAP_BEGIN);
if (!pass_gen_present(deadline))
(void)interp_present(alpha);
latency_ring_mark(LAT_SWAP_END);
s_interp_pending_deadline = 0;
}
interp_wait_until(frame_interpolation_schedule_end(&s_interp_schedule),
frequency);
if (!asynchronous)
interp_wait_until(frame_interpolation_schedule_end(&s_interp_schedule), frequency);
now = SDL_GetPerformanceCounter();
if (!diag_start) {
@@ -8343,15 +8359,25 @@ int gl_renderer_pass_ready(void) {
return gl_renderer_pass_unavailable() == PSX_MOD_RENDER_PASS_READY;
}
/* Slots per generation that fit a 256 MiB budget for the whole queue. */
/* Keep the historical 256 MiB queue budget, but permit two images per
* generation at the requested capture size, up to a 1 GiB hard ceiling.
* Eight-kilopixel output needs about 760 MiB for those six images; the old
* cap silently disabled temporal rendering above roughly 1440 lines. */
static uint32_t pass_slot_cap(int tex_w, int tex_h) {
double bytes = (double)tex_w * (double)tex_h * 4.0 * PASS_GENERATIONS;
uint32_t cap = bytes > 0.0 ? (uint32_t)((256.0 * 1024.0 * 1024.0) / bytes)
double budget = 256.0 * 1024.0 * 1024.0;
if (2.0 * bytes > budget) budget = 2.0 * bytes;
if (budget > 1024.0 * 1024.0 * 1024.0) budget = 1024.0 * 1024.0 * 1024.0;
uint32_t cap = bytes > 0.0 ? (uint32_t)(budget / bytes)
: PASS_SLOTS;
if (cap > PASS_SLOTS) cap = PASS_SLOTS;
return cap;
}
void gl_renderer_pass_cancel_plan(void) {
if (s_rth_on && !rt_on_render_thread()) gl_rth_release();
}
uint32_t gl_renderer_pass_plan(uint32_t period_vblanks,
uint32_t shown_after_vblanks,
uint32_t *alpha_q16, uint32_t max,
@@ -8365,15 +8391,17 @@ uint32_t gl_renderer_pass_plan(uint32_t period_vblanks,
if (wanted) *wanted = 0;
/* One plan per game frame: close the previous frame's host-time books. */
s_idle_ticks_last = s_idle_ticks_accum;
uint64_t frontend_idle = atomic_exchange_explicit(&s_pass_frontend_idle, 0, memory_order_relaxed);
if (frontend_idle > s_idle_ticks_last) s_idle_ticks_last = frontend_idle;
s_pass_ticks_last = s_pass_ticks_accum;
s_present_ticks_last = s_present_ticks_accum;
s_idle_ticks_accum = s_pass_ticks_accum = s_present_ticks_accum = 0;
live = s_intervals_since_plan > 0; /* turbo/headless present nothing */
s_intervals_since_plan = 0;
if (!gl_renderer_pass_ready() || !live || !alpha_q16 || max == 0) return 0;
if (!gl_renderer_pass_ready() || !live || !alpha_q16 || max == 0) { gl_renderer_pass_cancel_plan(); return 0; }
if (s_interp_schedule.target_period <= 0.0 ||
s_interp_schedule.source_deadline <= 0.0)
return 0;
{ gl_renderer_pass_cancel_plan(); return 0; }
if (s_pass_budget_pct < 0) {
const char *e = getenv("PSX_RENDER_PASS_BUDGET");
int v = e ? atoi(e) : 0;
@@ -8382,7 +8410,7 @@ uint32_t gl_renderer_pass_plan(uint32_t period_vblanks,
freq = (double)SDL_GetPerformanceFrequency();
sp = freq / s_interp_source_hz;
cap = pass_slot_cap(s_interp_w, s_interp_h);
if (cap < 2) return 0;
if (cap < 2) { gl_renderer_pass_cancel_plan(); return 0; }
memset(&in, 0, sizeof in);
in.next_deadline = s_interp_schedule.next_present_deadline;
in.target_period = s_interp_schedule.target_period;
@@ -8432,6 +8460,7 @@ uint32_t gl_renderer_pass_plan(uint32_t period_vblanks,
if (want && !n && !(in.budget > 0.0))
s_pass_plan_diag.zero_credit_refusals++;
}
if (!n) gl_renderer_pass_cancel_plan();
return n;
}
}
@@ -9483,6 +9512,13 @@ static int pass_gen_present(uint64_t deadline) {
}
void gl_renderer_pass_service_presents(void) {
/* The transaction has restored guest/GPU state. Resume queued rendering
* before servicing presentation so emulation never owns its deadline wait. */
if (s_rth_on && !rt_on_render_thread()) {
gl_rth_release();
RTH_REC(RTH_PASS_SERVICE, 0, 0);
return;
}
GL_RT_SYNC("pass_service_presents");
uint64_t deadline;
double sp;
@@ -9833,6 +9869,7 @@ static int s_pt_want = 0, s_pt_slots = 3;
static GLenum s_pt_format = GL_RGBA8; /* the window's: no alpha -> GL_RGB8 */
static SDL_GLContext s_pt_ctx = NULL;
static GLuint s_pts_tex[PT_MAX_SLOTS], s_pts_rb[PT_MAX_SLOTS], s_pts_fbo[PT_MAX_SLOTS];
static uint64_t s_pts_deadline[PT_MAX_SLOTS];
static int s_pts_w[PT_MAX_SLOTS], s_pts_h[PT_MAX_SLOTS];
static unsigned s_pts_gen[PT_MAX_SLOTS];
/* present thread's own objects (FBOs are per-context) */
@@ -9944,6 +9981,7 @@ static void *pt_cb_present(void *user, int slot, void *ready) {
GL_PRESENT_THREAD_TEST_HOOK(slot);
#endif
void *done = p_glFenceSync(PT_SYNC_GPU_COMMANDS_COMPLETE, 0);
interp_wait_until_ex(s_pts_deadline[slot], SDL_GetPerformanceFrequency(), 0);
gl_window_swap();
return done;
}
@@ -10025,6 +10063,7 @@ static void gl_present_swap(void) {
rthf_gpu_pause();
if (!s_pt_on) { gl_window_swap(); return; }
(void)pt_target_fbo(); /* a frame that never bound 0 still has its slot */
s_pts_deadline[pt_current()] = s_interp_pending_deadline;
void *ready = p_glFenceSync(PT_SYNC_GPU_COMMANDS_COMPLETE, 0);
glFlush();
void *done = NULL;
@@ -11492,7 +11531,7 @@ static int gl_rth_eligible(void) {
extern int psx_netplay_active(void);
return s_raster_ok && s_ctx && !s_cpu_auth_dual && !s_depth24_skip_up &&
!(s_hd_native_authority && s_rth_a0_open) &&
!gpu_display_is_depth24() && !s_interp_enabled && !s_pass_active &&
!gpu_display_is_depth24() && !s_pass_active &&
!psx_netplay_active() && !psx_openxr_session_active() &&
gr_backend() == GR_BACKEND_OPENGL;
}
@@ -14285,6 +14324,7 @@ static void gl_rth_exec(void *user, const RtCmd *c, const void *payload) {
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 (c->op == RTH_PASS_SERVICE) { gl_renderer_pass_service_presents(); return; }
if (!s_rthf_open && (atomic_load_explicit(&s_rthf_want, memory_order_relaxed) || s_fg_on))
rthf_begin();
rthf_gpu_resume();
+18 -3
View File
@@ -4045,6 +4045,7 @@ static int present_should_wall_pace(void) {
/* The single-context temporal-blend path subdivides this same stock frame
* interval. Do not run the outer pacer as well or guest timing doubles. */
if (g_frame_interpolation && g_gl_active &&
!gl_renderer_render_thread_active() &&
gl_renderer_interpolation_owns_cadence())
return 0;
/* Run the pacer even when driver vsync is SUPPOSED to own the cadence.
@@ -4924,7 +4925,21 @@ static void input_replay_frontend_tick(uint64_t frame) {
int bytes = gl_renderer_render_thread_json(renderer, sizeof renderer);
if (bytes > 0 && bytes < (int)sizeof renderer) {
std::ofstream detail(std::string(report) + ".renderer.json");
detail << "{" << renderer << "}";
char generation[8192];
int generated_bytes = gl_renderer_frame_gen_json(generation, sizeof generation);
RenderPassStats rp{}; render_pass_get_stats(&rp);
detail << "{" << renderer << ",\"render_pass\":{\"passes\":" << rp.passes
<< ",\"wanted\":" << rp.wanted << ",\"planned\":" << rp.planned
<< ",\"refused\":" << rp.refused << ",\"aborted\":" << rp.aborted
<< ",\"status_refused\":" << rp.status_refused << ",\"last_status\":" << rp.last_failure.status
<< ",\"discarded\":" << rp.discarded << ",\"verify_checks\":" << rp.verify_checks
<< ",\"verify_mismatch\":" << rp.verify_mismatch
<< ",\"avg_pass_ms\":" << rp.avg_pass_ms << ",\"avg_guest_ms\":" << rp.avg_guest_ms
<< ",\"avg_begin_ms\":" << rp.avg_begin_ms << ",\"avg_end_ms\":" << rp.avg_end_ms
<< ",\"avg_restore_ms\":" << rp.avg_restore_ms << "}";
if (generated_bytes > 0 && generated_bytes < (int)sizeof generation)
detail << ",\"frame_generation\":{" << generation << "}";
detail << "}";
if (!detail) throw std::runtime_error("cannot write input replay renderer report");
}
}
@@ -9712,6 +9727,7 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
const uint64_t waited = SDL_GetPerformanceCounter() - dyn_t0;
if (g_dynres.active) g_dynres.pacer_ticks += waited;
g_frame_report_wait_ticks += waited;
if (g_frame_interpolation) gl_renderer_pass_note_frontend_idle(waited);
}
runtime_perf_section_end(perf_start, &g_runtime_perf.pacer_ticks);
latency_ring_mark(LAT_PACED);
@@ -10296,8 +10312,7 @@ static void render_thread_vblank(void) {
}
} else {
std::fprintf(stdout, "psxrecomp: render thread requested but not started "
"(needs the OpenGL backend without netplay, frame interpolation "
"or a 24-bit display)\n");
"(needs the OpenGL backend without netplay or a 24-bit display)\n");
if (g_frame_generation)
std::fprintf(stdout, "psxrecomp: Smooth motion (frame generation) needs the render thread; off\n");
}
+8 -5
View File
@@ -414,10 +414,6 @@ int psx_mod_render_view(const PSXModRenderView *view) {
return 1;
}
static int passes_allowed(void) {
return pass_status() == PSX_MOD_RENDER_PASS_READY;
}
uint32_t psx_mod_render_pass_status(void) {
return pass_status();
}
@@ -440,7 +436,14 @@ uint32_t psx_mod_render_pass_plan(uint32_t period_vblanks,
if (!alpha_q16 || max == 0 || period_vblanks == 0 || period_vblanks > 8 ||
shown_after_vblanks > 8)
return 0;
if (!passes_allowed()) return 0;
{
uint32_t status = pass_status();
if (status != PSX_MOD_RENDER_PASS_READY) {
s_stats.status_refused++;
gl_renderer_pass_cancel_plan();
return pass_refuse("plan_status", status);
}
}
n = gl_renderer_pass_plan(period_vblanks, shown_after_vblanks,
alpha_q16, max, &wanted);
s_stats.wanted += wanted;
@@ -284,6 +284,25 @@ int main(void) {
&schedule, 0u, 1000000u, 60.0, 59.0),
"output below source cadence should be rejected");
frame_interpolation_schedule_reset(&schedule);
schedule.queued = 1;
for (int i = 1; i <= 6; ++i) {
uint64_t deadline;
float alpha;
CHECK(frame_interpolation_schedule_begin(&schedule, 1000000u,
1000000u, 60.0, 60.0), "queued interval accepted");
CHECK(frame_interpolation_schedule_next(&schedule, 1000000u,
&deadline, &alpha), "queued interval has one output");
CHECK(fabs((double)deadline - (1000000.0 + i * 1000000.0 / 60.0)) < 1.0,
"early producer retains every presentation deadline");
CHECK(!frame_interpolation_schedule_next(&schedule, 1000000u,
&deadline, &alpha), "queued interval has no duplicate output");
}
CHECK(frame_interpolation_schedule_begin(&schedule, 2000000u,
1000000u, 60.0, 60.0), "queued clock recovers after sustained stall");
CHECK(frame_interpolation_schedule_end(&schedule) == 2016667u,
"late queued producer reanchors after sustained stall");
printf(failures ? "FAILED (%d)\n" : "ALL PASS\n", failures);
return failures ? 1 : 0;
}
+1
View File
@@ -197,6 +197,7 @@ void gl_renderer_pass_end(uint32_t alpha_q16, int keep) {
}
uint32_t gl_renderer_pass_leaks(void) { return 0; }
int gl_renderer_pass_verify_vram(void) { return 1; }
void gl_renderer_pass_cancel_plan(void) {}
void gl_renderer_pass_note_cost(uint64_t t) { (void)t; }
void gl_renderer_pass_service_presents(void) {}
uint32_t g_psx_vblank_cycles = 564480u;
+1
View File
@@ -254,6 +254,7 @@ void gl_renderer_pass_end(uint32_t alpha_q16, int keep) {
}
uint32_t gl_renderer_pass_leaks(void) { return 0; }
int gl_renderer_pass_verify_vram(void) { return 1; }
void gl_renderer_pass_cancel_plan(void) {}
void gl_renderer_pass_note_cost(uint64_t t) { (void)t; }
void gl_renderer_pass_service_presents(void) {}
static uint32_t s_local_status = PSX_MOD_RENDER_PASS_READY;