rollback patching

This commit is contained in:
Alex Vanderveen
2026-07-30 17:24:20 -04:00
parent abd2d60430
commit b3d99b05ff
17 changed files with 545 additions and 172 deletions
+46 -5
View File
@@ -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
---
+7 -1
View File
@@ -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,
+5 -1
View File
@@ -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
+3
View File
@@ -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
+2 -2
View File
@@ -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 <stddef.h>
+13 -1
View File
@@ -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);
+40 -16
View File
@@ -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,
+5 -4
View File
@@ -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);
+35 -1
View File
@@ -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) {
+64 -11
View File
@@ -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];
+3 -1
View File
@@ -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 */
+69 -1
View File
@@ -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)
+53
View File
@@ -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();
}
+110 -52
View File
@@ -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);
+84 -76
View File
@@ -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;
}
Binary file not shown.
+6
View File
@@ -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) {