Capture both HUD mattes with one checkpoint and readback

Co-authored-by: Yves <calibratedbeats@gmail.com>
This commit is contained in:
Yves
2026-10-23 03:25:18 +08:00
parent 4419d234ab
commit e4f1f98542
7 changed files with 128 additions and 51 deletions
@@ -0,0 +1,17 @@
# Capture both HUD mattes with one checkpoint and readback
Source: [FractalEngineer/psxrecomp](https://github.com/FractalEngineer/psxrecomp).
- [a876bae3](https://github.com/FractalEngineer/psxrecomp/commit/a876bae3385a8b972d8a5cf083ad5e30cf05c4bd)
- [d2554481](https://github.com/FractalEngineer/psxrecomp/commit/d2554481fac39409cebef7602e59bd155e000fe6)
Original author: Yves <calibratedbeats@gmail.com>; retained as commit author
and Co-authored-by on this reconstructed port.
Adapted: this feature and its tests. Excluded: other VR features, game policy,
assets and chronological investigation notes.
Base: `4419d234ab66c839da94dba597794ac47a09eb00` (current upstream master or the preceding feature).
Validation: Windows/GCC 13.2 Release: sandbox and abort tests pass (2/2), including both matte buffers and capture-only checkpoint reuse.
Consumer: [Medal of Honor v0.1.6](https://github.com/FractalEngineer/Medal-Of-Honor-PSX-Recomp/tree/1957e28a5dc7f5daa79e2b9b6ed867eb06b41ace).
Requires a framework pin bump and rebuild; no game/BIOS regeneration.
+1 -1
View File
@@ -136,7 +136,7 @@ uint32_t gl_renderer_stereo_unavailable(void);
int gl_renderer_stereo_begin(int x, int y, int w, int h, int reuse_backup);
int gl_renderer_stereo_end(uint32_t eye, int keep);
void gl_renderer_stereo_stage_reset(void);
int gl_renderer_stereo_stage_readback(uint32_t eye,uint32_t width,uint32_t height,uint8_t *rgba);
int gl_renderer_stereo_stage_readback_pair(uint32_t width,uint32_t height,uint8_t *rgba);
int gl_renderer_stereo_publish(uint64_t pair_id, uint64_t guest_cycle,
const int32_t eye_view[2][3]);
void gl_renderer_stereo_reset(void);
+14 -8
View File
@@ -651,14 +651,20 @@ uint32_t psx_mod_render_pass_plan(uint32_t period_vblanks,
* partially updated plugin state) across guest code, or tolerate the abort. */
int psx_mod_render_pass(struct CPUState* cpu, const PSXModRenderPass* pass,
PSXModRenderPassFn fn, void* user);
/* Isolated draw/readback using the stereo-capable rollback transaction.
* Never publishes an eye pair or changes presentation. Returns native-sized
* top-to-bottom RGBA8 pixels (alpha remains the PSX mask bit, not coverage).
* Call outside another pass. rgba_bytes must cover rect->w*rect->h*4. */
int psx_mod_render_capture_rgba(struct CPUState *cpu,const PSXModRenderPass *rect,
PSXModRenderPassFn fn,void *user,
uint8_t *rgba,uint32_t rgba_bytes);
#define PSX_MOD_RENDER_CAPTURE_RGBA 1
/* Isolated dual-matte draw/readback using the stereo-capable rollback
* transaction. Draws two mattes of the same slice, the first into eye 0 of the
* stage surface and the second into eye 1, so both planes of a frame cost one
* readback instead of two. Never publishes an eye pair or changes presentation.
* Returns native-sized top-to-bottom RGBA8 pixels (alpha remains the PSX mask
* bit, not coverage). Call outside another pass. rgba_bytes must cover
* 2*rect->w*rect->h*4; the first matte arrives in the buffer's first half and
* the second in its second half. */
int psx_mod_render_capture_rgba_pair(struct CPUState *cpu,
const PSXModRenderPass *rect,
PSXModRenderPassFn fn,void *user_first,
void *user_second,uint8_t *rgba,
uint32_t rgba_bytes);
#define PSX_MOD_RENDER_CAPTURE_RGBA_PAIR 1
/*
* Why passes cannot run right now, the host-time budget aside (a plan that
* returns 0 while this says READY was shed for time). A plugin that relies on
+14 -8
View File
@@ -8807,27 +8807,33 @@ void gl_renderer_stereo_stage_reset(void) {
GL_RT_SYNC("stereo_stage_reset");
s_stereo_pair[1 - s_stereo_current].mask = 0;
}
int gl_renderer_stereo_stage_readback(uint32_t eye,uint32_t w,uint32_t h,uint8_t *rgba) {
GL_RT_SYNC("capture_readback");
if(eye>=2 || !rgba || !w || !h || w>1024 || h>512)return 0;
/* Both staged eye images in one read. The blit target is double height, eye 0
* into its lower half and eye 1 into its upper half, so one glReadPixels
* returns the two height-sized planes contiguously, eye 0 first. */
int gl_renderer_stereo_stage_readback_pair(uint32_t w,uint32_t h,uint8_t *rgba) {
GL_RT_SYNC("capture_readback_pair");
if(!rgba || !w || !h || w>1024 || h>512)return 0;
StereoPair *pair=&s_stereo_pair[1-s_stereo_current];
if(!(pair->mask&(1u<<eye)))return 0;
if((pair->mask&3u)!=3u)return 0;
GLint read,draw,texture,pack,pbo,row,skipr,skipp;
GLboolean scissor=glIsEnabled(GL_SCISSOR_TEST),srgb=glIsEnabled(0x8DB9);
glGetIntegerv(0x8CAA,&read);glGetIntegerv(0x8CA6,&draw);glGetIntegerv(GL_TEXTURE_BINDING_2D,&texture);
glGetIntegerv(GL_PACK_ALIGNMENT,&pack);glGetIntegerv(0x88ED,&pbo);glGetIntegerv(GL_PACK_ROW_LENGTH,&row);
glGetIntegerv(GL_PACK_SKIP_ROWS,&skipr);glGetIntegerv(GL_PACK_SKIP_PIXELS,&skipp);
int ok=pass_make_color_fbo(&s_capture_read_tex,NULL,&s_capture_read_fbo,&s_capture_read_w,&s_capture_read_h,
w,h,GL_RGBA8,GL_RGBA,GL_UNSIGNED_BYTE);
w,h*2u,GL_RGBA8,GL_RGBA,GL_UNSIGNED_BYTE);
if(ok) {
glDisable(GL_SCISSOR_TEST);glDisable(0x8DB9);
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER,pair->fbo[eye]);
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER,s_capture_read_fbo);
p_glBlitFramebuffer(0,0,pair->tw[eye],pair->th[eye],0,0,w,h,GL_COLOR_BUFFER_BIT,GL_NEAREST);
for(uint32_t eye=0;eye<2u;eye++) {
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER,pair->fbo[eye]);
p_glBlitFramebuffer(0,0,pair->tw[eye],pair->th[eye],0,(GLint)(eye*h),w,(GLint)((eye+1u)*h),
GL_COLOR_BUFFER_BIT,GL_NEAREST);
}
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER,s_capture_read_fbo);
p_glBindBuffer(0x88EB,0);glPixelStorei(GL_PACK_ALIGNMENT,1);glPixelStorei(GL_PACK_ROW_LENGTH,0);
glPixelStorei(GL_PACK_SKIP_ROWS,0);glPixelStorei(GL_PACK_SKIP_PIXELS,0);
glReadPixels(0,0,w,h,GL_RGBA,GL_UNSIGNED_BYTE,rgba);
glReadPixels(0,0,w,h*2u,GL_RGBA,GL_UNSIGNED_BYTE,rgba);
}
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER,read);p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER,draw);
glBindTexture(GL_TEXTURE_2D,texture);glPixelStorei(GL_PACK_ALIGNMENT,pack);
+38 -17
View File
@@ -308,6 +308,12 @@ static int s_shed_geom_valid;
/* A HUD-only capture transaction reuses eye 0's slot but is not an eye draw;
* frame tracing must not attribute its cost to the left eye. */
static int s_capture_only;
/* The two transactions of one HUD capture call are a pair of their own: the
* second matte draws from exactly the state the restore that ended the first
* one put back, so it may reuse the first one's checkpoint the way a pair's
* right eye reuses the left's. Scoped to the capture call, so the reuse never
* reaches the pair that renders later in the same frame. */
static int s_capture_pair;
static int pass_refuse(const char *reason, uint32_t status) {
/* A refusal always carries a reason: last_failure.reason == NULL means
@@ -900,10 +906,10 @@ static int render_transaction(struct CPUState *cpu, const PSXModRenderPass *pass
s_open_generation = 0;
s_leaks_before = gl_renderer_pass_leaks();
/* A stereo eye whose machine state still equals the pair's captured
* baseline skips re-capturing it. */
int reuse_baseline = eye >= 0 && !s_capture_only &&
s_baseline_ready && baseline_reuse_enabled();
/* A stereo eye, or the second matte of a capture, whose machine state still
* equals the baseline captured for it skips re-capturing that state. */
int reuse_baseline = eye >= 0 && s_baseline_ready && baseline_reuse_enabled() &&
(!s_capture_only || s_capture_pair);
if (!checkpoint_save(cpu, reuse_baseline)) {
gl_renderer_pass_begin_diag(&s_attempt.gl);
s_stats.checkpoint_refused++;
@@ -1039,10 +1045,12 @@ static int render_transaction(struct CPUState *cpu, const PSXModRenderPass *pass
}
}
/* The restore just put the live machine back to the captured baseline, so
* the other eye of this pair may reuse it. Anything else - a non-stereo
* transaction, a HUD-only capture, a failed eye or an unverified restore -
* leaves nothing safe to reuse. */
s_baseline_ready = (eye >= 0 && !s_capture_only && ok && verify_ok) ? 1 : 0;
* the other eye of this pair, or the other matte of this capture, may reuse
* it. Anything else - a non-stereo transaction, a capture that is not part
* of a matte pair, a failed eye or an unverified restore - leaves nothing
* safe to reuse. */
s_baseline_ready = (eye >= 0 && ok && verify_ok &&
(!s_capture_only || s_capture_pair)) ? 1 : 0;
t1 = gl_renderer_perf_ticks();
{
@@ -1272,24 +1280,37 @@ void psx_mod_stereo_set_budget_ms(double ms) {
s_shed_xr_budget_ms = (ms > 0.0 && ms < 1000.0) ? ms : 0.0;
}
int psx_mod_render_capture_rgba(CPUState *cpu,const PSXModRenderPass *rect,
PSXModRenderPassFn fn,void *user,
uint8_t *rgba,uint32_t bytes) {
if(s_pair_busy || !cpu || !rect || !fn || !rgba || rect->struct_size<sizeof *rect ||
/* Two mattes of one slice in a single capture: the first transaction stages
* eye 0 and the second eye 1, so one read returns both. The two transactions
* are captures, not eye draws: neither publishes a pair nor samples the
* per-eye trace, and the checkpoint they share is their own. */
int psx_mod_render_capture_rgba_pair(CPUState *cpu,const PSXModRenderPass *rect,
PSXModRenderPassFn fn,void *user_first,
void *user_second,uint8_t *rgba,uint32_t bytes) {
if(s_pair_busy || !cpu || !rect || !fn || !rgba || !user_first || !user_second ||
rect->struct_size<sizeof *rect ||
!rect->w || !rect->h || rect->x+rect->w>1024 || rect->y+rect->h>512 ||
(uint64_t)rect->w*rect->h*4>bytes || transaction_status(1)!=PSX_MOD_RENDER_PASS_READY)return 0;
(uint64_t)rect->w*rect->h*8>bytes || transaction_status(1)!=PSX_MOD_RENDER_PASS_READY)return 0;
if(g_overlay_flush_pending_cycles)g_overlay_flush_pending_cycles();
uint64_t trace_t0=gl_renderer_perf_ticks();
s_pair_busy=1;s_plan_serial++;
gl_renderer_stereo_stage_reset();
PSXModRenderPass capture=*rect;capture.alpha_q16=0;
s_capture_only=1; /* this eye-0 transaction is a HUD capture, not an eye draw */
int ok=render_transaction(cpu,&capture,fn,user,0);
s_capture_only=0;
/* Start uncaptured: the ready flag carries the pair's baseline from the
* previous frame, which describes a machine the guest has since changed. */
s_capture_only=1;s_capture_pair=1;s_baseline_ready=0;
uint64_t cycle=psx_cycle_count,stores=g_guest_store_count;
int ok=render_transaction(cpu,&capture,fn,user_first,0);
/* The second matte must draw from the state the first one's restore put
* back. If the clock or the store count moved, that baseline no longer
* describes it, so capture the machine again instead of reusing it. */
if(psx_cycle_count!=cycle || g_guest_store_count!=stores)s_baseline_ready=0;
if(ok)ok=render_transaction(cpu,&capture,fn,user_second,1);
s_capture_only=0;s_capture_pair=0;s_baseline_ready=0;
uint64_t trace_t1=gl_renderer_perf_ticks();
psx_frame_trace_add(PSX_TRACE_HUD_DRAW,
trace_ticks_to_us(trace_t1-trace_t0));
if(ok)ok=gl_renderer_stereo_stage_readback(0,rect->w,rect->h,rgba);
if(ok)ok=gl_renderer_stereo_stage_readback_pair(rect->w,rect->h,rgba);
psx_frame_trace_add(PSX_TRACE_HUD_READBACK,
trace_ticks_to_us(gl_renderer_perf_ticks()-trace_t1));
gl_renderer_stereo_stage_reset();s_pair_busy=0;
+2 -2
View File
@@ -215,8 +215,8 @@ int gl_renderer_stereo_end(uint32_t eye, int keep) {
return 1;
}
void gl_renderer_stereo_stage_reset(void) { s_stereo_mask = 0; }
int gl_renderer_stereo_stage_readback(uint32_t eye,uint32_t w,uint32_t h,uint8_t *rgba) {
(void)eye;(void)w;(void)h;(void)rgba;return 0;
int gl_renderer_stereo_stage_readback_pair(uint32_t w,uint32_t h,uint8_t *rgba) {
(void)w;(void)h;(void)rgba;return 0;
}
void gl_renderer_stereo_reset(void) { s_stereo_mask = 0; s_stereo_published = 0; }
int gl_renderer_stereo_set_presentation(uint32_t mode) { return mode <= 1; }
+42 -15
View File
@@ -285,9 +285,9 @@ int gl_renderer_stereo_end(uint32_t eye, int keep) {
return 1;
}
void gl_renderer_stereo_stage_reset(void) { s_stereo_mask = 0; }
int gl_renderer_stereo_stage_readback(uint32_t eye,uint32_t w,uint32_t h,uint8_t *rgba) {
if(!(s_stereo_mask & (1u<<eye))) return 0;
memset(rgba,0x5A,(size_t)w*h*4);return 1;
int gl_renderer_stereo_stage_readback_pair(uint32_t w,uint32_t h,uint8_t *rgba) {
if((s_stereo_mask & 3u) != 3u) return 0;
memset(rgba,0x5A,(size_t)w*h*8);return 1;
}
void gl_renderer_stereo_reset(void) { s_stereo_mask = 0; s_stereo_published = 0; }
int gl_renderer_stereo_set_presentation(uint32_t mode) { return mode <= 1; }
@@ -933,11 +933,9 @@ static void test_trace_substages(void) {
render_pass_reset_session(); s_gl_status = 0; s_begin_ok = 1;
psx_frame_trace_set_enabled(1);
CHECK(psx_frame_trace_open(), "a frame record is open across the capture");
CHECK(psx_mod_render_capture_rgba(&cpu, &rect, traced_draw, &first,
pixels, sizeof pixels) &&
psx_mod_render_capture_rgba(&cpu, &rect, traced_draw, &second,
pixels, sizeof pixels),
"the captures rendered both mattes");
CHECK(psx_mod_render_capture_rgba_pair(&cpu, &rect, traced_draw, &first,
&second, pixels, sizeof pixels),
"the capture rendered both mattes");
CHECK(s_traced_calls == 2 && psx_frame_trace_read(&rec, 1) == 0,
"the capture replayed the draw and left no record of its own");
psx_frame_trace_commit();
@@ -1045,26 +1043,55 @@ static void test_texture_stream_journal(void) {
render_pass_journal_free(&j);
}
static void *s_dual_users[2];
static int s_dual_draws;
static int isolated_draw(CPUState *cpu,void *user,uint32_t alpha) {
(void)user;
CHECK(alpha==0,"isolated capture has no temporal phase");
if(s_dual_draws<2)s_dual_users[s_dual_draws]=user;
s_dual_draws++;
cpu->gpr[8]=0x1234;s_ram[0x456]=0x42;psx_advance_cycles(5000);
return 1;
}
static void test_isolated_capture(void) {
CPUState cpu={0};PSXModRenderPass rect={sizeof rect,0,0,0,2,2};
uint8_t pixels[16]={0};
int first=1,second=2;
uint8_t pixels[32]={0};
render_pass_reset_session();s_gl_status=0;s_begin_ok=1;s_netplay=0;
cpu.gpr[8]=123;s_ram[0x456]=0x77;
uint64_t cycle=psx_cycle_count,published=s_stereo_published;
CHECK(psx_mod_render_capture_rgba(&cpu,&rect,isolated_draw,NULL,pixels,sizeof pixels),
"isolated capture succeeds without publishing a stereo pair");
s_dual_draws=0;
CHECK(psx_mod_render_capture_rgba_pair(&cpu,&rect,isolated_draw,&first,&second,
pixels,sizeof pixels),
"isolated dual capture succeeds without publishing a stereo pair");
CHECK(s_dual_draws==2 && s_dual_users[0]==&first && s_dual_users[1]==&second,
"one transaction per matte, each with its own user");
CHECK(cpu.gpr[8]==123 && s_ram[0x456]==0x77 && psx_cycle_count==cycle,
"isolated capture restores CPU, RAM and the guest clock");
CHECK(s_stereo_published==published && s_stereo_mask==0 && pixels[0]==0x5A,
"isolated pixels copied and temporary staging discarded");
CHECK(!psx_mod_render_capture_rgba(&cpu,&rect,isolated_draw,NULL,pixels,sizeof pixels-1),
CHECK(s_stereo_published==published && s_stereo_mask==0 &&
pixels[0]==0x5A && pixels[31]==0x5A,
"both isolated planes copied and temporary staging discarded");
/* The two mattes draw from one machine state, so the second one reuses the
* checkpoint the first one captured, the way a pair's right eye does. */
RenderStereoStats stats;
render_stereo_get_stats(&stats);
CHECK(stats.checkpoint_saves==1 && stats.checkpoint_reuses==1,
"the two mattes of one capture share one checkpoint");
s_dual_draws=0;
CHECK(!psx_mod_render_capture_rgba_pair(&cpu,&rect,isolated_draw,&first,&second,
pixels,sizeof pixels-1),
"undersized pixel buffer refused");
CHECK(s_dual_draws==0 && !psx_mod_render_capture_rgba_pair(&cpu,&rect,isolated_draw,
&first,NULL,pixels,sizeof pixels),
"a missing second matte is refused before any draw");
/* The capture's shared checkpoint is scoped to the capture: the pair that
* renders later in the same frame must still capture its first eye. */
PSXModStereoFrame f={sizeof f,2,0,240,320,240};
render_pass_reset_session();s_gl_status=0;s_begin_ok=1;s_eye_decline=-1;s_eye_calls=0;
CHECK(psx_mod_render_stereo(&cpu,&f,pair_draw,NULL)==1,
"a pair still renders after a capture");
render_stereo_get_stats(&stats);
CHECK(stats.checkpoint_saves==1 && stats.checkpoint_reuses==1,
"the pair after a capture captures its own baseline");
}
int main(void) {
test_store_policy();