mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
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:
@@ -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).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user