netplay: let a title present its own sandboxed local view

psx_mod_render_local_view() runs a title's draw code in the render-pass
sandbox on a forward netplay frame and keeps the drawn display rect in the
OpenGL presenter's surface only. The authoritative CPU VRAM, guest RAM,
rollback snapshots and digests keep the canonical frame; the presenter shows
this peer's own image. A committed image cancels any local-view crop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Shokunin
2026-10-06 11:24:17 -07:00
parent c4215396e3
commit f379771c93
12 changed files with 286 additions and 26 deletions
+10
View File
@@ -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).
---
+31
View File
@@ -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,
+9
View File
@@ -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);
+28
View File
@@ -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.
+4
View File
@@ -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,
+2
View File
@@ -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 */
+5
View File
@@ -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,
+59 -10
View File
@@ -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);
+6
View File
@@ -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)
+54 -14
View File
@@ -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);
+6
View File
@@ -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);
+72 -2
View File
@@ -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);