mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
Capture both HUD mattes with one checkpoint and readback
Co-authored-by: Yves <calibratedbeats@gmail.com>
This commit is contained in:
@@ -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.
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user