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