From 8d4350082c0f4eed890ed0a000eb4a522f1976d0 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Sun, 18 Oct 2026 17:30:11 -0700 Subject: [PATCH] Add protected offline local view pair presentation --- runtime/include/gpu_gl_renderer.h | 3 ++ runtime/include/mod_plugins.h | 8 +++++ runtime/src/gpu_gl_renderer.c | 45 ++++++++++++++++++++---- runtime/src/render_pass.c | 43 ++++++++++++++-------- runtime/tests/test_render_pass_abort.c | 29 ++++++++++++++- runtime/tests/test_render_pass_sandbox.c | 37 +++++++++++++++++++ 6 files changed, 144 insertions(+), 21 deletions(-) diff --git a/runtime/include/gpu_gl_renderer.h b/runtime/include/gpu_gl_renderer.h index bd5c910c..d84a04cf 100644 --- a/runtime/include/gpu_gl_renderer.h +++ b/runtime/include/gpu_gl_renderer.h @@ -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 { diff --git a/runtime/include/mod_plugins.h b/runtime/include/mod_plugins.h index 4b86b332..d3667dec 100644 --- a/runtime/include/mod_plugins.h +++ b/runtime/include/mod_plugins.h @@ -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. */ diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index e37d6591..708f03e5 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -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); diff --git a/runtime/src/render_pass.c b/runtime/src/render_pass.c index 86b6d2c6..e87a926c 100644 --- a/runtime/src/render_pass.c +++ b/runtime/src/render_pass.c @@ -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); +} diff --git a/runtime/tests/test_render_pass_abort.c b/runtime/tests/test_render_pass_abort.c index 9a2abd1c..a16ea07e 100644 --- a/runtime/tests/test_render_pass_abort.c +++ b/runtime/tests/test_render_pass_abort.c @@ -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; } diff --git a/runtime/tests/test_render_pass_sandbox.c b/runtime/tests/test_render_pass_sandbox.c index fdc81e80..a151efe8 100644 --- a/runtime/tests/test_render_pass_sandbox.c +++ b/runtime/tests/test_render_pass_sandbox.c @@ -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);