mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
rollback patches
This commit is contained in:
+140
-24
@@ -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`,
|
||||
|
||||
+1
-1
Submodule lib/recomp-net updated: 2623cf2ac4...23eec611eb
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 */
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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, ¶ms, &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
|
||||
|
||||
+893
-170
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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.
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user