From b3d99b05ff015291b7b72be781ed6a45cfee686e Mon Sep 17 00:00:00 2001 From: Alex Vanderveen Date: Thu, 30 Jul 2026 17:24:20 -0400 Subject: [PATCH] rollback patching --- docs/ROLLBACK_MOTK_HOOKUP.md | 51 +++++++- runtime/include/boot_state.h | 8 +- runtime/include/gpu.h | 6 +- runtime/include/interrupts.h | 3 + runtime/include/netplay_snap_ring.h | 4 +- runtime/include/netplay_state_digest.h | 14 ++- runtime/src/boot_state.c | 56 ++++++--- runtime/src/debug_server.c | 9 +- runtime/src/gpu.c | 36 +++++- runtime/src/interrupts.c | 75 ++++++++++-- runtime/src/netplay_snap_ring.c | 4 +- runtime/src/netplay_state_digest.c | 70 ++++++++++- runtime/src/psx_netplay.c | 53 ++++++++ runtime/src/psx_netplay_rb.c | 162 +++++++++++++++++-------- runtime/src/sio.c | 160 ++++++++++++------------ runtime/tests/test_netplay_snap_ring | Bin 21304 -> 21360 bytes runtime/tests/test_netplay_snap_ring.c | 6 + 17 files changed, 545 insertions(+), 172 deletions(-) diff --git a/docs/ROLLBACK_MOTK_HOOKUP.md b/docs/ROLLBACK_MOTK_HOOKUP.md index 26c0d132..355406a5 100644 --- a/docs/ROLLBACK_MOTK_HOOKUP.md +++ b/docs/ROLLBACK_MOTK_HOOKUP.md @@ -20,6 +20,7 @@ forces classic delay-sync `try_admit`. | Keep delay-sync `RNetSession` working behind a flag | Lobby / ICE / save-xfer stay useful; Disable Rollback opts out | | Predicted rows promote only via `hash_confirm` (or host protect) | Library invariant; NULL `hash_confirm_promote` = always rewind | | Digests must be bit-identical across peers for the same sealed inputs | Otherwise every invent becomes an episode storm | +| **Same bit-identical binary on both peers** | Mixed `build-release` vs packaged `motk-*` can match live digests for hundreds of ticks then fork GPRs/timers in Replay (pin zlib skew is the symptom) | | Snapshots must restore at the exact sim tick requested | Ring load is the only rewind mechanism | | Single-thread ownership of session + rings | Same as delay-sync | @@ -51,8 +52,9 @@ RTT (BattleShip tiers / phase_lock); optional Manual Input Delay / Prediction. resume is pending or while still in `AwaitingBaseline` - [x] **Baseline gate:** send/enter Replay only after `g_episode_snap_applied` (peer BASELINE can arrive during SealInputs — must not skip restore) -- [x] Live snap rate-limit: every `PSX_NET_SNAP_INTERVAL` ticks (default **4**); - resim still snaps every tick. Full `boot_state` ~1.3MB still dominates FPS +- [x] Live snap rate-limit: every `PSX_NET_SNAP_INTERVAL` ticks (default **8**); + resim still snaps every tick. Ring uses **raw** `boot_state` (no zlib) — + `compress2` on RAM+VRAM+SPU was the live FPS tax; disk `.pst` still zlib - [x] Snap PC must be `psx_is_dispatchable` — pick IRQ/BB-edge resume PC (not `cpu->pc` at present, often 0); defer save / abort episode instead of `resume_at(0)` → `trap_crash` @@ -87,10 +89,15 @@ RTT (BattleShip tiers / phase_lock); optional Manual Input Delay / Prediction. FIFO on RB restore - [x] Baseline/POST/hash_confirm agree on **core** digest only (`cd=` audit); CD-only forks were aborting good title/menu Start corrections -- [x] Storm calm: 12-tick rewind cooldown + promote-sweep after commit/realign; +- [x] Storm calm: 12-tick rewind cooldown + promote-sweep after **commit**; MotK digital release-only → promote (no episode) **only while cores still match**; else open episode. Live/seal invent is **idle** (not hold-last). Freeze `cdrom_advance` during Replay (CD IRQ timing was forking POST cores) +- [x] **Abort cooldown from live sim** (before realign rewinds the clock). + Old path armed `until` from rewound tip → uncapped catch-up burned a + 12-tick window in ms → char-select episode storms (`STALE COOLDOWN`). + Failed episode: 60 ticks from live (120 on streak≥2; 90 if no tip). + Reconcile promotes wire for the whole window (`promote-no-resim`) - [x] Netplay forces **software GPU** — GL/VK `glReadPixels` VRAM readback was forking peer snaps (core matched, pin zlib ~220KB apart) and mid-resim cores; baseline/POST also agree on `av=` (GPU+VRAM) via dig_b / POST input_digest @@ -113,14 +120,47 @@ RTT (BattleShip tiers / phase_lock); optional Manual Input Delay / Prediction. `cd=`/`aux=`/`spad=`/`dma=`/`sio=`. Zero host `last_sector_frame` on CD snap wire. mid-Replay FRAME_COMMIT abort on core mismatch (no false POST); `rb audit fin` + abort dump parts + bus digests + `cpu-split` +- [x] **SIO resim fork:** `sioP` showed **fsm-only** forks with matched + regs/pads/mc + bit-identical guest. Fixes: (1) `sio_pace_walk` no longer + drops leftover cycles after a transition cap (peers batching advances + differently left divergent shift/ack remainders); (2) reseat + `sio_trace_seq` on snap load; (3) netplay `sio=` / baseline_ext fold only + through fsm **pace** (exclude host-audit meta/byte_seq); + (4) mid-Replay cycle-watchdog pump drains FRAME_COMMIT only — no + reconcile/`rb_pump`. Logs: `sioP=regs/pads/mc/pace/meta` - [x] Replay entry: `hc_prime_after(load-1)` drops live invent commits (false `resim core diverge`); first `ready=1` baseline burst bypasses rate limit (initiator was ready-timeout while follower solo-Replayed) - [x] Present-edge digests clear PC (FRAME_COMMIT / audit fin / POST) — parked `cpu->pc=0` vs live BB + host-local sticky forked dig_cpu with matched RAM/clk; `rb cpu-split` logs gpr/hi_lo/cop0 vs raw_pc +- [x] **Core digest folds GTE** (was snap-only) — MFC2 can fork GPRs with + matched RAM/clk/SIO; `rb cpu-split` adds `gte=` + `rb gpr-dump` on + fin/abort/post so peers can diff which regs forked +- [x] Log `rb binary path=… size=…` at rollback start — **peers must run the + same bit-identical binary**. Diags showed host `build-release/` vs guest + `motk-0.1.0-linux-x64/` with pin zlib ~1.34M vs ~1.13M, matched baselines, + then Replay GPR-only (ep1) / cpu+tim+ram (ep2 tick after a good Up) +- [x] FPS: raw in-memory snaps + default interval 8; resim audit skips AV/CD + on `arm` (VRAM CRC only on `fin` / baseline / POST). Still open: strip + CDROM/MDEC from MotK ring if match path allows (further RAM headroom — + raw ring ≈ 3.5 MiB × depth) +- [x] **MotK menu v0-only Replay fork:** matched baseline + tick N (all GPRs), + tick N+1 only `r2` differs (host `1` / guest `0x5bd2`) with sticky + `0x8006CDA0` wait-loop, matched RAM/GTE/cycles. BIOS left `v0=1` when + same-thread GPR restore skipped (mode-1 PC heuristic miss) while peer + restored the post-`lw` countdown. Fix: netplay auto + `PSX_SAME_THREAD_RESTORE=3` — same-TCB RFE/SYSCALL always restores + (ChangeThread still skips). Diag: `rb irqctx` on fin/abort with + `restored`/`v0_exit`/`v0_saved`. Do **not** canonicalize v0 in digests. +- [x] **Netplay BB-edge present:** after restore-3, idle sealed resim still + forked `cpu`+`ram` in one tick — `sdl_vblank_present`/`finish_frame` ran + nested from `fire_vblank_edge` mid-`psx_cyc_step` in the wait loop, so + peers digested different instr points. Fix: under netplay, queue the + GPU vblank callback and flush in `psx_check_interrupts` (BB edge); + clear deferred pending on snap resync. Offline still presents immediately. - [ ] Memory budget / thinner snap: optional strip CDROM/MDEC if MotK match - path allows (further FPS headroom) + path allows (further RAM headroom) - [x] Standalone ring bookkeeping test: `runtime/tests/test_netplay_snap_ring.c` **Vtable:** `save_state` / `load_state` → ring only (never disk slots). @@ -230,7 +270,8 @@ RTT (BattleShip tiers / phase_lock); optional Manual Input Delay / Prediction. 4. ~~Episode resim wiring~~ 5. ~~Lobby flag + UI~~ 6. ~~Rate-limit live snaps (`PSX_NET_SNAP_INTERVAL`) + safe episode resume~~ -7. **Thinner snap / FMV policy** — further FPS + movie digest stability +7. **Thinner snap / FMV policy** — strip CDROM/MDEC + movie digest stability + (raw ring snaps + interval 8 already landed for FPS) 8. **Dual-instance soak** — prove Done-when items --- diff --git a/runtime/include/boot_state.h b/runtime/include/boot_state.h index b6271e7a..96a64f6c 100644 --- a/runtime/include/boot_state.h +++ b/runtime/include/boot_state.h @@ -99,11 +99,17 @@ int boot_state_save(const CPUState* cpu, uint32_t bios_checksum, uint32_t entry_pc, const char* path); /* Same as boot_state_save, but into a malloc'd buffer (caller frees *out_data). - * Used by the netplay snap ring for per-tick in-memory snapshots. */ + * Compresses large sections (disk-oriented). */ int boot_state_save_buffer(const CPUState* cpu, uint32_t bios_checksum, uint32_t entry_pc, uint8_t** out_data, size_t* out_len); +/* In-memory ring snaps: same sections, no zlib. Load accepts either form. + * Avoids compress2 on ~3.5 MiB RAM+VRAM+SPU every live/resim snap (FPS). */ +int boot_state_save_buffer_raw(const CPUState* cpu, uint32_t bios_checksum, + uint32_t entry_pc, uint8_t** out_data, + size_t* out_len); + /* Load + validate (integrity key) + restore the full machine. On any mismatch * or incompleteness returns 0 (caller then boots normally and recaptures). */ int boot_state_load(const char* path, uint32_t bios_checksum, diff --git a/runtime/include/gpu.h b/runtime/include/gpu.h index 811b220e..35eb077d 100644 --- a/runtime/include/gpu.h +++ b/runtime/include/gpu.h @@ -111,9 +111,13 @@ uint32_t gpu_gp0_ring_max_words(void); int gpu_gp0_ring_dump_frame(uint32_t frame, GpuGp0RingEntry *out, int max_out); void gpu_gp0_ring_frame_span(uint32_t *out_oldest, uint32_t *out_newest); -/* Vblank presentation callback — called from gpu_vblank_tick(). */ +/* Vblank presentation callback — called from gpu_vblank_tick(). + * Under netplay the callback is deferred to gpu_vblank_flush_present() at + * BB / IRQ-check edges so finish_frame digests are not sampled mid-block. */ typedef void (*gpu_vblank_cb)(void); void gpu_set_vblank_callback(gpu_vblank_cb cb); +void gpu_vblank_flush_present(void); +void gpu_vblank_clear_deferred_present(void); /* Present-time screen-colour model (see color_lut.h ScreenKind: 0=raw, * 1=crt, 2=composite, 3=trinitron). Config/launcher-driven; the PSX_SCREEN diff --git a/runtime/include/interrupts.h b/runtime/include/interrupts.h index 31ae9177..d33f1e18 100644 --- a/runtime/include/interrupts.h +++ b/runtime/include/interrupts.h @@ -30,6 +30,9 @@ void interrupts_init(void); * catches up to the pre-load host timeline (warm-reload picture freeze). */ void interrupts_resync_after_restore(void); +/* Netplay diag: last VBLANK irqctx exit/restore (+ v0) for peer diff. */ +void interrupts_log_last_vblank_irqctx(const char *tag); + /* Central IRQ-raise choke point: sets the I_STAT bit AND records the raise edge * into the always-on device-event ring (device_trace) with the guest cycle. * Every hardware source (VBLANK/GPU/CDROM/DMA/timers/SIO/SPU) funnels its raise diff --git a/runtime/include/netplay_snap_ring.h b/runtime/include/netplay_snap_ring.h index 222b43d7..e52fbebb 100644 --- a/runtime/include/netplay_snap_ring.h +++ b/runtime/include/netplay_snap_ring.h @@ -6,8 +6,8 @@ * * Depth defaults to 64 (enough for early soaks; raise toward * RNET_RB_SEAL_MAX_SPAN=128 when episodes deepen). Each slot owns a - * boot_state_save_buffer blob. save/load call the full-machine serializer; - * store/peek are for tests and for hosts that already hold a blob. + * boot_state_save_buffer_raw blob (uncompressed — FPS). save/load call the + * full-machine serializer; store/peek are for tests / pre-serialized blobs. */ #include diff --git a/runtime/include/netplay_state_digest.h b/runtime/include/netplay_state_digest.h index c1f7d0a9..0dbbbee0 100644 --- a/runtime/include/netplay_state_digest.h +++ b/runtime/include/netplay_state_digest.h @@ -23,7 +23,7 @@ extern "C" { /* Partition CRCs folded into netplay_core_digest — for first-diverge diags. */ typedef struct NetplayCoreParts { - uint32_t cpu; /* gpr + pc + hi/lo + status/cause/epc */ + uint32_t cpu; /* gpr + pc + hi/lo + status/cause/epc + GTE */ uint32_t clock_irq; /* cycle count + i_stat/i_mask */ uint32_t timers; /* timer snapshot */ uint32_t ram; /* 2 MiB main RAM */ @@ -45,6 +45,18 @@ uint32_t netplay_aux_digest(void); /* crc(spu, mdec) */ uint32_t netplay_spad_digest(void); /* 1 KiB scratchpad */ uint32_t netplay_dma_digest(void); uint32_t netplay_sio_digest(void); + +/* SIO snapshot section CRCs — which subsystem forked a resim (a single SIO + * digest could not say). Sections mirror sio_snap_emit order. + * netplay_sio_digest / baseline_ext fold only through fsm_pace (not meta). */ +typedef struct NetplaySioParts { + uint32_t regs; /* tx/rx/stat/mode/ctrl/baud */ + uint32_t pads; /* pad_analog/connected/state/response/config/type_req */ + uint32_t mc; /* memcard working vars + both slot FSMs + active_device */ + uint32_t pace; /* shift/ack/irq countdown — guest-visible timing */ + uint32_t meta; /* irq source/slot/delay/byte_seq (host audit only) */ +} NetplaySioParts; +void netplay_sio_digest_parts(NetplaySioParts *out); /* Baseline dig_c: crc(aux, cd, spad, dma, sio). Refuse doomed Replay. */ uint32_t netplay_baseline_ext_digest(void); diff --git a/runtime/src/boot_state.c b/runtime/src/boot_state.c index 9959d3ec..7ffb157f 100644 --- a/runtime/src/boot_state.c +++ b/runtime/src/boot_state.c @@ -90,6 +90,7 @@ typedef struct BsOut { uint8_t* data; /* memory sink (owned by caller / save_buffer) */ size_t len; size_t cap; + int no_zlib; /* 1 => always raw sections (netplay snap ring) */ } BsOut; static int bs_write(BsOut* o, const void* p, size_t n) { @@ -144,10 +145,11 @@ static int write_section_raw(BsOut* o, uint32_t tag, uint32_t flags, } /* Prefer zlib for large blobs (smaller disk + faster load on slow storage). - * Falls back to raw if compressBound/compress fails. */ + * Falls back to raw if compressBound/compress fails. + * o->no_zlib skips compress entirely (in-memory netplay ring). */ static int write_section(BsOut* o, uint32_t tag, const void* data, uint64_t len) { if (!data && len) return 0; - if (len >= BOOT_STATE_ZLIB_MIN && len <= 0xffffffffu) { + if (!o->no_zlib && len >= BOOT_STATE_ZLIB_MIN && len <= 0xffffffffu) { uLong bound = compressBound((uLong)len); uint8_t* packed = (uint8_t*)malloc(4u + (size_t)bound); if (packed) { @@ -266,16 +268,23 @@ static int boot_state_save_to(BsOut* o, const CPUState* cpu, else { gr_vram_transfer_out(0, 0, VRAM_W, VRAM_H, vbuf); /* VRAM is uint16 LE guest pixels — emit as LE u16 stream. - * pst_w_pod is a memcpy on LE hosts (see pst_wire.h). */ - uint8_t* wire = (uint8_t*)malloc(VRAM_SIZE); - if (!wire) ok = 0; - else { - PstW w; - pst_w_init(&w, wire, VRAM_SIZE); - ok = pst_w_pod(&w, vbuf, VRAM_SIZE, 2) && - write_section(o, BS_SEC_VRAM, wire, VRAM_SIZE); - free(wire); + * pst_w_pod is a memcpy on LE hosts (see pst_wire.h); skip the + * extra wire alloc there (netplay ring snaps every few ticks). */ +#if defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) + ok = write_section(o, BS_SEC_VRAM, vbuf, VRAM_SIZE); +#else + { + uint8_t* wire = (uint8_t*)malloc(VRAM_SIZE); + if (!wire) ok = 0; + else { + PstW w; + pst_w_init(&w, wire, VRAM_SIZE); + ok = pst_w_pod(&w, vbuf, VRAM_SIZE, 2) && + write_section(o, BS_SEC_VRAM, wire, VRAM_SIZE); + free(wire); + } } +#endif free(vbuf); } } @@ -318,16 +327,17 @@ int boot_state_save(const CPUState* cpu, uint32_t bios_checksum, return ok; } -int boot_state_save_buffer(const CPUState* cpu, uint32_t bios_checksum, - uint32_t entry_pc, uint8_t** out_data, - size_t* out_len) { +static int boot_state_save_buffer_ex(const CPUState* cpu, uint32_t bios_checksum, + uint32_t entry_pc, uint8_t** out_data, + size_t* out_len, int no_zlib) { BsOut o; if (!out_data || !out_len) return 0; *out_data = NULL; *out_len = 0; memset(&o, 0, sizeof o); - /* Typical MotK .pst is ~1.3–1.5 MiB compressed; start with 2 MiB. */ - o.cap = 2u * 1024u * 1024u; + o.no_zlib = no_zlib ? 1 : 0; + /* Compressed MotK ~1.3–1.5 MiB; raw ~3.5–4 MiB (RAM+VRAM+SPU). */ + o.cap = no_zlib ? (5u * 1024u * 1024u) : (2u * 1024u * 1024u); o.data = (uint8_t*)malloc(o.cap); if (!o.data) return 0; if (!boot_state_save_to(&o, cpu, bios_checksum, entry_pc)) { @@ -339,6 +349,20 @@ int boot_state_save_buffer(const CPUState* cpu, uint32_t bios_checksum, return 1; } +int boot_state_save_buffer(const CPUState* cpu, uint32_t bios_checksum, + uint32_t entry_pc, uint8_t** out_data, + size_t* out_len) { + return boot_state_save_buffer_ex(cpu, bios_checksum, entry_pc, out_data, + out_len, 0); +} + +int boot_state_save_buffer_raw(const CPUState* cpu, uint32_t bios_checksum, + uint32_t entry_pc, uint8_t** out_data, + size_t* out_len) { + return boot_state_save_buffer_ex(cpu, bios_checksum, entry_pc, out_data, + out_len, 1); +} + /* ============================ LOAD ============================ */ static int apply_section(uint32_t tag, const uint8_t* p, uint32_t len, diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index 706c05fe..e5241acc 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -8838,8 +8838,8 @@ static void handle_irqctx_ring(int id, const char *json) typedef struct { uint64_t seq, cycle; uint32_t frame, istat, imask, sr, d44, cdrom_active, is_vblank; int dma_depth; uint32_t take_pc, real_epc, exit_pc, exit_reason, same_thread, - restored, v1_exit, v1_saved, ra_exit, ra_saved, redirects, - entry_sp, pump_site; } E; + restored, v0_exit, v0_saved, v1_exit, v1_saved, ra_exit, + ra_saved, redirects, entry_sp, pump_site; } E; extern E g_irqctx_ring[]; extern uint64_t g_irqctx_seq; /* Ring cap must track IRQCTX_RING_CAP in interrupts.c. */ uint32_t cap = 4096u; @@ -8871,6 +8871,7 @@ static void handle_irqctx_ring(int id, const char *json) "\"sr\":\"0x%08X\",\"istat\":\"0x%08X\",\"imask\":\"0x%08X\"," "\"take_pc\":\"0x%08X\",\"real_epc\":\"0x%08X\",\"exit_pc\":\"0x%08X\"," "\"exit_reason\":%u,\"same_thread\":%u,\"restored\":%u," + "\"v0_exit\":\"0x%08X\",\"v0_saved\":\"0x%08X\"," "\"v1_exit\":\"0x%08X\",\"v1_saved\":\"0x%08X\"," "\"ra_exit\":\"0x%08X\",\"ra_saved\":\"0x%08X\",\"redirects\":%u," "\"entry_sp\":\"0x%08X\",\"pump_site\":%u}", @@ -8878,8 +8879,8 @@ static void handle_irqctx_ring(int id, const char *json) e->frame, e->is_vblank, e->d44, e->cdrom_active, e->dma_depth, e->sr, e->istat, e->imask, e->take_pc, e->real_epc, e->exit_pc, e->exit_reason, e->same_thread, - e->restored, e->v1_exit, e->v1_saved, e->ra_exit, e->ra_saved, - e->redirects, e->entry_sp, e->pump_site); + e->restored, e->v0_exit, e->v0_saved, e->v1_exit, e->v1_saved, + e->ra_exit, e->ra_saved, e->redirects, e->entry_sp, e->pump_site); emitted++; } pos += snprintf(buf + pos, BUF_SZ - pos, "],\"emitted\":%d}", emitted); diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index b78d320c..a9ba7fda 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -2468,6 +2468,29 @@ static uint16_t rgb888_to_rgb555(uint32_t color24) { /* i_stat extern declared earlier in this file (above gpu_read_gpustat). */ +/* Netplay: present/finish_frame deferred from mid-psx_cyc_step to the next + * psx_check_interrupts BB edge. MotK menu wait-loop (0x8006CDA0) was digesting + * peers at different instr points (post-lw v0 vs post-slt v0=1) → cpu+ram fork + * on idle sealed resim. Guest VBlank raise / LCF stay immediate. */ +static int s_present_pending; +static int s_flushing_present; + +void gpu_vblank_clear_deferred_present(void) { + s_present_pending = 0; +} + +void gpu_vblank_flush_present(void) { + if (s_flushing_present || s_present_pending <= 0) + return; + s_flushing_present = 1; + while (s_present_pending > 0) { + s_present_pending--; + if (vblank_callback) + vblank_callback(); + } + s_flushing_present = 0; +} + void gpu_vblank_tick(void) { lcf ^= 1; /* GPUSTAT.13 (interlace FIELD): on real hardware this alternates per @@ -2495,7 +2518,18 @@ void gpu_vblank_tick(void) { * host presentation, pacing, turbo, or skipped frames. */ mod_runtime_on_vblank(); psx_irq_raise(0, 0); /* IRQ_VBLANK (gpu_vblank_tick) */ - if (vblank_callback) vblank_callback(); + if (!vblank_callback) + return; + { + extern int psx_netplay_active(void); + if (psx_netplay_active()) { + /* Cap: a stuck defer must not queue unbounded presents. */ + if (s_present_pending < 8) + s_present_pending++; + } else { + vblank_callback(); + } + } } const uint16_t* gpu_get_vram(void) { diff --git a/runtime/src/interrupts.c b/runtime/src/interrupts.c index b1c3943d..4ca61db7 100644 --- a/runtime/src/interrupts.c +++ b/runtime/src/interrupts.c @@ -64,6 +64,8 @@ typedef struct { uint32_t exit_reason; /* g_exc_escape_reason at exit */ uint32_t same_thread; /* same_thread_resume discriminator result */ uint32_t restored; /* saved_gpr restore fired */ + uint32_t v0_exit; /* cpu->gpr[2] at exit before restore decision */ + uint32_t v0_saved; /* saved_gpr[2] (interrupted code's v0) */ uint32_t v1_exit; /* cpu->gpr[3] at exit before restore decision */ uint32_t v1_saved; /* saved_gpr[3] (interrupted code's v1) */ uint32_t ra_exit; /* cpu->gpr[31] at exit before restore decision */ @@ -631,6 +633,36 @@ void interrupts_resync_after_restore(void) { s_defer_switch_pending = 0; s_defer_switch_target = 0; s_defer_switch_from = 0; + /* Drop mid-quantum present armed before the rewind — do not finish_frame + * against the restored tip with a stale pending count. */ + gpu_vblank_clear_deferred_present(); +} + +void interrupts_log_last_vblank_irqctx(const char *tag) +{ + uint64_t i; + if (g_irqctx_seq == 0) + return; + /* Walk newest→oldest for the last completed VBLANK delivery. */ + for (i = 0; i < g_irqctx_seq && i < IRQCTX_RING_CAP; i++) { + uint64_t idx = g_irqctx_seq - 1u - i; + const IrqCtxEntry *e = &g_irqctx_ring[idx & (IRQCTX_RING_CAP - 1u)]; + if (!e->is_vblank) + continue; + fprintf(stderr, + "psxrecomp: rb irqctx %s vb seq=%llu cyc=%llu restored=%u " + "same_thr=%u reason=%u exit_pc=%08x epc=%08x " + "v0_exit=%08x v0_saved=%08x v1_exit=%08x v1_saved=%08x\n", + tag ? tag : "?", + (unsigned long long)e->seq, (unsigned long long)e->cycle, + (unsigned)e->restored, (unsigned)e->same_thread, + (unsigned)e->exit_reason, (unsigned)e->exit_pc, + (unsigned)e->real_epc, (unsigned)e->v0_exit, + (unsigned)e->v0_saved, (unsigned)e->v1_exit, + (unsigned)e->v1_saved); + fflush(stderr); + return; + } } /* @@ -773,6 +805,9 @@ int psx_interrupt_delivery_needed(const CPUState* cpu) { void psx_check_interrupts(CPUState* cpu) { psx_cyc_batch_flush(); + /* Netplay: run deferred sdl_vblank_present here (BB edge), not from + * mid-block fire_vblank_edge → gpu_vblank_tick. Offline presents immediately. */ + gpu_vblank_flush_present(); extern int g_ls_suppress_record; #define PSX_CHECK_INTERRUPTS_RETURN() do { if (g_ls_suppress_record > 0) g_ls_suppress_record--; return; } while (0) #ifdef PSX_COSIM @@ -1465,18 +1500,26 @@ irq_deliver_eval: * the fix for Tomba's pause menu): * PSX_SAME_THREAD_RESTORE=0 original Fix B (legacy-sentinel-only restore) * PSX_SAME_THREAD_RESTORE=1 PC-equality heuristic (13c5e0c behavior) - * PSX_SAME_THREAD_RESTORE=2 kernel current-TCB equality (default; - * structural same-thread test — a genuine - * ChangeThread moves PCB[0] even when the new - * thread resumes at the SAME guest PC) */ - static int s_str_mode = -1; - if (s_str_mode < 0) { + * PSX_SAME_THREAD_RESTORE=2 kernel current-TCB equality (+ PC match) + * Offline default remains mode 1. Netplay (env unset) uses mode 3: + * same-TCB RFE/SYSCALL always restores — MotK menu Replay forked only + * v0 when one peer's PC heuristic missed and left BIOS v0=1 while the + * other restored the wait-loop load (0x5bd2) with matched RAM/cycles. */ + static int s_str_mode_env = -2; /* -2 unset; -1 = auto */ + int s_str_mode; + if (s_str_mode_env == -2) { const char *e = getenv("PSX_SAME_THREAD_RESTORE"); - s_str_mode = (e && *e) ? atoi(e) : 1; /* default = 13c5e0c behavior; - mode 2 (TCB check) is the hardening candidate, pending a Tomba - pause-menu gate. The MMX6 "not found" this selector was built to - verify turned out to be a STALE GENERATED IMAGE artifact. */ - if (s_str_mode < 0 || s_str_mode > 2) s_str_mode = 1; + s_str_mode_env = (e && *e) ? atoi(e) : -1; + if (s_str_mode_env < -1 || s_str_mode_env > 3) s_str_mode_env = -1; + } + { + extern int psx_netplay_active(void); + if (s_str_mode_env >= 0) + s_str_mode = s_str_mode_env; + else if (psx_netplay_active()) + s_str_mode = 3; /* netplay auto: TCB-stable always restore */ + else + s_str_mode = 1; } extern uint32_t psx_read_word(uint32_t addr); /* memory.c (plain RAM read) */ uint32_t exit_pcb = psx_read_word(0x108u); @@ -1493,6 +1536,14 @@ irq_deliver_eval: g_exception_real_epc != 0u && same_guest_pc(cpu->pc, g_exception_real_epc) && entry_tcb != 0u && entry_tcb == exit_tcb; + } else if (s_str_mode == 3) { + /* Netplay: PCB[0] unmoved ⇒ same thread. Ignore exit-PC noise that + * made mode-1 restore asymmetrically across peers (v0-only MotK + * Replay forks). Genuine ChangeThread still skips (TCB moved). */ + same_thread_resume = + (g_exc_escape_reason == PSX_EXC_ESCAPE_RFE_RETURN || + g_exc_escape_reason == PSX_EXC_ESCAPE_SYSCALL_RETURN) && + entry_tcb != 0u && entry_tcb == exit_tcb; } /* Same-thread completion of a SENTINEL (legacy) delivery via an explicit * guest RFE / ReturnFromException. The guest TCB never holds the host @@ -1524,6 +1575,8 @@ irq_deliver_eval: e->exit_reason = (uint32_t)g_exc_escape_reason; e->same_thread = (uint32_t)same_thread_resume; e->restored = (uint32_t)do_restore; + e->v0_exit = cpu->gpr[2]; + e->v0_saved = saved_gpr[2]; e->v1_exit = cpu->gpr[3]; e->v1_saved = saved_gpr[3]; e->ra_exit = cpu->gpr[31]; diff --git a/runtime/src/netplay_snap_ring.c b/runtime/src/netplay_snap_ring.c index cbf69e26..3716555d 100644 --- a/runtime/src/netplay_snap_ring.c +++ b/runtime/src/netplay_snap_ring.c @@ -161,7 +161,9 @@ int netplay_snap_ring_save(NetplaySnapRing* r, uint32_t tick, uint8_t* data = NULL; size_t len = 0; if (!r || !cpu) return 0; - if (!boot_state_save_buffer(cpu, bios_checksum, entry_pc, &data, &len)) + /* Raw (no zlib): compress2 on RAM+VRAM+SPU dominated live FPS. Load + * accepts uncompressed v4 sections. */ + if (!boot_state_save_buffer_raw(cpu, bios_checksum, entry_pc, &data, &len)) return 0; if (!netplay_snap_ring_store(r, tick, data, len)) return 0; /* store frees data on hard failure */ diff --git a/runtime/src/netplay_state_digest.c b/runtime/src/netplay_state_digest.c index d1371d4a..554ceeb8 100644 --- a/runtime/src/netplay_state_digest.c +++ b/runtime/src/netplay_state_digest.c @@ -27,6 +27,7 @@ extern uint32_t dma_snapshot_bytes(void); extern void dma_snapshot_write(uint8_t *p); extern uint32_t sio_snapshot_bytes(void); extern void sio_snapshot_write(uint8_t *p); +extern void sio_snapshot_section_ends(uint32_t out[5]); #define NP_SPAD_SIZE 1024u @@ -117,6 +118,8 @@ void netplay_core_digest_parts(const CPUState* cpu, NetplayCoreParts* out) if (!cpu) return; + /* GTE is in BS_SEC_CPU but was omitted here — MFC2 can fork GPRs with + * matched RAM/clk (menu resim). Keep snap + digest in lockstep. */ crc_cpu = crc32_update(crc_cpu, (const uint8_t*)cpu->gpr, sizeof(cpu->gpr)); crc_cpu = crc32_update(crc_cpu, (const uint8_t*)&cpu->pc, sizeof(cpu->pc)); crc_cpu = crc32_update(crc_cpu, (const uint8_t*)&cpu->hi, sizeof(cpu->hi)); @@ -124,6 +127,8 @@ void netplay_core_digest_parts(const CPUState* cpu, NetplayCoreParts* out) crc_cpu = crc32_update(crc_cpu, (const uint8_t*)&cpu->cop0[12], sizeof(uint32_t)); crc_cpu = crc32_update(crc_cpu, (const uint8_t*)&cpu->cop0[13], sizeof(uint32_t)); crc_cpu = crc32_update(crc_cpu, (const uint8_t*)&cpu->cop0[14], sizeof(uint32_t)); + crc_cpu = crc32_update(crc_cpu, (const uint8_t*)cpu->gte_data, sizeof(cpu->gte_data)); + crc_cpu = crc32_update(crc_cpu, (const uint8_t*)cpu->gte_ctrl, sizeof(cpu->gte_ctrl)); crc = crc32_update(crc, (const uint8_t*)cpu->gpr, sizeof(cpu->gpr)); crc = crc32_update(crc, (const uint8_t*)&cpu->pc, sizeof(cpu->pc)); crc = crc32_update(crc, (const uint8_t*)&cpu->hi, sizeof(cpu->hi)); @@ -131,6 +136,8 @@ void netplay_core_digest_parts(const CPUState* cpu, NetplayCoreParts* out) crc = crc32_update(crc, (const uint8_t*)&cpu->cop0[12], sizeof(uint32_t)); crc = crc32_update(crc, (const uint8_t*)&cpu->cop0[13], sizeof(uint32_t)); crc = crc32_update(crc, (const uint8_t*)&cpu->cop0[14], sizeof(uint32_t)); + crc = crc32_update(crc, (const uint8_t*)cpu->gte_data, sizeof(cpu->gte_data)); + crc = crc32_update(crc, (const uint8_t*)cpu->gte_ctrl, sizeof(cpu->gte_ctrl)); { uint64_t cyc = psx_cycle_count; @@ -227,7 +234,68 @@ uint32_t netplay_dma_digest(void) uint32_t netplay_sio_digest(void) { - return digest_module(sio_snapshot_bytes, sio_snapshot_write); + /* Fold only through fsm_pace — exclude host-audit meta (byte_seq tracks + * unreseeded sio_trace_seq and was false-forking MotK Replay digests). */ + static uint8_t *buf; + static uint32_t cap; + uint32_t n = sio_snapshot_bytes(); + uint32_t ends[5]; + uint32_t crc; + + if (!n) return 0; + if (n > cap) { + uint8_t *nb = (uint8_t *)realloc(buf, n); + if (!nb) return 0; + buf = nb; + cap = n; + } + sio_snapshot_write(buf); + sio_snapshot_section_ends(ends); + if (ends[4] != n || ends[3] > ends[4]) + return digest_module(sio_snapshot_bytes, sio_snapshot_write); + crc = 0xFFFFFFFFu; + crc = crc32_update(crc, buf, ends[3]); + return crc ^ 0xFFFFFFFFu; +} + +void netplay_sio_digest_parts(NetplaySioParts *out) +{ + static uint8_t *buf; + static uint32_t cap; + uint32_t n = sio_snapshot_bytes(); + uint32_t ends[5]; + uint32_t crc; + + if (!out) return; + memset(out, 0, sizeof(*out)); + if (!n) return; + if (n > cap) { + uint8_t *nb = (uint8_t *)realloc(buf, n); + if (!nb) return; + buf = nb; + cap = n; + } + sio_snapshot_write(buf); + sio_snapshot_section_ends(ends); + if (ends[4] != n || ends[0] > ends[1] || ends[1] > ends[2] || + ends[2] > ends[3] || ends[3] > ends[4]) + return; /* layout drifted — leave parts zero rather than lie */ + + crc = 0xFFFFFFFFu; + crc = crc32_update(crc, buf, ends[0]); + out->regs = crc ^ 0xFFFFFFFFu; + crc = 0xFFFFFFFFu; + crc = crc32_update(crc, buf + ends[0], ends[1] - ends[0]); + out->pads = crc ^ 0xFFFFFFFFu; + crc = 0xFFFFFFFFu; + crc = crc32_update(crc, buf + ends[1], ends[2] - ends[1]); + out->mc = crc ^ 0xFFFFFFFFu; + crc = 0xFFFFFFFFu; + crc = crc32_update(crc, buf + ends[2], ends[3] - ends[2]); + out->pace = crc ^ 0xFFFFFFFFu; + crc = 0xFFFFFFFFu; + crc = crc32_update(crc, buf + ends[3], ends[4] - ends[3]); + out->meta = crc ^ 0xFFFFFFFFu; } uint32_t netplay_baseline_ext_digest(void) diff --git a/runtime/src/psx_netplay.c b/runtime/src/psx_netplay.c index 9a8708d8..51e69954 100644 --- a/runtime/src/psx_netplay.c +++ b/runtime/src/psx_netplay.c @@ -2084,6 +2084,44 @@ int psx_netplay_start(const PsxNetplayConfig *cfg) np_rb_bind_and_start(); printf("psxrecomp: netplay mode=rollback (D=%d P=%d invent+contract)\n", g_np.input_delay, g_np.input_prediction); + /* Peers MUST share one binary — mixed build-release vs packaged + * motk-* left matched digests for hundreds of ticks then GPR/tim + * forks in Replay, with pin zlib ~1.34M vs ~1.13M. */ + { + char exe[512]; + long long sz = -1; +#if defined(_WIN32) + DWORD n = GetModuleFileNameA(NULL, exe, (DWORD)sizeof(exe)); + if (n == 0 || n >= sizeof(exe)) + snprintf(exe, sizeof(exe), "(unknown)"); + { + WIN32_FILE_ATTRIBUTE_DATA fad; + if (GetFileAttributesExA(exe, GetFileExInfoStandard, &fad)) { + ULARGE_INTEGER u; + u.HighPart = fad.nFileSizeHigh; + u.LowPart = fad.nFileSizeLow; + sz = (long long)u.QuadPart; + } + } +#else + ssize_t n = readlink("/proc/self/exe", exe, sizeof(exe) - 1); + if (n < 0) { + snprintf(exe, sizeof(exe), "(unknown)"); + } else { + exe[n] = '\0'; + { + struct stat st; + if (stat(exe, &st) == 0) + sz = (long long)st.st_size; + } + } +#endif + fprintf(stderr, + "psxrecomp: rb binary path=%s size=%lld " + "(peers must match bit-identical)\n", + exe, sz); + fflush(stderr); + } fflush(stdout); } if (g_np.slot_count >= 3) @@ -2627,6 +2665,21 @@ void psx_netplay_pump(void) { if (!psx_netplay_active()) return; + /* Mid-guest cycle-watchdog pump during Replay must not run reconcile / + * rb_pump (seal/baseline apply, hist promote) — that is host-asymmetric + * work and was a candidate for SIO fsm forks with matched guest cycles. + * Still drain transport + FRAME_COMMIT so mid-resim core aborts stay live. + * Full pump resumes at admit / present edges. */ + if (g_np.rollback && psx_netplay_rb_is_resimulating()) { +#if defined(PSX_HAS_LOBBY_CLIENT) + if (g_np.use_ice || psx_lobby_connected()) + psx_lobby_pump(); +#endif + drain_lobby_signals(); + rnet_session_pump(g_np.session); + np_drain_peer_frame_commits(); + return; + } np_pump_session(); psx_netplay_diag_tick(); } diff --git a/runtime/src/psx_netplay_rb.c b/runtime/src/psx_netplay_rb.c index b4e928cc..9f74dd8b 100644 --- a/runtime/src/psx_netplay_rb.c +++ b/runtime/src/psx_netplay_rb.c @@ -88,12 +88,14 @@ static uint32_t g_pending_resume_pc; * until restore (peer BASELINE can arrive while we are still sealing). */ static int g_episode_snap_applied; -/* Live-path snap every N ticks (full boot_state ~1.3MB). Resim still snaps - * every tick so episode baselines stay dense. Override: PSX_NET_SNAP_INTERVAL. */ +/* Live-path snap every N ticks (raw boot_state ~3.5MB, no zlib). Resim still + * snaps every tick so episode baselines stay dense. + * Override: PSX_NET_SNAP_INTERVAL (default 8 — was 4; zlib removal + wider + * interval are the main FPS wins). */ static uint32_t snap_interval(void) { static int latched; - static uint32_t iv = 4u; + static uint32_t iv = 8u; if (!latched) { const char *e = getenv("PSX_NET_SNAP_INTERVAL"); if (e && e[0]) { @@ -177,14 +179,18 @@ static uint32_t g_last_begin_mismatch = 0xffffffffu; #define RB_BASELINE_BURST_MS 40u /* Initiator waits for follower ready-ACK; do NOT solo-enter Replay. */ #define RB_READY_TIMEOUT_MS 4000u -/* MotK title/menu: invent edges thrash every 1–3 ticks without this. */ +/* Clean commit: short promote-only window (invent edges thrash without this). */ #define RB_REWIND_COOLDOWN_TICKS 12u +/* Failed episode (resim/baseline/post): calm from *live* sim before realign. */ +#define RB_ABORT_COOLDOWN_TICKS 60u +/* Repeated abort streak — longer promote-only (char-select storm). */ +#define RB_STORM_COOLDOWN_TICKS 120u /* Keep deferring a few frames after MDEC goes quiet (cutover / credits). */ #define RB_FMV_MDEC_HYSTERESIS 8u /* After leaving FMV: stay lockstep (no invent / no rewind) so cutover heals. */ #define RB_FMV_SETTLE_TICKS 90u /* Baseline mismatch with no agreed tip — don't reopen every few ticks. */ -#define RB_BASELINE_MISMATCH_COOLDOWN_TICKS 60u +#define RB_BASELINE_MISMATCH_COOLDOWN_TICKS 90u static int g_was_in_fmv; static uint32_t g_fmv_settle_until; @@ -332,12 +338,15 @@ void psx_netplay_rb_cpu_for_present_digest(struct CPUState *out, out->pc = 0; } -/* Split dig_cpu so logs tell PC-only forks from true GPR/COP0 forks. */ +/* Split dig_cpu so logs tell PC-only forks from true GPR/COP0/GTE forks. */ static void log_cpu_digest_split(const CPUState *raw, const char *tag) { uint32_t gpr = 0xFFFFFFFFu; uint32_t hil = 0xFFFFFFFFu; uint32_t c0 = 0xFFFFFFFFu; + uint32_t gte = 0xFFFFFFFFu; + int dump_regs; + int i; if (!raw) return; gpr = crc32_update(gpr, (const uint8_t *)raw->gpr, sizeof(raw->gpr)); @@ -346,12 +355,25 @@ static void log_cpu_digest_split(const CPUState *raw, const char *tag) c0 = crc32_update(c0, (const uint8_t *)&raw->cop0[12], sizeof(uint32_t)); c0 = crc32_update(c0, (const uint8_t *)&raw->cop0[13], sizeof(uint32_t)); c0 = crc32_update(c0, (const uint8_t *)&raw->cop0[14], sizeof(uint32_t)); + gte = crc32_update(gte, (const uint8_t *)raw->gte_data, sizeof(raw->gte_data)); + gte = crc32_update(gte, (const uint8_t *)raw->gte_ctrl, sizeof(raw->gte_ctrl)); fprintf(stderr, "psxrecomp: rb cpu-split %s raw_pc=%08x sticky=%08x gpr=%08x " - "hi_lo=%08x cop0=%08x\n", + "hi_lo=%08x cop0=%08x gte=%08x\n", tag ? tag : "?", (unsigned)raw->pc, (unsigned)g_last_good_bb_pc, (unsigned)(gpr ^ 0xFFFFFFFFu), (unsigned)(hil ^ 0xFFFFFFFFu), - (unsigned)(c0 ^ 0xFFFFFFFFu)); + (unsigned)(c0 ^ 0xFFFFFFFFu), (unsigned)(gte ^ 0xFFFFFFFFu)); + /* Full GPR words on fin/abort — peers can diff which regs forked. */ + dump_regs = tag && (strcmp(tag, "fin") == 0 || strcmp(tag, "abort") == 0 || + strcmp(tag, "post") == 0); + if (dump_regs) { + fprintf(stderr, "psxrecomp: rb gpr-dump %s", tag); + for (i = 0; i < 32; i++) + fprintf(stderr, " r%02d=%08x", i, (unsigned)raw->gpr[i]); + fprintf(stderr, "\n"); + /* Last VBLANK irq restore — peers diff restored/v0 on r2-only forks. */ + interrupts_log_last_vblank_irqctx(tag); + } fflush(stderr); } @@ -459,8 +481,17 @@ static void abort_episode(const char *why) /* After a failed episode: rewind Live to a tip both peers already agreed on. */ static void abort_episode_realign(const char *why) { + RNetSession *s = sess(); + /* Capture LIVE sim BEFORE schedule_live_realign rewinds the clock. + * Arming cooldown from the rewound tip let uncapped catch-up burn a + * 12-tick window in ms → char-select episode storms (stale until < live). */ + uint32_t live_sim = s ? rnet_session_sim_tick(s) : 0u; uint32_t tick = 0; int have = 0; + uint32_t cool_n; + char cool_why[160]; + int hard_fail; + /* Prefer the frozen pre-resim baseline — ring load_tick was often overwritten. */ if (g_episode_baseline_matched && g_pin_valid) { tick = g_pin_tick; @@ -477,20 +508,30 @@ static void abort_episode_realign(const char *why) abort_episode(why); if (have) schedule_live_realign(tick, why); - { - RNetSession *s = sess(); - uint32_t sim = s ? rnet_session_sim_tick(s) : (have ? tick + 1u : 0u); - /* Baseline mismatch before any commit: no tip to realign — long cooldown - * so cutover doesn't reopen doomed episodes every few ticks. */ - if (!have) { - g_bl_mismatch_streak++; - arm_rewind_cooldown_ticks(sim, RB_BASELINE_MISMATCH_COOLDOWN_TICKS, - why ? why : "desync"); - } else { - g_bl_mismatch_streak = 0; - arm_rewind_cooldown(sim, why ? why : "realign"); - } - } + + hard_fail = !have || + (why && (strstr(why, "baseline") != NULL || + strstr(why, "resim core") != NULL || + strstr(why, "post ") != NULL || + strstr(why, "desync") != NULL)); + if (hard_fail) + g_bl_mismatch_streak++; + else + g_bl_mismatch_streak = 0; + + if (!have) + cool_n = RB_BASELINE_MISMATCH_COOLDOWN_TICKS; + else if (g_bl_mismatch_streak >= 2u) + cool_n = RB_STORM_COOLDOWN_TICKS; + else if (hard_fail) + cool_n = RB_ABORT_COOLDOWN_TICKS; + else + cool_n = RB_ABORT_COOLDOWN_TICKS; + + snprintf(cool_why, sizeof(cool_why), "%s (live=%u streak=%u)", + why ? why : "realign", (unsigned)live_sim, + (unsigned)g_bl_mismatch_streak); + arm_rewind_cooldown_ticks(live_sim, cool_n, cool_why); } static int host_save_state(void *ctx, uint32_t tick) @@ -867,21 +908,28 @@ static void maybe_send_baseline(void) g_baseline_handshake_ms = rb_mono_ms(); g_last_baseline_burst_ms = 0; /* allow immediate burst */ send_baseline_burst(0, RB_BASELINE_BURST, 0); - fprintf(stderr, - "psxrecomp: rb baseline sent load=%u core=%08x av=%08x ext=%08x cd=%08x " - "(burst=%d) parts cpu=%08x clk=%08x tim=%08x ram=%08x dirty=%08x " - "spu=%08x mdec=%08x aux=%08x spad=%08x dma=%08x sio=%08x\n", - (unsigned)rnet_rb_get_load_tick(g_rb), - (unsigned)g_local_baseline_digest, - (unsigned)g_local_baseline_av, (unsigned)g_local_baseline_aux, - (unsigned)netplay_cdrom_digest(), RB_BASELINE_BURST, - (unsigned)parts.cpu, (unsigned)parts.clock_irq, - (unsigned)parts.timers, (unsigned)parts.ram, - (unsigned)parts.dirty, (unsigned)netplay_spu_digest(), - (unsigned)netplay_mdec_digest(), - (unsigned)netplay_aux_digest(), - (unsigned)netplay_spad_digest(), (unsigned)netplay_dma_digest(), - (unsigned)netplay_sio_digest()); + { + NetplaySioParts sp; + netplay_sio_digest_parts(&sp); + fprintf(stderr, + "psxrecomp: rb baseline sent load=%u core=%08x av=%08x ext=%08x cd=%08x " + "(burst=%d) parts cpu=%08x clk=%08x tim=%08x ram=%08x dirty=%08x " + "spu=%08x mdec=%08x aux=%08x spad=%08x dma=%08x sio=%08x " + "sioP=%08x/%08x/%08x/%08x/%08x\n", + (unsigned)rnet_rb_get_load_tick(g_rb), + (unsigned)g_local_baseline_digest, + (unsigned)g_local_baseline_av, (unsigned)g_local_baseline_aux, + (unsigned)netplay_cdrom_digest(), RB_BASELINE_BURST, + (unsigned)parts.cpu, (unsigned)parts.clock_irq, + (unsigned)parts.timers, (unsigned)parts.ram, + (unsigned)parts.dirty, (unsigned)netplay_spu_digest(), + (unsigned)netplay_mdec_digest(), + (unsigned)netplay_aux_digest(), + (unsigned)netplay_spad_digest(), (unsigned)netplay_dma_digest(), + (unsigned)netplay_sio_digest(), + (unsigned)sp.regs, (unsigned)sp.pads, (unsigned)sp.mc, + (unsigned)sp.pace, (unsigned)sp.meta); + } fflush(stderr); return; } @@ -914,8 +962,8 @@ static void log_resim_tick_audit(uint32_t sim, const char *tag) int n = g_b.slot_count ? *g_b.slot_count : 0; uint32_t dig = 0u; NetplayCoreParts parts; - uint32_t av = netplay_av_digest(); - uint32_t cd = netplay_cdrom_digest(); + uint32_t av = 0u; + uint32_t cd = 0u; uint32_t aux = 0u; uint32_t spad = 0u; uint32_t dma = 0u; @@ -937,7 +985,10 @@ static void log_resim_tick_audit(uint32_t sim, const char *tag) } } if (want_parts) { + /* fin only: full bus digests (av = full VRAM CRC — skip on arm). */ netplay_core_digest_parts(&dig_cpu, &parts); + av = netplay_av_digest(); + cd = netplay_cdrom_digest(); aux = netplay_aux_digest(); spad = netplay_spad_digest(); dma = netplay_dma_digest(); @@ -945,7 +996,7 @@ static void log_resim_tick_audit(uint32_t sim, const char *tag) dig = netplay_master_digest(&dig_cpu); } else { parts.core = netplay_core_digest(&dig_cpu); - dig = netplay_master_digest(&dig_cpu); + dig = parts.core; /* arm: skip master/cd/av — diag only */ } } fprintf(stderr, @@ -961,13 +1012,17 @@ static void log_resim_tick_audit(uint32_t sim, const char *tag) fprintf(stderr, " s%d=----", slot); } if (want_parts) { + NetplaySioParts sp; + netplay_sio_digest_parts(&sp); fprintf(stderr, " | cpu=%08x clk=%08x tim=%08x ram=%08x dirty=%08x aux=%08x " - "spad=%08x dma=%08x sio=%08x", + "spad=%08x dma=%08x sio=%08x sioP=%08x/%08x/%08x/%08x/%08x", (unsigned)parts.cpu, (unsigned)parts.clock_irq, (unsigned)parts.timers, (unsigned)parts.ram, (unsigned)parts.dirty, (unsigned)aux, (unsigned)spad, - (unsigned)dma, (unsigned)sio); + (unsigned)dma, (unsigned)sio, + (unsigned)sp.regs, (unsigned)sp.pads, (unsigned)sp.mc, + (unsigned)sp.pace, (unsigned)sp.meta); } fprintf(stderr, "\n"); fflush(stderr); @@ -1494,19 +1549,18 @@ int psx_netplay_rb_begin_rewind(uint32_t mismatch_tick, int slot) } return 0; } - /* No shared tip yet and baselines keep mismatching — wait out cooldown. */ - if (!g_agreed_valid && g_bl_mismatch_streak >= 2u && - sim < g_rewind_cooldown_until) { - return 0; - } - /* Coalesce: refuse reopen storms after commit/realign; reconcile promotes. */ + /* Coalesce: refuse reopen after commit/abort; reconcile promotes wire. + * until is armed from live sim at failure (not rewound tip) so catch-up + * cannot burn a short window before the next d-pad edge. */ if (sim < g_rewind_cooldown_until) { static uint32_t s_cd_log_sim; if (s_cd_log_sim != sim) { fprintf(stderr, - "psxrecomp: rb begin COOLDOWN mismatch=%u sim=%u until=%u\n", + "psxrecomp: rb begin COOLDOWN mismatch=%u sim=%u until=%u " + "streak=%u\n", (unsigned)mismatch_tick, (unsigned)sim, - (unsigned)g_rewind_cooldown_until); + (unsigned)g_rewind_cooldown_until, + (unsigned)g_bl_mismatch_streak); fflush(stderr); s_cd_log_sim = sim; } @@ -1840,18 +1894,22 @@ int psx_netplay_rb_abort_resim_core_mismatch(uint32_t tick, uint32_t local_core, c = cpu(); if (c) { NetplayCoreParts parts; + NetplaySioParts sp; CPUState dig_cpu; log_cpu_digest_split(c, "abort"); psx_netplay_rb_cpu_for_present_digest(&dig_cpu, c); netplay_core_digest_parts(&dig_cpu, &parts); + netplay_sio_digest_parts(&sp); fprintf(stderr, "psxrecomp: rb abort-parts sim=%u cpu=%08x clk=%08x tim=%08x " - "ram=%08x dirty=%08x spad=%08x dma=%08x sio=%08x aux=%08x " - "cd=%08x av=%08x\n", + "ram=%08x dirty=%08x spad=%08x dma=%08x sio=%08x " + "sioP=%08x/%08x/%08x/%08x/%08x aux=%08x cd=%08x av=%08x\n", (unsigned)tick, (unsigned)parts.cpu, (unsigned)parts.clock_irq, (unsigned)parts.timers, (unsigned)parts.ram, (unsigned)parts.dirty, (unsigned)netplay_spad_digest(), (unsigned)netplay_dma_digest(), (unsigned)netplay_sio_digest(), + (unsigned)sp.regs, (unsigned)sp.pads, (unsigned)sp.mc, + (unsigned)sp.pace, (unsigned)sp.meta, (unsigned)netplay_aux_digest(), (unsigned)netplay_cdrom_digest(), (unsigned)netplay_av_digest()); fflush(stderr); diff --git a/runtime/src/sio.c b/runtime/src/sio.c index 1e57e8a6..c738e487 100644 --- a/runtime/src/sio.c +++ b/runtime/src/sio.c @@ -2275,19 +2275,15 @@ static uint64_t s_sio_advance_with_work = 0; uint64_t sio_get_advance_called(void) { return s_sio_advance_called; } uint64_t sio_get_advance_with_work(void) { return s_sio_advance_with_work; } -void sio_advance(uint32_t cycles) { - if (cycles == 0) return; - s_sio_advance_called++; - if (!g_sio_timing_active) return; - s_sio_advance_with_work++; - - int remaining = (int)cycles; - int transitions = 0; - const int MAX_TRANSITIONS = 8; - while (transitions < MAX_TRANSITIONS && - (remaining > 0 || - (sio_shift_active && sio_shift_remaining <= 0) || - (sio_pending_ack && sio_ack_remaining <= 0))) { +/* Walk shift/ack deadlines for `cycles`. Never drop leftover time: a fixed + * transition cap used to exit with remaining>0, so peers that batched + * advances differently left divergent sio_shift_remaining / ack_remaining + * after the same guest cycle total (MotK menu Replay fsm-only fork). */ +static void sio_pace_walk(int cycles) { + int remaining = cycles; + while (remaining > 0 || + (sio_shift_active && sio_shift_remaining <= 0) || + (sio_pending_ack && sio_ack_remaining <= 0)) { int dt = remaining; int next_event = -1; if (sio_shift_active && sio_shift_remaining > 0 @@ -2311,16 +2307,22 @@ void sio_advance(uint32_t cycles) { remaining -= dt; if (next_event == 0) { sio_handle_shift_complete(); - transitions++; } else if (next_event == 1) { sio_pending_ack = 0; sio_fire_ack_irq(); - transitions++; } else { break; } } } + +void sio_advance(uint32_t cycles) { + if (cycles == 0) return; + s_sio_advance_called++; + if (!g_sio_timing_active) return; + s_sio_advance_with_work++; + sio_pace_walk((int)cycles); +} #else /* Macro=0: stubs for ABI symmetry. */ void sio_advance(uint32_t cycles) { (void)cycles; } @@ -2330,52 +2332,12 @@ uint64_t sio_get_advance_with_work(void) { return 0; } void sio_tick(int cycles) { #if SIO_MODEL_CYCLE_PACED - /* Cycle-paced advance. In 1.0c-v2 sio_shift_active and - * sio_pending_ack are never set (TX path still synchronous), so this - * loop finds no events and returns immediately. Plus, the dispatch - * caller is gated by g_sio_timing_active so this function isn't even - * called when nothing is pending. */ - if (cycles > 0) { - int remaining = cycles; - int transitions = 0; - const int MAX_TRANSITIONS = 8; - while (transitions < MAX_TRANSITIONS && - (remaining > 0 || - (sio_shift_active && sio_shift_remaining <= 0) || - (sio_pending_ack && sio_ack_remaining <= 0))) { - int dt = remaining; - int next_event = -1; - if (sio_shift_active && sio_shift_remaining > 0 - && sio_shift_remaining <= dt) { - dt = sio_shift_remaining; - next_event = 0; - } - if (sio_pending_ack && sio_ack_remaining > 0 - && (sio_ack_remaining < dt || - (sio_ack_remaining == dt && next_event < 0))) { - dt = sio_ack_remaining; - next_event = 1; - } - if (sio_shift_active && sio_shift_remaining <= 0) { - dt = 0; next_event = 0; - } else if (sio_pending_ack && sio_ack_remaining <= 0) { - dt = 0; next_event = 1; - } - if (sio_shift_active) sio_shift_remaining -= dt; - if (sio_pending_ack) sio_ack_remaining -= dt; - remaining -= dt; - if (next_event == 0) { - sio_handle_shift_complete(); - transitions++; - } else if (next_event == 1) { - sio_pending_ack = 0; - sio_fire_ack_irq(); - transitions++; - } else { - break; - } - } - } + /* Access-paced callers pass 0 (MMIO) — still flush overdue shift/ack. + * Quantum / tests pass a positive cycle budget. */ + if (cycles > 0 || + (sio_shift_active && sio_shift_remaining <= 0) || + (sio_pending_ack && sio_ack_remaining <= 0)) + sio_pace_walk(cycles > 0 ? cycles : 0); #else (void)cycles; #endif @@ -2474,11 +2436,16 @@ static int sio_r_mcslot(PstR *r, McSlotState *s) { return 1; } -static int sio_snap_emit(PstW *w) { - if (!pst_w_u8(w, sio_tx_data) || !pst_w_u8(w, sio_rx_data) || - !pst_w_u16(w, sio_stat) || !pst_w_u16(w, sio_mode) || - !pst_w_u16(w, sio_ctrl) || !pst_w_u16(w, sio_baud)) - return 0; +/* Snapshot wire is emitted in four sections so netplay diags can CRC each + * independently (regs / pads / memcard / shift+irq FSM) — a single SIO digest + * could not say WHICH subsystem forked a resim. Order and bytes unchanged. */ +static int sio_snap_emit_regs(PstW *w) { + return pst_w_u8(w, sio_tx_data) && pst_w_u8(w, sio_rx_data) && + pst_w_u16(w, sio_stat) && pst_w_u16(w, sio_mode) && + pst_w_u16(w, sio_ctrl) && pst_w_u16(w, sio_baud); +} + +static int sio_snap_emit_pads(PstW *w) { /* Pad arrays sized by PSX_MAX_PLAYERS. Default MAX=2 keeps the historical * 2-pad snap layout byte-identical. pad_response runtime buffer is larger * for multitap bulk (34); snap still stores the first 8 bytes. */ @@ -2492,6 +2459,10 @@ static int sio_snap_emit(PstW *w) { if (!pst_w_i16(w, (int16_t)pad_type_req[s])) return 0; } + return 1; +} + +static int sio_snap_emit_mc(PstW *w) { if (!pst_w_u32(w, (uint32_t)mc_state) || !pst_w_i32(w, (int32_t)mc_slot) || !pst_w_u8(w, mc_cmd) || !pst_w_u16(w, mc_sector) || !pst_w_u8(w, mc_sector_msb) || !pst_w_u8(w, mc_sector_lsb) || @@ -2500,8 +2471,11 @@ static int sio_snap_emit(PstW *w) { return 0; if (!sio_w_mcslot(w, &mc_slots[0]) || !sio_w_mcslot(w, &mc_slots[1])) return 0; - if (!pst_w_u32(w, (uint32_t)active_device)) - return 0; + return pst_w_u32(w, (uint32_t)active_device); +} + +/* Load-bearing pacing state (guest-visible IRQ timing). */ +static int sio_snap_emit_fsm_pace(PstW *w) { #if SIO_MODEL_CYCLE_PACED if (!pst_w_i32(w, (int32_t)g_sio_timing_active) || !pst_w_i32(w, (int32_t)sio_shift_active) || !pst_w_u8(w, sio_shift_byte) || @@ -2515,14 +2489,44 @@ static int sio_snap_emit(PstW *w) { !pst_w_u32(w, (uint32_t)sio_bus_owner) || !pst_w_u32(w, sio_bus_byte_index)) return 0; #endif - if (!pst_w_i32(w, (int32_t)sio_irq_pending) || - !pst_w_i32(w, (int32_t)sio_irq_countdown) || - !pst_w_i32(w, (int32_t)sio_ack_visible_reads) || - !pst_w_u8(w, sio_irq_pending_source) || !pst_w_u8(w, sio_irq_pending_slot) || - !pst_w_u8(w, sio_irq_pending_delay) || !pst_w_u8(w, sio_irq_pending_mc_state) || - !pst_w_u32(w, sio_irq_pending_byte_seq)) - return 0; - return 1; + return pst_w_i32(w, (int32_t)sio_irq_pending) && + pst_w_i32(w, (int32_t)sio_irq_countdown) && + pst_w_i32(w, (int32_t)sio_ack_visible_reads); +} + +/* Audit/diag metadata — must not drive netplay digests (byte_seq tracks the + * host-local sio_trace_seq counter, which is not itself a guest register). */ +static int sio_snap_emit_fsm_meta(PstW *w) { + return pst_w_u8(w, sio_irq_pending_source) && pst_w_u8(w, sio_irq_pending_slot) && + pst_w_u8(w, sio_irq_pending_delay) && pst_w_u8(w, sio_irq_pending_mc_state) && + pst_w_u32(w, sio_irq_pending_byte_seq); +} + +static int sio_snap_emit_fsm(PstW *w) { + return sio_snap_emit_fsm_pace(w) && sio_snap_emit_fsm_meta(w); +} + +static int sio_snap_emit(PstW *w) { + return sio_snap_emit_regs(w) && sio_snap_emit_pads(w) && + sio_snap_emit_mc(w) && sio_snap_emit_fsm(w); +} + +/* Cumulative section end offsets in the snapshot wire: + * out[0]=regs, out[1]=pads, out[2]=memcard, out[3]=fsm_pace (netplay), + * out[4]=full including fsm_meta (== sio_snapshot_bytes()). */ +void sio_snapshot_section_ends(uint32_t out[5]) { + PstW w; + pst_w_init(&w, NULL, 0); + (void)sio_snap_emit_regs(&w); + out[0] = (uint32_t)w.written; + (void)sio_snap_emit_pads(&w); + out[1] = (uint32_t)w.written; + (void)sio_snap_emit_mc(&w); + out[2] = (uint32_t)w.written; + (void)sio_snap_emit_fsm_pace(&w); + out[3] = (uint32_t)w.written; + (void)sio_snap_emit_fsm_meta(&w); + out[4] = (uint32_t)w.written; } static int sio_snap_parse(PstR *r) { @@ -2600,6 +2604,10 @@ static int sio_snap_parse(PstR *r) { !pst_r_u8(r, &sio_irq_pending_delay) || !pst_r_u8(r, &sio_irq_pending_mc_state) || !pst_r_u32(r, &sio_irq_pending_byte_seq)) return 0; + /* sio_trace_seq is host-local and not on the wire; reseat it from the + * restored byte_seq so the next TX does not stamp a peer-divergent + * seq into sio_irq_pending_byte_seq (was forking fsm digests alone). */ + sio_trace_seq = sio_irq_pending_byte_seq; return 1; } diff --git a/runtime/tests/test_netplay_snap_ring b/runtime/tests/test_netplay_snap_ring index c5e3d4fd9e2aa78ed61213233f032940fcfa2acc..868a6c045fe59db912b87e0ee2623d63827b3115 100755 GIT binary patch delta 3362 zcmZ8k3s6+o8NTPjOCfFqnwKn2tjhRk6(lierD8`O1kthb+0fpRt=bXDJ_ss15zI*=f z|Nj5nd+zStzWr=;KkG zgR;ZbkWk-XWG!3lZociV8Fji>_#bsw3^_e7;U-7KfJ3Z~2rvTV+f4igWZQ~(L*|_zYGUHt2%$07F*>`j#1%jF7#^en)DY(XH>|*_HG*w7ZpB|89y~;bR)x~y`Ezf=mZf7GkktzD*P2PJ0JjBJR1rcVgQXXrPFNgfT%l`*#_O|h0(QU2F_$*>( zx1pi{TLfcKVLZ#Iut@5CCjEsQRAqNL&hu%n_J}FE?FMgm3p|9`1`yG1x0ENCRsf&YhisM`CR`R>b*Sy;`+Nzk?1w81J|sgK?&y6D@eOhcgYYB_Lw;hopQoY~})O z6q*w6*d>F#rr@;^?lqWUc>Nnhc&(N4Uzy~6^km9h_71hD7g^bOQ*?{WXHQQc+3?1>N@Yvzu4&R5{T$-mz3`|$xl%If*ePc3_4B0=Y`iZ z0k6#guXrg>G07jL&laS&jPY5ob~a>Y(_z9(4R}o~GWt3x<-I2P;1|<-hHsab#Ev_O zBs}3M9_d z--5u0C%}C@sj(q{%vsG^dU3{IPtIJ_bw6$EwkGG8V_rX(-M>g=qFkUQwHg9nbkg$YD^nYYD5>|?SPLW8ZMj1o}*i3 zR^}1M--_d5e#|{-sQ+t|>OhUpF(0Ye6sJqh2aY$XwcMKYHKI|fO>vo`q1YGEWDkm2 zk^~l|;EOd!X7aaEJ~Frhz0bibBOk>Ql*&M6Zmpqs39nmxydptq#C)U@uWT^Y^ixTN zwQ9Q&U1y568E)U?w+CYwo%8XUF+ZFj&&BNo-A7#&R@-|HML#nzkj&`rivIK7I2o0?K1?#2SRoPLq@b1S)0FP4Vs%*=> u{}uzQ3N6$BN+`-zXqo=U;;VK!ROYA7#|kYQDy2;Zoqep3{gR>{Py0XjiY(v& delta 3237 zcmZ8k4Nz3q6@K^0Gc2faSzvu2KdTTx4a=V;8k7KwtlB7wNt+p)Nn6`s(@A4%qiJhb zVu^0aX2g?Z>V!;9CQSmNExMTose+TtM4j|UV#mfrBASX&K$qB7OtG)u{aFI<&cONJ zIp6utIeXvU_cro6>wKN<&t*HC^Da7$Cuw3GCnxhd_8IbOAFXCR&fKN)YSNOa=Q!(? zchb@Zcqcj0917~?j&>=awNtO|b{;B`my?!m>Az0Q>P@Lz99cHH=5lA(quX{omi*ha zr^4+sytDP*xNzUxhZy}JAtU*%G)?n2RgZVYK*JyWi$8dB(DgrhEnx|JfqD}L^f|Zb zs4J6=(8n$h`yNfWd~>oVEQ{xGt~n82m-qmSdG`y;qDPXKgwH0mGB!lJ+{gzW!8D;^oD1BZrzQJb%=`%yj7^G zT;;@&@wScMM@Q4sYmUj_c3bd`F+Tmvg-Yvr0MT(s(%0MQJ1IRQ#s6IyblHNdgjQLh z(kccJTBVYHZSM3r++(ym!>pMlqepDfD-qtjFqsH_&#;j2=U?qCB6tih*wMcl{=?*IwwNG%-WhE?hYA0v_*e&op<_@SBV_} z5Mo`De!q?WBxQP1{0C&vX$$&<)^e}XDg+Q(-PakFRh?S2jo@kYM(JDQXf7ZsI7^a?uS=5siU-PtNG}st#5A(L$7OS>j z0T6B1N&03Ry^);Rx&9?Gm|zQ53$1&jT18Q<+ng6NMsaRVJ%D|+fc+_KJ?wvB_rZoPTh^Pf ze}X*)yYw^58id^h8;g75N3glDFT?s_pBX?tZ2DE?>)KsfFTk5{e18xEuq8tffSrJi zouO%upy^y4tt2U7G`KO-VXlk%9>Q42}Y7ExFgTCB3ISV!2g@(rEU)8YHBvjDwP;nqJ$pstF1_Ej3H@UmZ# zS-FEXQgfwSKj)^l%6#@Xov$osr^&1;Ws7J-m6v&_p~`LkA_ekTd~^BALQe_WEbTQq zT;+ClWJ_=KeZ zvxvyl>5|WFIEMed))Dxfx~YA=RymaE zhCfr&=#EuR<0Ie&d~(kXuA*aqRj<*UTilQN=Z80}+MtJz`}1dnBWwG0J(5TtuFH2s zccFtE>-VyDYF_VkYIr3QOW79cUGH^#`C9L&Ep<%(AfVN?rL!l0EbyNPR2bt5AF3_T Y*%|8HSnBYvl4?$J)|KK*v$`(l|5hp(CjbBd diff --git a/runtime/tests/test_netplay_snap_ring.c b/runtime/tests/test_netplay_snap_ring.c index 57748630..13c921ff 100644 --- a/runtime/tests/test_netplay_snap_ring.c +++ b/runtime/tests/test_netplay_snap_ring.c @@ -27,6 +27,12 @@ int boot_state_save_buffer(const CPUState* cpu, uint32_t bios_checksum, *out_len = 4; return 1; } +int boot_state_save_buffer_raw(const CPUState* cpu, uint32_t bios_checksum, + uint32_t entry_pc, uint8_t** out_data, + size_t* out_len) { + return boot_state_save_buffer(cpu, bios_checksum, entry_pc, out_data, + out_len); +} int boot_state_load_buffer(const uint8_t* file, size_t file_len, uint32_t bios_checksum, uint32_t entry_pc, CPUState* cpu) {