diff --git a/docs/NETPLAY.md b/docs/NETPLAY.md index 1f1ea85f..a6b6229b 100644 --- a/docs/NETPLAY.md +++ b/docs/NETPLAY.md @@ -161,6 +161,16 @@ that identical frame; only what its window shows changes: savestate, and is ignored offline. The headless present-image ring records the same rectangle, which is how a harness checks it. Vulkan, which netplay replaces with the software present, ignores it. +- `psx_mod_render_local_view(cpu, rect, fn, user)` (`mod_plugins.h`): this + peer draws its *own* image of a display rect with the game's code, for + example its seat's single full-screen view, inside the render-pass sandbox + (docs/RENDER_PASSES.md, "Netplay local view"). The machine is restored + afterwards, so the canonical frame stays in the authoritative VRAM, rollback + snapshots and digests; only the OpenGL presenter's surface keeps the image. + A committed image cancels any `psx_netplay_present_local_view` crop + (`psx_netplay_local_view_clear`). Forward netplay frames only; a title falls + back to the crop when `psx_mod_render_local_view_status()` is not ready + (software present, Vulkan, resimulation). --- diff --git a/docs/RENDER_PASSES.md b/docs/RENDER_PASSES.md index eb0a1430..bebf1f42 100644 --- a/docs/RENDER_PASSES.md +++ b/docs/RENDER_PASSES.md @@ -221,6 +221,37 @@ the interpreter's active/phase/precise flags, resume latch and pending load its callback's normal return resets. After 8 faults (watchdog or refused writes) passes stay off for the session. +## Netplay local view + +```c +int psx_mod_render_local_view(struct CPUState *cpu, const PSXModRenderPass *rect, + PSXModRenderPassFn fn, void *user); +uint32_t psx_mod_render_local_view_status(void); +``` + +A title whose netplay frame draws every seat's view (so guest state stays +identical on every peer) can give each player their own seat's single view: +at the point where the next flip will show `rect` and the guest has finished +drawing it, `fn` redraws that rect with the game's code (alpha 0) in the same +sandbox as a pass. Everything a pass restores is restored; the difference is +the product. The OpenGL presenter's surface keeps the rect's colour (the +internal-resolution surface and the native-wide band) until the guest draws +there again, while the stencil, raw mirror, CPU VRAM rows, out-of-rect journal +and coherency state are put back. Its draws skip the software raster, so the +authoritative CPU VRAM never sees them. A committed image cancels any +`psx_netplay_present_local_view()` crop. + +It needs a netplay session on a forward frame and the OpenGL dual-raster +presenter (a surface separate from the authoritative VRAM); it does not need +frame interpolation. The status is `SESSION` offline, while resimulating, in +rewind or lockstep replay; `BACKEND` without dual raster or in the +high-resolution window; `NO_PRESENTER` without an OpenGL surface or with +24-bit scanout; plus the usual `FAST_FORWARD`, `DISABLED` and `BUSY`. +`render_pass_stats` counts `local_attempts` and `local_views` and reports +`local_status`. `PSX_RENDER_PASS_VERIFY=1` checks the guest state as for a +pass (the kept rect is the product, so its VRAM comparison is skipped). +R4's online Link Battle (`src/mods/r4_link_netplay.c`) is the worked example. + ## Gates The plan returns 0 in netplay, rollback resimulation, self-check, rewind, diff --git a/runtime/include/gpu_gl_renderer.h b/runtime/include/gpu_gl_renderer.h index 0af25dc2..08f4199e 100644 --- a/runtime/include/gpu_gl_renderer.h +++ b/runtime/include/gpu_gl_renderer.h @@ -103,6 +103,15 @@ uint64_t gl_renderer_pass_backups_reused(void); uint32_t gl_renderer_pass_image_textures(uint64_t *bytes); /* Debug: dump the images of the next `generations` shown frames as PNGs. */ void gl_renderer_pass_dump_arm(const char *dir, int generations); +/* Netplay local view (psx_mod_render_local_view): the VRAM transaction with + * no generation. Available only with a presenter surface separate from the + * authoritative CPU VRAM (dual raster). While it is open, draws reach only + * that surface. end(keep=1) keeps the rect's presented colour (hr surface and + * native-wide band) and restores everything else (stencil, raw mirror, CPU + * rows, out-of-rect journal, coherency state); keep=0 restores all. */ +uint32_t gl_renderer_local_view_unavailable(void); +int gl_renderer_local_view_begin(int x, int y, int w, int h); +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); diff --git a/runtime/include/mod_plugins.h b/runtime/include/mod_plugins.h index 975efc9e..3edd22cb 100644 --- a/runtime/include/mod_plugins.h +++ b/runtime/include/mod_plugins.h @@ -500,6 +500,34 @@ enum { PSX_MOD_RENDER_PASS_BUSY = 6 }; uint32_t psx_mod_render_pass_status(void); +/* + * Netplay local view: this peer's own image of a display rect, drawn by the + * game's code inside the render-pass sandbox, replaces what the presenter + * shows of that rect. For a title whose netplay frame draws every seat's view + * (so guest state stays identical on every peer) but whose players should + * each see their own seat's single full-screen view. + * + * `fn` runs as a render pass does (frozen guest time, sandboxed stores, the + * watchdog; CPU with the GTE, RAM, scratchpad, devices and the authoritative + * VRAM restored afterwards) and draws into rect->x/y/w/h; alpha_q16 is 0. When + * it returns nonzero the presenter's own copy of the rect keeps the image + * until the guest draws there again: the canonical frame stays in the + * authoritative VRAM, savestates, rollback snapshots and digests. A committed + * image also cancels any psx_netplay_present_local_view() crop. Call it where + * the next flip will show rect and the guest has finished drawing it. + * + * Only in a netplay session on forward frames, with the OpenGL presenter + * keeping a surface separate from the authoritative VRAM (dual raster); never + * while resimulating, in rewind, lockstep replay, fast-forward, inside an + * exception or a pass. psx_mod_render_local_view_status() says why not, with + * the PSX_MOD_RENDER_PASS_* reasons; a title then shows its canonical frame + * (for example its own view's part of it through + * psx_netplay_present_local_view). Returns 1 when the image was committed. + */ +int psx_mod_render_local_view(struct CPUState* cpu, + const PSXModRenderPass* rect, + PSXModRenderPassFn fn, void* user); +uint32_t psx_mod_render_local_view_status(void); /* Simultaneous stereo capture, independent of temporal interpolation. Each * eye starts from the same guest state; CPU/RAM/devices/VRAM are restored * before the other eye and on failure. Publish only after both succeed. diff --git a/runtime/include/psx_netplay.h b/runtime/include/psx_netplay.h index cd078622..223ee4e4 100644 --- a/runtime/include/psx_netplay.h +++ b/runtime/include/psx_netplay.h @@ -143,6 +143,10 @@ int psx_netplay_sim_pad(int seat, PsxNetPad *out); * up. Ignored while netplay is off. */ void psx_netplay_present_local_view(uint32_t x, uint32_t y, uint32_t w, uint32_t h); +/* Drop any current request at once (the full frame is presented again). + * A committed psx_mod_render_local_view() image calls it: the peer's own + * image of the display supersedes a crop of the canonical frame. */ +void psx_netplay_local_view_clear(void); /* 1 and the rectangle while a current request fits a display of * display_w x display_h. */ int psx_netplay_local_view(uint32_t display_w, uint32_t display_h, diff --git a/runtime/include/render_pass.h b/runtime/include/render_pass.h index 5cbb5ffe..7408d968 100644 --- a/runtime/include/render_pass.h +++ b/runtime/include/render_pass.h @@ -43,6 +43,8 @@ typedef struct RenderPassStats { uint64_t argument_refused, status_refused, begin_refused, checkpoint_refused; RenderPassFailure last_failure; /* latched across successes; session reset clears */ uint64_t passes; /* passes run to completion and presented */ + uint64_t local_views; /* psx_mod_render_local_view images committed */ + uint64_t local_attempts; /* psx_mod_render_local_view calls */ uint64_t aborted; /* passes rolled back by a fault */ uint64_t discarded; /* passes whose plugin declined the image */ uint64_t watchdog; /* of which: guest-cycle watchdog overruns */ diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index e4266db4..7573fdcf 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -8282,6 +8282,8 @@ static void handle_render_pass_stats(int id, const char *json) "\"argument_refused\":%llu,\"status_refused\":%llu," "\"begin_refused\":%llu,\"checkpoint_refused\":%llu," "\"spans\":%llu,\"span_failures\":%llu," + "\"local_views\":%llu,\"local_attempts\":%llu," + "\"local_status\":%u," "\"last_failure\":%s,\"last_abort_detail\":\"%s\"," "\"span_fail\":{\"reason\":%u,\"pc\":\"0x%08X\",\"start\":\"0x%08X\"," "\"stop\":\"0x%08X\",\"ra\":\"0x%08X\",\"after\":\"0x%08X\"," @@ -8316,6 +8318,9 @@ static void handle_render_pass_stats(int id, const char *json) (unsigned long long)st.argument_refused, (unsigned long long)st.status_refused, (unsigned long long)st.begin_refused, (unsigned long long)st.checkpoint_refused, (unsigned long long)st.spans, (unsigned long long)st.span_failures, + (unsigned long long)st.local_views, + (unsigned long long)st.local_attempts, + psx_mod_render_local_view_status(), failure_json, abort_detail_json, (unsigned)sf.reason, (unsigned)sf.pc, (unsigned)sf.start_pc, (unsigned)sf.stop_pc, (unsigned)sf.ra, (unsigned)sf.after, diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index 8a37e461..b4a89364 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -500,6 +500,11 @@ static uint64_t s_idle_ticks_accum_fwd(uint64_t add); * open, GPU writes are confined to its rect: the scissor is intersected with * it and writes that bypass the scissor are refused and counted. */ static int s_pass_active = 0; +/* The open transaction is a netplay local view (gl_renderer_local_view_*): + * its draws reach only the presenter surface, never the authoritative CPU + * VRAM, and end(keep) keeps the rect's presented colour. */ +static int s_pass_local = 0; +static uint64_t s_local_commits = 0; /* Textures/framebuffers made for passes (pass_make_color_fbo, pass image * slots), and the count when the current pass began: a pass that allocated * is left out of the pass-cost average (render_pass_cost_add). */ @@ -3755,6 +3760,11 @@ static void glb_set_draw_offset(int x,int y) { flush_flat_batch(); flush_tex_bat static void depth24_upload_policy(void); static int cpu_raster_required(void) { depth24_upload_policy(); + /* A netplay local view draws a presentation-only image: the CPU mirror + * keeps the canonical frame, so its draws skip the software raster + * (depth24 scanout still presents from the CPU mirror and keeps it). */ + if (s_pass_active && s_pass_local && s_cpu_auth_dual && !s_depth24_skip_up) + return !s_raster_ok; return !s_raster_ok || cpu_vram_authoritative(); } /* The sub-pixel / perspective override describes exactly one triangle; drop it @@ -6429,13 +6439,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. */ static int transaction_begin(int x, int y, int w, int h, int open_gen, - uint32_t period_vblanks, int reuse_backup, int stereo) { + uint32_t period_vblanks, int reuse_backup, int kind) { + const int stereo = kind == 1, local = kind == 2; 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 = stereo ? gl_renderer_stereo_unavailable() - : gl_renderer_pass_unavailable(); + s_pass_begin_diag.status = local ? gl_renderer_local_view_unavailable() + : stereo ? gl_renderer_stereo_unavailable() + : gl_renderer_pass_unavailable(); s_pass_begin_diag.active = s_pass_active; s_pass_begin_diag.open_gen = open_gen; s_pass_begin_diag.hr_scale = S; @@ -6471,7 +6484,9 @@ static int transaction_begin(int x, int y, int w, int h, int open_gen, s_pass_begin_diag.requested_h = th; gi = 1 - s_pgen_cur; g = &s_pgen[gi]; - if (stereo) { + if (local) { + /* No generation: the image stays in the presenter surface. */ + } else if (stereo) { StereoPair *pair = &s_stereo_pair[1 - s_stereo_current]; pair->x = x; pair->y = y; pair->w = w; pair->h = h; } else if (open_gen) { @@ -6558,24 +6573,27 @@ backed_up: if (s_pass_verify) pass_verify_read(&s_pv_hr, &s_pv_hr_cap, &s_pv_raw, &s_pv_raw_cap); s_pj_cpu.n = 0; s_pass_active = 1; + s_pass_local = local; return 1; } -static void transaction_restore(void) { +/* keep_color: a committed netplay local view keeps the rect's presented + * colour (hr surface, native-wide band); everything else is put back. */ +static void transaction_restore_ex(int keep_color) { int S = s_hr_scale, gi = 1 - s_pgen_cur; + const GLbitfield hr_bits = keep_color ? GL_STENCIL_BUFFER_BIT + : GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT; (void)gi; if (!s_pass_active) return; /* Roll the journal, then the rect back. */ pass_journal_rollback(); pass_blit(s_pb_hr_fbo, s_hr_fbo, 0, 0, s_pass_x * S, s_pass_y * S, - s_pass_w * S, s_pass_h * S, - GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + s_pass_w * S, s_pass_h * S, hr_bits); pass_blit(s_pb_raw_fbo, s_raw_fbo, 0, 0, s_pass_x, s_pass_y, s_pass_w, s_pass_h, GL_COLOR_BUFFER_BIT); if (s_pb_wide_src && s_pb_wide_src == pass_wide_fbo_for(s_pass_x)) pass_blit(s_pb_wide_fbo, s_pb_wide_src, 0, 0, 0, s_pass_y * S, - g_wide_w * S, s_pass_h * S, - GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + g_wide_w * S, s_pass_h * S, hr_bits); for (int row = 0; row < s_pass_h; row++) memcpy(s_vram + (size_t)(s_pass_y + row) * VRAM_W + s_pass_x, s_pb_cpu + (size_t)row * s_pass_w, @@ -6592,7 +6610,15 @@ static void transaction_restore(void) { s_last_dx = s_pb_last_dx; s_last_dy = s_pb_last_dy; s_last_dw = s_pb_last_dw; s_last_dh = s_pb_last_dh; s_pass_active = 0; - s_pb_valid = 1; + s_pass_local = 0; + s_pb_valid = !keep_color; + if (keep_color) { + /* The rect now differs from the backup and from the last present. */ + present_dirty_rect(s_pass_x, s_pass_y, s_pass_x + s_pass_w - 1, + s_pass_y + s_pass_h - 1, 1); + s_pv_ok = 1; /* the kept rect is the product, not a leak */ + return; + } if (s_pass_verify) { static uint8_t *after_hr = NULL, *after_raw = NULL; @@ -6606,6 +6632,29 @@ static void transaction_restore(void) { } } +static void transaction_restore(void) { transaction_restore_ex(0); } + +uint32_t gl_renderer_local_view_unavailable(void) { + pass_refusal_init(); + if (!s_ctx || !s_raster_ok || !s_hr_fbo || gpu_display_is_depth24()) + return PSX_MOD_RENDER_PASS_NO_PRESENTER; + /* Only a presenter surface separate from the authoritative VRAM can hold + * a peer's own image; the high-resolution window presents other tiles. */ + if (!s_cpu_auth_dual || s_hiw || s_pass_force_refuse > 0) + return PSX_MOD_RENDER_PASS_BACKEND; + return PSX_MOD_RENDER_PASS_READY; +} +int gl_renderer_local_view_begin(int x, int y, int w, int h) { + return transaction_begin(x, y, w, h, 0, 0, 0, 2); +} +int gl_renderer_local_view_end(int keep) { + if (!s_pass_active || !s_pass_local) return 0; + flush_flat_batch(); flush_tex_batch(); flush_cpu_upload(); + transaction_restore_ex(keep ? 1 : 0); + if (keep) s_local_commits++; + return 1; +} + int gl_renderer_pass_begin(int x, int y, int w, int h, int open_gen, uint32_t period, int reuse) { return transaction_begin(x, y, w, h, open_gen, period, reuse, 0); diff --git a/runtime/src/psx_netplay.c b/runtime/src/psx_netplay.c index 2e86fe17..79087976 100644 --- a/runtime/src/psx_netplay.c +++ b/runtime/src/psx_netplay.c @@ -467,6 +467,7 @@ void psx_netplay_present_local_view(uint32_t x, uint32_t y, (void)w; (void)h; } +void psx_netplay_local_view_clear(void) {} int psx_netplay_local_view(uint32_t display_w, uint32_t display_h, uint32_t *x, uint32_t *y, uint32_t *w, uint32_t *h) @@ -3392,6 +3393,11 @@ void psx_netplay_present_local_view(uint32_t x, uint32_t y, x, y, w, h); } +void psx_netplay_local_view_clear(void) +{ + psx_netplay_local_view_reset(&s_local_view); +} + int psx_netplay_local_view(uint32_t display_w, uint32_t display_h, uint32_t *x, uint32_t *y, uint32_t *w, uint32_t *h) diff --git a/runtime/src/render_pass.c b/runtime/src/render_pass.c index c197cb6d..0e7eaffe 100644 --- a/runtime/src/render_pass.c +++ b/runtime/src/render_pass.c @@ -59,6 +59,7 @@ extern void psx_irq_refresh_cause_ip2(void); extern int psx_get_in_exception(void); extern int psx_netplay_active(void); extern int psx_netplay_is_resimulating(void); +extern void psx_netplay_local_view_clear(void); extern int psx_selfcheck_resim_active(void); extern int psx_rewind_is_open(void); extern int psx_presentation_fast_forward(void); @@ -328,17 +329,24 @@ int psx_mod_set_render_pass_flip(uint32_t mode) { return 1; } +/* Transaction kinds: what `eye` holds in render_transaction. */ +#define RP_EYE_PASS (-1) /* temporal pass */ +#define RP_EYE_LOCAL (-2) /* netplay local view */ + /* Everything that must hold before guest code may run frozen, as a - * PSX_MOD_RENDER_PASS_* reason (READY = all of it holds). */ -static uint32_t transaction_status(int stereo) { + * PSX_MOD_RENDER_PASS_* reason (READY = all of it holds). kind: 0 temporal + * pass, 1 stereo, 2 netplay local view (forward netplay frames only). */ +static uint32_t transaction_status(int kind) { uint32_t gl; if (s_stats.disabled) return PSX_MOD_RENDER_PASS_DISABLED; - if (psx_netplay_active() || psx_netplay_is_resimulating() || + if ((kind == 2 ? !psx_netplay_active() : psx_netplay_active()) || + psx_netplay_is_resimulating() || psx_selfcheck_resim_active() || psx_rewind_is_open() || g_ls_mode || g_ls_replay_active) return PSX_MOD_RENDER_PASS_SESSION; if (psx_presentation_fast_forward()) return PSX_MOD_RENDER_PASS_FAST_FORWARD; - gl = stereo ? gl_renderer_stereo_unavailable() : gl_renderer_pass_unavailable(); + gl = 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_render_pass_active || psx_get_in_exception() || dma_gpu_linked_list_active()) @@ -594,8 +602,13 @@ static void note_fault(const char *what) { } } +static int transaction_kind(int eye) { + return 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 == RP_EYE_LOCAL) return gl_renderer_local_view_end(keep); gl_renderer_pass_end(alpha, keep); return 1; } @@ -623,12 +636,12 @@ static int render_transaction(struct CPUState *cpu, const PSXModRenderPass *pass } if (!cpu || !pass || !fn || pass->struct_size < sizeof *pass || - pass->w == 0 || pass->h == 0 || (eye < 0 && + pass->w == 0 || pass->h == 0 || (eye == RP_EYE_PASS && (pass->alpha_q16 == 0 || pass->alpha_q16 >= 65536u))) { s_stats.argument_refused++; - return pass_refuse("arguments", transaction_status(eye >= 0)); + return pass_refuse("arguments", transaction_status(transaction_kind(eye))); } - status = transaction_status(eye >= 0); + status = transaction_status(transaction_kind(eye)); if (status != PSX_MOD_RENDER_PASS_READY) { s_stats.status_refused++; return pass_refuse("status", status); @@ -641,18 +654,21 @@ static int render_transaction(struct CPUState *cpu, const PSXModRenderPass *pass s_attempt.guest_cycle = psx_cycle_count; t0 = gl_renderer_perf_ticks(); - open = eye < 0 ? s_open_generation : 0; + open = eye == RP_EYE_PASS ? s_open_generation : 0; { static double inv = 0.0; if (inv == 0.0) inv = 1000.0 / (double)gl_renderer_perf_frequency(); s_ms_per_tick = inv; } - reuse = !open && s_restored_valid && s_restored_plan == s_plan_serial && + reuse = !open && eye != RP_EYE_LOCAL && s_restored_valid && + s_restored_plan == s_plan_serial && 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, pass->h, reuse) + : eye == RP_EYE_LOCAL ? gl_renderer_local_view_begin(pass->x, pass->y, + pass->w, pass->h) : gl_renderer_pass_begin(pass->x, pass->y, pass->w, pass->h, open, s_plan_period, reuse))) { gl_renderer_pass_begin_diag(&s_attempt.gl); @@ -667,7 +683,7 @@ static int render_transaction(struct CPUState *cpu, const PSXModRenderPass *pass gl_renderer_pass_begin_diag(&s_attempt.gl); s_stats.checkpoint_refused++; /* Capture the rejection status before rolling the GL transaction back. */ - status = transaction_status(eye >= 0); + status = transaction_status(transaction_kind(eye)); transaction_end(eye, 0, 0); return pass_refuse(s_checkpoint_failure ? s_checkpoint_failure : "checkpoint", status); } @@ -727,7 +743,9 @@ static int render_transaction(struct CPUState *cpu, const PSXModRenderPass *pass checkpoint_restore(cpu); psx_cycle_freeze_end(&s_freeze); s_nesting = 0; - s_restored_valid = 1; + /* A committed local view changed the presented rect: no later pass may + * reuse this backup. */ + s_restored_valid = !(eye == RP_EYE_LOCAL && ok); s_restored_plan = s_plan_serial; s_restored_cycle = psx_cycle_count; s_restored_stores = g_guest_store_count; @@ -759,16 +777,38 @@ static int render_transaction(struct CPUState *cpu, const PSXModRenderPass *pass s_stats.avg_pass_ms = s_stats.avg_pass_ms > 0.0 ? s_stats.avg_pass_ms * 0.9 + ms * 0.1 : ms; } - if (eye < 0) gl_renderer_pass_note_cost(t1 - t0); - if (ok) s_stats.passes++; + if (eye == RP_EYE_PASS) gl_renderer_pass_note_cost(t1 - t0); + if (ok && eye == RP_EYE_LOCAL) { + s_stats.local_views++; + /* The peer's own image supersedes a crop of the canonical frame. */ + psx_netplay_local_view_clear(); + } else if (ok) s_stats.passes++; else if (leaks) note_fault("VRAM write outside the pass rect"); else if (s_stats.watchdog_flag) note_fault("guest-cycle watchdog"); s_stats.watchdog_flag = 0; /* Present anything that fell due while the pass ran. */ - if (eye < 0) gl_renderer_pass_service_presents(); + if (eye == RP_EYE_PASS) gl_renderer_pass_service_presents(); return ok; } +int psx_mod_render_local_view(struct CPUState *cpu, const PSXModRenderPass *rect, + PSXModRenderPassFn fn, void *user) { + PSXModRenderPass r; + s_stats.local_attempts++; + if (!rect || rect->struct_size < sizeof r || + (uint32_t)rect->x + rect->w > 1024u || (uint32_t)rect->y + rect->h > 512u) { + s_stats.argument_refused++; + return pass_refuse("arguments", transaction_status(2)); + } + r = *rect; + r.alpha_q16 = 0; + return render_transaction(cpu, &r, fn, user, RP_EYE_LOCAL); +} + +uint32_t psx_mod_render_local_view_status(void) { + return transaction_status(2); +} + int psx_mod_render_pass(struct CPUState *cpu, const PSXModRenderPass *pass, PSXModRenderPassFn fn, void *user) { return render_transaction(cpu, pass, fn, user, -1); diff --git a/runtime/tests/test_render_pass_abort.c b/runtime/tests/test_render_pass_abort.c index fe416b9b..5e4edd48 100644 --- a/runtime/tests/test_render_pass_abort.c +++ b/runtime/tests/test_render_pass_abort.c @@ -105,6 +105,7 @@ void mdec_snapshot_write(uint8_t *p) { (void)p; } int psx_get_in_exception(void) { return 0; } int psx_netplay_active(void) { return 0; } int psx_netplay_is_resimulating(void) { return 0; } +void psx_netplay_local_view_clear(void) {} int psx_selfcheck_resim_active(void) { return 0; } int psx_selfcheck_enabled(void) { return 0; } int psx_rewind_is_open(void) { return 0; } @@ -192,6 +193,11 @@ int gl_renderer_pass_verify_vram(void) { return 1; } 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; +uint32_t gl_renderer_local_view_unavailable(void) { return 0; } +int gl_renderer_local_view_begin(int x, int y, int w, int h) { + return gl_renderer_pass_begin(x, y, w, h, 0, 0, 0); +} +int gl_renderer_local_view_end(int keep) { gl_renderer_pass_end(0, keep); return 1; } 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); diff --git a/runtime/tests/test_render_pass_sandbox.c b/runtime/tests/test_render_pass_sandbox.c index 25422578..909bfd6a 100644 --- a/runtime/tests/test_render_pass_sandbox.c +++ b/runtime/tests/test_render_pass_sandbox.c @@ -154,8 +154,10 @@ uint32_t mdec_snapshot_bytes(void) { return 0; } void mdec_snapshot_write(uint8_t *p) { (void)p; } int psx_get_in_exception(void) { return 0; } -int psx_netplay_active(void) { return 0; } -int psx_netplay_is_resimulating(void) { return 0; } +static int s_netplay, s_resim, s_crop_clears; +int psx_netplay_active(void) { return s_netplay; } +int psx_netplay_is_resimulating(void) { return s_resim; } +void psx_netplay_local_view_clear(void) { s_crop_clears++; } int psx_selfcheck_resim_active(void) { return 0; } int psx_selfcheck_enabled(void) { return 0; } int psx_rewind_is_open(void) { return 0; } @@ -242,6 +244,17 @@ uint32_t gl_renderer_pass_leaks(void) { return 0; } int gl_renderer_pass_verify_vram(void) { return 1; } 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; +static int s_local_kept; +uint32_t gl_renderer_local_view_unavailable(void) { return s_local_status; } +int gl_renderer_local_view_begin(int x, int y, int w, int h) { + return gl_renderer_pass_begin(x, y, w, h, 0, 0, 0); +} +int gl_renderer_local_view_end(int keep) { + gl_renderer_pass_end(0, 0); + if (keep) s_local_kept++; + return 1; +} static uint32_t s_stereo_mask; static uint64_t s_stereo_published; static int s_capture_fail_eye = -1; @@ -717,6 +730,62 @@ static void test_stereo(void) { CHECK(!psx_mod_render_stereo(&cpu, &f, pair_draw, NULL), "invalid cadence refused"); } +static int local_draw(CPUState *cpu, void *user, uint32_t alpha) { + (void)user; + CHECK(alpha == 0, "a local view has no phase"); + cpu->gpr[8] = 0xBEEFu; + s_ram[0x456] = 0x11; /* the game's draw code writes RAM */ + psx_advance_cycles(5000u); + return 1; +} + +/* Netplay local view: forward netplay frames only; guest state restored + * exactly; a committed image cancels the crop request. */ +static void test_local_view(void) { + CPUState cpu = {0}; + PSXModRenderPass r; + RenderPassStats st; + uint64_t cycle; + render_pass_reset_session(); s_gl_status = 0; s_begin_ok = 1; + memset(&r, 0, sizeof r); + r.struct_size = sizeof r; + r.y = 240; r.w = 320; r.h = 240; + cpu.gpr[8] = 7; s_ram[0x456] = 0x77; + cycle = psx_cycle_count; + s_netplay = 0; + CHECK(psx_mod_render_local_view_status() == PSX_MOD_RENDER_PASS_SESSION, + "local view refused offline"); + CHECK(!psx_mod_render_local_view(&cpu, &r, never_fn, NULL), "no offline call"); + s_netplay = 1; + CHECK(psx_mod_render_pass_status() == PSX_MOD_RENDER_PASS_SESSION, + "temporal passes stay off in netplay"); + CHECK(psx_mod_render_local_view_status() == PSX_MOD_RENDER_PASS_READY, + "local view ready in netplay"); + s_resim = 1; + CHECK(psx_mod_render_local_view_status() == PSX_MOD_RENDER_PASS_SESSION && + !psx_mod_render_local_view(&cpu, &r, never_fn, NULL), + "no local view while resimulating"); + s_resim = 0; + s_local_status = PSX_MOD_RENDER_PASS_BACKEND; + CHECK(psx_mod_render_local_view_status() == PSX_MOD_RENDER_PASS_BACKEND && + !psx_mod_render_local_view(&cpu, &r, never_fn, NULL), + "no local view without a separate presenter surface"); + s_local_status = PSX_MOD_RENDER_PASS_READY; + s_crop_clears = 0; + CHECK(psx_mod_render_local_view(&cpu, &r, local_draw, NULL) == 1, + "local view committed"); + render_pass_get_stats(&st); + CHECK(st.local_views == 1 && s_local_kept == 1 && s_open_passes == 0 && + st.passes == 0, "one kept local image, transaction closed"); + CHECK(s_crop_clears == 1, "the committed image cancels the crop request"); + CHECK(cpu.gpr[8] == 7 && s_ram[0x456] == 0x77 && psx_cycle_count == cycle, + "guest state restored after the local view"); + r.x = 900; + CHECK(!psx_mod_render_local_view(&cpu, &r, never_fn, NULL), + "rect outside VRAM refused"); + s_netplay = 0; +} + static void test_texture_stream_journal(void) { RenderPassJournal j = {0}; memcpy(s_vram, s_vram0, sizeof s_vram); @@ -744,6 +813,7 @@ int main(void) { test_ram_8mb(); test_journal(); test_stereo(); + test_local_view(); test_texture_stream_journal(); CHECK(s_prec_open == 0, "precision checkpoints balanced"); printf(failures ? "FAILED (%d)\n" : "ALL PASS\n", failures);