Merge pull request #687 from RetroPortingToolKit/feat/local-view-pair38

Add protected offline two-view presentation for local co-op
This commit is contained in:
Matt Stanley
2026-10-18 18:26:27 -07:00
committed by GitHub
6 changed files with 144 additions and 21 deletions
+3
View File
@@ -136,12 +136,15 @@ int gl_renderer_local_view_end(int keep);
/* Stereo shares the VRAM transaction, not the temporal generation/schedule. */
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_local_pair_begin(int x, int y, int w, int h, int reuse_backup);
uint32_t gl_renderer_local_pair_unavailable(void);
int gl_renderer_stereo_end(uint32_t eye, int keep);
void gl_renderer_stereo_stage_reset(void);
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);
int gl_renderer_stereo_set_presentation(uint32_t mode);
int gl_renderer_local_pair_set_presentation(int enabled);
void gl_renderer_stereo_dump_arm(const char *dir, int pairs);
/* Bounded metadata for explicitly requested PNG evidence, queried over TCP. */
typedef struct GLRenderStereoCapture {
+8
View File
@@ -647,6 +647,14 @@ typedef struct PSXModStereoFrame {
typedef int (*PSXModStereoFn)(struct CPUState*, void*, uint32_t eye);
int psx_mod_render_stereo(struct CPUState *cpu, const PSXModStereoFrame *frame,
PSXModStereoFn fn, void *user);
/* Offline local multiplayer: draw views0/1 from one checkpoint, using the
* reversible local-view sandbox. Both must succeed before publication.
* Callbacks run with psx_mod_local_view_scope()==1. This never advances the
* world or enables stereo/VR. Presentation uses two native4:3 views (8:3).
* Unsupported backends refuse and leave ordinary presentation available. */
int psx_mod_render_local_pair(struct CPUState *cpu, const PSXModStereoFrame *frame,
PSXModStereoFn fn, void *user);
int psx_mod_set_local_pair_presentation(int enabled);
uint32_t psx_mod_render_stereo_status(void);
/* 0 disables stereo output; 1 shows a complete pair side by side. Capture
* itself does not enable presentation. Default 0; applies to this session. */
+39 -6
View File
@@ -8210,14 +8210,16 @@ static void pass_journal_rollback(void) {
render_pass_journal_rollback(&s_pj_cpu, s_vram, VRAM_W);
}
/* kind: 0 temporal pass, 1 stereo eye, 2 netplay local view. */
/* kind: 0 temporal, 1 stereo, 2 netplay own view, 3 offline view pair. */
static int transaction_begin(int x, int y, int w, int h, int open_gen,
uint32_t period_vblanks, int reuse_backup, int kind) {
const int stereo = kind == 1, local = kind == 2;
const int stereo = kind == 1 || kind == 3;
const int local = kind == 2 || (kind == 3 && s_cpu_auth_dual);
int S = s_hr_scale, gi, wide, tw, th;
PassGen *g;
memset(&s_pass_begin_diag, 0, sizeof s_pass_begin_diag);
s_pass_begin_diag.status = local ? gl_renderer_local_view_unavailable()
s_pass_begin_diag.status = kind == 3 ? gl_renderer_local_pair_unavailable()
: local ? gl_renderer_local_view_unavailable()
: stereo ? gl_renderer_stereo_unavailable()
: gl_renderer_pass_unavailable();
s_pass_begin_diag.active = s_pass_active;
@@ -8270,7 +8272,7 @@ static int transaction_begin(int x, int y, int w, int h, int open_gen,
s_pass_begin_diag.requested_h = th;
gi = s_pgen_write;
g = &s_pgen[gi];
if (local) {
if (local && !stereo) {
/* No generation: the image stays in the presenter surface. */
} else if (stereo) {
StereoPair *pair = &s_stereo_pair[1 - s_stereo_current];
@@ -8461,6 +8463,28 @@ int gl_renderer_stereo_begin(int x, int y, int w, int h, int reuse) {
GL_RT_SYNC("stereo_begin");
return transaction_begin(x, y, w, h, 0, 0, reuse, 1);
}
int gl_renderer_local_pair_begin(int x, int y, int w, int h, int reuse) {
GL_RT_SYNC("local_pair_begin");
/* Offline GL may still own newer native textures/CLUTs. Own-view game
* renderers read CPU VRAM while rebuilding authored tile/sprite banks;
* synchronize the canonical image before freezing either view. Netplay's
* dual raster already keeps this mirror current. */
if (!s_pass_active && gl_renderer_local_pair_unavailable()==PSX_MOD_RENDER_PASS_READY)
ensure_cpu();
return transaction_begin(x, y, w, h, 0, 0, reuse, 3);
}
uint32_t gl_renderer_local_pair_unavailable(void) {
GL_RT_SYNC("local_pair_unavailable");
pass_refusal_init();
if (!s_ctx || !s_raster_ok || !s_hr_fbo || gpu_display_is_depth24())
return PSX_MOD_RENDER_PASS_NO_PRESENTER;
/* The reversible transaction journals the wide surface too. The pair
* captures each native rect after an own-view draw, so an allocated wide
* surface is not itself a reason to refuse offline side-by-side. */
if (s_hd_native_authority || s_hiw || s_pass_force_refuse > 0)
return PSX_MOD_RENDER_PASS_BACKEND;
return PSX_MOD_RENDER_PASS_READY;
}
void gl_renderer_pass_end(uint32_t alpha_q16, int keep) {
GL_RT_SYNC("pass_end");
int gi = s_pgen_write;
@@ -8513,6 +8537,13 @@ int gl_renderer_stereo_set_presentation(uint32_t mode) {
s_stereo_mode = (int)mode;
return 1;
}
int gl_renderer_local_pair_set_presentation(int enabled) {
GL_RT_SYNC("local_pair_set_presentation");
int mode = enabled ? 2 : 0;
if (s_stereo_mode != mode) stereo_invalidate();
s_stereo_mode = mode;
return 1;
}
static void stereo_resources_release(void) {
for (int gi = 0; gi < 2; gi++) for (int eye = 0; eye < 2; eye++) {
StereoPair *p = &s_stereo_pair[gi];
@@ -8805,10 +8836,12 @@ static int stereo_present(int w, int h) {
return 0;
if (!s_stereo_mode || !s_stereo_valid || s_pass_active ||
p->w != w || p->h != h || p->tw[0] != w * s_hr_scale ||
gl_renderer_stereo_unavailable() != PSX_MOD_RENDER_PASS_READY) return 0;
(s_stereo_mode == 2 ? gl_renderer_local_pair_unavailable() :
gl_renderer_stereo_unavailable()) != PSX_MOD_RENDER_PASS_READY) return 0;
SDL_GL_GetDrawableSize(s_win, &ww, &wh);
gl_perf_present_enter();
letterbox_rect_aspect(ww, wh, s_aspect_num * 2, s_aspect_den, &lx, &ly, &lw, &lh);
letterbox_rect_aspect(ww, wh, s_stereo_mode == 2 ? 8 : s_aspect_num * 2,
s_stereo_mode == 2 ? 3 : s_aspect_den, &lx, &ly, &lw, &lh);
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0);
glDisable(GL_SCISSOR_TEST); glViewport(0, 0, ww, wh);
glClearColor(0.f, 0.f, 0.f, 1.f); glClear(GL_COLOR_BUFFER_BIT);
+29 -14
View File
@@ -363,7 +363,7 @@ int psx_mod_set_render_pass_flip(uint32_t mode) {
static uint32_t transaction_status(int kind) {
uint32_t gl;
if (s_stats.disabled) return PSX_MOD_RENDER_PASS_DISABLED;
if ((kind == 2 ? !psx_netplay_active() :
if ((kind == 2 ? !psx_netplay_active() : kind == 3 ? psx_netplay_active() :
(psx_netplay_active() && !s_netplay_allowed)) || psx_netplay_is_resimulating() ||
psx_selfcheck_resim_active() || psx_rewind_is_open() ||
g_ls_mode || g_ls_replay_active)
@@ -373,7 +373,8 @@ static uint32_t transaction_status(int kind) {
* redraw during catch-up exposes the canonical peer's image. Any load
* shedding must skip a whole presentation, preserving the last displayed
* local image, rather than substitute the shared frame here. */
gl = kind == 2 ? gl_renderer_local_view_unavailable()
gl = kind == 3 ? gl_renderer_local_pair_unavailable()
: kind == 2 ? gl_renderer_local_view_unavailable()
: kind == 1 ? gl_renderer_stereo_unavailable() : gl_renderer_pass_unavailable();
if (gl != PSX_MOD_RENDER_PASS_READY) return gl;
if (s_nesting || g_psx_guest_time_frozen || psx_get_in_exception() ||
@@ -390,6 +391,9 @@ void render_stereo_get_stats(RenderStereoStats *out) { if (out) *out = s_stereo;
int psx_mod_set_stereo_presentation(uint32_t mode) {
return mode <= 1u ? gl_renderer_stereo_set_presentation(mode) : 0;
}
int psx_mod_set_local_pair_presentation(int enabled) {
return gl_renderer_local_pair_set_presentation(enabled);
}
int psx_mod_render_view_offset(int32_t x, int32_t y, int32_t z) {
int32_t xyz[3] = {x, y, z};
if (!g_psx_render_pass_active) return 0;
@@ -666,11 +670,11 @@ static void local_scope_set(int on) {
int psx_mod_local_view_scope(void) { return s_local_scope; }
static int transaction_kind(int eye) {
return eye >= 0 ? 1 : eye == RP_EYE_LOCAL ? 2 : 0;
return eye >= 2 ? 3 : eye >= 0 ? 1 : eye == RP_EYE_LOCAL ? 2 : 0;
}
static int transaction_end(int eye, uint32_t alpha, int keep) {
if (eye >= 0) return gl_renderer_stereo_end((uint32_t)eye, keep);
if (eye >= 0) return gl_renderer_stereo_end((uint32_t)eye & 1u, keep);
if (eye == RP_EYE_LOCAL) return gl_renderer_local_view_end(keep);
gl_renderer_pass_end(alpha, keep);
return 1;
@@ -728,7 +732,9 @@ static int render_transaction(struct CPUState *cpu, const PSXModRenderPass *pass
s_restored_cycle == psx_cycle_count &&
s_restored_stores == g_guest_store_count;
/* Frame N's own image is captured by the first pass after a plan. */
if (!(eye >= 0 ? gl_renderer_stereo_begin(pass->x, pass->y, pass->w,
if (!(eye >= 2 ? gl_renderer_local_pair_begin(pass->x, pass->y, pass->w,
pass->h, reuse)
: eye >= 0 ? gl_renderer_stereo_begin(pass->x, pass->y, pass->w,
pass->h, reuse)
: eye == RP_EYE_LOCAL ? gl_renderer_local_view_begin(pass->x, pass->y,
pass->w, pass->h)
@@ -757,14 +763,14 @@ static int render_transaction(struct CPUState *cpu, const PSXModRenderPass *pass
s_device_read_fault = 0;
cycles_before = psx_cycle_count;
if (eye >= 0) {
s_stereo.eye_cycle[eye] = cycles_before;
s_stereo.eye_hash[eye] = verify_on() ? hash_before : 0;
s_stereo.eye_cycle[eye & 1] = cycles_before;
s_stereo.eye_hash[eye & 1] = verify_on() ? hash_before : 0;
}
(void)psx_cycle_freeze_begin(&s_freeze, watchdog_cycles(),
watchdog_overrun);
/* A netplay local view is the one place a peer's presentation-only mods
* act on guest code (own-view plugins, widescreen cull margin). */
if (eye == RP_EYE_LOCAL) local_scope_set(1);
if (eye == RP_EYE_LOCAL || eye >= 2) local_scope_set(1);
if (setjmp(s_abort_jmp) == 0) {
s_pass_cpu = cpu;
s_abort_armed = 1;
@@ -792,7 +798,7 @@ static int render_transaction(struct CPUState *cpu, const PSXModRenderPass *pass
ok = 0;
}
s_stats.guest_cycles_last = psx_cycle_count - cycles_before;
if (eye >= 0) gte_render_view_get(s_stereo.eye_view[eye]);
if (eye >= 0) gte_render_view_get(s_stereo.eye_view[eye & 1]);
tg = gl_renderer_perf_ticks();
s_pass_cpu = NULL;
@@ -895,8 +901,8 @@ static int stereo_eye_draw(CPUState *cpu, void *user, uint32_t unused) {
return c->fn(cpu, c->user, c->eye);
}
int psx_mod_render_stereo(CPUState *cpu, const PSXModStereoFrame *frame,
PSXModStereoFn fn, void *user) {
static int render_pair(CPUState *cpu, const PSXModStereoFrame *frame,
PSXModStereoFn fn, void *user, int local) {
PSXModRenderPass rect;
StereoCallback call;
uint64_t t0, cycle, stores, mismatches, aborted;
@@ -913,13 +919,13 @@ int psx_mod_render_stereo(CPUState *cpu, const PSXModStereoFrame *frame,
(uint32_t)frame->y + frame->h > 512u) {
s_stereo.refused++; s_stereo.last_failure = "arguments"; return 0;
}
status = transaction_status(1);
status = transaction_status(local ? 3 : 1);
if (status != PSX_MOD_RENDER_PASS_READY) {
s_stereo.refused++; s_stereo.last_failure = "status"; return 0;
}
budget_ms = frame->period_vblanks * (double)g_psx_vblank_cycles /
33868.8 * 0.8;
if (s_stereo.avg_pair_ms > budget_ms && ++s_pair_retry < 30u) {
if (!local && s_stereo.avg_pair_ms > budget_ms && ++s_pair_retry < 30u) {
s_stereo.shed++; return 0;
}
s_pair_retry = 0;
@@ -942,7 +948,8 @@ int psx_mod_render_stereo(CPUState *cpu, const PSXModStereoFrame *frame,
if (psx_cycle_count != cycle || g_guest_store_count != stores) {
ok = 0; s_stereo.last_failure = "checkpoint_changed"; break;
}
if (!render_transaction(cpu, &rect, stereo_eye_draw, &call, (int)call.eye)) {
if (!render_transaction(cpu, &rect, stereo_eye_draw, &call,
(int)call.eye + (local ? 2 : 0))) {
ok = 0;
if (!s_stereo.last_failure) s_stereo.last_failure =
s_stats.aborted != aborted ? "eye_abort" : "eye_refused_or_discarded";
@@ -978,3 +985,11 @@ int psx_mod_render_stereo(CPUState *cpu, const PSXModStereoFrame *frame,
s_pair_busy = 0;
return ok;
}
int psx_mod_render_stereo(CPUState *cpu, const PSXModStereoFrame *frame,
PSXModStereoFn fn, void *user) {
return render_pair(cpu,frame,fn,user,0);
}
int psx_mod_render_local_pair(CPUState *cpu, const PSXModStereoFrame *frame,
PSXModStereoFn fn, void *user) {
return render_pair(cpu,frame,fn,user,1);
}
+28 -1
View File
@@ -38,6 +38,7 @@ static int failures;
#define CHECK(c, m) do { if (!(c)) { fprintf(stderr, "FAIL: %s\n", m); failures++; } } while (0)
/* ---- runtime globals the two units share with the rest of the runtime -- */
int g_boot_state_cpu_timing_restored = 0;
int g_ls_replay_active = 0;
int g_ls_mode = 0;
int g_precise_mode = 0;
@@ -110,7 +111,8 @@ int psx_netplay_active(void) { return 0; }
int psx_netplay_is_resimulating(void) { return 0; }
void psx_netplay_local_view_clear(void) {}
int psx_netplay_local_view_shed(void) { return 0; }
void gpu_ws_set_local_view_scope(int on) { (void)on; }
static int s_local_scope;
void gpu_ws_set_local_view_scope(int on) { s_local_scope=on; }
int psx_selfcheck_resim_active(void) { return 0; }
int psx_selfcheck_enabled(void) { return 0; }
int psx_rewind_is_open(void) { return 0; }
@@ -207,6 +209,11 @@ uint32_t gl_renderer_stereo_unavailable(void) { return 0; }
int gl_renderer_stereo_begin(int x, int y, int w, int h, int reuse) {
return gl_renderer_pass_begin(x, y, w, h, 0, 0, reuse);
}
uint32_t gl_renderer_local_pair_unavailable(void) { return 0; }
int gl_renderer_local_pair_begin(int x, int y, int w, int h, int reuse) {
return gl_renderer_pass_begin(x,y,w,h,0,0,reuse);
}
int gl_renderer_local_pair_set_presentation(int enabled) { (void)enabled;return 1; }
int gl_renderer_stereo_end(uint32_t eye, int keep) {
gl_renderer_pass_end(0, keep);
if (keep) s_stereo_mask |= 1u << eye;
@@ -619,6 +626,26 @@ int main(void) {
"normal drawing recovers after a rejected device read");
}
/* Offline view pairs use the same unwind protection, including the
* own-view scope which stereo rendering does not enter. */
{
PSXModStereoFrame frame={sizeof frame,1,0,0,320,240};
uint64_t kept;
snap(&live);
CHECK(psx_mod_render_local_pair(&cpu,&frame,stereo_draw,NULL)==1,
"complete local pair publishes");
kept=s_stereo_published;
CHECK(psx_mod_render_local_pair(&cpu,&frame,stereo_draw,&runaway)==0,
"local second-view watchdog discards the pair");
CHECK(s_stereo_published==kept && s_stereo_mask==0,
"local abort cannot publish half a pair");
CHECK(!s_local_scope && !psx_mod_local_view_scope(),
"local abort closes the own-view scope");
check_live(&live,"after local second-view watchdog");
CHECK(psx_mod_render_local_pair(&cpu,&frame,stereo_draw,NULL)==1,
"local pair recovers after watchdog");
}
printf(failures ? "FAILED (%d)\n" : "ALL PASS\n", failures);
return failures ? 1 : 0;
}
+37
View File
@@ -273,6 +273,11 @@ uint32_t gl_renderer_stereo_unavailable(void) { return s_gl_status; }
int gl_renderer_stereo_begin(int x, int y, int w, int h, int reuse) {
return gl_renderer_pass_begin(x, y, w, h, 0, 0, reuse);
}
uint32_t gl_renderer_local_pair_unavailable(void) { return s_local_status; }
int gl_renderer_local_pair_begin(int x, int y, int w, int h, int reuse) {
return gl_renderer_pass_begin(x,y,w,h,0,0,reuse);
}
int gl_renderer_local_pair_set_presentation(int enabled) { (void)enabled;return 1; }
int gl_renderer_stereo_end(uint32_t eye, int keep) {
gl_renderer_pass_end(0, keep);
if (keep && (int)eye == s_capture_fail_eye) return 0;
@@ -770,6 +775,37 @@ static int local_draw(CPUState *cpu, void *user, uint32_t alpha) {
psx_advance_cycles(5000u);
return 1;
}
static int local_pair_draw(CPUState *cpu,void *user,uint32_t seat) {
CHECK(psx_mod_local_view_scope() && s_ws_scope,
"both local multiplayer views run in the own-view scope");
return pair_draw(cpu,user,seat);
}
static void test_local_pair(void) {
CPUState cpu={0};PSXModStereoFrame f={sizeof f,1,0,240,320,240};
uint64_t cycle=psx_cycle_count,published;
render_pass_reset_session();s_netplay_active=0;s_eye_calls=0;
s_gl_status=PSX_MOD_RENDER_PASS_BACKEND;s_local_status=0;s_begin_ok=1;
cpu.gpr[8]=123;s_ram[0x456]=0x77;
CHECK(psx_mod_render_local_pair(&cpu,&f,local_pair_draw,NULL),
"dual raster supports a local pair when ordinary stereo is unavailable");
CHECK(s_eye_calls==2 && cpu.gpr[8]==123 && s_ram[0x456]==0x77 &&
psx_cycle_count==cycle && !psx_mod_local_view_scope(),
"two local views restore one common simulation checkpoint and close their scopes");
published=s_stereo_published;s_eye_decline=1;
CHECK(!psx_mod_render_local_pair(&cpu,&f,local_pair_draw,NULL) &&
s_stereo_published==published && !s_stereo_mask,
"failed second local view cannot publish a partial frame");
CHECK(cpu.gpr[8]==123 && s_ram[0x456]==0x77 && psx_cycle_count==cycle,
"failed local pair preserves live state");
s_eye_decline=-1;s_netplay_active=1;psx_mod_set_render_pass_netplay(1);
int calls=s_eye_calls;
CHECK(!psx_mod_render_local_pair(&cpu,&f,local_pair_draw,NULL) && s_eye_calls==calls,
"offline local pair is refused online even if temporal replay was permitted");
s_netplay_active=0;s_local_status=PSX_MOD_RENDER_PASS_BACKEND;
CHECK(!psx_mod_render_local_pair(&cpu,&f,local_pair_draw,NULL) && s_eye_calls==calls,
"unsupported local presenter refuses before guest drawing");
s_gl_status=s_local_status=0;
}
/* Netplay local view: forward netplay frames only; guest state restored
* exactly; a committed image cancels the crop request. */
@@ -856,6 +892,7 @@ int main(void) {
test_journal();
test_stereo();
test_local_view();
test_local_pair();
test_texture_stream_journal();
CHECK(s_prec_open == 0, "precision checkpoints balanced");
printf(failures ? "FAILED (%d)\n" : "ALL PASS\n", failures);