rollback patches

This commit is contained in:
Alex Vanderveen
2026-07-31 17:14:35 -04:00
parent d61bfca914
commit fe403e6ce1
31 changed files with 3048 additions and 358 deletions
+140 -24
View File
@@ -5,11 +5,13 @@ Status: **hooked (experimental)** · branch `feat/rollback-netplay` · depends o
Today MotK / psxrecomp lobby netplay defaults to **rollback**:
`stage_local → poll_admit (invent within P) → guest frame → finish_frame`.
Missing remotes are invented (**idle**, not hold-last — MotK digital) only while
Missing remotes are invented (**hold-last**, MotK digital) only while
`wire_need ≤ highest_remote + P`; outside that window admit stalls (BattleShip
phase_lock). Late wire goes through the input contract; rewinds open an
`RNetRbSession` episode. Host **Disable Rollback** (or `PSX_NET_MODE=delay`)
forces classic delay-sync `try_admit`.
phase_lock). Idle invent re-mismatched every held D-pad tick after commit
(char-select episode storm). Seal gap-fill stays idle. Late wire goes through
the input contract; rewinds open an `RNetRbSession` episode. Menu releases
rewind (FMV soft-promote release-only only). Host **Disable Rollback** (or
`PSX_NET_MODE=delay`) forces classic delay-sync `try_admit`.
---
@@ -52,9 +54,10 @@ 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 **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] Live snap rate-limit: every `PSX_NET_SNAP_INTERVAL` ticks (default **16**,
max 32); 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. (Skipping VRAM on live snaps is unsafe for rewind.)
- [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`
@@ -89,24 +92,86 @@ 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**;
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] Storm calm: **abort/storm** cooldown + promote-sweep only (not after
clean commit). Post-commit promote-only made char-select D-pad feel
rejected (hist ok, sim not rolled). Live invent is **hold-last** (idle
invent → hold mismatch every tick = episode storm). Seal gap-fill idle.
Menu release soft-promote removed (forked sticky Up with hold-last);
FMV settle still soft-promotes releases. Press/release → episode; held
matching invent → no episode.
**Char-select “hang” with matched POST:** digital button edges always
rewind in `rnet_input_contract` (no `hash_confirm` promote for buttons).
A tap used to be two episodes (press + release). Fixed in recomp-net:
**tip-extend** (`rnet_rb_extend_target` / MotK `psx_netplay_rb_tip_extend`)
+ **TipHold** (`rnet_rb_enter_tip_hold` after POST match: Live continues,
`tip_runway` quiet window, late release tip-extends without a second
baseline) coalesce press+release into one episode. Tip-extend from
TipHold/Verify clears the POST handshake; follower FOLLOW mirrors
initiator rereplay; TipHold ignores stale rexmit POSTs.
Post-TipHold `choose_load_tick` must not pick Live tip snaps above
`agreed_through` unless `hash_confirm` confirms them (else follower
NACK past frontier).
`tip_seal_slack` (default 2) sets initial seal headroom;
`tip_runway` (default 12) caps TipHold + suggest slack;
**light tip**
(`RNET_RB_CORR_LIGHT_TIP`) skips ready-ACK RTT when load is at the
shared frontier and depth ≤ 8. Do **not** soft-promote releases.
**Resim depth (main-menu "back to title" hang):** `choose_load_tick`
always applied interval rounding + one-interval tip slack, so a mismatch
one tick after a commit (798) re-loaded 768 and every menu tap replayed
~30 ticks twice. Fixed with a **shared-frontier fast path**: ticks the
peer provably holds need no slack — (a) `hash_confirm` resolved_through
(both simmed, core digests matched → same interval snaps) and (b) the
last committed replay span (both peers resim-save a snap at every
replayed tick). Release-after-press now loads at commit target, ~2-tick
replay. Peer eviction still covered by follower NACK (abort, not hang).
Do **not** freeze `cdrom_advance` during Replay (FMV skip resim hung).
Promote wire into hist **before** `begin_rewind` seal.
FMV media + **digest-gated post-FMV lockstep**: no invent + refuse
begin/follow for at least MIN=90 ticks (title Start was
invent→`0x8006CDA0` episode hang); hold until hash_confirm matched
CONFIRM=16 contiguous ticks or MAX=180 (cross-game cutover). Dense
tip snaps during media/lockstep/+32 after invent unlock so the first
invent-miss loads near the mismatch (was 954→944). Diag `FIRST CORE
DIVERGE` at first post-lockstep press is invent≠wire (e.g. `ffff`→
`ffbf` Cross), not residual FMV non-determinism — live digs matched
through lockstep.
Symmetric ready: follower ACK → initiator GO → both Replay.
**`flush_resume` releases `gpu_vblank_flush_present` reentrancy guard**
before longjmp (stuck `s_flushing_present` blocked all later presents /
`finish_frame` — MotK `0x8006CDA0` Replay hang). Mid-guest pump runs
`poll_replay_stall` (5s abort).
**Menu wait-loop resim (CD54↔CDA0):** do **not** arm deferred present on
flush_resume / do **not** re-finish `load_tick` (snap is already
post-present). Phantom fin@load before the latched VBlank IRQ put peers
on opposite ping-pong edges (`v0=1` vs countdown, ~5-cycle skew). First
arm is `load+1`; `hc` primed after load. Netplay flushes deferred present
**after** IRQ delivery when both are due (post-RFE digest).
**Returning-leaf flush_resume (`0x8006A9F8` → PC=0):** prefer IRQ/sticky/
`$ra` over bare function-entry snap PCs; `flush_resume` clears deferred
present + bb_defer nest; sentinel same-thread path must not publish
`pc=0` during top-level RB resume; scheduler recovers null-pc via `$ra`.
- [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`).
short 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`)
Reconcile promotes wire for the whole abort window (`promote-no-resim`).
Clean commit does **not** arm cooldown.
- [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
- [x] SW GPU **before window** + late `force_sw` builds `SDL_Renderer` after GL
teardown (first match was black: cleared `g_gl_active` with null present)
- [x] No rollback episodes during depth24 / recent MDEC (FMV-defer); CD-frozen
Replay was scrambling movies — promote late wire until 15-bit gameplay
- [x] FMV lockstep: stall admit (no invent) during movie + ~90-tick settle after
cutover; baseline-mismatch without agreed tip uses 60-tick cooldown
- [x] `flush_resume` releases present-flush reentrancy guard (no latched-VBlank
re-arm — that phantom-finished load and forked menu wait resim).
Symmetric ready GO. Live snaps stay on during media. Replay arms
`load+1` with present-after-IRQ on netplay BB edges.
- [x] Netplay depth24: present 1/4 vblanks; skip live FRAME_COMMIT
(full-RAM CRC) + prime `hash_confirm` when media ends
- [x] MDEC SSE2 IDCT + 24bpp YCbCr row encode (Beetle-matching, bit-identical
to scalar; self-check at `mdec_init`) — raises offline/netplay FMV on
slow peers; remaining FMV FPS is mostly host CPU ceiling
- [x] MDEC snap `last_color_age` is guest-cycle relative (was host
`s_frame_count` → false baseline aux trips with matched FIFO/SPU)
- [x] Diag: `rb wire promote-no-resim` / `rewind-request` — late wire into hist
@@ -137,6 +202,8 @@ RTT (BattleShip tiers / phase_lock); optional Manual Input Delay / Prediction.
- [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] **Core digest = fold of part digests** (one RAM CRC pass, not dual
stream); `core=` values change vs older binaries — peers must match
- [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,
@@ -159,6 +226,16 @@ RTT (BattleShip tiers / phase_lock); optional Manual Input Delay / Prediction.
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.
- [x] **Menu wait resim phase (CD54↔CDA0):** after present-guard fix, ep1
matched fin@load then forked fin@load+1 (`r2=1` vs countdown, cyc±5).
Cause: flush_resume armed deferred present for latched I_STAT → phantom
finish_frame before latched VBlank IRQ. Fix: arm `load+1` only; no
latched re-arm; present-after-IRQ when delivery is due.
- [x] **Returning-leaf resume crash:** after load+1 fix, Start-class episode
resumed at `0x8006A9F8` (`jr $ra` leaf) with IEc=0 → top-level
`execution completed, PC=0` (cps: `final_ra=0x8006CCA0`). Fix: resume
PC pick prefers IRQ/sticky/`$ra`; clear present+bb_defer on flush;
no sentinel `pc=0` while `top_level_resume`; scheduler null-pc recover.
- [ ] Memory budget / thinner snap: optional strip CDROM/MDEC if MotK match
path allows (further RAM headroom)
- [x] Standalone ring bookkeeping test: `runtime/tests/test_netplay_snap_ring.c`
@@ -187,7 +264,8 @@ RTT (BattleShip tiers / phase_lock); optional Manual Input Delay / Prediction.
- [x] Per-slot input history ring (`netplay_input_hist`)
- [x] Local path: `is_predicted = 0`
- [x] Remote invent = **hold-last** (neutral if no prior); stall when ahead of remote tip by > P
- [x] Remote invent = **hold-last** (neutral if no prior); stall when ahead of
remote tip by > P. Seal `get_input_row` gap-fill remains **idle**.
- [x] Late wire → `rnet_input_contract_stick_replace_decide` + `hash_confirm_promote`
- [x] Rewind → `psx_netplay_rb_begin_rewind` (episode)
- [x] `get_input_row` vtable → `netplay_ih_get`
@@ -243,7 +321,43 @@ RTT (BattleShip tiers / phase_lock); optional Manual Input Delay / Prediction.
- [x] Netplay clears mods (`mod_runtime_clear_for_netplay` / launcher hook)
- [x] Same BIOS stem + disc identity on both peers (existing verify)
- [ ] Audit non-deterministic host clocks in sim path — soak-driven
- [x] Audit non-deterministic host clocks in sim path — **selfcheck-driven**
(was soak-driven). `PSX_RB_SELFCHECK=1` (offline, single process,
`runtime/src/psx_selfcheck.c`): every INTERVAL boundaries snap the
machine at a savestate BB edge, record SPAN ticks of applied pad rows +
full digest partitions, then rewind and resim the window **twice** from
the same snap. Replay#1 vs replay#2 is the rollback invariant (both
peers resim in an episode) → PASS/FAIL with per-part DIVERGE lines;
live-vs-resim is reported as `restore-drift` (diagnostic; VBlank phase
re-applied from snap). Verdict = warm #2vs#3 after ambient-prime.
Fixes via selfcheck: VBlank-phase restore; overlay freeze; discard
dirty/interp load-delay on restore; pin host frame; RECORD=resim
host-skip profile; idle_skip off; turbo/FMV-skip gated; top-level
resume; IRQ escape + check-cycle/poll-throttle clear; sticky
`text_diverged` / kernel-bless / overlay validation memos cleared on
RAM restore; **span-end load** via `psx_selfcheck_flush_load` after
present-body RAII (BB fast-poll tails forked #2/#3 load sites);
snap PC uses RB `resume_pc_ok` (reject 0xB0). MotK attract soak
`/tmp/selfcheck_spanend2.log`: **316+ PASS / 0 FAIL** through the
classic win#70/#73/#118/#138/#139/#277/#279/#301 set. Re-verified after
hold-last invent (`/tmp/selfcheck_verify.log`): win#70/#73/#118/#139
PASS, **157+ PASS / 0 FAIL** and climbing. Post-FMV cutover soak
(`/tmp/selfcheck_fmvcut.log`, INTERVAL=120 SPAN=48): **52 PASS / 0
FAIL** through early attract/menu — confirms 954-class peer fork is
invent≠wire, not offline resim non-determinism. Tune:
`PSX_RB_SELFCHECK_INTERVAL/SPAN/FAULT/PRIME`.
**Mash:** `PSX_RB_SELFCHECK_MASH=1` (+ optional `MASH_SEED` /
`MASH_RATE`) synthesizes fighter-style face/D-pad/shoulder spam on
live boundaries so headless soaks stress invent edges / wait-loop
resim without a controller. Rows are recorded and replayed.
**Stuck hash_confirm / doomed tip loads:** a transient FIRST CORE
(e.g. sim 201) left `resolved_through` stuck; once that tick aged
out of the 128-slot dig ring, `choose_load_tick` fell through to
tip-slack (912) while confirmed snaps were gone — baseline then
aborted on cpu-only MotK wait-loop PC forks (`CD54`↔`CDA0`) despite
matched ram/clk. Fix: `netplay_hc_heal_stale_gap`, refuse loads past
the shared frontier, canonicalize wait-loop snap PC to `CDA0`.
`rb live dig local` is a breadcrumb (not peer match).
- [x] FMV / depth24: defer rewind (promote wire only); follow NACKs; RB snap
load uses light frontend hook (no FMV cutover/present thrash)
- [x] Multitap / N-slot: history + seal tables sized to `slot_count`
@@ -270,8 +384,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** — strip CDROM/MDEC + movie digest stability
(raw ring snaps + interval 8 already landed for FPS)
7. ~~Thinner snap / FMV policy~~ — interval **16**, settle invent ok, digest
fold (skip-VRAM on live snaps unsafe for rewind; optional later)
8. **Dual-instance soak** — prove Done-when items
---
@@ -293,10 +407,12 @@ RTT (BattleShip tiers / phase_lock); optional Manual Input Delay / Prediction.
- [ ] Two MotK instances with `PSX_NET_MODE=rollback` complete a match without
admit-stall on one-frame remote loss
- [ ] Forced remote stick correction triggers one episode and both digests match
post-commit
- [x] Forced remote digital correction opens episode(s); POST digests match
(char-select L/R soak: 4 commits, live dig ok after). Tap = press+release
= two episodes by input-contract (button deltas always rewind).
- [ ] `hash_confirm` invent path shows promotes in diag without opening episodes
when master hashes still agree
when master hashes still agree (stick/analog path; **not** MotK digital
buttons — contract always rewinds button deltas)
- [x] Delay-sync path (`PSX_NET_MODE=delay`) unchanged for lobby rematch / save xfer
Reference: `lib/recomp-net/docs/rollback.md`,
+10 -1
View File
@@ -46,6 +46,12 @@ int dirty_ram_dispatch(CPUState* cpu, uint32_t addr, uint32_t stop_addr);
* interpreter or an exception is delivered, so no register write is dropped. */
void dirty_ram_ld_delay_flush(CPUState* cpu);
/* Drop a deferred load-delay writeback WITHOUT applying it. Required on
* savestate/RB restore: the snap already has architectural GPRs, and a
* host-static pending load from the pre-load timeline must not stomp them
* on the next interp step (selfcheck MotK v0=countdown vs v0=1 forks). */
void dirty_ram_ld_delay_discard(void);
/* Overlay-cache windows — the address ranges eligible for capture, offline
* recompilation, and per-entry-validated native execution (Rule 18 code that
* does not exist in any compile-time image):
@@ -102,6 +108,7 @@ int dirty_ram_is_dirty(uint32_t phys);
int psx_kernel_bless_dispatchable(uint32_t phys);
void psx_kernel_bless_note_range(uint32_t phys, uint32_t len);
void psx_kernel_bless_stats(uint64_t out[6]);
void psx_kernel_bless_resync_after_restore(void);
/* Capture/candidate window membership. Kernel window and overlay region are
* unconditional; main-EXE text [KERNEL_WINDOW_END, FLOOR) is included only
@@ -119,8 +126,10 @@ uint32_t dirty_ram_get_bitmap_word(uint32_t word_index);
uint32_t dirty_ram_get_bitmap_word_count(void);
void dirty_ram_set_bitmap_words(const uint32_t* words, uint32_t count);
/* After bulk RAM restore (savestate): bump overlay page gens + lazy-miss epoch
* so native overlays re-hash against restored bytes. */
* so native overlays re-hash against restored bytes; also drop sticky
* text_diverged/modified bitmaps (host-only — restored RAM may match ref). */
void overlay_watch_invalidate_after_ram_restore(void);
void dirty_ram_text_guard_resync_after_restore(void);
void dirty_ram_mark_executable_range(uint32_t phys, uint32_t len);
void dirty_ram_register_text_image(uint32_t phys_lo, const uint8_t *bytes,
uint32_t len);
+5
View File
@@ -118,6 +118,11 @@ 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);
/* Queue one deferred present (netplay RB resume when I_STAT already has
* VBlank — snap resync cleared the pending callback). */
void gpu_vblank_arm_deferred_present(void);
/* Clear flush_present reentrancy guard before longjmp (flush_resume). */
void gpu_vblank_release_present_flush_guard(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
+9
View File
@@ -80,6 +80,12 @@ void psx_check_interrupts_dispatch_entry(struct CPUState* cpu, uint32_t resume_p
void interrupts_advance_cycles(uint32_t cycles);
void interrupts_service_scheduled_events(void);
uint32_t interrupts_cycles_to_vblank(void);
/* VBlank phase within the current frame (0 .. VBLANK_CYCLES-1). Self-check
* snaps save/restore this so resim keeps the snap's phase instead of the
* warm-load rebase to 0 (which is correct for UI savestates, wrong for
* bit-identical resim of a mid-frame tip). */
uint32_t interrupts_get_cycles_since_vblank(void);
void interrupts_set_cycles_since_vblank(uint32_t v);
/* Cycle-budgeted precise event slicing: minimum guest-CPU-cycle distance to the
* next DELIVERABLE hardware interrupt (source raises its I_STAT bit AND that bit
@@ -94,6 +100,9 @@ int psx_get_in_exception(void);
* Used by the post-savestate freeze probe (vblank-time "where was the game"). */
uint32_t psx_last_irq_check_pc(void);
uint32_t psx_compiled_irq_resume_pc(void);
uint64_t psx_last_irq_check_cycle(void);
uint64_t psx_interrupt_total_checks(void);
uint32_t psx_interrupt_fast_maintenance(void);
/* Snapshot internal counters for the freeze_check diagnostic. Any out_*
* pointer may be NULL. All counters are monotonically non-decreasing
+7
View File
@@ -55,6 +55,13 @@ uint8_t netplay_hc_local_digest(const NetplayHashConfirm* hc, uint32_t tick,
uint8_t netplay_hc_peek_mismatch(const NetplayHashConfirm* hc, uint32_t* tick_out,
uint32_t* local_out, uint32_t* peer_out);
/* Heal a stuck watermark when the next tick after resolved_through has aged
* out of the ring (slot reused) and is no longer a live mismatch. Scans the
* ring for the highest tick where local==peer with no newer mismatch present,
* then advances resolved_through there (ring contents kept).
* Returns 1 if the watermark moved. Call from FRAME_COMMIT ingress. */
uint8_t netplay_hc_heal_stale_gap(NetplayHashConfirm* hc);
#ifdef __cplusplus
}
#endif
+2 -2
View File
@@ -49,8 +49,8 @@ int netplay_ih_get(const NetplayInputHist *h, int slot, uint32_t tick, RNetRbFr
int netplay_ih_invent_hold_last(NetplayInputHist *h, int slot, uint32_t tick,
RNetRbFrame *out);
/* MotK digital: invent neutral only (never hold-last). Sticky D-pad invent
* after FMV settle forked RAM while soft-promote release-only skipped resim. */
/* Neutral invent (buttons 0xFFFF). Seal gap-fill only — live MotK admit uses
* hold-last so a held D-pad does not re-episode every tick. */
int netplay_ih_invent_idle(NetplayInputHist *h, int slot, uint32_t tick,
RNetRbFrame *out);
+16
View File
@@ -43,6 +43,22 @@ void overlay_loader_check_cache(uint32_t load_addr, uint32_t size,
* 0 if not found (fall through to interpreter). */
int overlay_loader_dispatch(CPUState *cpu, uint32_t addr);
/* Freeze new DLL discovery/load (lazy try_load_region, live publish commit,
* rescan). Already-registered natives keep running. Required for rollback
* resim / selfcheck: overlay registration is host-only and not in the snap,
* so a load mid-pass#1 that is visible at the start of pass#2 forks BB-edge
* IRQ cadence (matched clocks, mismatched GPRs). */
void overlay_loader_set_load_freeze(int freeze);
int overlay_loader_load_frozen(void);
/* Drop the negative lazy-lookup memo (host-only). Call on snap restore so
* resim#1 and resim#2 take the same dispatch path. */
void overlay_loader_clear_lazy_miss(void);
/* After RAM restore: bust overlay candidate gen fast-path + static-match
* cache so native vs interp is re-decided against restored bytes. */
void overlay_loader_resync_validation_after_restore(void);
/* Step 2.8: re-scan the cache dir for DLLs compiled after init and clear the
* checked-regions memo so the next dispatch reconsiders the cache. Idempotent
* (loaded DLLs stay loaded); emu thread only. */
+4
View File
@@ -22,6 +22,10 @@ extern "C" {
/* Initialize interpreter state. Call once after CPUState is allocated. */
void interp_init(CPUState* cpu);
/* Drop host-static pending load-delay (not in boot_state). See
* dirty_ram_ld_delay_discard — same restore contract for the debug interp. */
void interp_ld_delay_discard(void);
/* Execute exactly `count` instructions. Returns number actually executed
* (may be less if a breakpoint fires). */
uint32_t interp_step(CPUState* cpu, uint32_t count);
+29 -8
View File
@@ -46,27 +46,47 @@ void psx_netplay_rb_poll(struct CPUState *cpu, uint32_t resume_pc);
* success. */
void psx_netplay_rb_flush_resume(void);
/* 1 after flush_resume longjmp until finish_frame / abort — top-level
* dispatch has no native call chain under the resume PC. */
int psx_netplay_rb_top_level_resume_active(void);
/* After outermost psx_dispatch returns pc==0 following an RB resume: retry
* at $ra / sticky BB if safe. Returns 1 and arms RESUME_CURRENT via out_pc. */
int psx_netplay_rb_recover_null_pc(struct CPUState *cpu, uint32_t *out_pc);
/* After a live tick completes: request snap at tick for next safe poll. */
void psx_netplay_rb_request_snap(uint32_t tick);
/* Initiator: start episode from invent/contract rewind.
* Returns 1 if an episode opened, 0 if refused (no snap, already active,
* post-commit/realign cooldown, …). On refuse, reconcile should promote wire. */
* abort/storm cooldown, FMV lockstep, …). On refuse, reconcile should promote
* wire (or tip-extend if active). Clean commit does not arm cooldown. */
int psx_netplay_rb_begin_rewind(uint32_t mismatch_tick, int slot);
/* 1 while begin_rewind is suppressed (cooldown after commit / realign). */
/* Active episode: grow target / resign sealed rows for a late wire edge
* (press+release coalesce). Host must promote wire into hist first.
* Returns 1 if the correction was absorbed into the current episode. */
int psx_netplay_rb_tip_extend(uint32_t mismatch_tick, int slot);
/* 1 while begin_rewind is suppressed (cooldown after abort / realign). */
int psx_netplay_rb_rewind_suppressed(void);
/* 1 during depth24 / recent MDEC / post-FMV settle — MotK FMV. Reconcile
* promotes wire; no episodes (CD-frozen Replay corrupts the movie). */
/* 1 during depth24 / recent MDEC / post-FMV settle — MotK FMV. */
int psx_netplay_rb_fmv_defer_rewind(void);
/* 1 in the same FMV/settle window: admit must stall for remote wire instead
* of inventing (promote-only during FMV left peers desynced at cutover). */
/* 1 while depth24 or recent MDEC (not settle). */
int psx_netplay_rb_fmv_media_active(void);
/* 1 during FMV media + post-FMV lockstep (~90 ticks) — admit waits for remote
* wire (title Start / movie skip). Ticks the FMV→settle tracker. */
int psx_netplay_rb_lockstep_no_invent(void);
/* 1 once after commit/abort/realign — reconcile should promote all late wire
* without opening another episode (clears invent poison). */
/* Mid-guest resim pump: abort if Replay has made no finish_frame progress.
* Full rb_pump stays admit/present-edge only (host-asymmetric). */
void psx_netplay_rb_poll_replay_stall(void);
/* 1 once after abort/realign cooldown arm — reconcile should promote all late
* wire without opening another episode (clears invent poison). */
int psx_netplay_rb_take_promote_sweep(void);
/* Drain peer RB_* + drive Seal/Baseline/Replay/Verify. Call from pump. */
@@ -75,6 +95,7 @@ void psx_netplay_rb_pump(void);
/* 1 while episode is active (seal/baseline/replay/verify). */
int psx_netplay_rb_active(void);
int psx_netplay_rb_is_resimulating(void);
int psx_netplay_rb_tip_holding(void);
/* FRAME_COMMIT mismatch during Replay — abort before a false POST commit.
* Returns 1 if an episode was aborted. */
+9 -1
View File
@@ -99,9 +99,17 @@ void psx_clear_return_to_lobby(void);
/* Save-state restore: unwind to psx_scheduler_run and re-dispatch resume_pc
* (the restored CPUState's PC). Call on the scheduler fiber at a block-leader
* boundary, in_exception == 0. Never returns (longjmp). */
* boundary, in_exception == 0. Never returns (longjmp). Arms top-level
* resume (see below) for the first post-resume frame. */
void psx_scheduler_resume_at(uint32_t resume_pc);
/* After resume_at, dispatch is top-level — there is no abandoned mid-block
* native chain under the resume PC. Sentinel same-thread RFE must not publish
* pc=0 / "continue live chain" (that is GUEST_EXIT). Active until the first
* finish_frame / explicit clear. Shared by savestate, selfcheck, and RB. */
int psx_scheduler_top_level_resume_active(void);
void psx_scheduler_top_level_resume_clear(void);
/* HLE-tier standing subsystem replacement (CLAUDE.md §0 amendments
* 2026-06-29 + 2026-07-02). 1 = deterministic TCB scheduler (default, both
* BIOS backends — the LLE host-fiber bridge it replaces is non-deterministic);
+96
View File
@@ -0,0 +1,96 @@
#ifndef PSX_SELFCHECK_H
#define PSX_SELFCHECK_H
/*
* Solo rollback resim self-check (PSX_RB_SELFCHECK=1, offline only).
*
* Periodically during normal offline play: snapshot the machine at a safe
* BB edge, keep playing N ticks while recording the per-boundary pad rows
* the sampler applied to SIO plus the full netplay digest partitions, then
* rewind to the snapshot and resim the same rows, comparing digests at every
* boundary. Any live-vs-resim fork is exactly the class of determinism bug
* that otherwise only surfaces as a doomed netplay episode in a two-peer
* soak (SIO pace, GTE fold, present-edge PC, v0 restore, ...).
*
* Uses the same machinery as netplay rollback: boot_state raw buffer snaps,
* netplay_state_digest partitions, RB restore recipe (cycle/IRQ resync, SPU
* CD FIFO reset, NO cdrom_accelerate, light frontend hook). No recomp-net
* dependency; fully offline, single process.
*
* Env:
* PSX_RB_SELFCHECK=1 enable
* PSX_RB_SELFCHECK_INTERVAL=N boundaries between windows (default 600)
* PSX_RB_SELFCHECK_SPAN=N window length in boundaries (default 32)
* PSX_RB_SELFCHECK_FAULT=1 corrupt one recorded digest per window to
* prove the comparator/report path
* PSX_RB_SELFCHECK_PRIME=N throwaway resims before counted passes
* (default 1, max 4) — clears live-RECORD ambient
* PSX_RB_SELFCHECK_MASH=1 synthetic fighter-style button spam on live
* boundaries (IDLE/SAVE_WAIT/RECORD). Recorded
* rows replay during resim. Headless-safe.
* PSX_RB_SELFCHECK_MASH_SEED=N xorshift seed (default 0xC0FFEE)
* PSX_RB_SELFCHECK_MASH_RATE=N % chance to start a new press/release each
* boundary when not mid-hold (default 75)
*
* Verdict = warm resim#2 vs #3 (peer episode path). Cold #1 vs #2 is dig only.
*/
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
struct CPUState;
/* Startup bind (after savestate_configure; needs HLE scheduler for the
* rewind longjmp). Reads env; no-op unless PSX_RB_SELFCHECK=1. */
void psx_selfcheck_init(struct CPUState *cpu, uint32_t bios_checksum,
uint32_t entry_pc);
int psx_selfcheck_enabled(void);
/* 1 while a window is open (record/replay/load). Offline GPU present stays
* immediate — full BB-edge present-defer skews clk/tim between warm peers. */
int psx_selfcheck_defer_present(void);
/* 1 while the resim replay is running: skip wall pacer / host audio pump /
* host CD boost exactly like netplay resim. */
int psx_selfcheck_resim_active(void);
/* 1 during replay: main.cpp must skip live pad sampling entirely (the
* self-check republishes the recorded rows itself). */
int psx_selfcheck_replay_input(void);
/* 1 during record or replay: main.cpp must skip the mid-frame low-latency
* input re-sample so pads stay boundary-latched (netplay tick semantics). */
int psx_selfcheck_input_locked(void);
/* Offline pad sampler notes what it applied to SIO this boundary (called
* from apply_pad_slot_to_sio / apply_input_override_to_sio). */
void psx_selfcheck_note_pad(int slot, uint16_t buttons, uint8_t lx, uint8_t ly,
uint8_t rx, uint8_t ry, uint8_t analog);
/* 1 when mash owns this boundary's P1 pad word (active-low). Latched once
* per offline vblank so mid-frame resampling sees a stable word. */
int psx_selfcheck_mash_override(uint16_t *out_buttons);
/* Once per offline vblank boundary, after pad sampling (main.cpp present
* body). defer_window!=0 postpones opening a new window (e.g. multitap
* arming still pending). */
void psx_selfcheck_finish_frame(int defer_window);
/* Safe BB-edge poll (beside savestate_poll in interrupts.c): performs the
* deferred snapshot save. Loads only as a fallback when span-end flush did
* not run (e.g. headless paths that skip present). Near-free when idle. */
void psx_selfcheck_poll(struct CPUState *cpu, uint32_t resume_pc);
/* After present-body C++ RAII: if a window span just ended, rewind+resume
* immediately at that VBlank boundary (no post-span BB tail). Longjmps. */
void psx_selfcheck_flush_load(void);
#ifdef __cplusplus
}
#endif
#endif /* PSX_SELFCHECK_H */
+1
View File
@@ -209,6 +209,7 @@ set(PSXRECOMP_RUNTIME_SOURCES
${PSXRECOMP_ROOT}/runtime/src/netplay_hash_confirm.c
${PSXRECOMP_ROOT}/runtime/src/netplay_input_hist.c
${PSXRECOMP_ROOT}/runtime/src/psx_netplay_rb.c
${PSXRECOMP_ROOT}/runtime/src/psx_selfcheck.c
${PSXRECOMP_ROOT}/runtime/src/bios_hle.c
${PSXRECOMP_ROOT}/runtime/src/bios_hle_plan.c
${PSXRECOMP_ROOT}/runtime/src/savestate.c
+5 -10
View File
@@ -17,7 +17,6 @@
#include "event_ring.h"
#include "audio_trace.h"
#include "psx_cycles.h"
#include "psx_netplay.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
@@ -2473,18 +2472,14 @@ uint32_t cdrom_cycles_to_irq(uint32_t i_mask) {
}
void cdrom_advance(uint32_t cycles) {
/* Rollback Replay: freeze CD FSM aging. Uncapped resim + host CD timing
* forks peer POST cores even with identical seals/baseline; core digest
* excludes CD, but CD IRQs still steer guest execution. Both peers freeze
* identically from the restored snap. */
if (psx_netplay_is_resimulating()) {
refresh_cdrom_irq_line();
return;
}
/* Age the response presentation latency. Once it elapses, the following
* refresh_cdrom_irq_line() presents the held response to INTC — at this
* cdrom_advance boundary, not synchronously inside the guest store that
* armed it. */
* armed it.
*
* Note: do NOT freeze CD during rollback Replay. MotK FMV skip resim
* resumes into VLC/sector waits; frozen IRQs → no finish_frame hang.
* Menu CD asymmetry is contained by no-invent during media + core POST. */
if (cdrom_irq_present_delay > 0) {
cdrom_irq_present_delay -= (int)cycles;
if (cdrom_irq_present_delay < 0) cdrom_irq_present_delay = 0;
+7
View File
@@ -315,6 +315,13 @@ void dirty_ram_ld_delay_flush(CPUState *cpu) {
cpu->gpr[0] = 0;
}
void dirty_ram_ld_delay_discard(void) {
s_ld_pend_armed = 0;
s_ld_pend_age = 0;
s_ld_pend_rt = 0;
s_ld_pend_val = 0;
}
uint32_t g_insn_gate_lo = 0; /* extra always-log phys range [lo,hi) */
uint32_t g_insn_gate_hi = 0; /* 0 = extra range disabled */
uint32_t g_insn_freeze_addr = 0; /* candidate phys entry to watch */
+16
View File
@@ -2477,6 +2477,18 @@ static int s_flushing_present;
void gpu_vblank_clear_deferred_present(void) {
s_present_pending = 0;
/* longjmp from flush_resume abandons the flush_present stack frame —
* must drop the reentrancy guard or every later flush no-ops forever. */
s_flushing_present = 0;
}
void gpu_vblank_arm_deferred_present(void) {
if (s_present_pending < 8)
s_present_pending++;
}
void gpu_vblank_release_present_flush_guard(void) {
s_flushing_present = 0;
}
void gpu_vblank_flush_present(void) {
@@ -2522,6 +2534,10 @@ void gpu_vblank_tick(void) {
return;
{
extern int psx_netplay_active(void);
/* Offline selfcheck keeps immediate present: BB-edge defer +
* post-IRQ flush reintroduces clk/tim/csv phase skew between warm
* resim peers (selfcheck soak: many FAILs with d_cyc≠0). Netplay
* defers — both peers share the same present contract from boot. */
if (psx_netplay_active()) {
/* Cap: a stuck defer must not queue unbounded presents. */
if (s_present_pending < 8)
+94 -12
View File
@@ -346,6 +346,14 @@ uint32_t interrupts_cycles_to_vblank(void) {
return VBLANK_CYCLES - cycles_since_vblank;
}
uint32_t interrupts_get_cycles_since_vblank(void) {
return cycles_since_vblank;
}
void interrupts_set_cycles_since_vblank(uint32_t v) {
cycles_since_vblank = v;
}
void interrupts_advance_cycles(uint32_t cycles) {
cycles_since_vblank += cycles;
interrupts_service_scheduled_events();
@@ -451,9 +459,15 @@ extern int g_psx_dispatch_depth;
static uint32_t s_compiled_interrupt_resume_pc = 0;
static uint32_t s_last_interrupt_check_pc = 0;
static uint64_t s_last_interrupt_check_cycle = UINT64_MAX;
/* Poll-throttle counter for fast IRQ paths — host-only; must not survive
* rewind or load#N can hit the 16K poll edge at a different guest point. */
static uint32_t s_fast_maintenance = 0;
uint32_t psx_last_irq_check_pc(void) { return s_last_interrupt_check_pc; }
uint32_t psx_compiled_irq_resume_pc(void) { return s_compiled_interrupt_resume_pc; }
uint64_t psx_last_irq_check_cycle(void) { return s_last_interrupt_check_cycle; }
uint64_t psx_interrupt_total_checks(void) { return total_checks; }
uint32_t psx_interrupt_fast_maintenance(void) { return s_fast_maintenance; }
/* Deferred cooperative thread switch from nested exception delivery.
*
@@ -633,6 +647,35 @@ void interrupts_resync_after_restore(void) {
s_defer_switch_pending = 0;
s_defer_switch_target = 0;
s_defer_switch_from = 0;
/* Host-only IRQ/escape ambient — not in the snap. Stale RFE/SENTINEL
* escape flags from the pre-load timeline can bias the first same-thread
* restore decision after rewind. Also zero resume-PC latches: the poll
* that triggered this load left the PRE-LOAD timeline's BB PC in
* s_compiled_interrupt_resume_pc; if sticky I_STAT delivers before the
* first post-resume BB edge rewrites it, EPC/saved_gpr fork warm
* resim#2 vs #3 (selfcheck MotK attract win#70/#73, matched clocks). */
g_exception_real_epc = 0;
g_exc_escape_reason = PSX_EXC_ESCAPE_NONE;
g_rfe_escape_pending = 0;
g_pending_exception_longjmp = 0;
s_compiled_interrupt_resume_pc = 0;
s_last_interrupt_check_pc = 0;
/* De-dupe key for dispatch_entry: a stale absolute cycle from the pre-load
* timeline can equal the restored tip and skip the first post-resume IRQ
* check at that PC (warm selfcheck MotK #2≠#3 at matched clocks). */
s_last_interrupt_check_cycle = UINT64_MAX;
s_fast_maintenance = 0;
total_checks = 0;
{
extern uint32_t g_dirty_safe_resume_pc;
g_dirty_safe_resume_pc = 0;
}
/* Load longjmp from mid-path abandons PSX_CHECK_INTERRUPTS_RETURN's
* g_ls_suppress_record-- — clamp so suppress cannot accumulate. */
{
extern int g_ls_suppress_record;
g_ls_suppress_record = 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();
@@ -805,11 +848,23 @@ 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();
/* Netplay: deferred sdl_vblank_present at BB edge (not mid-block
* fire_vblank_edge). When an IRQ is also deliverable, flush AFTER
* delivery so finish_frame digests post-RFE GPRs at the interrupted
* EPC — MotK menu CD54↔CDA0 was presenting pre-IRQ on opposite edges.
* Selfcheck stays on immediate present (see gpu.c). */
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)
extern int psx_netplay_active(void);
int np_present_after_irq = 0;
if (psx_netplay_active() && psx_interrupt_delivery_needed(cpu))
np_present_after_irq = 1;
else
gpu_vblank_flush_present();
#define PSX_CHECK_INTERRUPTS_RETURN() do { \
if (np_present_after_irq) gpu_vblank_flush_present(); \
if (g_ls_suppress_record > 0) g_ls_suppress_record--; \
return; \
} while (0)
#ifdef PSX_COSIM
extern uint32_t g_dirty_safe_resume_pc;
#define COSIM_IRQ_TAKE_PC() (g_dirty_safe_resume_pc ? g_dirty_safe_resume_pc : s_compiled_interrupt_resume_pc)
@@ -823,7 +878,6 @@ void psx_check_interrupts(CPUState* cpu) {
* mid-path bookkeeping / irq_deliver_eval that used to run every BB.
* Guest-visible timing unchanged (same non-delivery). LTO can collapse
* this into the VLC call sites. */
static uint32_t s_fast_maintenance;
if (!in_exception && !s_defer_switch_pending && (i_stat & i_mask) != 0) {
uint32_t sr = cpu->cop0[COP0_SR];
if (!(sr & 0x01u) || !(sr & (1u << 10))) {
@@ -831,12 +885,14 @@ void psx_check_interrupts(CPUState* cpu) {
if ((++s_fast_maintenance & 0x3FFFu) == 0) {
extern void savestate_poll(CPUState* cpu, uint32_t resume_pc);
extern void psx_netplay_poll_snap(CPUState* cpu, uint32_t resume_pc);
extern void psx_selfcheck_poll(CPUState* cpu, uint32_t resume_pc);
savestate_poll(cpu, s_compiled_interrupt_resume_pc);
/* MotK FMV/VLC live here — must flush pending RB snaps too. */
psx_netplay_poll_snap(cpu, s_compiled_interrupt_resume_pc);
psx_selfcheck_poll(cpu, s_compiled_interrupt_resume_pc);
debug_server_poll();
}
return;
PSX_CHECK_INTERRUPTS_RETURN();
}
}
@@ -881,15 +937,18 @@ void psx_check_interrupts(CPUState* cpu) {
if ((++s_fast_maintenance & 0x3FFFu) == 0) {
extern void savestate_poll(CPUState* cpu, uint32_t resume_pc);
extern void psx_netplay_poll_snap(CPUState* cpu, uint32_t resume_pc);
extern void psx_selfcheck_poll(CPUState* cpu, uint32_t resume_pc);
savestate_poll(cpu, s_compiled_interrupt_resume_pc);
psx_netplay_poll_snap(cpu, s_compiled_interrupt_resume_pc);
psx_selfcheck_poll(cpu, s_compiled_interrupt_resume_pc);
debug_server_poll();
}
return;
PSX_CHECK_INTERRUPTS_RETURN();
}
/* Idle skip advanced time and a device raised I_STAT — deliver below. */
}
if (in_exception && !(cpu->cop0[COP0_SR] & 0x01u)) return;
if (in_exception && !(cpu->cop0[COP0_SR] & 0x01u))
PSX_CHECK_INTERRUPTS_RETURN();
/* Mid path: unmasked IRQ already pending, ordinary compiled edge.
* MotK VLC / FMV spend most BB edges here (sticky CD/VBlank bits).
@@ -905,10 +964,12 @@ void psx_check_interrupts(CPUState* cpu) {
if ((total_checks & 0x3FFFu) == 0) {
extern void savestate_poll(CPUState* cpu, uint32_t resume_pc);
extern void psx_netplay_poll_snap(CPUState* cpu, uint32_t resume_pc);
extern void psx_selfcheck_poll(CPUState* cpu, uint32_t resume_pc);
savestate_poll(cpu, check_pc);
/* Sticky CD/VBlank mid-path is MotK's FMV hot edge — without this
* the RB snap ring never fills (pending save never polled). */
psx_netplay_poll_snap(cpu, check_pc);
psx_selfcheck_poll(cpu, check_pc);
debug_server_poll();
}
goto irq_deliver_eval;
@@ -954,8 +1015,10 @@ void psx_check_interrupts(CPUState* cpu) {
if (!in_exception) {
extern void savestate_poll(CPUState* cpu, uint32_t resume_pc);
extern void psx_netplay_poll_snap(CPUState* cpu, uint32_t resume_pc);
extern void psx_selfcheck_poll(CPUState* cpu, uint32_t resume_pc);
savestate_poll(cpu, s_last_interrupt_check_pc);
psx_netplay_poll_snap(cpu, s_last_interrupt_check_pc);
psx_selfcheck_poll(cpu, s_last_interrupt_check_pc);
}
/* Deferred cooperative thread switch: honor at the next real thread-save
@@ -1514,10 +1577,11 @@ irq_deliver_eval:
}
{
extern int psx_netplay_active(void);
extern int psx_selfcheck_enabled(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 if (psx_netplay_active() || psx_selfcheck_enabled())
s_str_mode = 3; /* netplay/selfcheck: TCB-stable always restore */
else
s_str_mode = 1;
}
@@ -1557,13 +1621,29 @@ irq_deliver_eval:
* live chain, and one hop later lands on an un-re-enterable mid-function
* PC — the top-level "execution completed, PC=0" abnormal exit (Tomba 2
* splash, frame 1385). A GENUINE in-handler switch (PCB[0] moved) skips
* this and is honored/deferred by the scheduler block below. */
* this and is honored/deferred by the scheduler block below.
*
* Exception: after netplay RB flush_resume, dispatch is top-level — there
* is no live native chain. Publishing pc=0 there is GUEST_EXIT. Prefer the
* compiled IRQ resume PC (or keep the post-RFE guest PC). */
if (g_exception_real_epc == (uint32_t)PSX_EXC_SENTINEL_PC &&
entry_tcb != 0u && entry_tcb == exit_tcb &&
(g_exc_escape_reason == PSX_EXC_ESCAPE_RFE_RETURN ||
g_exc_escape_reason == PSX_EXC_ESCAPE_SYSCALL_RETURN)) {
same_thread_resume = 1;
cpu->pc = 0; /* continue the interrupted live chain */
{
extern int psx_scheduler_top_level_resume_active(void);
if (psx_scheduler_top_level_resume_active()) {
uint32_t resume = s_compiled_interrupt_resume_pc;
if (resume == 0u)
resume = s_last_interrupt_check_pc;
if (resume != 0u)
cpu->pc = resume;
/* else keep post-RFE cpu->pc — never publish 0 */
} else {
cpu->pc = 0; /* continue the interrupted live chain */
}
}
}
int do_restore =
(g_exc_escape_reason == PSX_EXC_ESCAPE_LEGACY_SENTINEL || same_thread_resume);
@@ -1748,6 +1828,8 @@ irq_deliver_eval:
#undef COSIM_IRQ_NOTE
#undef COSIM_IRQ_TAKE_PC
#endif
if (np_present_after_irq)
gpu_vblank_flush_present();
#undef PSX_CHECK_INTERRUPTS_RETURN
}
+73 -23
View File
@@ -42,6 +42,7 @@ extern "C" void psx_event_step_conservative_env_init(void);
#endif
#include "psx_netplay.h"
#include "psx_netplay_rb.h"
#include "psx_selfcheck.h"
#include "psx_lobby_client.h"
#include "spu.h"
#include "audio_trace.h"
@@ -1252,7 +1253,9 @@ static uint64_t g_legacy_underruns = 0;
int g_audio_unmute_resync = 0;
/* Actual device rate the host opened at (bridge mode may differ from 44100;
* the T3 tap ring runs at this rate and its WAV dump must say so). */
extern "C" int g_audio_host_rate = 44100;
extern "C" {
int g_audio_host_rate = 44100;
}
static void sdl_drc_callback(void* /*user*/, Uint8* stream, int len) {
if (!s_drc_ready) { std::memset(stream, 0, (size_t)len); return; }
@@ -1994,7 +1997,8 @@ static void sdl_audio_pump(bool discard_output = false) {
* across the first ~50 ms of samples (sdl_audio_pump above). */
/* Bridge/legacy output health, surfaced through the audio_stats TCP command
* (debug_server.c) — no stderr probe; rule 3. */
extern "C" int psx_audio_out_stats(double *fill_ms, uint64_t *underruns,
extern "C" int psx_audio_out_stats(double *fill_ms, double *target_ms,
uint64_t *underruns,
uint64_t *overflow_drops, double *correction,
int *legacy, int *host_rate)
{
@@ -2005,6 +2009,7 @@ extern "C" int psx_audio_out_stats(double *fill_ms, uint64_t *underruns,
? (double)psx_sdl_audio_queued_size(sdl_audio_device)
/ (44100.0 * 4.0) * 1000.0
: 0.0;
*target_ms = 0.0; /* push queue — no DRC fill target */
*underruns = g_legacy_underruns;
*overflow_drops = 0;
*correction = 0.0;
@@ -2013,6 +2018,7 @@ extern "C" int psx_audio_out_stats(double *fill_ms, uint64_t *underruns,
rab_stats st;
rab_get_stats(&s_drc, &st);
*fill_ms = st.last_fill_ms;
*target_ms = s_drc.cfg.target_ms;
*underruns = st.underrun_events;
*overflow_drops = st.overflow_drops;
*correction = st.last_correction;
@@ -2252,11 +2258,12 @@ static void runtime_perf_diag_tick() {
RuntimePerfSnapshot current = runtime_perf_snapshot(now);
AudioTraceStats audio;
audio_trace_get_stats(&audio);
double fill_ms = 0.0, correction = 0.0;
double fill_ms = 0.0, target_ms = 0.0, correction = 0.0;
uint64_t underruns = 0, overflows = 0;
int legacy = 0, host_rate = 0;
psx_audio_out_stats(&fill_ms, &underruns, &overflows, &correction,
psx_audio_out_stats(&fill_ms, &target_ms, &underruns, &overflows, &correction,
&legacy, &host_rate);
(void)target_ms;
uint64_t up[6] = {0};
gl_renderer_runtime_diag(up);
uint64_t overlay_load_us = 0, overlay_load_max_us = 0, overlay_load_last_us = 0;
@@ -3196,6 +3203,8 @@ static void apply_input_override_to_sio(int override_word) {
if (!eff_analog) { st[0] = st[1] = st[2] = st[3] = 0x80; }
sio_set_pad_sticks(0, st[0], st[1], st[2], st[3]);
sio_request_pad_type(0, eff_analog);
psx_selfcheck_note_pad(0, w, st[0], st[1], st[2], st[3],
(uint8_t)(eff_analog ? 1 : 0));
}
/* Capture one SIO slot's host pad into a netplay/local blob. Returns 1 if a
@@ -3333,6 +3342,9 @@ static void apply_pad_slot_to_sio(int s, const PsxNetPad& pad) {
sio_set_pad_state_slot(s, pad.buttons);
sio_set_pad_sticks(s, pad.lx, pad.ly, pad.rx, pad.ry);
sio_request_pad_type(s, pad.analog ? 1 : 0);
/* Solo resim self-check records exactly what was applied this boundary. */
psx_selfcheck_note_pad(s, pad.buttons, pad.lx, pad.ly, pad.rx, pad.ry,
pad.analog);
}
/* Local human pad for delay-sync: sample the host PlayerInput selected for
@@ -3585,6 +3597,12 @@ static void netplay_barrier_admit(int override) {
}
static void sample_pad_into_sio(int override) {
/* Selfcheck fighter mash owns P1 when enabled (headless-safe). */
if (override < 0) {
uint16_t mash = 0xFFFFu;
if (psx_selfcheck_mash_override(&mash))
override = (int)mash;
}
if (override >= 0) {
apply_input_override_to_sio(override);
return;
@@ -3607,6 +3625,11 @@ static void sample_pad_into_sio(int override) {
}
static void sample_headless_pad_into_sio(int override) {
if (override < 0) {
uint16_t mash = 0xFFFFu;
if (psx_selfcheck_mash_override(&mash))
override = (int)mash;
}
if (override >= 0) {
apply_input_override_to_sio(override);
return;
@@ -3860,6 +3883,10 @@ struct NetplayVblankEpilogue {
/* Called from gpu_vblank_tick() at each simulated vblank. */
static NetplayVblankEpilogue sdl_vblank_present_body(void) {
NetplayVblankEpilogue ep{};
/* Guest quantum for this vblank is complete. Drop top-level-resume armed
* by any resume_at (savestate / selfcheck / RB) during that quantum — the
* next tick has a live native chain under its dispatch again. */
psx_scheduler_top_level_resume_clear();
int probe_turbo = 0;
int probe_reached = 0;
struct PostLoadProbeScope {
@@ -4093,10 +4120,19 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
"tap pads forced digital)\n",
sio_get_multitap_port() + 1);
}
if (g_headless)
sample_headless_pad_into_sio(override);
else
sample_pad_into_sio(override);
/* Solo resim self-check replay republishes the recorded rows itself —
* live sampling must not touch SIO during the replay window. */
if (!psx_selfcheck_replay_input()) {
if (g_headless)
sample_headless_pad_into_sio(override);
else
sample_pad_into_sio(override);
}
/* Offline vblank boundary: record/replay/compare (PSX_RB_SELFCHECK).
* Defer opening a window while multitap arming is still pending —
* the arm writes SIO once, host-latched, and would not replay. */
psx_selfcheck_finish_frame(
(g_offline_pad_count >= 3 && !sio_get_multitap()) ? 1 : 0);
}
/* Latency ring: open this present cycle's slot, stamping when input was
@@ -4105,7 +4141,7 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
/* Post-savestate CD delay boost (ReadTOC/seek clamp window).
* Skip during rollback resim — boost is host-local and not in the CD snap. */
if (!psx_netplay_is_resimulating())
if (!psx_netplay_is_resimulating() && !psx_selfcheck_resim_active())
cdrom_savestate_boost_vblank();
/* Turbo-active test shared by the pacing/present gate below. */
@@ -4115,9 +4151,12 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
int load_run_value = 0;
static int load_run = 0;
static int release_run = 0;
if (g_turbo_loads_enabled && !psx_netplay_active()) {
if (g_turbo_loads_enabled && !psx_netplay_active() &&
!psx_selfcheck_resim_active()) {
/* Require a short sustained predicate on entry, then retain turbo over
* a short false gap after it has engaged. Netplay keeps wall pacing. */
* a short false gap after it has engaged. Netplay / selfcheck keep
* wall-pacing off via their own gates; turbo engage hysteresis must
* not leak host ambient across resim passes. */
if (logical_load_active) {
if (load_run < (1 << 20)) load_run++;
if (load_run >= TURBO_LOADS_ENGAGE_FRAMES || release_run > 0) {
@@ -4164,7 +4203,8 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
int fmv_skip_active = 0;
/* Never inject START / poke movie totals under netplay — host-side skip
* forks peers (and stomps SIO after sealed publish during resim). */
if (g_auto_skip_fmv && !psx_netplay_active()) {
if (g_auto_skip_fmv && !psx_netplay_active() &&
!psx_selfcheck_resim_active()) {
uint32_t mc = mdec_get_decode_count();
int mdec_decoding = (mc != s_fmv_skip_last_mdec);
s_fmv_skip_last_mdec = mc;
@@ -4198,7 +4238,7 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
* retains the hard mute because it deliberately fast-forwards a movie.
* Rollback resim: skip host audio pump (uncapped frames) so SPU/CD audio
* paths cannot fork peers during the episode window. */
if (!psx_netplay_is_resimulating()) {
if (!psx_netplay_is_resimulating() && !psx_selfcheck_resim_active()) {
sdl_audio_update(fmv_skip_active,
turbo_loads_active && g_turbo_audio_sink_enabled);
}
@@ -4269,22 +4309,24 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
}
}
/* Netplay FMV: skip present every other depth24 vblank to cut GPU cost
* (admit + wall pace still run in the epilogue every tick). Do NOT skip
* pace here — that let MotK movies run ~90fps once decode was fast enough. */
/* Netplay FMV: present 1 of every 4 depth24 vblanks. SW RGB888 scanout
* dominates guest ms; admit + wall pace still run every tick in the
* epilogue. Do NOT skip pace — MotK ran ~90fps when decode was fast and
* pace was skipped here. */
if (psx_netplay_active() && gpu_display_is_depth24()) {
if (s_netplay_depth24_present_skip) {
s_netplay_depth24_present_skip = 0;
if (s_netplay_depth24_present_skip > 0) {
s_netplay_depth24_present_skip--;
return ep;
}
s_netplay_depth24_present_skip = 1;
s_netplay_depth24_present_skip = 3; /* skip next three presents */
} else {
s_netplay_depth24_present_skip = 0;
}
/* Offline wall-clock pacing before present. Netplay paces in the epilogue
* AFTER present so Swap overlaps the peer's guest quantum. */
if (!psx_netplay_active()) {
* AFTER present so Swap overlaps the peer's guest quantum. Self-check
* replay runs uncapped like netplay resim. */
if (!psx_netplay_active() && !psx_selfcheck_resim_active()) {
uint64_t perf_start = runtime_perf_section_begin();
frame_pacer_wait(&s_frame_pacer, g_frame_period_ms);
runtime_perf_section_end(perf_start, &g_runtime_perf.pacer_ticks);
@@ -4293,8 +4335,10 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
/* Low-latency input: the early sample above is now ~one pacer-wait old.
* Refresh the device state and re-sample right before present so the next
* CPU frame reads near-fresh input (the dominant input->photon cost on a
* vsync-light box). Re-stamp the ring's input mark to measure from here. */
if (g_low_latency_input) {
* vsync-light box). Re-stamp the ring's input mark to measure from here.
* Self-check record keeps pads boundary-latched (netplay tick
* semantics) — the mid-frame re-sample would fork the resim. */
if (g_low_latency_input && !psx_selfcheck_input_locked()) {
SDL_GameControllerUpdate(); /* refresh pad state after the wait */
SDL_PumpEvents(); /* refresh keyboard state */
sample_pad_into_sio(override);
@@ -4674,6 +4718,10 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
static void sdl_vblank_present(void) {
NetplayVblankEpilogue ep = sdl_vblank_present_body();
/* Selfcheck span-end rewind: after present-body C++ RAII, before any
* further guest progress. Longjmps on success — keeps every resim load
* on the same VBlank boundary (BB fast-poll tails forked #2 vs #3). */
psx_selfcheck_flush_load();
if (!ep.do_epilogue)
return;
if (!psx_return_to_lobby_requested())
@@ -8933,6 +8981,8 @@ session_reboot:
debug_server_set_cpu(&cpu);
/* Master digests / FRAME_COMMIT for netplay hash_confirm watermark. */
psx_netplay_bind_cpu(&cpu);
/* Solo rollback resim self-check (PSX_RB_SELFCHECK=1, offline only). */
psx_selfcheck_init(&cpu, memory_get_bios_checksum(), game_entry_pc);
/* Execute. */
std::fprintf(stdout, "psxrecomp runtime: executing from PC=0x%08X\n", cpu.pc);
+316 -41
View File
@@ -6,6 +6,17 @@
#include <stdlib.h>
#include <string.h>
/* x86-64 always has SSE2; MSVC x64 does not define __SSE2__. */
#if defined(__SSE2__) || defined(_M_X64) || defined(_M_AMD64) || \
(defined(_M_IX86_FP) && _M_IX86_FP >= 2)
#include <emmintrin.h>
#define MDEC_HAVE_SSE2 1
#endif
#if !defined(MDEC_HAVE_SSE2) && (defined(__ARM_NEON) || defined(__ARM_NEON__))
#include <arm_neon.h>
#define MDEC_HAVE_NEON 1
#endif
extern uint64_t s_frame_count;
/* FMV-activity detector: host display-frame stamp of the newest colour
@@ -236,73 +247,172 @@ static int mask9_clamp_s8(int v) {
return v;
}
/* Faithful R3000A MDEC IDCT (Beetle IDCT/IDCT_1D_Multi, mdec.cpp:243-291).
* Two separable 1-D passes over the >>3 scale matrix: pass 1 keeps int16 and
* transposes (out[x*8+col]); pass 2 clamps to int8 via Mask9ClampS8. Rounding
* is (sum + 0x4000) >> 15 (vs the old (sum+0xFFF)/0x2000 + per-tap /8, whose
* bias differed from hardware → the washed/wrong-contrast FMV). The block buffer
* holds int8-range samples on return. */
static void idct_block(int16_t *block) {
/* DC-only fast path: bit-identical to the loops below when AC is zero
* (common in dark/flat FMV macroblocks). Pass1 only fills tmp[*][0];
* pass2 only multiplies by scale[*][0]. */
/* Faithful R3000A MDEC IDCT (Beetle IDCT/IDCT_1D_Multi). Two separable 1-D
* passes over the >>3 scale matrix: pass 1 keeps int16 and transposes
* (out[x*8+col]); pass 2 clamps via Mask9ClampS8. Rounding (sum+0x4000)>>15.
* SSE2 path matches Beetle's madd_epi16 reduce (bit-identical to scalar). */
static void idct_block_dc_only(int16_t *block)
{
int16_t tmp0[8];
int dc = block[0];
int x, col;
for (x = 0; x < 8; x++) {
int sum = dc * (int)mdec.scale[x * 8];
tmp0[x] = (int16_t)((sum + 0x4000) >> 15);
}
for (col = 0; col < 8; col++) {
for (x = 0; x < 8; x++) {
int sum = (int)tmp0[col] * (int)mdec.scale[x * 8];
block[col * 8 + x] =
(int16_t)mask9_clamp_s8((sum + 0x4000) >> 15);
}
}
}
static void idct_block_scalar(int16_t *block)
{
int ac = 0;
for (int i = 1; i < 64; i++) ac |= block[i];
int i, col, x, u;
int16_t tmp[64];
for (i = 1; i < 64; i++)
ac |= block[i];
if (!ac) {
int16_t tmp0[8];
int dc = block[0];
for (int x = 0; x < 8; x++) {
int sum = dc * (int)mdec.scale[x * 8];
tmp0[x] = (int16_t)((sum + 0x4000) >> 15);
}
for (int col = 0; col < 8; col++) {
for (int x = 0; x < 8; x++) {
int sum = (int)tmp0[col] * (int)mdec.scale[x * 8];
block[col * 8 + x] =
(int16_t)mask9_clamp_s8((sum + 0x4000) >> 15);
}
}
idct_block_dc_only(block);
return;
}
int16_t tmp[64];
/* pass 1 — int16 out, transposed. Skip all-zero source columns
* (sparse AC after zig-zag); zero column ⇒ zero tmp[*][col]. */
for (int col = 0; col < 8; col++) {
for (col = 0; col < 8; col++) {
const int16_t *src = block + col * 8;
int col_or = (int)src[0] | (int)src[1] | (int)src[2] | (int)src[3]
| (int)src[4] | (int)src[5] | (int)src[6] | (int)src[7];
int col_or = (int)src[0] | (int)src[1] | (int)src[2] | (int)src[3] |
(int)src[4] | (int)src[5] | (int)src[6] | (int)src[7];
if (!col_or) {
for (int x = 0; x < 8; x++) tmp[x * 8 + col] = 0;
for (x = 0; x < 8; x++)
tmp[x * 8 + col] = 0;
continue;
}
for (int x = 0; x < 8; x++) {
for (x = 0; x < 8; x++) {
int sum = 0;
const int16_t *sc = mdec.scale + x * 8;
for (int u = 0; u < 8; u++)
for (u = 0; u < 8; u++)
sum += (int)src[u] * (int)sc[u];
tmp[x * 8 + col] = (int16_t)((sum + 0x4000) >> 15);
}
}
/* pass 2 — int8 out (Mask9ClampS8), no transpose. Same zero-row skip. */
for (int col = 0; col < 8; col++) {
for (col = 0; col < 8; col++) {
const int16_t *src = tmp + col * 8;
int col_or = (int)src[0] | (int)src[1] | (int)src[2] | (int)src[3]
| (int)src[4] | (int)src[5] | (int)src[6] | (int)src[7];
int col_or = (int)src[0] | (int)src[1] | (int)src[2] | (int)src[3] |
(int)src[4] | (int)src[5] | (int)src[6] | (int)src[7];
if (!col_or) {
for (int x = 0; x < 8; x++) block[col * 8 + x] = 0;
for (x = 0; x < 8; x++)
block[col * 8 + x] = 0;
continue;
}
for (int x = 0; x < 8; x++) {
for (x = 0; x < 8; x++) {
int sum = 0;
const int16_t *sc = mdec.scale + x * 8;
for (int u = 0; u < 8; u++)
for (u = 0; u < 8; u++)
sum += (int)src[u] * (int)sc[u];
block[col * 8 + x] = (int16_t)mask9_clamp_s8((sum + 0x4000) >> 15);
block[col * 8 + x] =
(int16_t)mask9_clamp_s8((sum + 0x4000) >> 15);
}
}
}
#if defined(MDEC_HAVE_SSE2)
/* Horizontal sum of 4×i32 after madd_epi16 — same reduce as Beetle mdec.c. */
static int idct_sse2_dot8(const int16_t *src8, const int16_t *scale8)
{
__m128i c = _mm_loadu_si128((const __m128i *)src8);
__m128i m = _mm_loadu_si128((const __m128i *)scale8);
__m128i sum = _mm_madd_epi16(m, c);
int32_t tmp[4] __attribute__((aligned(16)));
sum = _mm_add_epi32(sum, _mm_shuffle_epi32(sum, _MM_SHUFFLE(0, 1, 2, 3)));
sum = _mm_add_epi32(sum, _mm_shuffle_epi32(sum, _MM_SHUFFLE(0, 0, 0, 1)));
_mm_store_si128((__m128i *)tmp, sum);
return tmp[0];
}
static void idct_block_sse2(int16_t *block)
{
int ac = 0;
int i, col, x;
int16_t tmp[64] __attribute__((aligned(16)));
for (i = 1; i < 64; i++)
ac |= block[i];
if (!ac) {
idct_block_dc_only(block);
return;
}
for (col = 0; col < 8; col++) {
const int16_t *src = block + col * 8;
int col_or = (int)src[0] | (int)src[1] | (int)src[2] | (int)src[3] |
(int)src[4] | (int)src[5] | (int)src[6] | (int)src[7];
if (!col_or) {
for (x = 0; x < 8; x++)
tmp[x * 8 + col] = 0;
continue;
}
for (x = 0; x < 8; x++) {
int sum = idct_sse2_dot8(src, mdec.scale + x * 8);
tmp[x * 8 + col] = (int16_t)((sum + 0x4000) >> 15);
}
}
for (col = 0; col < 8; col++) {
const int16_t *src = tmp + col * 8;
int col_or = (int)src[0] | (int)src[1] | (int)src[2] | (int)src[3] |
(int)src[4] | (int)src[5] | (int)src[6] | (int)src[7];
if (!col_or) {
for (x = 0; x < 8; x++)
block[col * 8 + x] = 0;
continue;
}
for (x = 0; x < 8; x++) {
int sum = idct_sse2_dot8(src, mdec.scale + x * 8);
block[col * 8 + x] =
(int16_t)mask9_clamp_s8((sum + 0x4000) >> 15);
}
}
}
static void idct_simd_selfcheck(void)
{
uint32_t seed = 0xC0FFEEu;
int n;
for (n = 0; n < 64; n++)
mdec.scale[n] = (int16_t)(((n % 9) - 4) * 256);
/* Deterministic patterns + PRNG — abort if SSE2 ever drifts from scalar. */
for (n = 0; n < 512; n++) {
int16_t a[64], b[64];
int i;
for (i = 0; i < 64; i++) {
seed = seed * 1664525u + 1013904223u;
a[i] = (int16_t)((int)(seed >> 16) - 32768);
if (n < 8)
a[i] = (i == 0) ? (int16_t)(n * 100) : 0; /* DC-only */
b[i] = a[i];
}
idct_block_scalar(a);
idct_block_sse2(b);
if (memcmp(a, b, sizeof(a)) != 0)
abort();
}
}
static void idct_block(int16_t *block)
{
idct_block_sse2(block);
}
#else
static void idct_block(int16_t *block)
{
idct_block_scalar(block);
}
#endif
static int decode_rle_block(int16_t *block, const uint8_t *quant,
uint32_t *pos, uint32_t end) {
memset(block, 0, 64 * sizeof(int16_t));
@@ -399,6 +509,96 @@ static void append_luma_block(const int16_t *yblk) {
mdec.output_size += 64u;
}
#if defined(MDEC_HAVE_SSE2)
/* Beetle mdec.c EncodeRow24 — 8 luma + 4 chroma → 24 RGB bytes, bit-exact. */
#define MDEC_MUL32(a, b) \
_mm_or_si128( \
_mm_and_si128(_mm_mul_epu32((a), (b)), _mm_set1_epi64x(0xFFFFFFFFull)),\
_mm_slli_epi64( \
_mm_mul_epu32(_mm_srli_epi64((a), 32), _mm_srli_epi64((b), 32)), \
32))
#define MDEC_M9(v) \
_mm_min_epi16(_mm_max_epi16(_mm_srai_epi16(_mm_slli_epi16((v), 7), 7), \
_mm_set1_epi16(-128)), \
_mm_set1_epi16(127))
static void mdec_encode_row24_sse2(const int16_t *by, const int16_t *cb4,
const int16_t *cr4, uint8_t rgb_xor,
uint8_t *out)
{
int16_t cbd[8], crd[8];
int8_t by8[8];
int l, i;
__m128i R, G, B;
__m128i flip = _mm_set1_epi16((short)(0x80 ^ rgb_xor));
uint8_t r8[8], g8[8], b8[8];
__m128i y16, CB, CR, cb_lo, cb_hi, cr_lo, cr_hi, y_lo, y_hi;
const __m128i k359 = _mm_set1_epi32(359);
const __m128i k454 = _mm_set1_epi32(454);
const __m128i km88 = _mm_set1_epi32(-88);
const __m128i km183 = _mm_set1_epi32(-183);
const __m128i bias = _mm_set1_epi32(0x80);
__m128i r_lo, r_hi, b_lo, b_hi, a0, a1, c0, c1, g_lo, g_hi;
for (l = 0; l < 8; l++)
by8[l] = (int8_t)by[l];
for (l = 0; l < 4; l++) {
cbd[2 * l] = cbd[2 * l + 1] = (int16_t)cb4[l];
crd[2 * l] = crd[2 * l + 1] = (int16_t)cr4[l];
}
y16 = _mm_srai_epi16(
_mm_slli_epi16(
_mm_unpacklo_epi8(_mm_loadl_epi64((const __m128i *)by8),
_mm_setzero_si128()),
8),
8);
CB = _mm_loadu_si128((const __m128i *)cbd);
CR = _mm_loadu_si128((const __m128i *)crd);
cb_lo = _mm_srai_epi32(_mm_unpacklo_epi16(CB, CB), 16);
cb_hi = _mm_srai_epi32(_mm_unpackhi_epi16(CB, CB), 16);
cr_lo = _mm_srai_epi32(_mm_unpacklo_epi16(CR, CR), 16);
cr_hi = _mm_srai_epi32(_mm_unpackhi_epi16(CR, CR), 16);
y_lo = _mm_srai_epi32(_mm_unpacklo_epi16(y16, y16), 16);
y_hi = _mm_srai_epi32(_mm_unpackhi_epi16(y16, y16), 16);
r_lo = _mm_add_epi32(
y_lo, _mm_srai_epi32(_mm_add_epi32(MDEC_MUL32(k359, cr_lo), bias), 8));
r_hi = _mm_add_epi32(
y_hi, _mm_srai_epi32(_mm_add_epi32(MDEC_MUL32(k359, cr_hi), bias), 8));
b_lo = _mm_add_epi32(
y_lo, _mm_srai_epi32(_mm_add_epi32(MDEC_MUL32(k454, cb_lo), bias), 8));
b_hi = _mm_add_epi32(
y_hi, _mm_srai_epi32(_mm_add_epi32(MDEC_MUL32(k454, cb_hi), bias), 8));
a0 = _mm_andnot_si128(_mm_set1_epi32(0x1F), MDEC_MUL32(km88, cb_lo));
a1 = _mm_andnot_si128(_mm_set1_epi32(0x1F), MDEC_MUL32(km88, cb_hi));
c0 = _mm_andnot_si128(_mm_set1_epi32(0x07), MDEC_MUL32(km183, cr_lo));
c1 = _mm_andnot_si128(_mm_set1_epi32(0x07), MDEC_MUL32(km183, cr_hi));
g_lo = _mm_add_epi32(
y_lo,
_mm_srai_epi32(_mm_add_epi32(_mm_add_epi32(a0, c0), bias), 8));
g_hi = _mm_add_epi32(
y_hi,
_mm_srai_epi32(_mm_add_epi32(_mm_add_epi32(a1, c1), bias), 8));
R = MDEC_M9(_mm_packs_epi32(r_lo, r_hi));
G = MDEC_M9(_mm_packs_epi32(g_lo, g_hi));
B = MDEC_M9(_mm_packs_epi32(b_lo, b_hi));
R = _mm_xor_si128(_mm_and_si128(R, _mm_set1_epi16(0xFF)), flip);
G = _mm_xor_si128(_mm_and_si128(G, _mm_set1_epi16(0xFF)), flip);
B = _mm_xor_si128(_mm_and_si128(B, _mm_set1_epi16(0xFF)), flip);
_mm_storel_epi64((__m128i *)r8, _mm_packus_epi16(R, R));
_mm_storel_epi64((__m128i *)g8, _mm_packus_epi16(G, G));
_mm_storel_epi64((__m128i *)b8, _mm_packus_epi16(B, B));
for (i = 0; i < 8; i++) {
out[i * 3 + 0] = r8[i];
out[i * 3 + 1] = g8[i];
out[i * 3 + 2] = b8[i];
}
}
#undef MDEC_MUL32
#undef MDEC_M9
#endif /* MDEC_HAVE_SSE2 */
static void append_color_macroblock(const int16_t *crblk, const int16_t *cbblk,
const int16_t yblk[4][64]) {
/* 16×16: 512 bytes @15bpp, 768 @24bpp — reserve once per MB. */
@@ -406,6 +606,27 @@ static void append_color_macroblock(const int16_t *crblk, const int16_t *cbblk,
uint8_t *out = output_reserve(need);
if (!out) return;
uint8_t *p = out;
#if defined(MDEC_HAVE_SSE2)
/* MotK FMV is 24bpp — row SIMD (Beetle EncodeRow24). 16bpp stays scalar. */
if (mdec.output_depth != 3) {
uint8_t rgb_xor = mdec.output_signed ? 0x80u : 0x00u;
int py;
for (py = 0; py < 16; py++) {
int y_left = (py >= 8 ? 2 : 0);
int y_right = y_left + 1;
int ly = py & 7;
int crow = (py >> 1) * 8;
mdec_encode_row24_sse2(&yblk[y_left][ly * 8], &cbblk[crow],
&crblk[crow], rgb_xor, p);
p += 24;
mdec_encode_row24_sse2(&yblk[y_right][ly * 8], &cbblk[crow + 4],
&crblk[crow + 4], rgb_xor, p);
p += 24;
}
mdec.output_size += need;
return;
}
#endif
for (int py = 0; py < 16; py++) {
for (int px = 0; px < 16; px++) {
int y_index = (py >= 8 ? 2 : 0) + (px >= 8 ? 1 : 0);
@@ -568,6 +789,60 @@ void mdec_init(void) {
}
mdec.output_depth = 3;
mdec.current_block = 4;
#if defined(MDEC_HAVE_SSE2)
idct_simd_selfcheck();
memset(mdec.scale, 0, sizeof(mdec.scale));
/* YCbCr row vs scalar emit — MotK FMV path (depth 2 = 24bpp). */
{
uint32_t seed = 0xBADC0DEEu;
int n, i;
mdec.output_depth = 2;
mdec.output_signed = 0;
for (n = 0; n < 256; n++) {
int16_t by[8], cb4[4], cr4[4];
uint8_t sse[24], ref[24];
uint8_t *rp = ref;
for (i = 0; i < 8; i++) {
seed = seed * 1664525u + 1013904223u;
by[i] = (int16_t)((int)(seed % 255u) - 128);
}
for (i = 0; i < 4; i++) {
seed = seed * 1664525u + 1013904223u;
cb4[i] = (int16_t)((int)(seed % 255u) - 128);
seed = seed * 1664525u + 1013904223u;
cr4[i] = (int16_t)((int)(seed % 255u) - 128);
}
mdec_encode_row24_sse2(by, cb4, cr4, 0, sse);
for (i = 0; i < 8; i++)
rp = emit_rgb_pixel(rp, by[i], cr4[i >> 1], cb4[i >> 1]);
if (memcmp(sse, ref, 24) != 0)
abort();
}
mdec.output_signed = 1;
for (n = 0; n < 64; n++) {
int16_t by[8], cb4[4], cr4[4];
uint8_t sse[24], ref[24];
uint8_t *rp = ref;
for (i = 0; i < 8; i++) {
seed = seed * 1664525u + 1013904223u;
by[i] = (int16_t)((int)(seed % 255u) - 128);
}
for (i = 0; i < 4; i++) {
seed = seed * 1664525u + 1013904223u;
cb4[i] = (int16_t)((int)(seed % 255u) - 128);
seed = seed * 1664525u + 1013904223u;
cr4[i] = (int16_t)((int)(seed % 255u) - 128);
}
mdec_encode_row24_sse2(by, cb4, cr4, 0x80u, sse);
for (i = 0; i < 8; i++)
rp = emit_rgb_pixel(rp, by[i], cr4[i >> 1], cb4[i >> 1]);
if (memcmp(sse, ref, 24) != 0)
abort();
}
mdec.output_signed = 0;
mdec.output_depth = 3;
}
#endif
}
uint32_t mdec_read(uint32_t addr) {
+28
View File
@@ -267,6 +267,15 @@ void psx_kernel_bless_stats(uint64_t out[6]) {
out[5] = kbless_invalidations;
}
void psx_kernel_bless_resync_after_restore(void) {
/* kbless_state is host-only. CLEAN/MISMATCH are sticky across guest
* stores only via kbless_note_write; savestate RAM memcpy never hits
* that path. Leaving MISMATCH blocks native forever against restored
* bytes that may match ROM again; leaving CLEAN skips re-verify against
* restored patches. Both fork RB/selfcheck peers. */
memset(kbless_state, KBLESS_UNKNOWN, sizeof(kbless_state));
}
static inline void dirty_ram_mark_kernel_write(uint32_t phys) {
if (phys >= DIRTY_RAM_KERNEL_TRACK_BYTES) return;
dirty_ram_mark_page(phys);
@@ -638,11 +647,30 @@ uint32_t overlay_watch_pagegen_sum(uint32_t phys, uint32_t len) {
* bumps overlay_page_gen. Without this, ENTRY_VALID overlays keep the gen-gated
* fast path and run native code against restored bytes they were not validated
* for — hang / freeze after the restored frame presents. */
void dirty_ram_text_guard_resync_after_restore(void) {
/* text_diverged_bitmap is sticky: once a page's entry bytes fail the
* reference compare, native stays blocked forever. That is correct for
* forward sim, but after a savestate/RB rewind the restored RAM may
* match the reference again — leaving the pre-load sticky bit forces
* dirty-interp where the peer (or the prior resim) still runs native,
* forking MotK selfcheck warm #2vs#3 at matched clocks (win#118 class:
* cold≡0, warm FAIL; post-span irq_resume also drifts). Drop both
* host-only text-guard bitmaps; live writes re-arm modified, and the
* next native_ok compare re-decides diverge against restored bytes. */
memset(text_modified_bitmap, 0, sizeof(text_modified_bitmap));
memset(text_diverged_bitmap, 0, sizeof(text_diverged_bitmap));
g_text_diverged_pages = 0;
}
void overlay_watch_invalidate_after_ram_restore(void) {
for (uint32_t pg = 0; pg < DIRTY_RAM_PAGE_COUNT; pg++)
overlay_page_gen[pg]++;
extern void overlay_loader_note_code_write(void);
extern void overlay_loader_resync_validation_after_restore(void);
overlay_loader_note_code_write();
dirty_ram_text_guard_resync_after_restore();
psx_kernel_bless_resync_after_restore();
overlay_loader_resync_validation_after_restore();
}
static inline void overlay_watch_note_write(uint32_t phys, uint32_t size) {
+41
View File
@@ -112,3 +112,44 @@ uint8_t netplay_hc_peek_mismatch(const NetplayHashConfirm* hc, uint32_t* tick_ou
if (peer_out) *peer_out = pd;
return 1u;
}
uint8_t netplay_hc_heal_stale_gap(NetplayHashConfirm* hc) {
uint32_t next;
uint32_t best = 0u;
uint8_t have_best = 0u;
uint32_t i;
uint32_t ld = 0, pd = 0;
if (!hc || !hc->resolved_valid)
return 0u;
if (hc->resolved_through == 0xffffffffu)
return 0u;
next = hc->resolved_through + 1u;
/* Still have both digests for next: let try_advance / peek_mismatch own it. */
if (local_at(hc, next, &ld) && peer_at(hc, next, &pd))
return 0u;
/* Incomplete (one side only) — wait; do not jump over a pending commit. */
if (local_at(hc, next, NULL) || peer_at(hc, next, NULL))
return 0u;
/* next aged out of the ring. Refuse heal if any ring slot above the
* watermark still records a local≠peer pair (live fork still visible). */
for (i = 0; i < NETPLAY_HC_RING; i++) {
uint32_t t;
if (!hc->local_valid[i] || !hc->peer_valid[i])
continue;
if (hc->local_tick[i] != hc->peer_tick[i])
continue;
t = hc->local_tick[i];
if (t <= hc->resolved_through)
continue;
if (hc->local_digest[i] != hc->peer_digest[i])
return 0u; /* known fork still in ring */
if (!have_best || t > best) {
best = t;
have_best = 1u;
}
}
if (!have_best || best <= hc->resolved_through)
return 0u;
hc->resolved_through = best;
return 1u;
}
+23 -30
View File
@@ -105,21 +105,23 @@ void netplay_core_digest_parts(const CPUState* cpu, NetplayCoreParts* out)
uint32_t crc_tim = 0xFFFFFFFFu;
uint32_t crc_ram = 0xFFFFFFFFu;
uint32_t crc_drt = 0xFFFFFFFFu;
uint32_t crc = 0xFFFFFFFFu;
uint32_t fold = 0xFFFFFFFFu;
uint16_t counter[3], target[3];
uint32_t mode[3], frac[3];
int32_t irq_line[3];
uint32_t wc;
uint32_t i;
uint8_t* ram;
uint32_t cpu_h, clk_h, tim_h, ram_h, drt_h;
if (out)
memset(out, 0, sizeof(*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. */
/* Single pass per partition, then fold the five digests into core.
* Old path streamed every byte into BOTH part CRCs and a combined CRC —
* 2× full RDRAM (~4 MiB CRC/frame) and dominated rollback FPS. */
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));
@@ -129,24 +131,12 @@ void netplay_core_digest_parts(const CPUState* cpu, NetplayCoreParts* out)
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));
crc = crc32_update(crc, (const uint8_t*)&cpu->lo, sizeof(cpu->lo));
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;
crc_clk = crc32_update(crc_clk, (const uint8_t*)&cyc, sizeof(cyc));
crc_clk = crc32_update(crc_clk, (const uint8_t*)&i_stat, sizeof(i_stat));
crc_clk = crc32_update(crc_clk, (const uint8_t*)&i_mask, sizeof(i_mask));
crc = crc32_update(crc, (const uint8_t*)&cyc, sizeof(cyc));
crc = crc32_update(crc, (const uint8_t*)&i_stat, sizeof(i_stat));
crc = crc32_update(crc, (const uint8_t*)&i_mask, sizeof(i_mask));
}
timers_get_snapshot(counter, mode, target, irq_line, frac);
@@ -155,32 +145,35 @@ void netplay_core_digest_parts(const CPUState* cpu, NetplayCoreParts* out)
crc_tim = crc32_update(crc_tim, (const uint8_t*)target, sizeof(target));
crc_tim = crc32_update(crc_tim, (const uint8_t*)irq_line, sizeof(irq_line));
crc_tim = crc32_update(crc_tim, (const uint8_t*)frac, sizeof(frac));
crc = crc32_update(crc, (const uint8_t*)counter, sizeof(counter));
crc = crc32_update(crc, (const uint8_t*)mode, sizeof(mode));
crc = crc32_update(crc, (const uint8_t*)target, sizeof(target));
crc = crc32_update(crc, (const uint8_t*)irq_line, sizeof(irq_line));
crc = crc32_update(crc, (const uint8_t*)frac, sizeof(frac));
ram = memory_get_ram_ptr();
if (ram) {
if (ram)
crc_ram = crc32_update(crc_ram, ram, NP_RAM_SIZE);
crc = crc32_update(crc, ram, NP_RAM_SIZE);
}
wc = dirty_ram_get_bitmap_word_count();
for (i = 0; i < wc; i++) {
uint32_t w = dirty_ram_get_bitmap_word(i);
crc_drt = crc32_update(crc_drt, (const uint8_t*)&w, sizeof(w));
crc = crc32_update(crc, (const uint8_t*)&w, sizeof(w));
}
cpu_h = crc_cpu ^ 0xFFFFFFFFu;
clk_h = crc_clk ^ 0xFFFFFFFFu;
tim_h = crc_tim ^ 0xFFFFFFFFu;
ram_h = crc_ram ^ 0xFFFFFFFFu;
drt_h = crc_drt ^ 0xFFFFFFFFu;
fold = crc32_update(fold, (const uint8_t*)&cpu_h, sizeof(cpu_h));
fold = crc32_update(fold, (const uint8_t*)&clk_h, sizeof(clk_h));
fold = crc32_update(fold, (const uint8_t*)&tim_h, sizeof(tim_h));
fold = crc32_update(fold, (const uint8_t*)&ram_h, sizeof(ram_h));
fold = crc32_update(fold, (const uint8_t*)&drt_h, sizeof(drt_h));
if (out) {
out->cpu = crc_cpu ^ 0xFFFFFFFFu;
out->clock_irq = crc_clk ^ 0xFFFFFFFFu;
out->timers = crc_tim ^ 0xFFFFFFFFu;
out->ram = crc_ram ^ 0xFFFFFFFFu;
out->dirty = crc_drt ^ 0xFFFFFFFFu;
out->core = crc ^ 0xFFFFFFFFu;
out->cpu = cpu_h;
out->clock_irq = clk_h;
out->timers = tim_h;
out->ram = ram_h;
out->dirty = drt_h;
out->core = fold ^ 0xFFFFFFFFu;
}
}
+54 -1
View File
@@ -431,6 +431,31 @@ static int cand_selected_for_diff(const Candidate *c) {
return 0;
}
/* ---- Load freeze (rollback resim / selfcheck) --------------------------
* Overlay DLL registration is host-only and not part of boot_state. A lazy
* load mid-resim-pass#1 that is visible at the start of pass#2 changes
* native-vs-interp BB boundaries and forks MotK wait-loop GPRs at matched
* guest clocks. */
static int s_load_freeze = 0;
void overlay_loader_set_load_freeze(int freeze)
{
s_load_freeze = freeze ? 1 : 0;
}
int overlay_loader_load_frozen(void) { return s_load_freeze; }
static int overlay_loads_allowed(void)
{
if (s_load_freeze)
return 0;
{
extern int psx_netplay_is_resimulating(void);
if (psx_netplay_is_resimulating())
return 0;
}
return 1;
}
/* ---- Counters (surfaced via overlay_loader_status) --------------------- */
static int s_ndlls = 0; /* DLLs LoadLibrary'd */
static uint64_t s_load_total_us = 0;
@@ -2797,12 +2822,33 @@ static void mark_checked(uint32_t region_start) {
if (s_nchecked < MAX_CHECKED) s_checked[s_nchecked++] = region_start;
}
void overlay_loader_clear_lazy_miss(void)
{
lazy_miss_invalidate_loader();
s_nchecked = 0;
}
void overlay_loader_resync_validation_after_restore(void)
{
/* Host-only validation memos. Page gens were already bumped by
* overlay_watch_invalidate_after_ram_restore; force every candidate off
* the gen fast-path (including ENTRY_INVALID + gen==val_gen skips, and
* nranges==0 bodies whose gensum never moves). Static-match cache is the
* same class for AOT game/BIOS overlays. */
int i;
for (i = 0; i < s_cand_n; i++)
s_cand[i].val_gen ^= 0x80000000u;
#ifdef PSX_HAS_OVERLAY_DISPATCH
memset(s_static_match_cache, 0, sizeof(s_static_match_cache));
#endif
}
/* Re-scan the cache dir for DLLs compiled after init (step 2.8 autocompile)
* and clear the checked-regions memo so the next dispatch into a window
* region reconsiders the cache. Already-loaded DLLs stay loaded;
* dll_already_loaded() makes the re-walk idempotent. */
void overlay_loader_rescan(void) {
if (!s_active) return;
if (!s_active || !overlay_loads_allowed()) return;
scan_cache_dir();
load_bios_resident_shards();
s_nchecked = 0;
@@ -3042,6 +3088,10 @@ OverlayPreparedImage *overlay_loader_prepare_published(const char *dll_path) {
int overlay_loader_commit_published(OverlayPreparedImage *image) {
if (!image) return 0;
if (!overlay_loads_allowed()) {
overlay_loader_discard_prepared(image);
return 0;
}
OverlayLibraryHandle handle = image->handle;
image->handle = NULL;
int loaded = 0;
@@ -3209,6 +3259,9 @@ static int lazy_load_selected(int li) {
static int try_load_region(uint32_t phys) {
extern uint32_t dirty_ram_get_bitmap_word(uint32_t word_index);
if (!overlay_loads_allowed())
return 0;
uint32_t page_sz = 4096u;
/* Walk back over the contiguous dirty run to recover region_start — cache
+12 -2
View File
@@ -410,9 +410,11 @@ static void idle_snapshot_regs(const CPUState *cpu) {
static int idle_skip_on(void) {
/* Cycle-skip is a host enhancement; under netplay it forks peers (detector
* streak / skip quanta are not part of the shared snap). */
* streak / skip quanta are not part of the shared snap). Same for the
* solo resim self-check — keep the window on the faithful cycle path. */
extern int psx_netplay_active(void);
if (psx_netplay_active())
extern int psx_selfcheck_enabled(void);
if (psx_netplay_active() || psx_selfcheck_enabled())
return 0;
if (g_idle_skip_enabled < 0) {
/* No game config reached this process (for example a BIOS-only
@@ -580,6 +582,14 @@ void psx_cycles_resync_after_restore(CPUState *cpu) {
cpu->ld_which_t = 0x20u; /* no pending load dest */
cpu->ld_absorb = 0;
}
/* Dirty-RAM interpreter load-delay writebacks live in host statics, not
* BS_SEC_CPU. Discard (do not flush): snap GPRs are already architectural.
* A stale pending v0 write from the pre-load timeline was forking MotK
* resim peers (countdown vs BIOS v0=1) at matched guest clocks. */
{
extern void dirty_ram_ld_delay_discard(void);
dirty_ram_ld_delay_discard();
}
}
void psx_cycles_reset_for_boot(void) {
+6
View File
@@ -71,6 +71,7 @@ static uint32_t s_load_val;
/* ---- Init ---- */
void interp_init(CPUState* cpu) {
(void)cpu;
s_hit_bp = 0;
s_trace_on = 0;
s_halted = 0;
@@ -83,6 +84,11 @@ void interp_init(CPUState* cpu) {
memset(s_trace_ring, 0, sizeof(s_trace_ring));
}
void interp_ld_delay_discard(void) {
s_load_reg = 0;
s_load_val = 0;
}
/* ---- Breakpoints ---- */
int interp_break_add(uint32_t pc) {
+102 -31
View File
@@ -33,6 +33,7 @@
#include "netplay_state_digest.h"
#include "psx_netplay_rb.h"
#include "cpu_state.h"
#include "gpu.h"
#include "interrupts.h"
#include "psx_scheduler.h"
#if defined(PSX_HAS_LOBBY_CLIENT)
@@ -426,6 +427,19 @@ static void np_drain_peer_frame_commits(void)
if (!g_np.session) return;
while (rnet_session_take_rb_frame_commit(g_np.session, &through, &hash))
netplay_hc_note_peer(&g_np.hc, through, hash);
/* FIRST CORE can stick the watermark forever once that tick ages out of
* the 128-slot ring — heal so choose_load_tick sees a real frontier. */
if (netplay_hc_heal_stale_gap(&g_np.hc)) {
static uint32_t s_heal_log;
uint32_t rt = netplay_hc_resolved_through(&g_np.hc);
if (s_heal_log != rt) {
fprintf(stderr,
"psxrecomp: rb hash_confirm heal stale gap → resolved=%u\n",
(unsigned)rt);
fflush(stderr);
s_heal_log = rt;
}
}
np_check_core_diverge();
}
@@ -438,6 +452,12 @@ static void np_emit_frame_commit(uint32_t tick)
uint32_t aux = 0u;
int crumb;
if (!g_np.cpu || !g_np.session) return;
/* Live depth24: skip full-RAM FRAME_COMMIT (rewind already deferred).
* Resim still commits every tick. Leaving FMV primes hash_confirm so the
* watermark is not stuck on missing movie slots. */
if (gpu_display_is_depth24() &&
!(g_np.rollback && psx_netplay_rb_is_resimulating()))
return;
/* Present-edge: clear PC so parked-0 vs live-BB does not fork FRAME_COMMIT
* while GPRs/RAM/clk match (was aborting good Replay on dig_cpu alone). */
psx_netplay_rb_cpu_for_present_digest(&dig_cpu, g_np.cpu);
@@ -452,10 +472,12 @@ static void np_emit_frame_commit(uint32_t tick)
np_part_ring_put(tick, &parts, av, cd, aux);
netplay_hc_note_local(&g_np.hc, tick, parts.core);
(void)rnet_session_send_rb_frame_commit(g_np.session, tick, parts.core);
/* Breadcrumbs so both peers' logs line up by sim tick. */
(void)netplay_hc_heal_stale_gap(&g_np.hc);
/* Breadcrumbs so both peers' logs line up by sim tick. Tag is local-only
* (not peer agreement — that is hash_confirm resolved_through). */
if (crumb && s_live_dig_last_tick != tick) {
s_live_dig_last_tick = tick;
np_log_live_digest(tick, &parts, av, cd, aux, "ok");
np_log_live_digest(tick, &parts, av, cd, aux, "local");
}
np_check_core_diverge();
}
@@ -1301,8 +1323,7 @@ static void np_rb_bind_and_start(void)
}
/* MotK digital (active-low): wire only releases buttons vs invent; sticks equal.
* Menu press/release thrash opened an episode every edge — soft-promote releases
* only while cores still match (see reconcile). */
* Used for FMV soft-promote releases only (menu releases must rewind). */
static int np_digital_release_only(const RNetInputContractFrame *pub,
const RNetInputContractFrame *wire)
{
@@ -1319,6 +1340,19 @@ static int np_digital_release_only(const RNetInputContractFrame *pub,
return newly_pressed == 0 && newly_released != 0;
}
/* Wire has at least one newly pressed button vs published invent (active-low).
* FMV skip uses this — promote-without-resim left host mid-movie while peer
* already cut over. */
static int np_digital_new_press(const RNetInputContractFrame *pub,
const RNetInputContractFrame *wire)
{
uint16_t newly_pressed;
if (!pub || !wire)
return 0;
newly_pressed = (uint16_t)((uint16_t)(~wire->buttons) & pub->buttons);
return newly_pressed != 0;
}
/* Late authoritative wire vs published predicted rows → promote or queue rewind.
* Diag: promote-no-resim means hist took the late pad but guest sim did not
* rewind — looks like "remote input rejected" when cadence already drifted. */
@@ -1360,14 +1394,18 @@ static void np_rollback_reconcile_wire(void)
promote_sweep = psx_netplay_rb_take_promote_sweep();
cooldown = psx_netplay_rb_rewind_suppressed();
fmv_defer = psx_netplay_rb_fmv_defer_rewind();
/* FMV: never open episodes — same promote path as cooldown. */
no_resim = promote_sweep || cooldown || fmv_defer;
if (fmv_defer)
no_resim_why = "fmv";
else if (promote_sweep)
no_resim_why = "sweep";
else if (cooldown)
no_resim_why = "cooldown";
{
/* Media + post-FMV lockstep (title Start): promote-only; admit waits
* for wire so presses never open 0x8006CDA0 / VLC tip episodes. */
int fmv_lock = psx_netplay_rb_lockstep_no_invent();
no_resim = promote_sweep || cooldown || fmv_lock;
if (promote_sweep)
no_resim_why = "sweep";
else if (cooldown)
no_resim_why = "cooldown";
else if (fmv_lock)
no_resim_why = "fmv-lockstep";
}
sim = rnet_session_sim_tick(g_np.session);
for (slot = 0; slot < g_np.slot_count; ++slot) {
if (slot == g_np.local_slot) continue;
@@ -1401,7 +1439,7 @@ static void np_rollback_reconcile_wire(void)
pads_differ = (pub_c.buttons != wire_c.buttons) ||
(pub_c.stick_x != wire_c.stick_x) ||
(pub_c.stick_y != wire_c.stick_y);
/* After commit/realign/FMV: flush invent poison without another episode. */
/* After commit/realign: flush invent poison without another episode. */
if (no_resim) {
(void)netplay_ih_promote(&g_np.ih, slot, &wire_frame);
if (pads_differ) {
@@ -1420,11 +1458,11 @@ static void np_rollback_reconcile_wire(void)
d = rnet_input_contract_stick_replace_decide(
&pub_c, &wire_c, completed, &params, &gates);
if (rnet_input_contract_decision_is_rewind(d)) {
/* Soft-promote releases only when peers still agree. After a
* sticky invent forked RAM, promote-without-resim left cores
* desynced until the next press aborted on baseline mismatch. */
if (np_digital_release_only(&pub_c, &wire_c) &&
!netplay_hc_peek_mismatch(&g_np.hc, NULL, NULL, NULL)) {
/* FMV/settle only: soft-promote releases (skip must rewind on
* press). Menu soft-promote + hold-last invent forked RAM
* (sticky Up skipped resim). Live invent is hold-last again —
* menu releases open a real episode. */
if (fmv_defer && np_digital_release_only(&pub_c, &wire_c)) {
(void)netplay_ih_promote(&g_np.ih, slot, &wire_frame);
if (n_soft_release == 0) {
first_no_resim_t = t;
@@ -1435,6 +1473,21 @@ static void np_rollback_reconcile_wire(void)
n_soft_release++;
continue;
}
/* FMV: non-press pad noise → promote only (avoid CD thrash). */
if (fmv_defer && !np_digital_new_press(&pub_c, &wire_c)) {
(void)netplay_ih_promote(&g_np.ih, slot, &wire_frame);
if (pads_differ) {
if (n_no_resim == 0) {
first_no_resim_t = t;
first_no_resim_slot = slot;
first_pub_btn = pub_c.buttons;
first_wire_btn = wire_c.buttons;
no_resim_why = "fmv-nopress";
}
n_no_resim++;
}
continue;
}
if (!g_np.pending_rewind) {
g_np.pending_rewind = 1;
g_np.pending_rewind_tick = t;
@@ -1447,13 +1500,15 @@ static void np_rollback_reconcile_wire(void)
first_pub_btn = pub_c.buttons;
first_wire_btn = wire_c.buttons;
}
/* Only park the guest when an episode actually opens. */
/* Promote BEFORE begin — seal_inputs reads hist for the
* local seat and publish_sio falls back to hist before
* peer SEAL_ROWS land. Skipping promote left invent-idle
* pads sealed/published across the skip press tick. */
(void)netplay_ih_promote(&g_np.ih, slot, &wire_frame);
if (psx_netplay_rb_begin_rewind(t, slot)) {
g_np.needs_advance = 0;
n_episode_open++;
} else {
/* No snap / cooldown / already active — promote wire. */
(void)netplay_ih_promote(&g_np.ih, slot, &wire_frame);
n_begin_refused++;
}
g_np.pending_rewind = 0;
@@ -1526,10 +1581,9 @@ static int np_try_admit_rollback(void)
rnet_u32 wire;
int slot;
int pred;
int lockstep;
/* Tick FMV→settle tracker every admit (even when remotes are present). */
lockstep = psx_netplay_rb_lockstep_no_invent();
(void)psx_netplay_rb_lockstep_no_invent();
if (!rnet_session_prepare_local_tip(g_np.session, sim))
return 0;
@@ -1570,16 +1624,18 @@ static int np_try_admit_rollback(void)
netplay_ih_pad_to_frame(&pad, sim, 0, &row);
(void)netplay_ih_put(&g_np.ih, slot, &row);
} else {
/* FMV + post-FMV settle: never invent — wait for wire (lockstep).
* Promote-only during movies left peers core-desynced at cutover. */
if (lockstep)
/* FMV media + post-FMV lockstep: wait for remote wire (skip /
* title Start). Invent idle opened tip episodes that hung. */
if (psx_netplay_rb_lockstep_no_invent())
return 0;
/* Stall when invent would run more than P ahead of remote tip. */
if (wire > st.highest_remote_wire + (rnet_u32)pred)
return 0;
/* MotK digital: invent idle, not hold-last. Hold-last sticky Up
* after settle forked menu RAM (soft-promote then skipped resim). */
(void)netplay_ih_invent_idle(&g_np.ih, slot, sim, &row);
/* MotK digital: hold-last. Idle invent re-mismatched every held
* D-pad tick after commit → episode storm / char-select freeze.
* Menu release soft-promote is off (see reconcile) so sticky Up
* cannot skip a needed resim. Seal gap-fill stays idle. */
(void)netplay_ih_invent_hold_last(&g_np.ih, slot, sim, &row);
}
}
@@ -2678,6 +2734,8 @@ void psx_netplay_pump(void)
drain_lobby_signals();
rnet_session_pump(g_np.session);
np_drain_peer_frame_commits();
/* Stall abort only — full rb_pump stays off mid-guest (asymmetric). */
psx_netplay_rb_poll_replay_stall();
return;
}
np_pump_session();
@@ -2759,8 +2817,12 @@ int psx_netplay_poll_admit(void)
* while a post-abort realign load is queued (episode already inactive). */
if (g_np.rollback && g_np.xfer == NP_XFER_NONE &&
(psx_netplay_rb_active() || psx_netplay_rb_load_pending())) {
g_np.needs_advance = 0;
return psx_netplay_rb_try_admit();
if (psx_netplay_rb_tip_holding() && !psx_netplay_rb_load_pending()) {
/* TipHold: Live invent continues; episode stays open for tip-extend. */
} else {
g_np.needs_advance = 0;
return psx_netplay_rb_try_admit();
}
}
/* Already published this tick and waiting for finish_frame — do not
@@ -2887,6 +2949,11 @@ void psx_netplay_finish_frame(void)
if (g_np.rollback && psx_netplay_rb_is_resimulating()) {
rnet_u32 done = rnet_session_sim_tick(g_np.session);
/* Present-edge only: mid-guest pump skips reconcile. Coalesce late
* wire into the active episode (tip-extend) before POST/Verify. */
rnet_session_pump(g_np.session);
np_rollback_reconcile_wire();
psx_netplay_rb_pump();
psx_netplay_rb_finish_frame();
/* Exchange cores during Replay so mid-resim forks abort before POST. */
np_emit_frame_commit(done);
@@ -2903,6 +2970,10 @@ void psx_netplay_finish_frame(void)
np_emit_frame_commit(done);
if (g_np.rollback) {
uint32_t resume_hint = 0;
/* TipHold Live: reconcile late wire so tip-extend can schedule rereplay. */
if (psx_netplay_rb_tip_holding())
np_rollback_reconcile_wire();
psx_netplay_rb_pump();
psx_netplay_rb_request_snap(done);
/* Flush at vblank: MotK IRQ fast/mid paths used to skip poll_snap, so
* deferred BB-edge saves never ran and the ring stayed empty. Prefer
File diff suppressed because it is too large Load Diff
+989
View File
@@ -0,0 +1,989 @@
/*
* Solo rollback resim self-check — see psx_selfcheck.h.
*
* Window life cycle (boundary = one offline vblank finish_frame):
*
* IDLE ── window due ──▶ SAVE_WAIT (poll saves raw snap at next BB edge)
* SAVE_WAIT ─ saved ───▶ RECORD (per-boundary rows + digests, span N)
* RECORD ── span done ─▶ LOAD_WAIT (poll loads snap, longjmp resume)
* LOAD_WAIT ─ loaded ──▶ PRIME (optional throwaway span; primes ambient)
* PRIME ── span done ──▶ LOAD_WAIT (rewind again for counted resims)
* LOAD_WAIT ─ loaded ──▶ REPLAY #1 (republish rows, digest per boundary)
* REPLAY#1 ─ span done ▶ LOAD_WAIT (rewind to the SAME snap again)
* … REPLAY #2, #3 … ──▶ IDLE (verdict = #2 vs #3; cold dig = #1 vs #2)
*
* The snapshot is taken a few instructions into the tick after the boundary
* that armed it (first BB-edge poll), so comparisons start at the first
* boundary after the save completed — record and replay both cover exactly
* the guest work from the snap point to each boundary.
*
* Verdict = warm #2 vs #3 (both peers in an episode are post-rewind resims).
* Cold #1 vs #2 digs live-RECORD host ambient; ambient-prime (default on)
* collapses most of that. Live-vs-resim is reported as restore-drift.
*/
#include "psx_selfcheck.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "boot_state.h"
#include "cpu_state.h"
#include "dirty_ram_interp.h"
#include "gpu.h"
#include "interrupts.h"
#include "netplay_state_digest.h"
#include "overlay_loader.h"
#include "psx_cycles.h"
#include "psx_netplay.h"
#include "psx_scheduler.h"
#include "savestate.h"
#include "sio.h"
#include "spu.h"
extern int fntrace_is_game_started(void);
extern uint64_t psx_cycle_count;
extern uint64_t s_frame_count;
#define SC_MAX_SLOTS 8
#define SC_SPAN_MAX 120
#define SC_SPAN_DEF 32u
#define SC_INTERVAL_DEF 600u
#define SC_WARMUP_BOUNDARIES 900u /* ~15s after enable before first window */
#define SC_ARM_TIMEOUT 10u /* boundaries to find a BB-edge save/load */
enum {
SC_OFF = 0,
SC_IDLE,
SC_SAVE_WAIT,
SC_RECORD,
SC_LOAD_WAIT,
SC_REPLAY,
};
typedef struct {
uint16_t buttons;
uint8_t lx, ly, rx, ry;
uint8_t analog;
uint8_t present;
} ScRow;
typedef struct {
ScRow rows[SC_MAX_SLOTS];
NetplayCoreParts core;
uint32_t av, aux, cd, spad, dma;
NetplaySioParts sio;
} ScBoundary;
static int s_phase = SC_OFF;
static CPUState *s_cpu;
static uint32_t s_bios_checksum;
static uint32_t s_entry_pc;
static uint32_t s_interval = SC_INTERVAL_DEF;
static uint32_t s_span = SC_SPAN_DEF;
static int s_fault;
static int s_mash;
static uint32_t s_mash_rate; /* 0..100 */
static uint32_t s_mash_rng;
static uint16_t s_mash_word = 0xFFFFu;
static uint32_t s_mash_hold_left;
static uint32_t s_mash_latched_boundary = 0xffffffffu;
static uint32_t s_mash_edges; /* diagnostic: press/release transitions */
static uint32_t s_boundary; /* offline vblank boundaries seen */
static uint32_t s_next_window;
static uint32_t s_arm_deadline;
static uint32_t s_snap_boundary; /* boundary counter when snap completed */
static uint8_t *s_blob;
static size_t s_blob_len;
static uint32_t s_snap_csv; /* cycles_since_vblank at snap — restore for resim */
static uint64_t s_snap_host_frame; /* s_frame_count at snap — pin for resim */
static ScBoundary s_rec[SC_SPAN_MAX]; /* live pass rows + digests */
static ScBoundary s_r1[SC_SPAN_MAX]; /* replay #1 digests (rows unused) */
static ScRow s_staged[SC_MAX_SLOTS]; /* sampler notes for this boundary */
static uint32_t s_r1_gpr[SC_SPAN_MAX][32]; /* replay#1 GPRs for diverge dump */
static uint64_t s_r1_cyc[SC_SPAN_MAX];
static uint32_t s_r1_csv[SC_SPAN_MAX];
static int s_pass; /* 1..3 = replay #N (verdict = #2 vs #3) */
static uint32_t s_prime; /* throwaway spans before counted resims */
static uint32_t s_prime_left; /* remaining throwaways for this window */
static int s_warming; /* 1 = throwaway span to prime host ambient */
static uint32_t s_replay_idx;
static uint32_t s_mismatches; /* warm resim-vs-resim (#2 vs #3) */
static uint32_t s_first_bad_idx;
static uint32_t s_cold_mismatches; /* #1 vs #2 — host-ambient warm-up dig */
static uint32_t s_cold_first_idx;
static uint32_t s_drift; /* live-vs-resim forks (diagnostic) */
static uint32_t s_drift_first_idx;
static char s_drift_parts[128];
static uint32_t s_window_no;
static uint32_t s_windows_pass;
static uint32_t s_windows_fail;
/* Span just ended in finish_frame — load from flush_load after present RAII
* (not from a later fast-poll BB: #2 vs #3 were loading at different PCs). */
static int s_load_after_present;
/* Raw guest PC at each boundary (digests clear PC). */
static uint32_t s_rec_pc[SC_SPAN_MAX];
static uint32_t s_r1_pc[SC_SPAN_MAX];
/* Post-load identity (after restore recipe, before longjmp). */
static ScBoundary s_post_load_r1;
static int s_post_load_r1_valid;
static uint32_t s_post_load_r1_gpr[32];
static uint32_t s_post_load_r1_pc;
static uint64_t s_post_load_r1_cyc;
static uint64_t s_post_load_r1_host_frame;
static void sc_free_blob(void)
{
free(s_blob);
s_blob = NULL;
s_blob_len = 0;
}
static void sc_schedule_next(void)
{
s_next_window = s_boundary + s_interval;
}
static void sc_freeze_overlays(int on)
{
overlay_loader_set_load_freeze(on);
}
static void sc_cancel(const char *why)
{
fprintf(stderr, "psxrecomp: selfcheck win#%u aborted (%s)\n",
(unsigned)s_window_no, why);
fflush(stderr);
sc_free_blob();
sc_freeze_overlays(0);
s_load_after_present = 0;
s_warming = 0;
s_phase = SC_IDLE;
sc_schedule_next();
}
static void sc_capture_digests(ScBoundary *b)
{
/* Present-edge digest: clear PC (host parks cpu->pc=0 at present while a
* resim boundary may hold a live BB PC — the RB lesson). */
CPUState dig_cpu = *s_cpu;
dig_cpu.pc = 0;
netplay_core_digest_parts(&dig_cpu, &b->core);
b->av = netplay_av_digest();
b->aux = netplay_aux_digest();
b->cd = netplay_cdrom_digest();
b->spad = netplay_spad_digest();
b->dma = netplay_dma_digest();
netplay_sio_digest_parts(&b->sio);
}
static uint32_t sc_raw_pc(void)
{
return s_cpu ? s_cpu->pc : 0u;
}
/* Arm span-end load: prefer flush_load after present-body RAII so every
* rewind happens at the same VBlank boundary (no post-span BB walk). */
static void sc_arm_span_end_load(void)
{
s_phase = SC_LOAD_WAIT;
s_load_after_present = 1;
s_arm_deadline = s_boundary + SC_ARM_TIMEOUT;
}
static void sc_apply_rows(const ScBoundary *b)
{
int i;
for (i = 0; i < SC_MAX_SLOTS; ++i) {
const ScRow *r = &b->rows[i];
if (!r->present)
continue;
if (sio_pad_on_multitap(i))
sio_set_pad_config_capable(i, 0);
sio_set_pad_state_slot(i, r->buttons);
sio_set_pad_sticks(i, r->lx, r->ly, r->rx, r->ry);
sio_request_pad_type(i, r->analog ? 1 : 0);
}
}
/* Append the name of each differing partition to buf. Returns count. */
static int sc_diff_parts(const ScBoundary *a, const ScBoundary *b,
char *buf, size_t cap)
{
int n = 0;
size_t off = 0;
#define SC_PART(name, expr_a, expr_b) \
do { \
if ((expr_a) != (expr_b)) { \
int w = snprintf(buf + off, cap > off ? cap - off : 0, \
"%s%s", n ? "," : "", name); \
if (w > 0) off += (size_t)w; \
n++; \
} \
} while (0)
SC_PART("cpu", a->core.cpu, b->core.cpu);
SC_PART("clk", a->core.clock_irq, b->core.clock_irq);
SC_PART("tim", a->core.timers, b->core.timers);
SC_PART("ram", a->core.ram, b->core.ram);
SC_PART("dirty",a->core.dirty, b->core.dirty);
SC_PART("av", a->av, b->av);
SC_PART("aux", a->aux, b->aux);
SC_PART("cd", a->cd, b->cd);
SC_PART("spad", a->spad, b->spad);
SC_PART("dma", a->dma, b->dma);
SC_PART("sioR", a->sio.regs, b->sio.regs);
SC_PART("sioP", a->sio.pads, b->sio.pads);
SC_PART("sioM", a->sio.mc, b->sio.mc);
SC_PART("sioT", a->sio.pace, b->sio.pace);
#undef SC_PART
return n;
}
static uint32_t sc_env_u32(const char *name, uint32_t def, uint32_t lo,
uint32_t hi)
{
const char *e = getenv(name);
if (!e || !*e)
return def;
{
unsigned long v = strtoul(e, NULL, 10);
if (v < lo) v = lo;
if (v > hi) v = hi;
return (uint32_t)v;
}
}
/* PSX digital pad bits (active-low). Face + D-pad + shoulders — fighter mash
* set. START is rare (menu advance); SELECT/L3/R3 stay released. */
#define SC_PAD_START (1u << 3)
#define SC_PAD_UP (1u << 4)
#define SC_PAD_RIGHT (1u << 5)
#define SC_PAD_DOWN (1u << 6)
#define SC_PAD_LEFT (1u << 7)
#define SC_PAD_L2 (1u << 8)
#define SC_PAD_R2 (1u << 9)
#define SC_PAD_L1 (1u << 10)
#define SC_PAD_R1 (1u << 11)
#define SC_PAD_TRIANGLE (1u << 12)
#define SC_PAD_CIRCLE (1u << 13)
#define SC_PAD_CROSS (1u << 14)
#define SC_PAD_SQUARE (1u << 15)
static const uint16_t sc_mash_bits[] = {
SC_PAD_UP, SC_PAD_DOWN, SC_PAD_LEFT, SC_PAD_RIGHT,
SC_PAD_TRIANGLE, SC_PAD_CIRCLE, SC_PAD_CROSS, SC_PAD_SQUARE,
SC_PAD_L1, SC_PAD_R1, SC_PAD_L2, SC_PAD_R2,
};
#define SC_MASH_NBITS ((int)(sizeof(sc_mash_bits) / sizeof(sc_mash_bits[0])))
static uint32_t sc_mash_rng_next(void)
{
/* xorshift32 */
uint32_t x = s_mash_rng;
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
s_mash_rng = x ? x : 0xA5A5A5A5u;
return s_mash_rng;
}
static uint16_t sc_mash_advance(void)
{
uint16_t prev = s_mash_word;
if (s_mash_hold_left > 0u) {
s_mash_hold_left--;
return s_mash_word;
}
/* Decide whether to open a new chord or sit idle. */
if ((sc_mash_rng_next() % 100u) < s_mash_rate) {
uint16_t pressed = 0u;
int n = 1 + (int)(sc_mash_rng_next() % 3u); /* 1..3 simultaneous */
int i;
for (i = 0; i < n; ++i)
pressed |= sc_mash_bits[sc_mash_rng_next() % (uint32_t)SC_MASH_NBITS];
/* ~8% include START (menu / confirm spam). */
if ((sc_mash_rng_next() % 100u) < 8u)
pressed |= SC_PAD_START;
s_mash_word = (uint16_t)(0xFFFFu & ~pressed);
s_mash_hold_left = 1u + (sc_mash_rng_next() % 3u); /* hold 1..3 */
} else {
s_mash_word = 0xFFFFu; /* full release */
s_mash_hold_left = sc_mash_rng_next() % 3u; /* idle 0..2 */
}
if (s_mash_word != prev)
s_mash_edges++;
return s_mash_word;
}
int psx_selfcheck_mash_override(uint16_t *out_buttons)
{
if (!s_mash || s_phase == SC_OFF || s_phase == SC_REPLAY ||
s_phase == SC_LOAD_WAIT)
return 0;
if (!out_buttons)
return 0;
/* One stable word per offline boundary (finish_frame bumps s_boundary
* after sampling). Mid-frame low-latency resample must not see a new
* RNG roll within the same tick. */
if (s_mash_latched_boundary != s_boundary) {
s_mash_latched_boundary = s_boundary;
(void)sc_mash_advance();
}
*out_buttons = s_mash_word;
return 1;
}
void psx_selfcheck_init(struct CPUState *cpu, uint32_t bios_checksum,
uint32_t entry_pc)
{
const char *e = getenv("PSX_RB_SELFCHECK");
if (!e || !*e || *e == '0')
return;
if (!psx_hle_scheduler_enabled()) {
fprintf(stderr, "psxrecomp: selfcheck disabled — requires the HLE "
"scheduler (rewind longjmp)\n");
return;
}
s_cpu = cpu;
s_bios_checksum = bios_checksum;
s_entry_pc = entry_pc;
s_interval = sc_env_u32("PSX_RB_SELFCHECK_INTERVAL", SC_INTERVAL_DEF,
120u, 1000000u);
s_span = sc_env_u32("PSX_RB_SELFCHECK_SPAN", SC_SPAN_DEF, 4u, SC_SPAN_MAX);
s_fault = sc_env_u32("PSX_RB_SELFCHECK_FAULT", 0u, 0u, 1u) != 0u;
/* Default 1: throwaway resim(s) of the snap clear live-RECORD host
* ambient so counted #1 matches #2/#3. 0 disables; 2+ for stubborn windows. */
s_prime = sc_env_u32("PSX_RB_SELFCHECK_PRIME", 1u, 0u, 4u);
s_mash = sc_env_u32("PSX_RB_SELFCHECK_MASH", 0u, 0u, 1u) != 0u;
s_mash_rate = sc_env_u32("PSX_RB_SELFCHECK_MASH_RATE", 75u, 1u, 100u);
s_mash_rng = sc_env_u32("PSX_RB_SELFCHECK_MASH_SEED", 0xC0FFEEu, 1u,
0xffffffffu);
s_mash_word = 0xFFFFu;
s_mash_hold_left = 0u;
s_mash_latched_boundary = 0xffffffffu;
s_mash_edges = 0u;
s_phase = SC_IDLE;
s_next_window = SC_WARMUP_BOUNDARIES;
/* Match netplay: idle-skip advances guest cycles without lockstep device
* aging — host-trained streaks fork cold resim#1 from warm #2/#3. */
{
extern int g_idle_skip_enabled;
if (g_idle_skip_enabled != 0) {
g_idle_skip_enabled = 0;
fprintf(stderr, "psxrecomp: selfcheck — idle_skip forced off\n");
}
}
fprintf(stderr,
"psxrecomp: selfcheck enabled interval=%u span=%u fault=%d "
"prime=%u mash=%d rate=%u seed=%u\n",
(unsigned)s_interval, (unsigned)s_span, s_fault,
(unsigned)s_prime, s_mash, (unsigned)s_mash_rate,
(unsigned)s_mash_rng);
fflush(stderr);
}
int psx_selfcheck_enabled(void) { return s_phase != SC_OFF; }
int psx_selfcheck_defer_present(void)
{
/* API retained for netplay-parity probes; offline GPU present stays
* immediate (BB-edge defer skews clk/tim between warm peers). */
return s_phase == SC_SAVE_WAIT || s_phase == SC_RECORD ||
s_phase == SC_LOAD_WAIT || s_phase == SC_REPLAY;
}
int psx_selfcheck_resim_active(void)
{
/* RECORD must share the resim host-skip profile (no CD boost / audio pump /
* wall pacer). Otherwise the ambient left by the live record pass forks
* resim#1 from resim#2 while #2≡#3 (warm) stays bit-identical. */
return s_phase == SC_RECORD || s_phase == SC_REPLAY ||
s_phase == SC_LOAD_WAIT;
}
int psx_selfcheck_replay_input(void) { return s_phase == SC_REPLAY ||
s_phase == SC_LOAD_WAIT; }
int psx_selfcheck_input_locked(void)
{
return s_phase == SC_RECORD || s_phase == SC_LOAD_WAIT ||
s_phase == SC_REPLAY;
}
static void sc_dump_gpr_diff(const uint32_t *a, const uint32_t *b,
const char *tag)
{
int i;
int n = 0;
char buf[512];
size_t off = 0;
buf[0] = '\0';
for (i = 0; i < 32; ++i) {
if (a[i] == b[i])
continue;
int w = snprintf(buf + off, sizeof(buf) > off ? sizeof(buf) - off : 0,
"%sr%d=%08x/%08x", n ? " " : "", i,
(unsigned)a[i], (unsigned)b[i]);
if (w > 0) off += (size_t)w;
n++;
if (n >= 12) break; /* keep the line readable */
}
fprintf(stderr, "psxrecomp: selfcheck %s gpr_diff n=%d %s%s\n",
tag, n, n ? buf : "(none)", (n >= 12) ? " ..." : "");
fflush(stderr);
}
void psx_selfcheck_note_pad(int slot, uint16_t buttons, uint8_t lx, uint8_t ly,
uint8_t rx, uint8_t ry, uint8_t analog)
{
ScRow *r;
if (s_phase != SC_RECORD && s_phase != SC_SAVE_WAIT)
return;
if (slot < 0 || slot >= SC_MAX_SLOTS)
return;
r = &s_staged[slot];
r->buttons = buttons;
r->lx = lx; r->ly = ly; r->rx = rx; r->ry = ry;
r->analog = analog;
r->present = 1;
}
static void sc_report(void)
{
char drift[160];
if (s_drift)
snprintf(drift, sizeof(drift),
"restore-drift %u/%u (first idx=%u parts=%s)",
(unsigned)s_drift, (unsigned)s_span,
(unsigned)s_drift_first_idx, s_drift_parts);
else
snprintf(drift, sizeof(drift), "restore-drift 0/%u",
(unsigned)s_span);
if (s_mismatches == 0) {
s_windows_pass++;
fprintf(stderr,
"psxrecomp: selfcheck win#%u PASS warm-resim(#2vs#3) %u/%u; "
"cold(#1vs#2) mismatched=%u/%u; %s (pass=%u fail=%u)",
(unsigned)s_window_no, (unsigned)s_span, (unsigned)s_span,
(unsigned)s_cold_mismatches, (unsigned)s_span, drift,
(unsigned)s_windows_pass, (unsigned)s_windows_fail);
} else {
s_windows_fail++;
fprintf(stderr,
"psxrecomp: selfcheck win#%u FAIL warm-resim(#2vs#3) "
"mismatched=%u/%u first_idx=%u; cold(#1vs#2) "
"mismatched=%u/%u first_idx=%u; %s (pass=%u fail=%u)",
(unsigned)s_window_no, (unsigned)s_mismatches,
(unsigned)s_span, (unsigned)s_first_bad_idx,
(unsigned)s_cold_mismatches, (unsigned)s_span,
(unsigned)s_cold_first_idx, drift,
(unsigned)s_windows_pass, (unsigned)s_windows_fail);
}
if (s_mash)
fprintf(stderr, " mash_edges=%u", (unsigned)s_mash_edges);
fprintf(stderr, "\n");
fflush(stderr);
sc_freeze_overlays(0);
}
void psx_selfcheck_finish_frame(int defer_window)
{
if (s_phase == SC_OFF)
return;
if (psx_netplay_active()) {
/* Netplay owns the resim machinery from here on. */
fprintf(stderr, "psxrecomp: selfcheck off (netplay active)\n");
fflush(stderr);
sc_free_blob();
sc_freeze_overlays(0);
s_phase = SC_OFF;
return;
}
/* First boundary after resume_at — native chain under the snap PC is live
* again for subsequent ticks (same point netplay clears). */
if (s_phase == SC_REPLAY)
psx_scheduler_top_level_resume_clear();
s_boundary++;
switch (s_phase) {
case SC_IDLE:
if (s_boundary < s_next_window)
return;
if (defer_window || !fntrace_is_game_started() ||
gpu_display_is_depth24() || savestate_pending())
return; /* retry next boundary */
s_window_no++;
memset(s_staged, 0, sizeof(s_staged));
/* Freeze overlay discovery for the whole window (record+both
* resims) so host-only DLL registration cannot fork BB edges. */
sc_freeze_overlays(1);
s_phase = SC_SAVE_WAIT;
s_arm_deadline = s_boundary + SC_ARM_TIMEOUT;
return;
case SC_SAVE_WAIT:
memset(s_staged, 0, sizeof(s_staged)); /* rows latch per boundary */
if (s_boundary > s_arm_deadline)
sc_cancel("no BB-edge save opportunity");
return;
case SC_RECORD: {
uint32_t idx = s_boundary - s_snap_boundary - 1u;
if (savestate_pending()) {
sc_cancel("user savestate during record");
return;
}
if (idx < s_span) {
ScBoundary *b = &s_rec[idx];
memcpy(b->rows, s_staged, sizeof(b->rows));
sc_capture_digests(b);
s_rec_pc[idx] = sc_raw_pc();
}
memset(s_staged, 0, sizeof(s_staged));
if (idx + 1u >= s_span) {
/* Optional throwaway span(s): prime host ambient left by live
* RECORD so counted resim#1 matches #2/#3. */
s_pass = 0;
s_prime_left = s_prime;
s_warming = s_prime_left > 0u ? 1 : 0;
sc_arm_span_end_load();
}
return;
}
case SC_LOAD_WAIT:
/* Live boundaries past the window while waiting for a BB edge —
* discarded by the restore. */
if (s_boundary > s_arm_deadline)
sc_cancel("no BB-edge load opportunity");
return;
case SC_REPLAY: {
ScBoundary cur;
if (savestate_pending()) {
sc_cancel("user savestate during replay");
return;
}
if (s_replay_idx >= s_span) { /* defensive */
s_phase = SC_IDLE;
sc_schedule_next();
return;
}
/* Order matches record: rows applied (sampler there, us here), then
* digest at the same boundary point. */
sc_apply_rows(&s_rec[s_replay_idx]);
memset(&cur, 0, sizeof(cur));
sc_capture_digests(&cur);
if (s_warming) {
/* Throwaway — no compare. Advance; more primes or counted #1. */
s_replay_idx++;
if (s_replay_idx >= s_span) {
if (s_prime_left > 0u)
s_prime_left--;
if (s_prime_left > 0u) {
fprintf(stderr,
"psxrecomp: selfcheck win#%u ambient-prime "
"pass done (%u left)\n",
(unsigned)s_window_no, (unsigned)s_prime_left);
fflush(stderr);
s_warming = 1;
} else {
fprintf(stderr,
"psxrecomp: selfcheck win#%u ambient-prime done "
"— starting counted resims\n",
(unsigned)s_window_no);
fflush(stderr);
s_warming = 0;
s_pass = 0;
}
sc_arm_span_end_load();
}
return;
}
if (s_pass == 1) {
/* Replay #1: seed cold reference; live-vs-resim = drift dig. */
char parts[128];
memcpy(cur.rows, s_rec[s_replay_idx].rows, sizeof(cur.rows));
s_r1[s_replay_idx] = cur;
memcpy(s_r1_gpr[s_replay_idx], s_cpu->gpr, sizeof(s_r1_gpr[0]));
s_r1_cyc[s_replay_idx] = psx_cycle_count;
s_r1_csv[s_replay_idx] = interrupts_get_cycles_since_vblank();
s_r1_pc[s_replay_idx] = sc_raw_pc();
if (sc_diff_parts(&s_rec[s_replay_idx], &cur, parts,
sizeof(parts))) {
if (s_drift == 0) {
s_drift_first_idx = s_replay_idx;
snprintf(s_drift_parts, sizeof(s_drift_parts), "%s",
parts);
}
s_drift++;
}
} else if (s_pass == 2) {
/* Replay #2: cold dig (#1 vs #2), then become the warm reference. */
const ScBoundary *ref = &s_r1[s_replay_idx];
char parts[128];
int n = sc_diff_parts(ref, &cur, parts, sizeof(parts));
if (n) {
if (s_cold_mismatches == 0)
s_cold_first_idx = s_replay_idx;
s_cold_mismatches++;
if (s_cold_mismatches == 1) {
char tag[48];
fprintf(stderr,
"psxrecomp: selfcheck win#%u COLD idx=%u "
"parts=%s raw_pc=%08x/%08x\n",
(unsigned)s_window_no, (unsigned)s_replay_idx,
parts, (unsigned)s_r1_pc[s_replay_idx],
(unsigned)sc_raw_pc());
snprintf(tag, sizeof(tag), "win#%u cold idx=%u",
(unsigned)s_window_no, (unsigned)s_replay_idx);
sc_dump_gpr_diff(s_r1_gpr[s_replay_idx], s_cpu->gpr, tag);
interrupts_log_last_vblank_irqctx(tag);
fflush(stderr);
}
}
s_r1[s_replay_idx] = cur;
memcpy(s_r1_gpr[s_replay_idx], s_cpu->gpr, sizeof(s_r1_gpr[0]));
s_r1_cyc[s_replay_idx] = psx_cycle_count;
s_r1_csv[s_replay_idx] = interrupts_get_cycles_since_vblank();
s_r1_pc[s_replay_idx] = sc_raw_pc();
} else {
/* Replay #3: warm resim-vs-resim — the rollback invariant after
* host ambient has seen one resim of this snap. */
const ScBoundary *ref = &s_r1[s_replay_idx];
char parts[128];
int n = sc_diff_parts(ref, &cur, parts, sizeof(parts));
if (n) {
int first = (s_mismatches == 0);
if (first)
s_first_bad_idx = s_replay_idx;
s_mismatches++;
fprintf(stderr,
"psxrecomp: selfcheck win#%u DIVERGE idx=%u parts=%s "
"core=%08x/%08x ram=%08x/%08x cpu=%08x/%08x "
"clk=%08x/%08x av=%08x/%08x cd=%08x/%08x "
"sioT=%08x/%08x cyc=%llu/%llu d_cyc=%lld "
"csv=%u/%u host_f=%llu raw_pc=%08x/%08x\n",
(unsigned)s_window_no, (unsigned)s_replay_idx, parts,
(unsigned)ref->core.core, (unsigned)cur.core.core,
(unsigned)ref->core.ram, (unsigned)cur.core.ram,
(unsigned)ref->core.cpu, (unsigned)cur.core.cpu,
(unsigned)ref->core.clock_irq,
(unsigned)cur.core.clock_irq,
(unsigned)ref->av, (unsigned)cur.av,
(unsigned)ref->cd, (unsigned)cur.cd,
(unsigned)ref->sio.pace, (unsigned)cur.sio.pace,
(unsigned long long)s_r1_cyc[s_replay_idx],
(unsigned long long)psx_cycle_count,
(long long)psx_cycle_count -
(long long)s_r1_cyc[s_replay_idx],
(unsigned)s_r1_csv[s_replay_idx],
(unsigned)interrupts_get_cycles_since_vblank(),
(unsigned long long)s_frame_count,
(unsigned)s_r1_pc[s_replay_idx],
(unsigned)sc_raw_pc());
fflush(stderr);
if (first) {
char tag[48];
snprintf(tag, sizeof(tag), "win#%u idx=%u",
(unsigned)s_window_no,
(unsigned)s_replay_idx);
sc_dump_gpr_diff(s_r1_gpr[s_replay_idx], s_cpu->gpr, tag);
interrupts_log_last_vblank_irqctx(tag);
}
}
}
s_replay_idx++;
if (s_replay_idx >= s_span) {
if (s_pass == 1) {
if (s_fault) {
uint32_t fi = s_span / 2u;
s_r1[fi].core.ram ^= 1u;
fprintf(stderr,
"psxrecomp: selfcheck win#%u fault injected "
"idx=%u\n",
(unsigned)s_window_no, (unsigned)fi);
fflush(stderr);
}
sc_arm_span_end_load();
} else if (s_pass == 2) {
/* Warm reference captured — one more rewind for #3. */
sc_arm_span_end_load();
} else {
sc_report();
sc_free_blob();
s_load_after_present = 0;
s_phase = SC_IDLE;
sc_schedule_next();
}
}
return;
}
default:
return;
}
}
/* SC_LOAD_WAIT rewind. Mirrors the RB restore recipe — cycle + IRQ resync,
* SPU CD FIFO normalize, NO cdrom_accelerate, light frontend hook.
* Longjmps via psx_scheduler_resume_at — never returns on success. */
static void sc_do_load(struct CPUState *cpu, const char *via)
{
if (!cpu) {
sc_cancel("load with null cpu");
return;
}
if (!s_blob || !s_blob_len) {
sc_cancel("no snap blob");
return;
}
s_load_after_present = 0;
/* Host-only ambient from the PREVIOUS pass — sample before load. */
{
extern uint32_t g_dirty_safe_resume_pc;
extern uint32_t g_dirty_ram_code_gen;
extern int g_idle_skip_enabled;
extern int g_ls_suppress_record;
extern int g_ls_mode;
extern uint64_t g_guest_store_count;
extern uint64_t g_irqctx_seq;
extern uint32_t dirty_ram_text_diverged_pages(void);
uint64_t kb[6];
const int next_pass = s_warming ? 0 : (s_pass == 0 ? 1 : s_pass + 1);
psx_kernel_bless_stats(kb);
fprintf(stderr,
"psxrecomp: selfcheck win#%u load ambient pre "
"pass=%d via=%s dirty_resume=%08x irq_resume=%08x "
"irq_chk_pc=%08x irq_chk_cyc=%llu tot_chk=%llu "
"fast_m=%u code_gen=%u stores=%llu irqctx=%llu "
"ls_sup=%d ls_mode=%d idle_skip=%d ovl=%d "
"txt_div=%u kb_clean=%llu kb_mism=%llu\n",
(unsigned)s_window_no, next_pass, via ? via : "?",
(unsigned)g_dirty_safe_resume_pc,
(unsigned)psx_compiled_irq_resume_pc(),
(unsigned)psx_last_irq_check_pc(),
(unsigned long long)psx_last_irq_check_cycle(),
(unsigned long long)psx_interrupt_total_checks(),
(unsigned)psx_interrupt_fast_maintenance(),
(unsigned)g_dirty_ram_code_gen,
(unsigned long long)g_guest_store_count,
(unsigned long long)g_irqctx_seq,
g_ls_suppress_record, g_ls_mode, g_idle_skip_enabled,
overlay_loader_registered_count(),
(unsigned)dirty_ram_text_diverged_pages(),
(unsigned long long)kb[1], (unsigned long long)kb[2]);
fflush(stderr);
}
if (!boot_state_load_buffer(s_blob, s_blob_len, s_bios_checksum,
s_entry_pc, cpu)) {
sc_cancel("snap load failed");
return;
}
psx_cycles_resync_after_restore(cpu);
interrupts_resync_after_restore();
/* Keep the snap's VBlank phase (resync zeroed it for UI savestate safety). */
interrupts_set_cycles_since_vblank(s_snap_csv);
/* Pin host frame counter so present/GPU hysteresis that keys on
* s_frame_count cannot fork resim#1 vs resim#2 (+span otherwise). */
s_frame_count = s_snap_host_frame;
/* Host-only overlay negative-lookup memo — not in the snap; a fill during
* resim#1 made #2≡#3 while #1 forked (cold ambient). */
overlay_loader_clear_lazy_miss();
spu_cd_audio_reset();
psx_frontend_on_rb_snap_loaded();
/* Mirror RB flush_resume: longjmp abandons any flush_present frame. */
gpu_vblank_release_present_flush_guard();
gpu_vblank_clear_deferred_present();
s_replay_idx = 0;
s_cpu = cpu;
if (s_warming) {
/* Throwaway prime span — do not advance s_pass / post_load refs. */
s_phase = SC_REPLAY;
fprintf(stderr,
"psxrecomp: selfcheck win#%u ambient-prime begin pc=0x%08x "
"span=%u via=%s\n",
(unsigned)s_window_no, (unsigned)cpu->pc, (unsigned)s_span,
via ? via : "?");
fflush(stderr);
psx_scheduler_resume_at(cpu->pc);
return; /* unreachable */
}
if (s_pass == 0) { /* entering counted replay #1 */
s_mismatches = 0;
s_first_bad_idx = 0;
s_cold_mismatches = 0;
s_cold_first_idx = 0;
s_drift = 0;
s_drift_first_idx = 0;
s_drift_parts[0] = '\0';
s_post_load_r1_valid = 0;
}
s_pass = (s_pass == 0) ? 1 : (s_pass + 1);
s_phase = SC_REPLAY;
{
ScBoundary post;
memset(&post, 0, sizeof(post));
sc_capture_digests(&post);
if (s_pass == 1) {
s_post_load_r1 = post;
s_post_load_r1_valid = 1;
memcpy(s_post_load_r1_gpr, cpu->gpr, sizeof(s_post_load_r1_gpr));
s_post_load_r1_pc = cpu->pc;
s_post_load_r1_cyc = psx_cycle_count;
s_post_load_r1_host_frame = s_frame_count;
fprintf(stderr,
"psxrecomp: selfcheck win#%u replay#1 begin pc=0x%08x "
"span=%u post_load core=%08x ram=%08x cpu=%08x "
"clk=%08x cyc=%llu host_f=%llu ovl=%d freeze=%d\n",
(unsigned)s_window_no, (unsigned)cpu->pc,
(unsigned)s_span, (unsigned)post.core.core,
(unsigned)post.core.ram, (unsigned)post.core.cpu,
(unsigned)post.core.clock_irq,
(unsigned long long)psx_cycle_count,
(unsigned long long)s_frame_count,
overlay_loader_registered_count(),
overlay_loader_load_frozen());
} else if (s_post_load_r1_valid) {
char parts[128];
int n = sc_diff_parts(&s_post_load_r1, &post, parts,
sizeof(parts));
fprintf(stderr,
"psxrecomp: selfcheck win#%u replay#%d begin pc=0x%08x "
"span=%u post_load %s parts=%s core=%08x/%08x "
"ram=%08x/%08x cpu=%08x/%08x clk=%08x/%08x "
"cyc=%llu/%llu host_f=%llu/%llu ovl=%d freeze=%d\n",
(unsigned)s_window_no, s_pass, (unsigned)cpu->pc,
(unsigned)s_span, n ? "MISMATCH" : "MATCH",
n ? parts : "-",
(unsigned)s_post_load_r1.core.core,
(unsigned)post.core.core,
(unsigned)s_post_load_r1.core.ram,
(unsigned)post.core.ram,
(unsigned)s_post_load_r1.core.cpu,
(unsigned)post.core.cpu,
(unsigned)s_post_load_r1.core.clock_irq,
(unsigned)post.core.clock_irq,
(unsigned long long)s_post_load_r1_cyc,
(unsigned long long)psx_cycle_count,
(unsigned long long)s_post_load_r1_host_frame,
(unsigned long long)s_frame_count,
overlay_loader_registered_count(),
overlay_loader_load_frozen());
if (n) {
char tag[48];
snprintf(tag, sizeof(tag), "win#%u post_load",
(unsigned)s_window_no);
sc_dump_gpr_diff(s_post_load_r1_gpr, cpu->gpr, tag);
} else if (memcmp(s_post_load_r1_gpr, cpu->gpr,
sizeof(s_post_load_r1_gpr)) != 0) {
/* Digests clear PC; catch GPR-only forks digests miss. */
char tag[48];
snprintf(tag, sizeof(tag), "win#%u post_load_gpr",
(unsigned)s_window_no);
sc_dump_gpr_diff(s_post_load_r1_gpr, cpu->gpr, tag);
}
if (cpu->pc != s_post_load_r1_pc) {
fprintf(stderr,
"psxrecomp: selfcheck win#%u post_load PC "
"0x%08x vs 0x%08x\n",
(unsigned)s_window_no, (unsigned)s_post_load_r1_pc,
(unsigned)cpu->pc);
}
}
fflush(stderr);
}
psx_scheduler_resume_at(cpu->pc);
}
void psx_selfcheck_flush_load(void)
{
if (s_phase != SC_LOAD_WAIT || !s_load_after_present)
return;
if (psx_netplay_active())
return;
/* Prefer span-end present: same VBlank boundary for every rewind. */
sc_do_load(s_cpu, "present");
}
/* Same gate as rb_resume_pc_ok — psx_is_dispatchable still accepts 0xB0. */
static int sc_resume_pc_ok(uint32_t pc)
{
uint32_t phys;
if (!psx_is_dispatchable(pc))
return 0;
if ((pc & 3u) != 0u)
return 0;
if (pc == 0x80000080u || pc == 0xbfc00180u || pc == 0x80000000u)
return 0;
if ((pc & 0xfff00000u) == 0xbfc00000u)
return 1;
phys = pc & 0x1fffffffu;
if (phys >= 0x1000u && phys < 0x200000u)
return 1;
return 0;
}
static uint32_t sc_pick_snap_pc(struct CPUState *cpu, uint32_t hint)
{
const uint32_t cands[5] = {
hint,
psx_last_irq_check_pc(),
psx_compiled_irq_resume_pc(),
cpu ? cpu->gpr[31] : 0u,
cpu ? cpu->pc : 0u,
};
int i;
for (i = 0; i < 5; ++i) {
if (sc_resume_pc_ok(cands[i]))
return cands[i];
}
return 0;
}
void psx_selfcheck_poll(struct CPUState *cpu, uint32_t resume_pc)
{
if (s_phase != SC_SAVE_WAIT && s_phase != SC_LOAD_WAIT)
return;
if (!cpu || psx_netplay_active())
return;
if (s_phase == SC_SAVE_WAIT) {
CPUState snap;
uint32_t snap_pc = sc_pick_snap_pc(cpu, resume_pc);
if (!snap_pc)
return; /* retry — reject 0xB0 / vector junk (win#138 class) */
snap = *cpu;
snap.pc = snap_pc;
sc_free_blob();
if (!boot_state_save_buffer_raw(&snap, s_bios_checksum, s_entry_pc,
&s_blob, &s_blob_len)) {
sc_cancel("snap save failed");
return;
}
s_snap_boundary = s_boundary;
s_snap_csv = interrupts_get_cycles_since_vblank();
s_snap_host_frame = s_frame_count;
s_phase = SC_RECORD;
fprintf(stderr,
"psxrecomp: selfcheck win#%u snap boundary=%u pc=0x%08x "
"bytes=%zu span=%u csv=%u host_f=%llu%s\n",
(unsigned)s_window_no, (unsigned)s_boundary,
(unsigned)snap_pc, s_blob_len, (unsigned)s_span,
(unsigned)s_snap_csv, (unsigned long long)s_snap_host_frame,
(snap_pc != resume_pc) ? " (picked)" : "");
fflush(stderr);
return;
}
/* LOAD_WAIT: span-end flush owns the load. BB-poll is fallback only
* (e.g. if present was skipped). Never load from fast-poll while a
* present flush is armed — that was the #2/#3 load-site skew. */
if (s_load_after_present)
return;
sc_do_load(cpu, "poll");
}
+35 -1
View File
@@ -695,6 +695,19 @@ static int psx_request_thread_switch(CPUState* cpu, uint32_t target_tcb)
* on the old stack (safe: the restored state is complete in CPUState/RAM/devices
* and every block leader is re-enterable). MUST be called on the scheduler fiber
* (HLE mode, default) at a block-leader boundary with in_exception == 0. */
/* Set by resume_at; cleared on first post-resume finish_frame (or abort). */
static int g_sched_top_level_resume;
int psx_scheduler_top_level_resume_active(void)
{
return g_sched_top_level_resume;
}
void psx_scheduler_top_level_resume_clear(void)
{
g_sched_top_level_resume = 0;
}
void psx_scheduler_resume_at(uint32_t resume_pc)
{
if (!psx_is_dispatchable(resume_pc)) {
@@ -707,6 +720,9 @@ void psx_scheduler_resume_at(uint32_t resume_pc)
g_sched_escape.target_tcb = 0;
g_sched_escape.resume_pc = resume_pc;
g_sched_escape.reason = PSX_RUN_RESUME_CURRENT;
/* longjmp abandons every mid-block CPS frame — sentinel RFE must not
* treat the new top-level dispatch as a continuable live chain. */
g_sched_top_level_resume = 1;
longjmp(g_scheduler_jmpbuf, 1); /* unwind to psx_scheduler_run; never returns */
}
@@ -796,6 +812,13 @@ void psx_scheduler_run(CPUState* cpu)
* nested-unit gate so IRQ checks are not wedged-off after the escape
* (backstop for the Ape memcard native<->interp fix). */
{ extern int g_call_unit_depth; g_call_unit_depth = 0; }
/* flush_resume / savestate longjmp skips generated bb_defer cleanup. */
{
extern int g_psx_cyc_bb_defer;
extern uint32_t g_psx_cyc_batch;
g_psx_cyc_bb_defer = 0;
g_psx_cyc_batch = 0;
}
}
switch (g_sched_escape.reason) {
@@ -843,7 +866,8 @@ void psx_scheduler_run(CPUState* cpu)
* dispatch saw cpu->pc==0. If a thread yielded to us and then ran to
* completion, hand control back to its yielder (one-level safety net)
* instead of tearing down the whole run. Otherwise it's the legacy
* top-level abnormal exit. */
* top-level abnormal exit — unless an RB top-level resume can recover
* at $ra / sticky BB (returning-leaf snap after flush_resume). */
if (psx_is_valid_tcb(cpu, g_sched_return_tcb) &&
g_sched_return_tcb != psx_current_tcb_ptr(cpu)) {
uint32_t yielder = g_sched_return_tcb;
@@ -854,6 +878,16 @@ void psx_scheduler_run(CPUState* cpu)
psx_current_tcb_ptr(cpu), yielder, 0);
continue;
}
{
extern int psx_netplay_rb_recover_null_pc(CPUState *c, uint32_t *out_pc);
uint32_t recover_pc = 0;
if (psx_netplay_rb_recover_null_pc(cpu, &recover_pc) && recover_pc) {
g_sched_escape.target_tcb = 0;
g_sched_escape.resume_pc = recover_pc;
g_sched_escape.reason = PSX_RUN_RESUME_CURRENT;
continue;
}
}
g_sched_escape.reason = PSX_RUN_GUEST_EXIT;
g_in_scheduler_run = 0;
return;
Binary file not shown.
+25
View File
@@ -67,6 +67,31 @@ int main(void) {
netplay_hc_note_peer(&hc, 820, 0xAAAAu);
CHECK(netplay_hc_resolved_through(&hc) == 820u, "prime then match load tick");
/* Stale FIRST-CORE gap: mismatch tick ages out of the ring; later tip
* digests match — heal must advance past the stuck watermark. */
netplay_hc_reset(&hc);
netplay_hc_note_local(&hc, 0, 0x1u);
netplay_hc_note_peer(&hc, 0, 0x1u);
CHECK(netplay_hc_resolved_through(&hc) == 0u, "heal setup resolved 0");
netplay_hc_note_local(&hc, 1, 0xAAAAu);
netplay_hc_note_peer(&hc, 1, 0xBBBBu); /* FIRST CORE */
CHECK(netplay_hc_resolved_through(&hc) == 0u, "mismatch sticks at 0");
CHECK(netplay_hc_peek_mismatch(&hc, NULL, NULL, NULL), "mismatch visible");
/* Overwrite slot for tick 1 (1 % 128 == 1) with a far tip match. */
netplay_hc_note_local(&hc, 1u + NETPLAY_HC_RING, 0xCCCCu);
netplay_hc_note_peer(&hc, 1u + NETPLAY_HC_RING, 0xCCCCu);
netplay_hc_note_local(&hc, 50u + NETPLAY_HC_RING, 0xDDDDu);
netplay_hc_note_peer(&hc, 50u + NETPLAY_HC_RING, 0xDDDDu);
CHECK(!netplay_hc_peek_mismatch(&hc, NULL, NULL, NULL),
"aged mismatch no longer peekable");
CHECK(netplay_hc_heal_stale_gap(&hc), "heal advances over stale gap");
CHECK(netplay_hc_resolved_through(&hc) == 50u + NETPLAY_HC_RING,
"heal to highest matched tip");
/* Do not heal over a still-visible fork. */
netplay_hc_note_local(&hc, 60u + NETPLAY_HC_RING, 0x1111u);
netplay_hc_note_peer(&hc, 60u + NETPLAY_HC_RING, 0x2222u);
CHECK(!netplay_hc_heal_stale_gap(&hc), "heal refuses live fork in ring");
if (failures) {
printf("%d failure(s)\n", failures);
return 1;