mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
fix mmx6 timing divergences with cosim oracle
This commit is contained in:
@@ -0,0 +1,295 @@
|
||||
# Handoff — MMX6 first-divergence co-sim oracle (2026-06-30)
|
||||
|
||||
Continuation of the MMX6 cutscene→gameplay wedge hunt. Read `COSIM_ORACLE.md` first
|
||||
(the design + the four validation gates). This file = current state + exact next steps.
|
||||
|
||||
## The mandate (do not drift from this)
|
||||
|
||||
After ~2 weeks of confident-but-wrong "this is it" root causes, the user forbade chasing
|
||||
any more single hypotheses — **including** the currently-leading-but-UNPROVEN
|
||||
GPU-draw-time-backpressure idea (Beetle-anchored, bisect-confirmed as a *lever*, never
|
||||
rendered a frame). We are building a **decision procedure**: a full-architectural-state
|
||||
lockstep co-sim of the two backends that halts at the FIRST divergence. Whatever it halts
|
||||
on *is* the first divergence — no hypothesis can be "wrong." It will confirm or kill GPU
|
||||
backpressure (and everything else) on its own. **Do not implement GPU backpressure or any
|
||||
other fix until the oracle names the first divergence.**
|
||||
|
||||
## Established facts (proven; do NOT re-chase — each cost days)
|
||||
|
||||
- Compiled backend SKIPS the intro FMV (main-state 0→1, no substate 6) → wedges black at
|
||||
`02 02`. Interp (`PSX_FORCE_INTERP=1`) plays it. SAME binary.
|
||||
- Given identical inputs, compiled == interp byte-for-byte (block+function lockstep, 12.9M
|
||||
+ 560K units, zero divergence). So it's NOT codegen — it's WHEN an event is taken
|
||||
relative to the instruction stream, which flips an early control-flow branch.
|
||||
- Ruled out with data: async-RFE, CD-DMA-0x79D44 corruption (interp does the same write),
|
||||
IRQ-storm/timeline, block-edge-vs-per-insn IRQ delivery, load-delay-slot exception
|
||||
corruption (built + refuted, reverted).
|
||||
- `PSX_LOAD_DELAY=0` makes compiled play the FMV (pixels) — but that's a secondary phase
|
||||
modulator, not the faithful root, and it does NOT fix the later `02 02` gameplay wedge.
|
||||
- TWO separate bugs: (1) FMV-skip, (2) `02 02` gameplay wedge (persists even with FMV fixed).
|
||||
|
||||
## What is BUILT (this session, all uncommitted on branch `codex/mmx6-precise-slice`)
|
||||
|
||||
Worktree: `F:\Projects\psxrecomp\_wt-tomba2\psxrecomp`. All cosim code is `#ifdef PSX_COSIM`
|
||||
(zero effect on normal builds).
|
||||
|
||||
New files:
|
||||
- `COSIM_ORACLE.md` — design + gates (READ FIRST).
|
||||
- `runtime/include/cosim_state.h`, `runtime/src/cosim_state.c` — full guest-arch-state
|
||||
canonical FNV hash. Reuses `boot_state.c` per-device `_snapshot_write` serializers; ADDS
|
||||
the CPU micro-state boot_state omits (muldiv_ts_done, gte_ts_done, read_absorb[33],
|
||||
read_absorb_which, read_fudge, ld_which_t, ld_absorb). Incremental RAM page-hashes via
|
||||
write hooks. VRAM excluded v1 (downstream, not causal). Gate-4 fault injection built in.
|
||||
- `runtime/src/cosim.c` — engine: CYCLE-KEYED checkpoints (ring + cumulative chain hash +
|
||||
park/step lockstep) + standalone minimal TCP server. Commands: status, chain, stride N,
|
||||
runto <cycle>, hash, sub, window N, inject ram|reg, reset.
|
||||
- `tools/cosim.py` — coordinator: launches 2 instances, cycle-locksteps via runto/chain,
|
||||
reports first divergence + sub-hash diff + window. Its --help documents the gate runs.
|
||||
|
||||
Modified (guarded):
|
||||
- `recompiler/src/code_generator.cpp` + `full_function_emitter.cpp` — emit guarded
|
||||
`cosim_block(pc)` at each block leader (+ extern decls). REQUIRES REGEN to take effect
|
||||
(see below); the CORE compare does NOT need it (see "why regen is optional for v1").
|
||||
- `runtime/src/psx_cycles.c` — `cosim_tick()` in `psx_advance_cycles` (THE alignment clock:
|
||||
the only counter both backends drive identically).
|
||||
- `runtime/src/dirty_ram_interp.c` — `cosim_block(pc)` in the interp loop.
|
||||
- `runtime/src/memory.c` — `cosim_note_ram_write` in word/half/byte RAM writes (covers
|
||||
CPU + DMA; DMA verified to route through psx_write_*_raw via the d44_ring evidence).
|
||||
- `runtime/src/interrupts.c` — `interrupts_cosim_hash()` (folds cycles_since_vblank +
|
||||
in_exception ONLY; EXCLUDES total_checks/dispatch_count/cooldown = backend-call-freq
|
||||
artifacts that would false-positive). ALSO: reverted the refuted load-delay save/restore.
|
||||
- `runtime/src/main.cpp` — `cosim_init()` at startup.
|
||||
- `runtime/runtime.cmake` — cosim sources added to shared list; `COSIM` target option →
|
||||
`PSX_COSIM=1 PSX_NO_DEBUG_TOOLS=1`.
|
||||
- `MegaManX6Recomp/CMakeLists.txt` — `psx-cosim` target gated on `-DPSX_BUILD_COSIM=ON`.
|
||||
|
||||
Also still uncommitted from earlier (Codex): the precise-slice / `--headless` mode / etc.
|
||||
(both emitters have `psx_precise_interrupt_at`). Leave as-is; irrelevant to the oracle.
|
||||
|
||||
## Build state (IN PROGRESS at handoff)
|
||||
|
||||
- `build-wt` CANNOT be reconfigured (pre-existing worktree cache-path error, NOT our
|
||||
changes). So the cosim build uses a FRESH dir:
|
||||
`F:\Projects\psxrecomp\MegaManX6Recomp\build-cosim` (configured OK with:
|
||||
`-G Ninja -DPSXRECOMP_V4_ROOT=F:/Projects/psxrecomp/_wt-tomba2/psxrecomp
|
||||
-DPSX_BUILD_COSIM=ON -DPSX_LAUNCHER=OFF` + the mingw64 gcc/g++/ninja from build-wt cache,
|
||||
RelWithDebInfo).
|
||||
- BUILD PROGRESS: ALL translation units compiled CLEAN under PSX_NO_DEBUG_TOOLS+PSX_COSIM
|
||||
(no unguarded-debug-call errors — the guards are correct). The ONLY failure was one link
|
||||
error: `cosim_init()` called from main.cpp (C++) needed `extern "C"` — FIXED
|
||||
(main.cpp:2486). Relinking at handoff. Expect the exe to link now; if any NEW link error
|
||||
appears it'll be another C/C++ linkage or a missing cosim symbol — all cosim funcs are in
|
||||
cosim.c/cosim_state.c (C files, added to the shared source list) so they should resolve.
|
||||
- Build/run commands:
|
||||
Configure (once):
|
||||
cmake -S F:/Projects/psxrecomp/MegaManX6Recomp -B F:/Projects/psxrecomp/MegaManX6Recomp/build-cosim \
|
||||
-G Ninja -DCMAKE_C_COMPILER=C:/msys64/mingw64/bin/gcc.exe \
|
||||
-DCMAKE_CXX_COMPILER=C:/msys64/mingw64/bin/g++.exe \
|
||||
-DCMAKE_MAKE_PROGRAM=C:/msys64/mingw64/bin/ninja.exe -DCMAKE_BUILD_TYPE=RelWithDebInfo \
|
||||
-DPSXRECOMP_V4_ROOT=F:/Projects/psxrecomp/_wt-tomba2/psxrecomp \
|
||||
-DPSX_BUILD_COSIM=ON -DPSX_LAUNCHER=OFF
|
||||
Build: cmake --build F:/Projects/psxrecomp/MegaManX6Recomp/build-cosim --target psx-cosim -j 4
|
||||
(exe at build-cosim/psx-cosim.exe; run coordinator: python tools/cosim.py --a compiled --b compiled)
|
||||
|
||||
## MILESTONE (2026-06-30 late): ORACLE BUILDS + RUNS + EARLY BOOT DETERMINISTIC
|
||||
|
||||
`psx-cosim.exe` links (build-cosim, 89MB). Coordinator smoke test
|
||||
(`--a compiled --b compiled --stride 262144 --max 8000000`, ~14 frames early boot):
|
||||
BOTH instances launch, connect (4600/4601), cycle-lockstep via `runto`, and report
|
||||
**ZERO divergence** — chains match at every checkpoint. So the full mechanism works:
|
||||
two-process launch, TCP, cycle-stepping, full-state hashing, chain compare. Early boot
|
||||
is deterministic. Deeper compiled-vs-compiled (to 250M cyc / ~440 frames) running to test
|
||||
overlay-load-timing determinism (result in build task b5lvwwi0i / next session).
|
||||
|
||||
## HARNESS BUG FOUND + FIXED (the gates working as intended, 2026-06-30)
|
||||
|
||||
Two bugs the validation caught before any real result was trusted:
|
||||
1. Coordinator `kv()` parser misaligned the `runto` reply (leading bare word `parked`),
|
||||
so `chain` parsed as None for BOTH sides → every compare was None==None == "equal" →
|
||||
the tool was SILENTLY BLIND (would report "no divergence" for a real one). Fixed
|
||||
(parse all adjacent token pairs) + added a FATAL guard if chain is ever unparseable.
|
||||
2. With the parser fixed, gate-4 immediately exposed a REAL harness bug: the two
|
||||
processes parked at DIFFERENT cycles for the same `runto` target (A=3.07M, B=4.03M) —
|
||||
a racy design where the guest free-runs from launch and only notices the async stop
|
||||
flag at a wall-time-dependent moment. `runto <cycle>` did not stop AT that cycle.
|
||||
FIX (redesign): the guest now parks at EVERY checkpoint boundary (a deterministic
|
||||
cycle = multiple of stride) and only advances when the coordinator grants budget via
|
||||
`step N`. Stride is fixed at LAUNCH via env PSX_COSIM_STRIDE (no set-stride race —
|
||||
the guest hits cp=1 before the coordinator can send a command). Protocol is now
|
||||
`step N` (not `runto`); coordinator steps both 1 checkpoint at a time and compares
|
||||
the cumulative chain, with a cycle-skew WARN if the two ever park at different cycles.
|
||||
REBUILDING psx-cosim with this fix at handoff (task bxuiye5u3).
|
||||
|
||||
GATE 4 PASSES (post-fix, verified 2026-06-30): injected `ram:1000:255` into B before
|
||||
cp 20 → tool HALTED at cp 21 (exact checkpoint after the fault), BOTH processes at the
|
||||
IDENTICAL cycle 5242880 (deterministic park — race fixed, no skew warning), and the ONLY
|
||||
differing subsystem hash was `ram` (cpu/irqctl/timers/clock/all devices identical). So the
|
||||
tool provably: detects divergence, halts at the right checkpoint, parks deterministically,
|
||||
localizes the subsystem. cp 1–19 identical before injection = partial Gate 1 passing.
|
||||
GATE 1 PASSES (compiled-vs-compiled, no inject): 457 checkpoints to ~210 frames, ZERO
|
||||
divergence, chains matched throughout — INCLUDING past overlay-load territory (frame
|
||||
118+). So determinism + hashing + host-state exclusion are all correct, and overlay
|
||||
loading is deterministic in compiled-vs-compiled (the warming concern did not bite).
|
||||
|
||||
==> TOOL VALIDATED (Gate 1 + Gate 4 both pass). THE compiled-vs-interp RUN is now running
|
||||
(task bhi8hf5p1): `python tools/cosim.py --a compiled --b interp --stride 262144
|
||||
--max 1000000000` (~frame 1766, past the FMV-skip ~1650). Interp is ~5 frames/sec so this
|
||||
takes ~6-10 min. When it halts: the printed cp/cycle = the first divergence; the FIRST
|
||||
differing subsystem hash (cpu / irqctl / a device / ram) = WHERE it split; the window =
|
||||
the last 16 checkpoints of both. THAT names the bug — read it, then design the faithful
|
||||
fix for whatever it actually is (could confirm GPU-backpressure, or something else).
|
||||
If it shows a cycle-skew WARN, the two backends charge cycles differently somewhere (would
|
||||
be its own finding). If interp is too slow to reach 1650, use a coarser --stride for a
|
||||
first bracket then drill finer.
|
||||
|
||||
## OVERLAY CACHE DECISION (user, correct): leave `overlay_cache=true` — the gcc native
|
||||
overlay shards ARE the compiled path we must measure; forcing them off would make the
|
||||
"compiled" instance interpret overlays. The gcc compile worker only fires on a cache
|
||||
MISS, so **do a WARMING RUN first** (free-run one psx-cosim instance through the FMV
|
||||
region so autocompile populates build-cosim/cache), then every cosim run loads the shards
|
||||
synchronously + deterministically (worker never fires). NOTE: at 14 frames no cache dir
|
||||
existed yet (overlays load later in boot). OPEN: verify the autocompile cmd resolves from
|
||||
build-cosim (game.toml cmd uses `../psxrecomp/...` relative → resolves to the MAIN checkout
|
||||
F:/Projects/psxrecomp/psxrecomp, NOT the worktree; may need the worktree recompiler path or
|
||||
a pre-populated cache). If autocompile can't run, overlays fall to dirty-interp in BOTH
|
||||
compiled instances (still deterministic, but "compiled" won't exercise native overlays).
|
||||
The Gate-1 deep run will REVEAL whether cold-cache overlay loading is nondeterministic.
|
||||
|
||||
## ★ FIRST RESULT (2026-06-30): oracle HALTED at cp=32, cycle 8,126,465 (frame ~14)
|
||||
|
||||
compiled-vs-interp first divergence is at frame ~14 (EARLY BIOS BOOT, long before the FMV
|
||||
skip ~frame 1650). Both at the IDENTICAL cycle (no skew). 31 checkpoints matched exactly,
|
||||
then cp32 splits. The ONLY differing subsystem is `cpu` — ram, scratch, gpu, spu, cdrom,
|
||||
dma, sio, timers, clock, dirty ALL identical. `istat=00000001` (VBLANK pending) at the
|
||||
split; interp's last leader = 0x80054240 (BIOS shell). A CPU-only divergence with memory
|
||||
+ devices identical and an IRQ pending is the SIGNATURE of an interrupt taken at a
|
||||
different instruction between backends — the exact "when is the event taken" mechanism,
|
||||
caught at its FIRST occurrence. BUT NOT YET CONFIRMED: need to know WHICH cpu field split.
|
||||
Added a `cpu` TCP command (full gpr/pc/cop0/micro-state dump) + coordinator
|
||||
`--cpudiff-at-cp N`. REBUILDING (task b5y3113cg). Next: run
|
||||
`python tools/cosim.py --a compiled --b interp --stride 262144 --cpudiff-at-cp 32` and read
|
||||
which field differs:
|
||||
- pc / cop0[EPC=14] / cop0[SR=12] / cop0[Cause=13] differ => REAL: an IRQ was taken in
|
||||
one backend and not the other at this instruction. THIS is the root mechanism. Chase
|
||||
why (the accurate-timing IRQ take-point), fix faithfully.
|
||||
- ONLY a lone micro-state field (read_fudge/read_absorb_which/ld_which_t/ld_absorb/
|
||||
muldiv_ts_done/gte_ts_done) differs, with pc+cop0+gpr identical => the divergence is a
|
||||
benign load-delay/stall MICRO-STATE difference that hasn't affected architectural state.
|
||||
Decide: is it a real cycle-model mismatch (compiled vs interp charge differently here)
|
||||
— investigate psx_cyc_* ordering — or should that field be excluded from the hash as
|
||||
non-architectural (refine per COSIM_ORACLE.md). Then continue toward frame 1650.
|
||||
- a gpr differs with pc identical => a real data computation divergence at this PC
|
||||
(contradicts the lockstep proof — investigate that instruction).
|
||||
CAVEAT: `pc` in the ring is g_last_leader_pc, only set by cosim_block; WITHOUT REGEN the
|
||||
compiled side never calls cosim_block for game/BIOS text (only the interp loop does), so
|
||||
the compiled ring `pc` is STALE/unreliable for labeling. The `cpu` command reads the REAL
|
||||
debug_cpu_ptr->pc, which IS reliable. (Regen game+BIOS to make ring pc labels meaningful.)
|
||||
|
||||
## ★ cp32 RESOLVED: it was `pc` currency, a FALSE POSITIVE (2026-06-30)
|
||||
|
||||
CPU field-diff at cp32: the ONLY differing field is `pc` (compiled=0x00000000,
|
||||
interp=0x80054240). EVERY gpr/cop0/hi/lo/micro-state IDENTICAL. Root: the compiled
|
||||
backend does not keep cpu->pc current mid-execution (writes it only at block transfers;
|
||||
transiently 0 between dispatches), while interp keeps it exact per-instruction — so at a
|
||||
mid-instruction cycle checkpoint they hold different pc while in the SAME architectural
|
||||
state. FIX: excluded cpu->pc from the cross-backend hash (cosim_state.c hash_cpu; still
|
||||
reported via the `cpu` command). NOT a blind spot — a real control-flow split shows as a
|
||||
differing gpr/memory value within one checkpoint (stride 262144 = ~50k instructions).
|
||||
Gate 1 (compiled-vs-compiled) could not catch this because both sides share pc-currency.
|
||||
REBUILDING (task b2n8aw3v0). This is the tool honing itself exactly as the gates intend:
|
||||
it surfaced a representational difference at a clean bracketed point with the exact field
|
||||
named, instead of letting it corrupt a result.
|
||||
|
||||
## ★★ SECOND RESULT (pc excluded): first REAL divergence at cp=42, frame ~18
|
||||
|
||||
Re-run compiled-vs-interp with pc excluded: CPU now MATCHES (pc fix worked), tool stepped
|
||||
past cp32 and halted at cp=42, cycle 10,747,916 (frame ~18), same cycle both sides. The
|
||||
differing subsystems are `irqctl` AND `gpu` — `cpu`, `ram`, `scratch`, `clock`, `timers`,
|
||||
cdrom, dma, sio, spu ALL identical. This is DEVICE/INTERRUPT-TIMING state, not CPU/data —
|
||||
cannot be a pc-currency artifact (both drive devices via the same psx_advance_cycles).
|
||||
`irqctl` = cycles_since_vblank + in_exception + i_mask (+ i_stat, which the window shows =1
|
||||
on both). HYPOTHESIS (unconfirmed, must field-check): VBLANK is raised at a different guest
|
||||
cycle because compiled checks interrupts at BLOCK edges while interp checks per-instruction
|
||||
→ cycles_since_vblank diverges AND gpu_vblank_tick (GPUSTAT LCF) diverges → both irqctl+gpu
|
||||
split. That IS the "when is the event taken" mechanism, at its first real occurrence.
|
||||
CONFIRM: added `dev` TCP cmd (csv=cycles_since_vblank, inexc, imask, istat, gpustat) +
|
||||
coordinator prints DEV field-diff in --cpudiff-at-cp mode. REBUILDING (task b8nhp6ub1).
|
||||
Run: `python tools/cosim.py --a compiled --b interp --stride 262144 --cpudiff-at-cp 42`.
|
||||
- dev.csv differs => CONFIRMED VBLANK-raise-timing divergence (interrupt-check frequency
|
||||
differs between backends). The faithful fix: raise VBLANK (and all scheduled device
|
||||
events) at their exact due guest-cycle in BOTH backends, independent of how often
|
||||
psx_check_interrupts is called — i.e. a due-cycle event scheduler, not a check-at-
|
||||
block-leader model. (This is the class fix CLAUDE.md/FAITHFUL_TIMING_PLAN point at.)
|
||||
- dev.gpustat differs but csv same => a GPU-internal divergence (could be the draw-time /
|
||||
GPUSTAT-ready model = the GPU-backpressure hypothesis, NOW testable at this exact point).
|
||||
- Add per-field device dumps as needed to pin the exact GPU field.
|
||||
|
||||
## ★★★ CONFIRMED (2026-06-30): first divergence = VBLANK raised at a different guest cycle
|
||||
|
||||
DEV field-diff at cp42 (cycle 10,747,916, frame ~18):
|
||||
dev.csv (cycles_since_vblank): compiled=0x590c (22796) interp=0x8f60c (587788)
|
||||
dev.gpustat: compiled=0x94800000 interp=0x14800000 (bit31 LCF differs)
|
||||
VBLANK_CYCLES=564480. Compiled's last VBLANK was ~22796 cyc ago; interp is at 587788
|
||||
(PAST the threshold, next VBLANK not yet fired). Their last-VBLANK cycles differ by
|
||||
~564992 ≈ ONE VBLANK period → compiled has raised ONE MORE VBLANK than interp at the same
|
||||
guest cycle; GPUSTAT LCF (bit31, toggled by gpu_vblank_tick) confirms. So the FIRST real
|
||||
compiled-vs-interp divergence is EVENT-PHASE: VBLANK is raised at a different guest cycle
|
||||
between backends. This is the measured root of the "when is the event taken" class; the
|
||||
FMV skip 1600 frames later cascades from it. cpu.pc also shows (A=0 transient, excluded).
|
||||
|
||||
WHY (next investigation — cycles_since_vblank increments identically via psx_advance_cycles
|
||||
in both; it only RESETS inside psx_check_interrupts when cycles_since_vblank>=VBLANK_CYCLES):
|
||||
whichever backend calls psx_check_interrupts first after crossing the threshold fires+resets
|
||||
first. Candidates: (a) psx_check_interrupts call FREQUENCY differs (compiled at block leaders
|
||||
vs interp per-instruction) so one crosses/fires at a different cycle; (b) the VBLANK DEFER
|
||||
logic (interrupts.c ~line 388: defer while sio_card_protocol_active() / progress_stale)
|
||||
evaluates differently between backends. SIO sub-hash matched at cp42, so lean toward (a).
|
||||
FAITHFUL FIX DIRECTION: raise VBLANK (and all scheduled device events) at their EXACT due
|
||||
guest-cycle independent of psx_check_interrupts call frequency — a due-cycle event scheduler
|
||||
(FAITHFUL_TIMING_PLAN.md). CONFER ChatGPT before implementing. Then rebuild psx-cosim,
|
||||
re-run compiled-vs-interp: the first divergence should move much later; iterate.
|
||||
|
||||
## NEXT (immediate): re-run compiled-vs-interp with pc excluded
|
||||
`python tools/cosim.py --a compiled --b interp --stride 262144 --max 1000000000`
|
||||
Expected: proceeds well past cp32 toward the FMV-skip (~frame 1650 / ~933M cyc). Outcomes:
|
||||
- Halts near frame 1650 with a CPU (cop0/gpr) or device subsystem diff => THE bug's first
|
||||
divergence. Use `--cpudiff-at-cp N` (or add device field dumps) to name the exact field.
|
||||
- Halts EARLIER on another currency artifact => field-diff it; if it's another
|
||||
mid-instruction-currency field (unlikely — only pc had this), exclude/normalize similarly.
|
||||
- Reaches frame 1650 with NO divergence => the FMV bug is NOT in the guest-architectural
|
||||
state the hash covers at this stride; drill finer, or the divergence is in a device
|
||||
internal not yet in the snapshot (audit boot_state device _snapshot completeness).
|
||||
|
||||
## NEXT STEPS (in order)
|
||||
|
||||
1. Finish the `psx-cosim` build; guard any `PSX_NO_DEBUG_TOOLS` link/compile errors.
|
||||
2. **Why regen is optional for v1:** the compare is the cycle-keyed full-state hash in
|
||||
`psx_advance_cycles` (works without regen). `cosim_block` only supplies a human-readable
|
||||
last-leader-PC for the report. So you can run the gates + first compiled-vs-interp pass
|
||||
on the CURRENT generated C, then regen (game+BIOS) later for nicer PC labels.
|
||||
3. **RUN THE GATES (do not skip — this is the whole point):**
|
||||
- Gate 1: `python tools/cosim.py --a compiled --b compiled` → MUST report zero
|
||||
divergence across boot. If it diverges, the clean build still has NONDETERMINISM
|
||||
(threads/host-time) OR the hash includes host-only state — fix that FIRST. Likely
|
||||
suspects: SPU worker thread, overlay compile worker, any wall-clock in the guest path,
|
||||
autocompile. The clean build should be single-threaded; verify no worker threads run.
|
||||
- Gate 1b: `--a interp --b interp` → zero divergence.
|
||||
- Gate 4: `--a compiled --b compiled --inject-at <cyc> --inject ram:100000:1` → MUST
|
||||
halt at ~that cycle. If it says "no divergence," the tool is BLIND — fix before trusting.
|
||||
4. **THE RUN:** `python tools/cosim.py --a compiled --b interp` → first divergence =
|
||||
the bug. Read the `sub` diff (which subsystem's hash split first) + `window` (last N
|
||||
checkpoints of both). That names it: cpu / irqctl / a device / RAM address.
|
||||
5. Only THEN design the faithful fix for whatever it actually is.
|
||||
|
||||
## Traps / notes
|
||||
|
||||
- Determinism is the load-bearing assumption (see COSIM_ORACLE.md "two instances"). Attract
|
||||
mode needs no input, which helps. Gate 1 PROVES determinism; don't believe any
|
||||
compiled-vs-interp result until Gate 1 + Gate 4 pass.
|
||||
- Reading parked state must be side-effect-free (the device _snapshot_write serializers are
|
||||
reads; verify none mutate).
|
||||
- Ports: A=4600, B=4601 (env PSX_COSIM_PORT). Coordinator sets BELOW_NORMAL priority.
|
||||
- `runto` timeout in cosim.c is ~600k ms of 1ms spins; a very large `--stride`×`--max` at
|
||||
interp speed can be slow — interp is ~10× slower, budget accordingly.
|
||||
- Pin/reference: `build-master/psx-runtime.PIN.exe` = the working (old-pin) MMX6. `build-wt`
|
||||
= the regressed accuracy build. Memory: `mmx6_regression_compiled_codegen.md`.
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
# COSIM_ORACLE.md — the first-divergence decision procedure
|
||||
|
||||
Status: **DESIGN + BUILD IN PROGRESS** (started 2026-06-30). This document is the
|
||||
durable spec so the plan cannot be lost across handoffs (the way the Beetle-anchored
|
||||
GPU-draw-time finding was lost — see history at bottom).
|
||||
|
||||
## Why this exists
|
||||
|
||||
For ~2 weeks the MMX6 cutscene→gameplay wedge has produced a string of confident
|
||||
"this is it" root causes that each turned out wrong: async-RFE, CD-DMA corruption,
|
||||
IRQ-storm, block-edge-vs-per-instruction IRQ delivery, load-delay-slot exception
|
||||
corruption, and (most recently, and still unproven) GPU draw-time backpressure. Every
|
||||
one was a **single-signal probe with a blind spot** — it saw a slice of the machine
|
||||
and let us pattern-match a story onto it.
|
||||
|
||||
The fix for that failure mode is not another probe. It is a **decision procedure**:
|
||||
a tool that compares the **complete architectural state** of two backends, stepped
|
||||
deterministically from boot, and halts at the **first** state that differs. Whatever
|
||||
it halts on *is* the first divergence — there is no hypothesis that can be "wrong."
|
||||
It will confirm or kill GPU-backpressure (and everything else) on its own.
|
||||
|
||||
**No hypothesis is assumed by this tool.** GPU draw-time backpressure is currently the
|
||||
leading *unproven* idea (Beetle-anchored, bisect-confirmed as a lever, but never shown
|
||||
to render a frame). The oracle neither assumes nor privileges it.
|
||||
|
||||
## The bug, stated precisely (symptom only)
|
||||
|
||||
MMX6, attract boot. The compiled backend's state machine at `0x800CD3F8` advances
|
||||
main-state `0→1` (skipping FMV substate `6`) around frame ~1638; the FMV never plays
|
||||
and it wedges black at `02 02`. The dirty-RAM interpreter (`PSX_FORCE_INTERP=1`) plays
|
||||
the FMV. **Same binary.** Proven: given identical inputs the two backends compute
|
||||
byte-identically (block + function lockstep, 12.9M + 560K units, zero divergence). So
|
||||
the divergence is purely **when an interrupt/event is taken relative to the guest
|
||||
instruction stream** — which then flips an early control-flow branch. Both backends
|
||||
also over-run vs the Beetle oracle (neither models GPU draw-time), so the *faithful*
|
||||
defect may be shared and only the phase differs between them.
|
||||
|
||||
## Design (conferred with ChatGPT "PSX Static Recompiler Debug", 2026-06-30)
|
||||
|
||||
### Architecture: two processes + coordinator (v1)
|
||||
|
||||
- Two instances of a **clean, stripped build** (`psx-cosim`), one forced to the
|
||||
compiled backend, one to `PSX_FORCE_INTERP`. NO other diagnostic tooling compiled
|
||||
in (the existing debug server is laggy — user constraint). Only: the cosim state
|
||||
engine + a minimal TCP command server.
|
||||
- A **coordinator** (Python) drives both over TCP in lockstep and compares.
|
||||
- Rejected: single machine with full snapshot/restore per block (too slow — 3 MB ×3
|
||||
per block; restore-bug-prone). Rejected: full `psx_machine_t` instancing (correct
|
||||
long-term, too big a refactor for v1). Two processes gives a trustworthy tool
|
||||
soonest because our runtime is global-state-heavy.
|
||||
|
||||
### Comparison granularity — cycle-keyed, NOT block-keyed (correctness)
|
||||
|
||||
The two backends do NOT share a block structure: compiled hooks per basic block (its
|
||||
gen-time CFG); the dirty interp hooks per instruction. So a block-leader hash sequence
|
||||
would not align between them. The ONLY backend-agnostic common clock is
|
||||
`psx_cycle_count` — both backends call `psx_advance_cycles` with byte-identical
|
||||
per-instruction charges (proven). So **checkpoint the hash on guest-cycle milestones**
|
||||
(hook in `psx_advance_cycles`): at cycle stride S both backends are at the same guest
|
||||
instruction (until the real divergence), so their checkpoints align. The block-leader
|
||||
hook (`cosim_block(pc)`) is kept ONLY to stash `g_last_leader_pc` for human-readable
|
||||
reporting (compiled `cpu->pc` is stale mid-block); the compare itself is the
|
||||
cycle-checkpoint full-state hash. Drill = shrink stride S→1 in the divergent window.
|
||||
|
||||
### Stepping + two-level search
|
||||
|
||||
1. **Fast mode:** advance both by one block (one dispatch); each returns
|
||||
`(seq, leader_pc, state_hash, sub_hashes)`. Coordinator compares. Batch K blocks
|
||||
per round-trip and return K hashes to amortize TCP; on a batch mismatch, re-run the
|
||||
batch one block at a time.
|
||||
2. **On first mismatch:** the bug class is "event taken relative to instruction N," so
|
||||
a block-level answer isn't enough. Drill: re-run the divergent block at
|
||||
**instruction granularity** (per-instruction state hashes) to name the exact
|
||||
instruction / control event where they split.
|
||||
|
||||
### Determinism (critical — do NOT mask the divergence)
|
||||
|
||||
Each process runs its **own** CD/GPU/DMA/timer scheduler from its **own** guest cycle
|
||||
counter. Both are fed the **same external bytes** (same disc image, same BIOS, same
|
||||
config, no pad input in attract). Do **not** share one live event schedule between the
|
||||
machines — that would hide the exact class we're hunting. Each backend reads **live**
|
||||
devices from its own state (our old lockstep fed *recorded* values and was thus blind
|
||||
to input divergence — that blind spot is the whole reason for this rebuild).
|
||||
|
||||
### Bounded reporting (LLM must not be firehosed — user constraint)
|
||||
|
||||
Because the coordinator steps in lockstep and **halts on the first mismatch**, nothing
|
||||
scrolls away: each process keeps only a **ring of the last N blocks**
|
||||
`(seq, leader_pc, state_hash, i_stat, i_mask, sr, cycle, irq_taken?)`. On halt, dump
|
||||
that N-window from both + a full field-level diff of the single divergent step. Query
|
||||
surface: `divergence` (the one row + diff) and `window?n=…` (the preceding N). Never a
|
||||
full trace.
|
||||
|
||||
## Full architectural state — the completeness checklist
|
||||
|
||||
The tool is only as truthful as this list. A missed execution-relevant field = a blind
|
||||
spot (false "no divergence" or false positive). `boot_state.c` already serializes most
|
||||
of it; the **gaps below must be added** and the whole thing proven by the gates.
|
||||
|
||||
Reuse `boot_state.c` per-module `_snapshot_bytes/write/read` for: RAM(2MB),
|
||||
scratchpad(1KB), IRQ(i_stat/i_mask), timers, guest clock, GPU regs, VRAM(1MB),
|
||||
SPU+SPURAM, CDROM, DMA, SIO, dirty-RAM bitmap. **Audit each device snapshot for
|
||||
timing/scheduling completeness** (scheduled event deadlines, in-flight DMA remaining
|
||||
words + completion deadline, CDROM command phase + next-event cycle, GPU draw budget,
|
||||
FIFO contents) — the save-state is taken at a *clean handoff*, so it may omit mid-
|
||||
transfer transient state that a cycle-exact lockstep needs.
|
||||
|
||||
**Known CPU gaps (NOT in boot_state `CpuRegs`) — MUST add to the hash:**
|
||||
- `muldiv_ts_done`, `gte_ts_done` (completion-stall deadlines)
|
||||
- load-delay interlock: `read_absorb[33]`, `read_absorb_which`, `read_fudge`,
|
||||
`ld_which_t`, `ld_absorb`
|
||||
|
||||
**Known `interrupts.c` static gaps — MUST add:**
|
||||
- `in_exception`, `post_exception_cooldown`, `dispatch_count`, `total_checks`,
|
||||
`cycles_since_vblank`, `g_irq_event_due_fast`, VBLANK schedule state, the async-RFE /
|
||||
precise-slice / dirty-interp resume state if it affects delivery.
|
||||
|
||||
**Exclude from the hash (host-only, would cause false divergences):** CPUState function
|
||||
pointers + `sljit_helpers`, jmpbufs, fiber handles, malloc addresses, file handles,
|
||||
struct padding, uninitialized bytes. Serialize little-endian explicitly; serialize
|
||||
arrays in fixed order; serialize any event/deadline queue **sorted** by a deterministic
|
||||
key (guest_cycle, priority, type, seq).
|
||||
|
||||
### Cheap per-block hashing
|
||||
|
||||
- RAM/scratchpad/VRAM: **incremental** hashing — 4 KB page hashes updated on write +
|
||||
a top hash; per-block read is O(pages touched), not O(2MB).
|
||||
- CPU + devices: serialize the (small) canonical blob per block and hash it.
|
||||
- `state_hash = H(cpu_hash, ram_hash, scratch_hash, vram_hash, device_hash)` — compare
|
||||
sub-hashes first on mismatch to localize which subsystem split.
|
||||
|
||||
## Validation gates — trust NOTHING until these pass
|
||||
|
||||
1. **compiled-vs-compiled = zero divergence across boot** (proves coordinator
|
||||
determinism + hashing + that we excluded all host-only state). Necessary, not
|
||||
sufficient.
|
||||
2. **interp-vs-interp = zero divergence** (proves interp determinism + device
|
||||
instancing).
|
||||
3. **hash-vs-byte audit:** every N segments, force a full byte compare even when hashes
|
||||
match (proves incremental-hash maintenance is correct).
|
||||
4. **injected-divergence test:** a debug knob flips one RAM byte / one CPU reg in one
|
||||
process after seq K; the tool MUST halt at exactly that field/address. (Proves it
|
||||
actually catches divergence and names it correctly.)
|
||||
|
||||
Only after 1–4 pass do we run **compiled-vs-interp** and believe its first-divergence
|
||||
report.
|
||||
|
||||
## Build plan
|
||||
|
||||
- [x] `COSIM_ORACLE.md` (this file).
|
||||
- [x] State-hash module `runtime/src/cosim_state.c` (+ `include/cosim_state.h`):
|
||||
canonical FNV serialize → incremental RAM page hashes + CPU micro-state
|
||||
(muldiv/gte/load-delay) + interrupts statics + device blobs (boot_state accessors)
|
||||
→ `state_hash` + sub-hashes. VRAM excluded in v1 (downstream, not causal).
|
||||
- [x] Engine `runtime/src/cosim.c`: cycle-keyed checkpoints (ring + cumulative chain
|
||||
hash + park/step) + minimal standalone TCP server. Commands: `status`, `chain`,
|
||||
`stride N`, `runto <cycle>`, `hash`, `sub`, `window N`, `inject ram|reg`, `reset`.
|
||||
- [x] Hooks (all `#ifdef PSX_COSIM`, zero effect on normal builds): `cosim_tick()` in
|
||||
`psx_advance_cycles` (the alignment clock); `cosim_block(pc)` emitted by BOTH
|
||||
emitters + called in the interp loop (pc reporting); `cosim_note_ram_write` in
|
||||
`memory.c` word/half/byte RAM writes (covers CPU+DMA — verified DMA routes through
|
||||
psx_write_*_raw); `interrupts_cosim_hash()`; `cosim_init()` in main.
|
||||
- [ ] Clean build target `psx-cosim` (CMake): `PSX_COSIM` + `PSX_NO_DEBUG_TOOLS`,
|
||||
single-threaded guest, software renderer, no host throttle, headless. Add
|
||||
cosim.c + cosim_state.c to the target. (Fix any unguarded debug-only calls the
|
||||
`PSX_NO_DEBUG_TOOLS` build surfaces.)
|
||||
- [ ] Regen game + BIOS (emitters now emit the guarded `cosim_block`).
|
||||
- [ ] Coordinator `tools/cosim.py`: launch 2 instances (compiled + PSX_FORCE_INTERP),
|
||||
cycle-keyed lockstep via `runto`/`chain`, stride coarse→1 drill, halt at first
|
||||
divergent checkpoint, dump bounded `window` + `sub` diff.
|
||||
- [ ] Run gates 1–4 (compiled-vs-compiled=0, interp-vs-interp=0, hash-vs-byte audit,
|
||||
injected-divergence halts at the right field), THEN compiled-vs-interp.
|
||||
|
||||
## History (do not lose again)
|
||||
|
||||
- The device-event IRQ ring (`device_trace`, commit `18c94a1`) + a prior session's
|
||||
Beetle diff found: native charges **zero guest cycles for GPU draw time** while
|
||||
Beetle halts the CPU (DrawTimeAvail/RecalcHalt), so native over-runs the data-paced
|
||||
asset-load loop (~5.4× GPU DMAs/frame) and completes `func_8001CB3C` ~150M cycles
|
||||
early → wrong branch. CD ruled out (faithful vs Beetle); MDEC decode cost
|
||||
implemented, did NOT fix. GPU draw cost bisect-confirmed as a **lever** but never
|
||||
validated to render the FMV. ChatGPT gave a full DrawTimeAvail implementation plan.
|
||||
**This is a hypothesis for the oracle to test, not an assumed answer.**
|
||||
- This session (2026-06-30): localized to the load-delay axis on the current build
|
||||
(`PSX_LOAD_DELAY=0` → FMV plays, pixels), but that is a **secondary phase modulator**,
|
||||
not the faithful root; and a load-delay save/restore-across-exception fix was built
|
||||
and **refuted** (no effect). Two separate bugs confirmed: FMV-skip and the later
|
||||
`02 02` gameplay wedge.
|
||||
@@ -1121,7 +1121,11 @@ std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr)
|
||||
code = gte_read + fmt::format("{} = cpu->gte_data[{}]; /* mfc2 */", reg_name(rt), rd);
|
||||
}
|
||||
} else if (cop_op == 0x02) { // CFC2 - move from COP2 control
|
||||
code = gte_read + fmt::format("{} = cpu->gte_ctrl[{}]; /* cfc2 */", reg_name(rt), rd);
|
||||
if (rd == 26 || rd == 27 || rd == 29 || rd == 30 || rd == 31) {
|
||||
code = gte_read + fmt::format("{} = gte_read_ctrl(cpu, {}); /* cfc2 */", reg_name(rt), rd);
|
||||
} else {
|
||||
code = gte_read + fmt::format("{} = cpu->gte_ctrl[{}]; /* cfc2 */", reg_name(rt), rd);
|
||||
}
|
||||
} else if (cop_op == 0x04) { // MTC2 - move to COP2 data
|
||||
if (rd == 7 || (rd >= 8 && rd <= 11) || rd == 14 || rd == 15 || rd == 28 || rd == 30) {
|
||||
code = gte_stall + fmt::format("gte_write_data(cpu, {}, {}); /* mtc2 */", rd, reg_name(rt));
|
||||
@@ -1129,7 +1133,11 @@ std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr)
|
||||
code = gte_stall + fmt::format("cpu->gte_data[{}] = {}; /* mtc2 */", rd, reg_name(rt));
|
||||
}
|
||||
} else if (cop_op == 0x06) { // CTC2 - move to COP2 control
|
||||
code = gte_stall + fmt::format("cpu->gte_ctrl[{}] = {}; /* ctc2 */", rd, reg_name(rt));
|
||||
if (rd == 26 || rd == 27 || rd == 29 || rd == 30 || rd == 31) {
|
||||
code = gte_stall + fmt::format("gte_write_ctrl(cpu, {}, {}); /* ctc2 */", rd, reg_name(rt));
|
||||
} else {
|
||||
code = gte_stall + fmt::format("cpu->gte_ctrl[{}] = {}; /* ctc2 */", rd, reg_name(rt));
|
||||
}
|
||||
} else if ((cop_op & 0x10) != 0) { // GTE command (bit 25 set)
|
||||
uint32_t gte_cmd = instr & 0x1FFFFFF;
|
||||
// Route ALL GTE commands through gte_execute() for correct behavior
|
||||
@@ -1231,6 +1239,12 @@ std::string CodeGenerator::translate_basic_block(
|
||||
ss << config_.indent
|
||||
<< fmt::format("debug_server_cyc_observe(0x{:08X}u);\n", block.start_addr);
|
||||
ss << "#endif\n";
|
||||
// First-divergence co-sim oracle (COSIM_ORACLE.md): lean per-block-leader hook,
|
||||
// present only in the clean PSX_COSIM build (independent of the debug tools).
|
||||
ss << "#ifdef PSX_COSIM\n";
|
||||
ss << config_.indent
|
||||
<< fmt::format("cosim_block(0x{:08X}u);\n", block.start_addr);
|
||||
ss << "#endif\n";
|
||||
|
||||
// Cycle-budgeted precise event slicing (PRECISE_IRQ_SLICE.md). At the block
|
||||
// leader — before any cycle is charged or the body runs — divert to the
|
||||
@@ -1337,6 +1351,11 @@ std::string CodeGenerator::translate_basic_block(
|
||||
ss << indent << fmt::format("psx_icache_fetch(cpu, 0x{:08X}u);\n", insn_addr);
|
||||
ss << "#endif\n";
|
||||
};
|
||||
auto emit_cosim_instr = [&](uint32_t insn_addr, const std::string& indent) {
|
||||
ss << "#ifdef PSX_COSIM\n";
|
||||
ss << indent << fmt::format("cosim_instr(0x{:08X}u);\n", insn_addr);
|
||||
ss << "#endif\n";
|
||||
};
|
||||
if (!cycle_per_insn && block_exec_cycles > 0) {
|
||||
ss << "#ifdef PSX_ENABLE_BLOCK_CYCLES\n";
|
||||
ss << config_.indent << fmt::format("psx_advance_cycles({}u);\n",
|
||||
@@ -1398,6 +1417,7 @@ std::string CodeGenerator::translate_basic_block(
|
||||
if (cycle_per_insn) emit_pre_icache(addr, config_.indent);
|
||||
if (cycle_per_insn) emit_pre_timing(instr, config_.indent);
|
||||
ss << translate_instruction(addr, instr) << "\n";
|
||||
emit_cosim_instr(addr, config_.indent);
|
||||
} else {
|
||||
// Control flow is handled at block exit
|
||||
if (addr == exit_branch_addr) {
|
||||
@@ -1410,6 +1430,28 @@ std::string CodeGenerator::translate_basic_block(
|
||||
if (cycle_per_insn)
|
||||
emit_pre_timing(block.exit_instr.instruction, config_.indent);
|
||||
|
||||
if (block.exit_instr.type == ControlFlowType::JumpLink) {
|
||||
ss << config_.indent
|
||||
<< fmt::format("cpu->gpr[31] = 0x{:08X}u; /* jal link before delay slot */\n",
|
||||
addr + 8);
|
||||
} else if (block.exit_instr.type == ControlFlowType::JumpLinkReg) {
|
||||
uint32_t rd = get_rd(block.exit_instr.instruction);
|
||||
if (rd != 0) {
|
||||
ss << config_.indent
|
||||
<< fmt::format("{} = 0x{:08X}u; /* jalr link before delay slot */\n",
|
||||
reg_name(rd), addr + 8);
|
||||
}
|
||||
} else if (block.exit_instr.type == ControlFlowType::Branch) {
|
||||
uint32_t branch_instr = block.exit_instr.instruction;
|
||||
uint32_t b_opcode = (branch_instr >> 26) & 0x3F;
|
||||
uint32_t regimm_op = (branch_instr >> 16) & 0x1F;
|
||||
if (b_opcode == 0x01 && (regimm_op == 0x10 || regimm_op == 0x11)) {
|
||||
ss << config_.indent
|
||||
<< fmt::format("cpu->gpr[31] = 0x{:08X}u; /* branch-and-link before delay slot */\n",
|
||||
addr + 8);
|
||||
}
|
||||
}
|
||||
|
||||
// MIPS delay slot handling: emit delay slot instruction BEFORE branch/jump
|
||||
std::string delay_saved_cond; // non-empty if condition was pre-captured
|
||||
if (block.exit_instr.has_delay_slot) {
|
||||
@@ -1424,6 +1466,7 @@ std::string CodeGenerator::translate_basic_block(
|
||||
ss << config_.indent << "/* delay slot (likely) - conditional execution */\n";
|
||||
ss << config_.indent << "if (" << generate_branch_condition(block.exit_instr.instruction) << ") {\n";
|
||||
ss << config_.indent << translate_instruction(delay_slot_addr, delay_instr) << "\n";
|
||||
emit_cosim_instr(delay_slot_addr, config_.indent + config_.indent);
|
||||
ss << config_.indent << "}\n";
|
||||
} else {
|
||||
// Normal delay slot - always executes.
|
||||
@@ -1442,6 +1485,7 @@ std::string CodeGenerator::translate_basic_block(
|
||||
if (cycle_per_insn) emit_pre_icache(delay_slot_addr, config_.indent);
|
||||
if (cycle_per_insn) emit_pre_timing(delay_instr, config_.indent);
|
||||
ss << translate_instruction(delay_slot_addr, delay_instr) << "\n";
|
||||
emit_cosim_instr(delay_slot_addr, config_.indent);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1861,20 +1905,20 @@ std::string CodeGenerator::translate_basic_block(
|
||||
}
|
||||
|
||||
// If no explicit control flow, fall through to the next block. When a
|
||||
// mid-function seed split placed the next instruction in a different C
|
||||
// function, tail-call that split piece so the physical fall-through is
|
||||
// preserved.
|
||||
// CFG split or mid-function seed split placed the next instruction behind
|
||||
// a C label/function boundary, preserve the physical fall-through without
|
||||
// consuming an interrupt/cooldown check at the artificial boundary.
|
||||
if (block.exit_instr.type == ControlFlowType::None && !block.successors.empty()) {
|
||||
ss << emit_interrupt_check(block.successors[0], config_.indent);
|
||||
ss << config_.indent << fmt::format("/* fall through to block_{:08X} */\n",
|
||||
block.successors[0]);
|
||||
} else if (block.exit_instr.type == ControlFlowType::None) {
|
||||
uint32_t next_addr = block.end_addr + 4;
|
||||
ss << emit_interrupt_check(next_addr, config_.indent);
|
||||
if (known_functions_.count(next_addr) > 0) {
|
||||
ss << config_.indent
|
||||
<< fmt::format("func_{:08X}(cpu); return; /* fallthrough to split piece */\n",
|
||||
next_addr);
|
||||
} else {
|
||||
ss << emit_interrupt_check(next_addr, config_.indent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2446,6 +2490,10 @@ std::string CodeGenerator::generate_file(
|
||||
ss << "#ifndef PSX_NO_DEBUG_TOOLS\n";
|
||||
ss << "extern void debug_server_cyc_observe(uint32_t block_leader_phys);\n";
|
||||
ss << "#endif\n";
|
||||
ss << "#ifdef PSX_COSIM\n";
|
||||
ss << "extern void cosim_block(uint32_t block_leader_phys);\n";
|
||||
ss << "extern void cosim_instr(uint32_t pc);\n";
|
||||
ss << "#endif\n";
|
||||
ss << "extern void psx_ws_sprite_tag(CPUState* cpu); /* widescreen prim tag (gpu.c) */\n";
|
||||
ss << "extern int psx_ws_x_margin(void); /* widescreen cull-margin term (gpu.c) */\n";
|
||||
ss << "extern int psx_ws_cull_sltiu(uint32_t sx, uint32_t imm); /* ws auto screen-x cull (gpu.c) */\n";
|
||||
@@ -2624,19 +2672,52 @@ std::string CodeGenerator::generate_file(
|
||||
if (f.alias_walk_lo == 0) func_starts.push_back(f.start_addr);
|
||||
}
|
||||
|
||||
// Group targets by containing function
|
||||
// Group targets by containing function. Some real compiler layouts
|
||||
// branch past an early jr-ra return into the bytes before the next
|
||||
// discovered function. Those targets are not "mid-function" by the
|
||||
// return-scanner's range, but they are executable and must still be
|
||||
// dispatchable CPS pieces.
|
||||
std::map<uint32_t, std::vector<uint32_t>> splits_by_func;
|
||||
std::set<uint32_t> gap_targets;
|
||||
for (uint32_t target : mid_targets) {
|
||||
auto it = std::upper_bound(func_starts.begin(), func_starts.end(), target);
|
||||
if (it == func_starts.begin()) continue;
|
||||
if (it == func_starts.begin()) {
|
||||
gap_targets.insert(target);
|
||||
continue;
|
||||
}
|
||||
--it;
|
||||
splits_by_func[*it].push_back(target);
|
||||
auto fit = std::find_if(functions_mut.begin(), functions_mut.end(),
|
||||
[&](const Function& f) { return f.start_addr == *it; });
|
||||
if (fit != functions_mut.end() && target > fit->start_addr && target < fit->end_addr) {
|
||||
splits_by_func[*it].push_back(target);
|
||||
} else {
|
||||
gap_targets.insert(target);
|
||||
}
|
||||
}
|
||||
|
||||
// Split each affected function
|
||||
std::vector<Function> new_funcs;
|
||||
std::set<uint32_t> affected;
|
||||
|
||||
for (uint32_t target : gap_targets) {
|
||||
auto next_it = std::upper_bound(func_starts.begin(), func_starts.end(), target);
|
||||
uint32_t gap_end = (next_it != func_starts.end()) ? *next_it : exe_end;
|
||||
if (target >= gap_end) continue;
|
||||
|
||||
Function nf;
|
||||
nf.start_addr = target;
|
||||
nf.end_addr = gap_end;
|
||||
nf.size = nf.end_addr - nf.start_addr;
|
||||
nf.name = fmt::format("func_{:08X}", nf.start_addr);
|
||||
nf.has_prologue = false;
|
||||
nf.has_epilogue = false;
|
||||
nf.stack_frame_size = 0;
|
||||
nf.is_data_section = false;
|
||||
new_funcs.push_back(nf);
|
||||
known_addrs.insert(nf.start_addr);
|
||||
affected.insert(nf.start_addr);
|
||||
}
|
||||
|
||||
for (auto& [func_start, targets] : splits_by_func) {
|
||||
std::sort(targets.begin(), targets.end());
|
||||
|
||||
|
||||
@@ -58,6 +58,21 @@ static uint32_t ram_alias_to_rom(uint32_t addr) {
|
||||
return addr;
|
||||
}
|
||||
|
||||
/* Map a BIOS ROM PC to the address the PSX CPU actually executes from after the
|
||||
* BIOS copies kernel/shell code into RAM. Literal interrupt resume PCs must use
|
||||
* this runtime address; otherwise psx_check_interrupts rejects ROM-shell EPCs
|
||||
* and falls back to the legacy sentinel path. */
|
||||
static uint32_t bios_runtime_pc(uint32_t rom_pc) {
|
||||
uint32_t phys = rom_pc & 0x1FFFFFFFu;
|
||||
if (phys >= 0x1FC18000u && phys <= 0x1FC42FFFu) {
|
||||
return 0x80030000u + (phys - 0x1FC18000u);
|
||||
}
|
||||
if (phys >= 0x1FC10000u && phys <= 0x1FC17FFFu) {
|
||||
return phys - 0x1FC10000u + 0x00000500u;
|
||||
}
|
||||
return rom_pc;
|
||||
}
|
||||
|
||||
uint32_t FullFunctionEmitter::read_u32_le(const std::vector<uint8_t>& rom, uint32_t offset) {
|
||||
return static_cast<uint32_t>(rom[offset + 0])
|
||||
| (static_cast<uint32_t>(rom[offset + 1]) << 8)
|
||||
@@ -118,12 +133,18 @@ bool FullFunctionEmitter::emit_function(
|
||||
return e == nullptr || e[0] != '0';
|
||||
}();
|
||||
auto emit_irq_check = [](uint32_t resume_pc, const std::string& indent = " ") {
|
||||
return indent + fmt::format("psx_check_interrupts_at(cpu, 0x{:08X}u);\n", resume_pc);
|
||||
return indent + fmt::format("psx_check_interrupts_at(cpu, 0x{:08X}u);\n",
|
||||
bios_runtime_pc(resume_pc));
|
||||
};
|
||||
auto emit_irq_check_expr = [](const std::string& resume_pc_expr,
|
||||
const std::string& indent = " ") {
|
||||
return indent + fmt::format("psx_check_interrupts_at(cpu, {});\n", resume_pc_expr);
|
||||
};
|
||||
auto emit_cosim_instr = [](uint32_t pc, const std::string& indent = " ") {
|
||||
return "#ifdef PSX_COSIM\n" + indent +
|
||||
fmt::format("cosim_instr(0x{:08X}u);\n", bios_runtime_pc(pc)) +
|
||||
"#endif\n";
|
||||
};
|
||||
|
||||
// Build sorted instruction list and a set for O(1) membership test.
|
||||
// sfr.instructions is already sorted by address.
|
||||
@@ -270,19 +291,7 @@ bool FullFunctionEmitter::emit_function(
|
||||
* Without this, handler/callback tables get ROM addresses instead of
|
||||
* RAM addresses, breaking pointer comparisons in the BIOS. */
|
||||
auto relocate_ra = [](uint32_t rom_ra) -> uint32_t {
|
||||
uint32_t phys = rom_ra & 0x1FFFFFFFu;
|
||||
/* Shell relocation upper bound matches the runtime dispatcher's
|
||||
* normalize() in SCPH1001_dispatch.c: shell covers RAM 0x30000-0x5AFFF
|
||||
* (= ROM 0x1FC18000-0x1FC42FFF inclusive). Previously this was
|
||||
* 0x1FC427FF, which left BFC42800-BFC42FFF JAL targets un-relocated
|
||||
* (target kept ROM PC[31:28]=0xB instead of RAM PC[31:28]=0x8). */
|
||||
if (phys >= 0x1FC18000u && phys <= 0x1FC42FFFu) {
|
||||
return 0x80030000u + (phys - 0x1FC18000u);
|
||||
}
|
||||
if (phys >= 0x1FC10000u && phys <= 0x1FC17FFFu) {
|
||||
return phys - 0x1FC10000u + 0x00000500u;
|
||||
}
|
||||
return rom_ra;
|
||||
return bios_runtime_pc(rom_ra);
|
||||
};
|
||||
|
||||
// First pass: identify terminators and their delay slots.
|
||||
@@ -430,6 +439,10 @@ bool FullFunctionEmitter::emit_function(
|
||||
out += fmt::format(" debug_server_cyc_observe(0x{:08X}u);\n",
|
||||
normalize_address(addr));
|
||||
out += "#endif\n";
|
||||
// First-divergence co-sim oracle (COSIM_ORACLE.md): lean block-leader hook.
|
||||
out += "#ifdef PSX_COSIM\n";
|
||||
out += fmt::format(" cosim_block(0x{:08X}u);\n", normalize_address(addr));
|
||||
out += "#endif\n";
|
||||
// Phase 1.0e-d: advance guest cycles for this block. Macro-
|
||||
// gated; when off, generated code matches pre-1.0e-d output.
|
||||
// In per-instruction mode the charge is emitted per instruction
|
||||
@@ -474,6 +487,16 @@ bool FullFunctionEmitter::emit_function(
|
||||
out += fmt::format(" /* 0x{:08X}: {:08X} {} */\n", addr, raw, tr.comment);
|
||||
|
||||
const std::string kind = tr.terminator_kind ? tr.terminator_kind : "";
|
||||
if (kind == "jal") {
|
||||
out += fmt::format(" cpu->gpr[31] = 0x{:08X}u; /* jal link before delay slot */\n",
|
||||
relocate_ra(addr + 8));
|
||||
} else if (kind == "jalr") {
|
||||
uint8_t rd = (raw >> 11) & 0x1F;
|
||||
if (rd != 0) {
|
||||
out += fmt::format(" cpu->gpr[{}] = 0x{:08X}u; /* jalr link before delay slot */\n",
|
||||
static_cast<int>(rd), relocate_ra(addr + 8));
|
||||
}
|
||||
}
|
||||
|
||||
if (is_branch_kind(tr.terminator_kind)) {
|
||||
// Emit pre-delay snapshot using a unique flag.
|
||||
@@ -799,6 +822,7 @@ bool FullFunctionEmitter::emit_function(
|
||||
// Non-terminator: emit normally.
|
||||
out += fmt::format(" /* 0x{:08X}: {:08X} {} */\n", addr, raw, tr.comment);
|
||||
out += fmt::format(" {}\n", tr.c_code);
|
||||
out += emit_cosim_instr(addr);
|
||||
|
||||
// Check if this instruction is a delay slot with pending resolution.
|
||||
if (pending_at.count(addr)) {
|
||||
@@ -1357,6 +1381,14 @@ void FullFunctionEmitter::emit_dispatch(
|
||||
out += "extern uint64_t g_dispatch_static_hits;\n";
|
||||
out += "\n";
|
||||
out += "int g_psx_dispatch_depth = 0;\n\n";
|
||||
out += "static void psx_dispatch_check_return_boundary(CPUState* cpu, uint32_t stop_addr) {\n";
|
||||
out += " if (stop_addr != 0u) {\n";
|
||||
out += " psx_check_interrupts_at(cpu, stop_addr);\n";
|
||||
out += " if (((cpu->pc ^ stop_addr) & 0x1FFFFFFFu) == 0) cpu->pc = 0;\n";
|
||||
out += " } else {\n";
|
||||
out += " psx_check_interrupts(cpu);\n";
|
||||
out += " }\n";
|
||||
out += "}\n\n";
|
||||
out += "static void psx_dispatch_impl(CPUState* cpu, uint32_t addr, uint32_t stop_addr) {\n";
|
||||
out += " /* Tail-call trampoline: functions signal tail calls by setting\n";
|
||||
out += " * cpu->pc to the target and returning. We loop here to re-dispatch\n";
|
||||
@@ -1447,7 +1479,7 @@ void FullFunctionEmitter::emit_dispatch(
|
||||
out += " cpu->pc = 0;\n";
|
||||
out += " --g_psx_dispatch_depth;\n";
|
||||
out += " if (outermost) {\n";
|
||||
out += " psx_check_interrupts(cpu);\n";
|
||||
out += " psx_dispatch_check_return_boundary(cpu, stop_addr);\n";
|
||||
out += " }\n";
|
||||
out += " return;\n";
|
||||
out += " }\n";
|
||||
@@ -1484,7 +1516,7 @@ void FullFunctionEmitter::emit_dispatch(
|
||||
out += " }\n";
|
||||
out += " --g_psx_dispatch_depth;\n";
|
||||
out += " if (outermost) {\n";
|
||||
out += " psx_check_interrupts(cpu);\n";
|
||||
out += " psx_dispatch_check_return_boundary(cpu, stop_addr);\n";
|
||||
out += " }\n";
|
||||
out += " return;\n";
|
||||
out += " }\n";
|
||||
@@ -1499,7 +1531,7 @@ void FullFunctionEmitter::emit_dispatch(
|
||||
out += " cpu->pc = 0;\n";
|
||||
out += " --g_psx_dispatch_depth;\n";
|
||||
out += " if (outermost) {\n";
|
||||
out += " psx_check_interrupts(cpu);\n";
|
||||
out += " psx_dispatch_check_return_boundary(cpu, stop_addr);\n";
|
||||
out += " }\n";
|
||||
out += " return;\n";
|
||||
out += " }\n";
|
||||
@@ -1589,6 +1621,10 @@ EmitStats FullFunctionEmitter::emit(
|
||||
full_c += "extern uint32_t gte_read_data(CPUState* cpu, uint8_t reg);\n";
|
||||
full_c += "extern void debug_server_log_call_entry(uint32_t func_addr);\n";
|
||||
full_c += "extern void debug_server_log_probe(uint32_t pc, CPUState *cpu);\n";
|
||||
full_c += "#ifdef PSX_COSIM\n";
|
||||
full_c += "extern void cosim_block(uint32_t block_leader_phys);\n";
|
||||
full_c += "extern void cosim_instr(uint32_t pc);\n";
|
||||
full_c += "#endif\n";
|
||||
full_c += "#ifndef PSX_NO_DEBUG_TOOLS\n";
|
||||
full_c += "extern void debug_server_cyc_observe(uint32_t block_leader_phys);\n";
|
||||
full_c += "#endif\n";
|
||||
|
||||
@@ -860,6 +860,7 @@ int main(int argc, char** argv) {
|
||||
std::ostringstream ds;
|
||||
ds << "/* Generated by PSXRecomp - dynamic dispatch table */\n";
|
||||
ds << "#include \"psx_runtime.h\"\n\n";
|
||||
ds << "extern void psx_check_interrupts_dispatch_entry(CPUState* cpu, uint32_t resume_pc);\n\n";
|
||||
|
||||
// Forward declarations
|
||||
ds << "/* Forward declarations for all recompiled functions */\n";
|
||||
@@ -886,20 +887,20 @@ int main(int argc, char** argv) {
|
||||
// entry-switch routes into the owning block. The unified flat
|
||||
// trampoline (psx_dispatch_impl) then drives every tail-transfer.
|
||||
for (uint32_t addr : dispatch_addrs) {
|
||||
ds << fmt::format(" case 0x{:08X}u: cpu->pc = 0; func_{:08X}(cpu); return 1;\n",
|
||||
addr, addr);
|
||||
ds << fmt::format(" case 0x{:08X}u: psx_check_interrupts_dispatch_entry(cpu, 0x{:08X}u); cpu->pc = 0; func_{:08X}(cpu); return 1;\n",
|
||||
addr, addr, addr);
|
||||
}
|
||||
const auto& conts = codegen.cps_continuations();
|
||||
ds << fmt::format(" /* {} continuation return-points (CPS) */\n", conts.size());
|
||||
for (const auto& [cont, owner] : conts) {
|
||||
if (dispatch_addrs.count(cont)) continue; /* already a function entry */
|
||||
ds << fmt::format(" case 0x{:08X}u: cpu->pc = 0x{:08X}u; func_{:08X}(cpu); return 1;\n",
|
||||
cont, cont, owner);
|
||||
ds << fmt::format(" case 0x{:08X}u: psx_check_interrupts_dispatch_entry(cpu, 0x{:08X}u); cpu->pc = 0x{:08X}u; func_{:08X}(cpu); return 1;\n",
|
||||
cont, cont, cont, owner);
|
||||
}
|
||||
} else {
|
||||
for (uint32_t addr : dispatch_addrs) {
|
||||
ds << fmt::format(" case 0x{:08X}u: func_{:08X}(cpu); return 1;\n",
|
||||
addr, addr);
|
||||
ds << fmt::format(" case 0x{:08X}u: psx_check_interrupts_dispatch_entry(cpu, 0x{:08X}u); func_{:08X}(cpu); return 1;\n",
|
||||
addr, addr, addr);
|
||||
}
|
||||
}
|
||||
ds << " default: return 0;\n";
|
||||
|
||||
@@ -1281,8 +1281,13 @@ TranslateResult StrictTranslator::translate(const PSXRecomp::DecodedInstruction&
|
||||
}
|
||||
if (cop_op == 0x02) { // CFC2 — move from COP2 control register
|
||||
r.supported = true;
|
||||
r.c_code = gte_read + emit_gpr_write(rt,
|
||||
fmt::format("cpu->gte_ctrl[{}]", static_cast<int>(rd)));
|
||||
if (rd == 26 || rd == 27 || rd == 29 || rd == 30 || rd == 31) {
|
||||
r.c_code = gte_read + emit_gpr_write(rt,
|
||||
fmt::format("gte_read_ctrl(cpu, {})", static_cast<int>(rd)));
|
||||
} else {
|
||||
r.c_code = gte_read + emit_gpr_write(rt,
|
||||
fmt::format("cpu->gte_ctrl[{}]", static_cast<int>(rd)));
|
||||
}
|
||||
r.comment = fmt::format("cfc2 {}, gte_ctrl[{}]", gpr_name(rt), rd);
|
||||
return r;
|
||||
}
|
||||
@@ -1304,9 +1309,15 @@ TranslateResult StrictTranslator::translate(const PSXRecomp::DecodedInstruction&
|
||||
}
|
||||
if (cop_op == 0x06) { // CTC2 — move to COP2 control register
|
||||
r.supported = true;
|
||||
r.c_code = gte_stall + fmt::format(
|
||||
"cpu->gte_ctrl[{}] = cpu->gpr[{}];",
|
||||
static_cast<int>(rd), static_cast<int>(rt));
|
||||
if (rd == 26 || rd == 27 || rd == 29 || rd == 30 || rd == 31) {
|
||||
r.c_code = gte_stall + fmt::format(
|
||||
"gte_write_ctrl(cpu, {}, cpu->gpr[{}]);",
|
||||
static_cast<int>(rd), static_cast<int>(rt));
|
||||
} else {
|
||||
r.c_code = gte_stall + fmt::format(
|
||||
"cpu->gte_ctrl[{}] = cpu->gpr[{}];",
|
||||
static_cast<int>(rd), static_cast<int>(rt));
|
||||
}
|
||||
r.comment = fmt::format("ctc2 {}, gte_ctrl[{}]", gpr_name(rt), rd);
|
||||
return r;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
/* cosim_state.h — full-architectural-state canonical hash for the first-divergence
|
||||
* co-simulation oracle. See COSIM_ORACLE.md.
|
||||
*
|
||||
* The single correctness rule: this hash must cover EVERY piece of state that can
|
||||
* influence future guest execution, and NOTHING that is host-only (pointers, padding,
|
||||
* jmpbufs, fiber/malloc addresses). A missed execution-relevant field is a blind spot
|
||||
* (false "no divergence"); an included host-only field is a false positive. The
|
||||
* validation gates in COSIM_ORACLE.md (compiled-vs-compiled == 0, injected-divergence
|
||||
* halts at the right field) exist to prove this list is exactly right.
|
||||
*/
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
#include "cpu_state.h"
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Per-subsystem sub-hashes so a mismatch localizes to a subsystem before a full
|
||||
* field/byte diff. All 64-bit FNV-1a over canonical little-endian serialization. */
|
||||
typedef struct {
|
||||
uint64_t cpu; /* GPRs/HI/LO/PC/COP0/GTE + micro-state (muldiv/gte/load-delay) */
|
||||
uint64_t irqctl; /* i_stat/i_mask + interrupts.c timing statics + VBLANK schedule */
|
||||
uint64_t ram; /* 2MB main RAM (incremental page-hash) */
|
||||
uint64_t scratch; /* 1KB scratchpad */
|
||||
uint64_t vram; /* 1MB VRAM (incremental page-hash) */
|
||||
uint64_t gpu; /* GPU regs + FIFO + draw budget + deadlines (boot_state blob) */
|
||||
uint64_t spu; /* SPU regs + voices + SPU RAM */
|
||||
uint64_t cdrom; /* CDROM command/FIFO/phase/next-event */
|
||||
uint64_t dma; /* DMA channel regs + in-flight/deadlines */
|
||||
uint64_t sio; /* SIO/pad/memcard transaction state */
|
||||
uint64_t timers; /* RootCounters counter/mode/target/frac/last-update */
|
||||
uint64_t clock; /* psx_cycle_count */
|
||||
uint64_t dirty; /* dirty-RAM page bitmap (affects dispatch/interp routing) */
|
||||
} CosimSubHashes;
|
||||
|
||||
/* Compute the full canonical state hash of the LIVE machine (debug_cpu_ptr + globals).
|
||||
* Fills `sub` (may be NULL) and returns the folded top hash. Cheap per call: RAM/VRAM
|
||||
* use incremental page hashes maintained by cosim_note_ram_write/cosim_note_vram_write;
|
||||
* everything else is small. */
|
||||
uint64_t cosim_state_hash(CosimSubHashes *sub);
|
||||
|
||||
/* Incremental page-hash maintenance. Call from the RAM/VRAM write chokepoints in the
|
||||
* cosim build: mark the touched page dirty (cheap); the page hash is recomputed lazily
|
||||
* inside cosim_state_hash(). phys is the byte offset into RAM (0..2MB) / VRAM (0..1MB). */
|
||||
void cosim_note_ram_write(uint32_t phys, uint32_t nbytes);
|
||||
void cosim_note_vram_write(uint32_t byte_off, uint32_t nbytes);
|
||||
|
||||
/* Reset all incremental state (call once at machine init / after a full state load). */
|
||||
void cosim_state_reset(void);
|
||||
|
||||
/* ---- engine (cosim.c) ---- */
|
||||
void cosim_init(void); /* start the TCP oracle server (call once at startup) */
|
||||
void cosim_block(uint32_t pc); /* block-leader pc stash (emitted + interp), reporting */
|
||||
uint32_t cosim_last_block(void); /* last block-leader pc, for diagnostic attribution */
|
||||
void cosim_tick(void); /* cycle-keyed checkpoint (called from psx_advance_cycles) */
|
||||
void cosim_instr(uint32_t pc); /* retire pending cycle checkpoints at an instruction boundary */
|
||||
uint32_t cosim_cycles_to_next_checkpoint(void);
|
||||
|
||||
/* Gate-4 fault injection: after this is armed, the next cosim_state_hash perturbs one
|
||||
* byte of RAM (addr) or one CPU GPR (reg 0..31, hi=32, lo=33) by XOR val, so the
|
||||
* oracle MUST halt at exactly that field. Returns via the same hash path. */
|
||||
void cosim_inject_ram(uint32_t phys, uint8_t xor_val);
|
||||
void cosim_inject_reg(int reg_index, uint32_t xor_val);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -32,12 +32,17 @@ void interrupts_init(void);
|
||||
void psx_irq_raise(uint32_t bit, uint32_t detail);
|
||||
|
||||
/* Called from the dispatch loop after each function returns.
|
||||
* Fires vblank on schedule, checks (i_stat & i_mask), and if
|
||||
* pending + COP0 allows, dispatches the exception handler. */
|
||||
* Services due scheduled edges, checks (i_stat & i_mask), and if pending + COP0
|
||||
* allows, dispatches the exception handler. */
|
||||
void psx_check_interrupts(struct CPUState* cpu);
|
||||
/* Interrupt check with the compiled guest PC to resume if a game-installed
|
||||
* handler later RFEs to the sentinel outside the synchronous host window. */
|
||||
void psx_check_interrupts_at(struct CPUState* cpu, uint32_t resume_pc);
|
||||
int psx_interrupts_checked_at_current_cycle(uint32_t resume_pc);
|
||||
/* Dispatch-entry check for re-enterable compiled PCs. De-dupes an immediately
|
||||
* preceding check at the same guest PC/cycle so generated transfers can keep
|
||||
* their explicit checks while dirty/interp-to-static entries get a boundary. */
|
||||
void psx_check_interrupts_dispatch_entry(struct CPUState* cpu, uint32_t resume_pc);
|
||||
|
||||
/* Accumulate emitted PSX cycles toward the next VBlank trigger.
|
||||
* Called from psx_advance_cycles() so the VBlank rate is gated on
|
||||
@@ -45,6 +50,8 @@ void psx_check_interrupts_at(struct CPUState* cpu, uint32_t resume_pc);
|
||||
* count (which was 5-6x too fast and squeezed game-time to ~60% of
|
||||
* real). One real-PSX VBlank = 564480 cycles (33.8688 MHz / 60). */
|
||||
void interrupts_advance_cycles(uint32_t cycles);
|
||||
void interrupts_service_scheduled_events(void);
|
||||
uint32_t interrupts_cycles_to_vblank(void);
|
||||
|
||||
/* 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
|
||||
|
||||
@@ -32,6 +32,26 @@ void ls_write_hook(uint32_t addr, int size, uint32_t val);
|
||||
/* Per-basic-block-leader entry, called from debug_server_cyc_observe. */
|
||||
void ls_at_leader(uint32_t leader_phys, CPUState *cpu);
|
||||
|
||||
/* Dispatch-segment comparator: wraps one clean psx_dispatch_game_compiled()
|
||||
* invocation and replays the same callable through exec_one until it reaches
|
||||
* the same returned cpu->pc. This measures the native CPS unit, not just one
|
||||
* basic block. */
|
||||
void ls_func_enter(uint32_t entry_pc, CPUState *cpu);
|
||||
void ls_func_exit(uint32_t entry_pc, CPUState *cpu, int handled);
|
||||
void ls_func_set_window(uint32_t frame_lo, uint32_t frame_hi);
|
||||
void ls_func_set_record_only(int on);
|
||||
|
||||
/* Called at real IRQ exception entry. Function-scope comparisons skip segments
|
||||
* that delivered an interrupt, keeping codegen/tooling checks separate from
|
||||
* event-phase behavior. */
|
||||
void ls_note_exception_entry(void);
|
||||
|
||||
/* Suppress lockstep memory recording while runtime/debug observers read guest
|
||||
* RAM for diagnostics. These reads are not guest instructions and must not
|
||||
* become part of the compiled-vs-interp trace. */
|
||||
void ls_suppress_begin(void);
|
||||
void ls_suppress_end(void);
|
||||
|
||||
/* Arm the comparator over a guest-frame window [lo,hi]. hi==0 => disabled. */
|
||||
void ls_set_window(uint32_t frame_lo, uint32_t frame_hi);
|
||||
|
||||
@@ -41,6 +61,7 @@ void ls_set_record_only(int on);
|
||||
|
||||
/* Drain the first-divergence record as JSON into buf. Returns bytes written. */
|
||||
int ls_get_diverge_json(char *buf, int buflen);
|
||||
int ls_get_func_json(char *buf, int buflen);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
+12
-1
@@ -135,6 +135,8 @@ set(PSXRECOMP_RUNTIME_SOURCES
|
||||
${PSXRECOMP_ROOT}/runtime/src/parity_trace.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/device_trace.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/boot_state.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/cosim_state.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/cosim.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/traps.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/crash_trace.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/freeze_heartbeat.c
|
||||
@@ -220,7 +222,7 @@ if(NOT PSXRECOMP_SKIP_BIOS_STALE_CHECK)
|
||||
endif()
|
||||
|
||||
function(psxrecomp_add_runtime_target target)
|
||||
set(options ORACLE)
|
||||
set(options ORACLE COSIM)
|
||||
set(oneValueArgs
|
||||
GAME_GENERATED_FULL_C
|
||||
GAME_GENERATED_DISPATCH_C
|
||||
@@ -387,6 +389,15 @@ function(psxrecomp_add_runtime_target target)
|
||||
target_compile_definitions(${target} PRIVATE PSX_NO_DEBUG_TOOLS=1)
|
||||
endif()
|
||||
|
||||
# First-divergence co-sim oracle (COSIM_ORACLE.md): the clean, deterministic build.
|
||||
# PSX_COSIM activates the cosim engine/hooks; PSX_NO_DEBUG_TOOLS strips ALL the laggy
|
||||
# diagnostic tooling (the debug server thread, per-block recording, rings) so the run
|
||||
# is single-threaded + fast + deterministic. The two instances (this + a FORCE_INTERP
|
||||
# run) are driven in cycle-lockstep by tools/cosim.py.
|
||||
if(PSXRT_COSIM)
|
||||
target_compile_definitions(${target} PRIVATE PSX_COSIM=1 PSX_NO_DEBUG_TOOLS=1)
|
||||
endif()
|
||||
|
||||
# Integrated RmlUi launcher (not in the oracle build — that's headless).
|
||||
if(PSX_LAUNCHER AND NOT PSXRT_ORACLE)
|
||||
psxrecomp_ensure_launcher_libs()
|
||||
|
||||
@@ -0,0 +1,371 @@
|
||||
#ifdef PSX_COSIM
|
||||
/* cosim.c — first-divergence co-simulation engine + minimal TCP server.
|
||||
* See COSIM_ORACLE.md. Active ONLY in the clean `psx-cosim` target (PSX_COSIM); the
|
||||
* whole translation unit is empty otherwise, so it is safe in the shared source list.
|
||||
*
|
||||
* Model: the guest runs normally on the main thread. At every basic-block leader the
|
||||
* emitted/interp hook calls cosim_block(pc): it stamps the block into a ring, folds a
|
||||
* cumulative chain hash, and — when the coordinator has asked to stop at a target
|
||||
* sequence — PARKS the guest thread there so the coordinator can read a quiescent
|
||||
* machine over TCP, compare, and resume. Two processes (compiled + interp) driven in
|
||||
* lockstep by tools/cosim.py converge on the first divergent block, then drill.
|
||||
*
|
||||
* Determinism is a hard requirement (both runs must be bit-identical until the real
|
||||
* divergence). The clean build runs headless, single-threaded guest, software renderer,
|
||||
* no host-time throttle. The compiled-vs-compiled gate proves it.
|
||||
*/
|
||||
#include "cosim_state.h"
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
#include <windows.h>
|
||||
typedef SOCKET sock_t;
|
||||
#define COSIM_SLEEP(ms) Sleep(ms)
|
||||
#else
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <unistd.h>
|
||||
#include <pthread.h>
|
||||
typedef int sock_t;
|
||||
#define COSIM_SLEEP(ms) usleep((ms)*1000)
|
||||
#endif
|
||||
|
||||
extern uint32_t i_stat, i_mask;
|
||||
extern uint64_t psx_cycle_count;
|
||||
|
||||
#define RING_N 8192u /* bounded reporting window (power of two) */
|
||||
typedef struct { uint64_t cp; uint32_t pc; uint64_t hash; uint32_t istat, imask; uint64_t cycle; } Entry;
|
||||
static Entry g_ring[RING_N];
|
||||
static uint64_t g_cp = 0; /* checkpoints crossed so far */
|
||||
static uint64_t g_chain = 1469598103934665603ULL; /* cumulative FNV over checkpoints */
|
||||
static uint64_t g_stride = 4096; /* guest cycles per checkpoint (coordinator sets) */
|
||||
static uint64_t g_next_cp = 0; /* next cycle at which to checkpoint */
|
||||
static uint32_t g_last_leader_pc = 0; /* set by cosim_block, reported at checkpoints */
|
||||
static uint64_t g_pending_first_cycle = 0;
|
||||
static uint64_t g_pending_count = 0;
|
||||
|
||||
/* lockstep control (written by TCP thread, read by guest thread).
|
||||
* The guest parks at EVERY checkpoint boundary (a deterministic guest cycle = multiple
|
||||
* of stride) and only advances when the coordinator grants budget via `step N`. It does
|
||||
* NOT free-run: the earlier "free-run until you notice an async stop cycle" design was
|
||||
* racy — two processes noticed the flag at different wall-times and parked at different
|
||||
* cycles (a HARNESS nondeterminism, not a guest one). Parking at fixed checkpoint
|
||||
* boundaries makes both processes stop at identical cycles by construction. */
|
||||
static volatile int64_t g_run_budget = 0; /* checkpoints the guest may cross before parking */
|
||||
static volatile int g_parked = 0;
|
||||
static volatile int g_go_token = 0; /* bump to wake a parked guest to re-check budget */
|
||||
|
||||
static inline uint64_t fold(uint64_t h, uint64_t v) {
|
||||
for (int i = 0; i < 8; i++) { h ^= (uint8_t)(v >> (i*8)); h *= 1099511628211ULL; }
|
||||
return h;
|
||||
}
|
||||
|
||||
/* Block-leader hook (emitted under PSX_COSIM + called from the interp loop). Cheap:
|
||||
* just stashes the last leader PC for human-readable reporting — the actual compare is
|
||||
* the cycle-keyed checkpoint below, which aligns across both backends. */
|
||||
void cosim_block(uint32_t pc) { g_last_leader_pc = pc; }
|
||||
uint32_t cosim_last_block(void) { return g_last_leader_pc; }
|
||||
|
||||
uint32_t cosim_cycles_to_next_checkpoint(void) {
|
||||
uint64_t now = psx_cycle_count;
|
||||
if (now >= g_next_cp) return 0;
|
||||
uint64_t d = g_next_cp - now;
|
||||
return d > 0xFFFFFFFFULL ? 0xFFFFFFFFu : (uint32_t)d;
|
||||
}
|
||||
|
||||
/* Cycle-keyed checkpoint — called from psx_advance_cycles (both backends, identical
|
||||
* per-instruction charges). Folds a full-state hash into the chain at each guest-cycle
|
||||
* stride and parks at the coordinator's stop cycle. This is the alignment clock. */
|
||||
static void cosim_record_checkpoint(uint64_t cycle, uint32_t pc) {
|
||||
uint64_t h = cosim_state_hash(NULL);
|
||||
uint64_t cp = ++g_cp;
|
||||
g_chain = fold(fold(g_chain, cycle), h);
|
||||
Entry *e = &g_ring[cp & (RING_N - 1u)];
|
||||
e->cp = cp; e->pc = pc; e->hash = h;
|
||||
e->istat = i_stat; e->imask = i_mask; e->cycle = cycle;
|
||||
|
||||
/* deterministic park: consume one checkpoint of budget, else block for `step`.
|
||||
* The guest ALWAYS stops here (a fixed cycle boundary), never at a wall-time point. */
|
||||
if (g_run_budget > 0) { g_run_budget--; return; }
|
||||
g_parked = 1;
|
||||
while (g_run_budget <= 0) {
|
||||
int tok = g_go_token;
|
||||
while (g_go_token == tok && g_run_budget <= 0) COSIM_SLEEP(1);
|
||||
}
|
||||
g_parked = 0;
|
||||
g_run_budget--;
|
||||
}
|
||||
|
||||
void cosim_tick(void) {
|
||||
uint64_t now = psx_cycle_count;
|
||||
if (now < g_next_cp) return;
|
||||
|
||||
if (g_cp == 0 && now == 0 && g_next_cp == 0) {
|
||||
cosim_record_checkpoint(0, g_last_leader_pc);
|
||||
g_next_cp = g_stride ? g_stride : 1;
|
||||
return;
|
||||
}
|
||||
|
||||
while (now >= g_next_cp) {
|
||||
if (g_pending_count == 0) g_pending_first_cycle = g_next_cp;
|
||||
g_pending_count++;
|
||||
g_next_cp += g_stride ? g_stride : 1;
|
||||
}
|
||||
}
|
||||
|
||||
void cosim_instr(uint32_t pc) {
|
||||
g_last_leader_pc = pc;
|
||||
while (g_pending_count > 0) {
|
||||
uint64_t cycle = g_pending_first_cycle;
|
||||
g_pending_first_cycle += g_stride ? g_stride : 1;
|
||||
g_pending_count--;
|
||||
cosim_record_checkpoint(cycle, pc);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------- minimal TCP command server (own thread) ---------------- */
|
||||
/* Line protocol, whitespace-separated. Replies one line.
|
||||
* seq -> "seq <n> chain <hex> parked <0|1>"
|
||||
* runto <n> -> set stop=n, resume; blocks until parked at n (or exit)
|
||||
* chain -> "chain <hex> seq <n>"
|
||||
* hash -> "hash <hex> pc <hex> istat <hex> imask <hex> cyc <n>"
|
||||
* sub -> per-subsystem hashes of the current state
|
||||
* window <n> -> last n ring rows (bounded to RING_N)
|
||||
* inject ram <phys> <xor> -> gate-4 fault into RAM
|
||||
* inject reg <idx> <xor> -> gate-4 fault into a CPU reg (idx 0..31, 32=hi,33=lo)
|
||||
* reset -> reset incremental hash state
|
||||
*/
|
||||
static void handle_line(sock_t s, char *line);
|
||||
|
||||
static int send_line(sock_t s, const char *buf) {
|
||||
size_t n = strlen(buf);
|
||||
size_t off = 0;
|
||||
while (off < n) {
|
||||
int sent = send(s, buf + off, (int)(n - off), 0);
|
||||
if (sent <= 0) return 0;
|
||||
off += (size_t)sent;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void server_loop(unsigned short port) {
|
||||
sock_t ls = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (ls == (sock_t)-1) return;
|
||||
int yes = 1;
|
||||
setsockopt(ls, SOL_SOCKET, SO_REUSEADDR, (const char*)&yes, sizeof yes);
|
||||
struct sockaddr_in a; memset(&a, 0, sizeof a);
|
||||
a.sin_family = AF_INET; a.sin_addr.s_addr = htonl(INADDR_LOOPBACK); a.sin_port = htons(port);
|
||||
if (bind(ls, (struct sockaddr*)&a, sizeof a) != 0) return;
|
||||
listen(ls, 4);
|
||||
for (;;) {
|
||||
sock_t c = accept(ls, NULL, NULL);
|
||||
if (c == (sock_t)-1) { COSIM_SLEEP(5); continue; }
|
||||
char buf[512]; int used = 0;
|
||||
for (;;) {
|
||||
int r = recv(c, buf + used, (int)sizeof(buf) - 1 - used, 0);
|
||||
if (r <= 0) break;
|
||||
used += r; buf[used] = 0;
|
||||
char *nl;
|
||||
while ((nl = strchr(buf, '\n')) != NULL) {
|
||||
*nl = 0;
|
||||
char *cr = strchr(buf, '\r'); if (cr) *cr = 0;
|
||||
handle_line(c, buf);
|
||||
size_t rest = strlen(nl + 1);
|
||||
memmove(buf, nl + 1, rest + 1);
|
||||
used = (int)rest;
|
||||
}
|
||||
if (used >= (int)sizeof(buf) - 1) used = 0; /* overlong line: drop */
|
||||
}
|
||||
#ifdef _WIN32
|
||||
closesocket(c);
|
||||
#else
|
||||
close(c);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
static void handle_line(sock_t s, char *line) {
|
||||
char out[1024];
|
||||
char cmd[32] = {0};
|
||||
if (sscanf(line, "%31s", cmd) != 1) { send_line(s, "err empty\n"); return; }
|
||||
|
||||
if (!strcmp(cmd, "status")) {
|
||||
snprintf(out, sizeof out, "cp %llu cycle %llu chain %016llx stride %llu parked %d\n",
|
||||
(unsigned long long)g_cp, (unsigned long long)psx_cycle_count,
|
||||
(unsigned long long)g_chain, (unsigned long long)g_stride, g_parked);
|
||||
send_line(s, out); return;
|
||||
}
|
||||
if (!strcmp(cmd, "chain")) {
|
||||
snprintf(out, sizeof out, "chain %016llx cp %llu cycle %llu\n",
|
||||
(unsigned long long)g_chain, (unsigned long long)g_cp,
|
||||
(unsigned long long)psx_cycle_count);
|
||||
send_line(s, out); return;
|
||||
}
|
||||
if (!strcmp(cmd, "stride")) {
|
||||
/* Stride fixes the checkpoint cycle boundaries; set ONCE before stepping so both
|
||||
* processes checkpoint at identical cycles. Changing it mid-run is refused. */
|
||||
unsigned long long n = 0;
|
||||
if (sscanf(line, "%*s %llu", &n) != 1 || n == 0) { send_line(s, "err stride\n"); return; }
|
||||
if (g_cp != 0) { send_line(s, "err stride-after-start\n"); return; }
|
||||
g_stride = (uint64_t)n; g_next_cp = 0; send_line(s, "ok\n"); return;
|
||||
}
|
||||
if (!strcmp(cmd, "step")) { /* step <N-checkpoints>; parks at cp+N */
|
||||
unsigned long long n = 1;
|
||||
sscanf(line, "%*s %llu", &n);
|
||||
g_run_budget += (int64_t)n;
|
||||
g_go_token++; /* wake the parked guest */
|
||||
/* wait until it parks again (crossed N checkpoints), or the guest exits */
|
||||
int spins = 0;
|
||||
while (g_run_budget > 0 && spins < 1200000) { COSIM_SLEEP(1); spins++; }
|
||||
/* small settle so g_parked/g_chain reflect the checkpoint just recorded */
|
||||
int s2 = 0; while (!g_parked && g_run_budget <= 0 && s2 < 2000) { COSIM_SLEEP(1); s2++; }
|
||||
snprintf(out, sizeof out, "%s cp %llu cycle %llu chain %016llx\n",
|
||||
g_parked ? "parked" : "running",
|
||||
(unsigned long long)g_cp, (unsigned long long)psx_cycle_count,
|
||||
(unsigned long long)g_chain);
|
||||
send_line(s, out); return;
|
||||
}
|
||||
if (!strcmp(cmd, "hash")) {
|
||||
Entry *e = &g_ring[g_cp & (RING_N - 1u)];
|
||||
snprintf(out, sizeof out, "hash %016llx pc %08x istat %08x imask %08x cyc %llu\n",
|
||||
(unsigned long long)e->hash, e->pc, e->istat, e->imask,
|
||||
(unsigned long long)e->cycle);
|
||||
send_line(s, out); return;
|
||||
}
|
||||
if (!strcmp(cmd, "sub")) {
|
||||
CosimSubHashes h; cosim_state_hash(&h);
|
||||
snprintf(out, sizeof out,
|
||||
"cpu %016llx irqctl %016llx ram %016llx scratch %016llx vram %016llx "
|
||||
"gpu %016llx spu %016llx cdrom %016llx dma %016llx sio %016llx "
|
||||
"timers %016llx clock %016llx dirty %016llx\n",
|
||||
(unsigned long long)h.cpu,(unsigned long long)h.irqctl,(unsigned long long)h.ram,
|
||||
(unsigned long long)h.scratch,(unsigned long long)h.vram,(unsigned long long)h.gpu,
|
||||
(unsigned long long)h.spu,(unsigned long long)h.cdrom,(unsigned long long)h.dma,
|
||||
(unsigned long long)h.sio,(unsigned long long)h.timers,(unsigned long long)h.clock,
|
||||
(unsigned long long)h.dirty);
|
||||
send_line(s, out); return;
|
||||
}
|
||||
if (!strcmp(cmd, "window")) {
|
||||
unsigned long long n = 32;
|
||||
sscanf(line, "%*s %llu", &n);
|
||||
if (n > RING_N) n = RING_N;
|
||||
if (n > g_cp) n = g_cp;
|
||||
for (uint64_t i = 0; i < n; i++) {
|
||||
uint64_t cp = g_cp - (n - 1 - i);
|
||||
Entry *e = &g_ring[cp & (RING_N - 1u)];
|
||||
snprintf(out, sizeof out, "row cp %llu pc %08x hash %016llx istat %08x cyc %llu\n",
|
||||
(unsigned long long)e->cp, e->pc, (unsigned long long)e->hash,
|
||||
e->istat, (unsigned long long)e->cycle);
|
||||
send_line(s, out);
|
||||
}
|
||||
send_line(s, "end\n"); return;
|
||||
}
|
||||
if (!strcmp(cmd, "inject")) {
|
||||
char what[8] = {0}; unsigned long a=0, b=0;
|
||||
if (sscanf(line, "%*s %7s %lu %lu", what, &a, &b) == 3) {
|
||||
if (!strcmp(what, "ram")) { cosim_inject_ram((uint32_t)a, (uint8_t)b); send_line(s, "ok\n"); return; }
|
||||
if (!strcmp(what, "reg")) { cosim_inject_reg((int)a, (uint32_t)b); send_line(s, "ok\n"); return; }
|
||||
}
|
||||
send_line(s, "err inject\n"); return;
|
||||
}
|
||||
if (!strcmp(cmd, "cpu")) {
|
||||
/* Full CPU field dump so the coordinator can field-diff a cpu-subsystem
|
||||
* divergence (which register / micro-state field actually split). */
|
||||
extern CPUState *debug_cpu_ptr;
|
||||
CPUState *c = debug_cpu_ptr;
|
||||
if (!c) { send_line(s, "err no-cpu\n"); return; }
|
||||
char *p = out; size_t rem = sizeof out;
|
||||
int w = snprintf(p, rem, "pc %08x hi %08x lo %08x", c->pc, c->hi, c->lo);
|
||||
p += w; rem -= w;
|
||||
for (int i = 0; i < 32; i++) { w = snprintf(p, rem, " r%d %08x", i, c->gpr[i]); p+=w; rem-=w; }
|
||||
for (int i = 0; i < 16; i++) { w = snprintf(p, rem, " c%d %08x", i, c->cop0[i]); p+=w; rem-=w; }
|
||||
w = snprintf(p, rem, " mdts %llx gtets %llx rfudge %02x rabw %02x ldw %02x ldabs %08x\n",
|
||||
(unsigned long long)c->muldiv_ts_done, (unsigned long long)c->gte_ts_done,
|
||||
c->read_fudge, c->read_absorb_which, c->ld_which_t, c->ld_absorb);
|
||||
send_line(s, out); return;
|
||||
}
|
||||
if (!strcmp(cmd, "gte")) {
|
||||
extern CPUState *debug_cpu_ptr;
|
||||
CPUState *c = debug_cpu_ptr;
|
||||
if (!c) { send_line(s, "err no-cpu\n"); return; }
|
||||
char big[4096];
|
||||
char *p = big; size_t rem = sizeof big;
|
||||
for (int i = 0; i < 32 && rem > 1; i++) {
|
||||
uint32_t val = c->gte_data[i];
|
||||
if ((i >= 8 && i <= 11) || i == 15 || i == 28 || i == 29 || i == 31) {
|
||||
val = gte_read_data(c, (uint8_t)i);
|
||||
}
|
||||
int w = snprintf(p, rem, " gd%d %08x", i, val);
|
||||
if (w < 0 || (size_t)w >= rem) { p += rem - 1; rem = 1; break; }
|
||||
p += w; rem -= (size_t)w;
|
||||
}
|
||||
for (int i = 0; i < 32 && rem > 1; i++) {
|
||||
int w = snprintf(p, rem, " gc%d %08x", i, c->gte_ctrl[i]);
|
||||
if (w < 0 || (size_t)w >= rem) { p += rem - 1; rem = 1; break; }
|
||||
p += w; rem -= (size_t)w;
|
||||
}
|
||||
snprintf(p, rem, "\n");
|
||||
send_line(s, big); return;
|
||||
}
|
||||
if (!strcmp(cmd, "dev")) {
|
||||
/* Device-timing field dump: the fields behind the `irqctl` and `gpu`
|
||||
* sub-hashes, so a device divergence can be field-diffed (esp.
|
||||
* cycles_since_vblank = VBLANK-raise-timing, and GPUSTAT). */
|
||||
extern void interrupts_cosim_dump(uint32_t*, int*);
|
||||
extern uint32_t gpu_read_gpustat(void);
|
||||
uint32_t csv = 0; int inexc = 0; interrupts_cosim_dump(&csv, &inexc);
|
||||
snprintf(out, sizeof out,
|
||||
"csv %08x inexc %d imask %08x istat %08x gpustat %08x\n",
|
||||
csv, inexc, i_mask, i_stat, gpu_read_gpustat());
|
||||
send_line(s, out); return;
|
||||
}
|
||||
if (!strcmp(cmd, "gpu")) {
|
||||
extern void gpu_cosim_dump(char *out, int cap);
|
||||
char big[4096];
|
||||
gpu_cosim_dump(big, (int)sizeof big);
|
||||
send_line(s, big); return;
|
||||
}
|
||||
if (!strcmp(cmd, "irqtrace")) {
|
||||
extern void interrupts_cosim_irq_dump(char *out, int cap);
|
||||
char big[32768];
|
||||
interrupts_cosim_irq_dump(big, (int)sizeof big);
|
||||
send_line(s, big); return;
|
||||
}
|
||||
if (!strcmp(cmd, "reset")) { cosim_state_reset(); send_line(s, "ok\n"); return; }
|
||||
send_line(s, "err unknown\n");
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
static DWORD WINAPI server_thread(LPVOID p) { server_loop((unsigned short)(uintptr_t)p); return 0; }
|
||||
#else
|
||||
static void *server_thread(void *p) { server_loop((unsigned short)(uintptr_t)p); return NULL; }
|
||||
#endif
|
||||
|
||||
/* Call once at startup (from the clean-build main, before the guest runs). Port from
|
||||
* env PSX_COSIM_PORT (default 4600). */
|
||||
void cosim_init(void) {
|
||||
cosim_state_reset();
|
||||
unsigned short port = 4600;
|
||||
const char *e = getenv("PSX_COSIM_PORT");
|
||||
if (e && *e) port = (unsigned short)atoi(e);
|
||||
/* Stride fixed at launch (env) so the checkpoint cycle boundaries are identical in
|
||||
* both processes before either runs a single instruction — no set-stride race. */
|
||||
const char *st = getenv("PSX_COSIM_STRIDE");
|
||||
if (st && *st) { unsigned long long v = strtoull(st, 0, 10); if (v) g_stride = v; }
|
||||
const char *sc = getenv("PSX_COSIM_START_CYCLE");
|
||||
if (sc && *sc) { unsigned long long v = strtoull(sc, 0, 10); g_next_cp = (uint64_t)v; }
|
||||
#ifdef _WIN32
|
||||
WSADATA w; WSAStartup(MAKEWORD(2,2), &w);
|
||||
CreateThread(NULL, 0, server_thread, (LPVOID)(uintptr_t)port, 0, NULL);
|
||||
#else
|
||||
pthread_t t; pthread_create(&t, NULL, server_thread, (void*)(uintptr_t)port);
|
||||
#endif
|
||||
fprintf(stdout, "cosim: first-divergence oracle listening on 127.0.0.1:%u\n", port);
|
||||
}
|
||||
|
||||
#endif /* PSX_COSIM */
|
||||
@@ -0,0 +1,220 @@
|
||||
#ifdef PSX_COSIM
|
||||
/* cosim_state.c — full guest-architectural-state canonical hash. See COSIM_ORACLE.md
|
||||
* and cosim_state.h. Active ONLY in the PSX_COSIM build; empty TU otherwise (safe in
|
||||
* the shared source list).
|
||||
*
|
||||
* Principle: hash exactly the state a real PSX exposes to the running program plus the
|
||||
* device-timing quantities that determine WHEN a guest-visible bit flips — never our
|
||||
* emulator's internal bookkeeping (e.g. total_checks / dispatch_count / cooldown are
|
||||
* counted in psx_check_interrupts CALLS, which legitimately differ between the compiled
|
||||
* and interp backends by call frequency; hashing them would report a false first
|
||||
* divergence). The genuine timing quantity that matters is cycles_since_vblank (guest
|
||||
* cycles), which is included. This exclusion list is refined by the injected-divergence
|
||||
* and compiled-vs-compiled gates.
|
||||
*/
|
||||
#include "cosim_state.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/* ---- FNV-1a 64 ---- */
|
||||
#define FNV_OFF 1469598103934665603ULL
|
||||
#define FNV_PRM 1099511628211ULL
|
||||
static inline uint64_t fnv(uint64_t h, const void *p, size_t n) {
|
||||
const uint8_t *b = (const uint8_t *)p;
|
||||
for (size_t i = 0; i < n; i++) { h ^= b[i]; h *= FNV_PRM; }
|
||||
return h;
|
||||
}
|
||||
static inline uint64_t fnv_u32(uint64_t h, uint32_t v) { return fnv(h, &v, 4); }
|
||||
static inline uint64_t fnv_u64(uint64_t h, uint64_t v) { return fnv(h, &v, 8); }
|
||||
|
||||
/* ---- runtime state accessors (existing modules) ---- */
|
||||
extern CPUState *debug_cpu_ptr;
|
||||
extern uint8_t *memory_get_ram_ptr(void);
|
||||
extern uint8_t *memory_get_scratchpad_ptr(void);
|
||||
extern uint32_t i_stat;
|
||||
extern uint32_t i_mask;
|
||||
extern uint64_t psx_cycle_count;
|
||||
extern uint32_t dirty_ram_get_bitmap_word(uint32_t word_index);
|
||||
extern uint32_t dirty_ram_get_bitmap_word_count(void);
|
||||
extern void timers_get_snapshot(uint16_t counter[3], uint32_t mode[3],
|
||||
uint16_t target[3], int32_t irq_line[3],
|
||||
uint32_t frac[3]);
|
||||
extern uint32_t gpu_cosim_snapshot_bytes(void); extern void gpu_cosim_snapshot_write(uint8_t*);
|
||||
extern uint32_t spu_snapshot_bytes(void); extern void spu_snapshot_write(uint8_t*);
|
||||
extern uint8_t* spu_get_ram_ptr(void); extern uint32_t spu_get_ram_bytes(void);
|
||||
extern uint32_t cdrom_snapshot_bytes(void); extern void cdrom_snapshot_write(uint8_t*);
|
||||
extern uint32_t dma_snapshot_bytes(void); extern void dma_snapshot_write(uint8_t*);
|
||||
extern uint32_t sio_snapshot_bytes(void); extern void sio_snapshot_write(uint8_t*);
|
||||
/* interrupts.c: fold the genuine guest-timing statics (NOT the call-count artifacts). */
|
||||
extern uint64_t interrupts_cosim_hash(uint64_t seed);
|
||||
/* renderer-agnostic VRAM readback (sw renderer = cheap memcpy). */
|
||||
extern void gr_vram_transfer_out(int x, int y, int w, int h, uint16_t *dst);
|
||||
|
||||
#define RAM_SIZE (2u * 1024u * 1024u)
|
||||
#define SPAD_SIZE (1024u)
|
||||
#define PAGE 4096u
|
||||
#define RAM_PAGES (RAM_SIZE / PAGE) /* 512 */
|
||||
#define VRAM_W 1024
|
||||
#define VRAM_H 512
|
||||
#define VRAM_SIZE ((uint32_t)(VRAM_W * VRAM_H * 2)) /* 1 MB */
|
||||
#define VRAM_PAGES (VRAM_SIZE / PAGE) /* 256 */
|
||||
|
||||
/* ---- incremental RAM page hashes ---- */
|
||||
static uint64_t s_ram_page_h[RAM_PAGES];
|
||||
static uint8_t s_ram_page_dirty[RAM_PAGES];
|
||||
static int s_ram_all_dirty = 1;
|
||||
|
||||
/* ---- VRAM: full-recompute when dirty (draws are periodic, not per-block) ---- */
|
||||
static uint64_t s_vram_h;
|
||||
static int s_vram_dirty = 1;
|
||||
static uint16_t s_vram_buf[VRAM_W * VRAM_H];
|
||||
|
||||
/* ---- reusable device-blob scratch ---- */
|
||||
static uint8_t *s_blob;
|
||||
static uint32_t s_blob_cap;
|
||||
|
||||
/* ---- gate-4 injection (applied to LIVE state so it flows into the hash naturally) -- */
|
||||
static int32_t s_inj_ram_phys = -1; static uint8_t s_inj_ram_xor;
|
||||
static int32_t s_inj_reg = -1; static uint32_t s_inj_reg_xor;
|
||||
|
||||
void cosim_state_reset(void) {
|
||||
memset(s_ram_page_h, 0, sizeof s_ram_page_h);
|
||||
memset(s_ram_page_dirty, 0, sizeof s_ram_page_dirty);
|
||||
s_ram_all_dirty = 1;
|
||||
s_vram_dirty = 1; s_vram_h = 0;
|
||||
s_inj_ram_phys = -1; s_inj_reg = -1;
|
||||
}
|
||||
|
||||
void cosim_note_ram_write(uint32_t phys, uint32_t nbytes) {
|
||||
if (phys >= RAM_SIZE) return;
|
||||
uint32_t last = phys + (nbytes ? nbytes - 1 : 0);
|
||||
if (last >= RAM_SIZE) last = RAM_SIZE - 1;
|
||||
for (uint32_t p = phys / PAGE; p <= last / PAGE; p++) s_ram_page_dirty[p] = 1;
|
||||
}
|
||||
void cosim_note_vram_write(uint32_t byte_off, uint32_t nbytes) {
|
||||
(void)byte_off; (void)nbytes; s_vram_dirty = 1;
|
||||
}
|
||||
void cosim_inject_ram(uint32_t phys, uint8_t xor_val) { s_inj_ram_phys = (int32_t)phys; s_inj_ram_xor = xor_val; }
|
||||
void cosim_inject_reg(int reg_index, uint32_t xor_val) { s_inj_reg = reg_index; s_inj_reg_xor = xor_val; }
|
||||
|
||||
static uint64_t blob_hash(uint64_t seed, uint32_t (*bytes)(void), void (*write)(uint8_t*)) {
|
||||
uint32_t n = bytes();
|
||||
if (n > s_blob_cap) { free(s_blob); s_blob = (uint8_t*)malloc(n); s_blob_cap = n; }
|
||||
if (n && !s_blob) return seed; /* OOM: leave unchanged (will surface in gate) */
|
||||
if (n) write(s_blob);
|
||||
return fnv(fnv_u32(seed, n), s_blob, n);
|
||||
}
|
||||
|
||||
static uint64_t hash_cpu(const CPUState *c) {
|
||||
uint64_t h = FNV_OFF;
|
||||
uint32_t gte_data[32];
|
||||
h = fnv(h, c->gpr, sizeof c->gpr);
|
||||
/* cpu->pc is DELIBERATELY EXCLUDED from the cross-backend hash. The compiled
|
||||
* backend does not keep cpu->pc current mid-execution — it writes pc only at block
|
||||
* transfers, and it is transiently 0 between dispatch calls — whereas the interp
|
||||
* keeps pc exact per-instruction. So at a mid-instruction cycle checkpoint the two
|
||||
* backends legitimately hold different pc values while being in the SAME
|
||||
* architectural state (verified: at the first flagged divergence, cp32, ONLY pc
|
||||
* differed; every gpr/cop0/hi/lo/micro-state matched). Including pc produced a false
|
||||
* first-divergence. This is NOT a blind spot: a REAL control-flow split shows up as a
|
||||
* differing gpr/memory value within one checkpoint. pc stays available via the `cpu`
|
||||
* TCP command for reporting. */
|
||||
h = fnv_u32(h, c->hi); h = fnv_u32(h, c->lo);
|
||||
h = fnv(h, c->cop0, sizeof c->cop0);
|
||||
memcpy(gte_data, c->gte_data, sizeof gte_data);
|
||||
/* Some GTE data registers are derived/read-special values. The compiled
|
||||
* backend reads these through gte_read_data(), while the dirty interpreter's
|
||||
* all-purpose gte_write_data() refreshes their backing cache as a side
|
||||
* effect. Hash the same canonical value the guest would read so stale cache
|
||||
* bits do not become oracle false positives. */
|
||||
CPUState *mc = (CPUState *)c;
|
||||
for (int i = 8; i <= 11; i++) gte_data[i] = gte_read_data(mc, (uint8_t)i);
|
||||
gte_data[15] = gte_read_data(mc, 15);
|
||||
gte_data[28] = gte_read_data(mc, 28);
|
||||
gte_data[29] = gte_read_data(mc, 29);
|
||||
gte_data[31] = gte_read_data(mc, 31);
|
||||
h = fnv(h, gte_data, sizeof gte_data);
|
||||
h = fnv(h, c->gte_ctrl, sizeof c->gte_ctrl);
|
||||
/* micro-state — the execution-critical fields boot_state omits */
|
||||
h = fnv_u64(h, c->muldiv_ts_done);
|
||||
h = fnv_u64(h, c->gte_ts_done);
|
||||
h = fnv(h, c->read_absorb, sizeof c->read_absorb);
|
||||
h = fnv(h, &c->read_absorb_which, 1);
|
||||
h = fnv(h, &c->read_fudge, 1);
|
||||
h = fnv(h, &c->ld_which_t, 1);
|
||||
h = fnv_u32(h, c->ld_absorb);
|
||||
/* EXCLUDED (host-only): read_word/.../write_byte fn ptrs, sljit_helpers. */
|
||||
return h;
|
||||
}
|
||||
|
||||
uint64_t cosim_state_hash(CosimSubHashes *sub) {
|
||||
CosimSubHashes s; memset(&s, 0, sizeof s);
|
||||
CPUState *cpu = debug_cpu_ptr;
|
||||
uint8_t *ram = memory_get_ram_ptr();
|
||||
|
||||
/* apply pending gate-4 injection to live state */
|
||||
if (s_inj_ram_phys >= 0 && (uint32_t)s_inj_ram_phys < RAM_SIZE) {
|
||||
ram[s_inj_ram_phys] ^= s_inj_ram_xor; cosim_note_ram_write((uint32_t)s_inj_ram_phys, 1);
|
||||
s_inj_ram_phys = -1;
|
||||
}
|
||||
if (s_inj_reg >= 0 && cpu) {
|
||||
if (s_inj_reg < 32) cpu->gpr[s_inj_reg] ^= s_inj_reg_xor;
|
||||
else if (s_inj_reg == 32) cpu->hi ^= s_inj_reg_xor;
|
||||
else if (s_inj_reg == 33) cpu->lo ^= s_inj_reg_xor;
|
||||
s_inj_reg = -1;
|
||||
}
|
||||
|
||||
if (cpu) s.cpu = hash_cpu(cpu);
|
||||
|
||||
/* RAM: recompute dirty pages, fold page hashes */
|
||||
for (uint32_t p = 0; p < RAM_PAGES; p++) {
|
||||
if (s_ram_all_dirty || s_ram_page_dirty[p]) {
|
||||
s_ram_page_h[p] = fnv(FNV_OFF, ram + (size_t)p * PAGE, PAGE);
|
||||
s_ram_page_dirty[p] = 0;
|
||||
}
|
||||
}
|
||||
s_ram_all_dirty = 0;
|
||||
s.ram = fnv(FNV_OFF, s_ram_page_h, sizeof s_ram_page_h);
|
||||
|
||||
s.scratch = fnv(FNV_OFF, memory_get_scratchpad_ptr(), SPAD_SIZE);
|
||||
|
||||
/* VRAM: EXCLUDED in v1. The guest state machine reads RAM + device registers to
|
||||
* make the FMV/handoff decision, never VRAM, so a VRAM divergence is downstream of
|
||||
* (not causal for) the first control-flow split — and a 1MB readback per cycle-
|
||||
* checkpoint would be prohibitively slow. Add incrementally (cosim_note_vram_write
|
||||
* page hashes) only if a gate shows a divergence that first surfaces in VRAM. */
|
||||
s.vram = 0;
|
||||
|
||||
/* interrupt controller: guest-visible i_stat/i_mask + genuine timing statics */
|
||||
s.irqctl = interrupts_cosim_hash(fnv_u32(fnv_u32(FNV_OFF, i_stat), i_mask));
|
||||
|
||||
/* timers */
|
||||
{ uint16_t cnt[3],tgt[3]; uint32_t mode[3],frac[3]; int32_t irq[3];
|
||||
timers_get_snapshot(cnt, mode, tgt, irq, frac);
|
||||
uint64_t h = FNV_OFF;
|
||||
h = fnv(h,cnt,sizeof cnt); h = fnv(h,mode,sizeof mode); h = fnv(h,tgt,sizeof tgt);
|
||||
h = fnv(h,irq,sizeof irq); h = fnv(h,frac,sizeof frac); s.timers = h; }
|
||||
|
||||
s.clock = fnv_u64(FNV_OFF, psx_cycle_count);
|
||||
|
||||
s.gpu = blob_hash(FNV_OFF, gpu_cosim_snapshot_bytes, gpu_cosim_snapshot_write);
|
||||
s.spu = blob_hash(FNV_OFF, spu_snapshot_bytes, spu_snapshot_write);
|
||||
s.spu = fnv(s.spu, spu_get_ram_ptr(), spu_get_ram_bytes());
|
||||
s.cdrom = blob_hash(FNV_OFF, cdrom_snapshot_bytes, cdrom_snapshot_write);
|
||||
s.dma = blob_hash(FNV_OFF, dma_snapshot_bytes, dma_snapshot_write);
|
||||
s.sio = blob_hash(FNV_OFF, sio_snapshot_bytes, sio_snapshot_write);
|
||||
|
||||
/* dirty-RAM page bitmap (affects dispatch/interp routing) */
|
||||
{ uint64_t h = FNV_OFF; uint32_t wc = dirty_ram_get_bitmap_word_count();
|
||||
for (uint32_t i = 0; i < wc; i++) h = fnv_u32(h, dirty_ram_get_bitmap_word(i));
|
||||
s.dirty = h; }
|
||||
|
||||
if (sub) *sub = s;
|
||||
|
||||
/* fold everything into the top hash */
|
||||
uint64_t top = FNV_OFF;
|
||||
top = fnv(top, &s, sizeof s);
|
||||
return top;
|
||||
}
|
||||
|
||||
#endif /* PSX_COSIM */
|
||||
@@ -39,6 +39,7 @@
|
||||
#include "card_data_writes.h"
|
||||
#include "crash_trace.h"
|
||||
#include "gpu_gl_renderer.h"
|
||||
#include "lockstep.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -696,7 +697,10 @@ static uint32_t trace_read_word(CPUState *cpu, uint32_t addr)
|
||||
/* Use psx_read_word directly (NOT cpu->read_word): debug instrumentation
|
||||
* must not charge guest main-RAM read wait states, and runs on the IO
|
||||
* thread where psx_advance_cycles would race the emulation thread. */
|
||||
return psx_read_word(addr);
|
||||
ls_suppress_begin();
|
||||
uint32_t v = psx_read_word(addr);
|
||||
ls_suppress_end();
|
||||
return v;
|
||||
}
|
||||
|
||||
static uint32_t trace_read_half(CPUState *cpu, uint32_t addr)
|
||||
@@ -707,7 +711,10 @@ static uint32_t trace_read_half(CPUState *cpu, uint32_t addr)
|
||||
(phys < 0x1F800000u || phys > 0x1F8003FEu)) {
|
||||
return 0;
|
||||
}
|
||||
return psx_read_half(addr);
|
||||
ls_suppress_begin();
|
||||
uint32_t v = psx_read_half(addr);
|
||||
ls_suppress_end();
|
||||
return v;
|
||||
}
|
||||
|
||||
static int sreg_trace_focus_func(uint32_t func)
|
||||
@@ -1938,6 +1945,7 @@ void debug_server_log_call_entry(uint32_t func_addr) {
|
||||
psx_native_stack_guard(func_addr); /* runs in debug AND release (before the early-return) */
|
||||
#endif
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
ls_suppress_begin();
|
||||
if (s_synth_recurse_armed) { s_synth_recurse_armed = 0; psx_synth_recurse(0); }
|
||||
/* cyc_watch: universal compiled-function-entry hook (game AND BIOS, incl.
|
||||
* relocated BIOS-shell funcs which dispatch oddly but still log their entry
|
||||
@@ -1956,13 +1964,14 @@ void debug_server_log_call_entry(uint32_t func_addr) {
|
||||
s_fn_direct_seen++;
|
||||
if (!debug_cpu_ptr) {
|
||||
s_fn_direct_no_cpu++;
|
||||
ls_suppress_end();
|
||||
return;
|
||||
}
|
||||
card_mgr_trace_record(func_addr, 0);
|
||||
sreg_trace_record(func_addr);
|
||||
call_focus_record(func_addr);
|
||||
if (!s_fn_entry) return;
|
||||
if (!fn_trace_in_filter(func_addr)) return;
|
||||
if (!s_fn_entry) { ls_suppress_end(); return; }
|
||||
if (!fn_trace_in_filter(func_addr)) { ls_suppress_end(); return; }
|
||||
s_fn_direct_filtered++;
|
||||
FnEntryEntry *e = &s_fn_entry[s_fn_entry_seq % FN_TRACE_CAP];
|
||||
e->seq = s_fn_entry_seq;
|
||||
@@ -1981,6 +1990,7 @@ void debug_server_log_call_entry(uint32_t func_addr) {
|
||||
e->depth = (uint32_t)s_fn_stack_top;
|
||||
e->frame = (uint32_t)s_frame_count;
|
||||
s_fn_entry_seq++;
|
||||
ls_suppress_end();
|
||||
}
|
||||
|
||||
/* Always-on A0/B0/C0 BIOS-call ring (ported from ape-fw for good-vs-bad
|
||||
@@ -2161,6 +2171,7 @@ void debug_server_trace_dispatch(uint32_t func_addr) {
|
||||
(void)func_addr;
|
||||
return;
|
||||
#endif
|
||||
ls_suppress_begin();
|
||||
/* cyc_watch: compiled-dispatch path. func_addr is already the physical
|
||||
* (normalized) block leader. Sampled before the block runs. */
|
||||
cyc_watch_observe(func_addr & 0x1FFFFFFFu);
|
||||
@@ -2243,6 +2254,7 @@ void debug_server_trace_dispatch(uint32_t func_addr) {
|
||||
s_dispatch_ring[s_dispatch_seq % DISPATCH_TRACE_CAP] = func_addr;
|
||||
s_dispatch_seq++;
|
||||
dispatch_unique_add(func_addr);
|
||||
ls_suppress_end();
|
||||
}
|
||||
|
||||
static int json_get_int(const char *json, const char *key, int def);
|
||||
@@ -10410,8 +10422,25 @@ static void handle_lockstep(int id, const char *json) {
|
||||
send_fmt("{\"id\":%d,\"ok\":true,\"lockstep\":%s}", id, buf);
|
||||
}
|
||||
|
||||
static void handle_lockstep_func(int id, const char *json) {
|
||||
/* Dispatch-segment lockstep comparator. Same arming shape as lockstep,
|
||||
* but the measured unit is one clean psx_dispatch_game_compiled() segment. */
|
||||
extern void ls_func_set_window(uint32_t, uint32_t);
|
||||
extern void ls_func_set_record_only(int);
|
||||
extern int ls_get_func_json(char*, int);
|
||||
int lo = json_get_int(json, "lo", -1);
|
||||
int hi = json_get_int(json, "hi", -1);
|
||||
int ro = json_get_int(json, "record_only", -1);
|
||||
if (ro >= 0) ls_func_set_record_only(ro);
|
||||
if (lo >= 0 && hi >= 0) ls_func_set_window((uint32_t)lo, (uint32_t)hi);
|
||||
char buf[8192];
|
||||
ls_get_func_json(buf, (int)sizeof(buf));
|
||||
send_fmt("{\"id\":%d,\"ok\":true,\"lockstep_func\":%s}", id, buf);
|
||||
}
|
||||
|
||||
static const CmdEntry s_commands[] = {
|
||||
{ "lockstep", handle_lockstep },
|
||||
{ "lockstep_func", handle_lockstep_func },
|
||||
{ "ping", handle_ping },
|
||||
{ "parity_dump", handle_parity_dump },
|
||||
{ "parity_ctl", handle_parity_ctl },
|
||||
|
||||
+523
-34
@@ -31,6 +31,7 @@
|
||||
#include "psx_instr_cost.h" /* psx_instr_base_cycles — single-source cycle cost */
|
||||
#include "gpu.h" /* psx_ws_is_backdrop_site / psx_ws_backdrop_x (interp hook) */
|
||||
#include "ws_backdrop_detect.h" /* shared backdrop-window detector (auto_backdrop) */
|
||||
#include "lockstep.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
@@ -48,6 +49,7 @@ uint64_t g_dirty_ram_guard_yields = 0;
|
||||
* advancing cycles without surfacing to psx_check_interrupts (the softlock). */
|
||||
uint64_t g_dirty_pump_max_gap_insns = 0;
|
||||
uint64_t g_dirty_pump_count = 0;
|
||||
static uint64_t s_last_dirty_irq_pump_insns = 0;
|
||||
|
||||
/* EPC de-overload signal (Tomba 2 frame-1997 fix). Set to the committed guest PC
|
||||
* immediately around a dirty-pump psx_check_interrupts() call, 0 otherwise. When
|
||||
@@ -73,6 +75,21 @@ int g_precise_mode = 0;
|
||||
* (g_ls_dirty_observe==0) from an interpreted per-instruction sample (==1). */
|
||||
int g_ls_dirty_observe = 0;
|
||||
extern int g_ls_replay_active; /* defined in the lockstep section; used by exec_one's jal/jalr guard */
|
||||
|
||||
#ifdef PSX_COSIM
|
||||
static int g_cosim_exec_one_hooked = 0;
|
||||
static void cosim_exec_one_begin(void) { g_cosim_exec_one_hooked = 0; }
|
||||
static int cosim_exec_one_did_hook(void) { return g_cosim_exec_one_hooked; }
|
||||
static void cosim_exec_one_transfer_hook(uint32_t pc) {
|
||||
extern void cosim_instr(uint32_t);
|
||||
cosim_instr(pc);
|
||||
g_cosim_exec_one_hooked = 1;
|
||||
}
|
||||
#else
|
||||
static void cosim_exec_one_begin(void) {}
|
||||
static int cosim_exec_one_did_hook(void) { return 0; }
|
||||
static void cosim_exec_one_transfer_hook(uint32_t pc) { (void)pc; }
|
||||
#endif
|
||||
uint64_t g_slice_fired = 0; /* diagnostic: slices actually run */
|
||||
uint64_t g_slice_irq_taken = 0; /* diagnostic: IRQs taken inside precise-mode */
|
||||
/* First-divergence trace for the precise slice (PRECISE_IRQ_SLICE.md Task #4). */
|
||||
@@ -84,6 +101,13 @@ uint32_t g_slice_last_istat = 0; /* i_stat at the take
|
||||
uint32_t g_slice_last_imask = 0; /* i_mask at the take */
|
||||
uint32_t g_slice_last_sr = 0; /* COP0 SR at the take */
|
||||
uint32_t g_slice_entry_deliverable = 0; /* was IRQ deliverable at slice entry? */
|
||||
uint32_t g_slice_exit_pc = 0; /* PC published by the last precise slice */
|
||||
uint32_t g_slice_exit_reason = 0; /* 1=safe, 2=unsupported, 3=bail, 4=guard */
|
||||
uint32_t g_slice_exit_iter = 0; /* loop iteration at exit */
|
||||
uint32_t g_slice_exit_dispatchable = 0; /* precise_pc_dispatchable(exit_pc) */
|
||||
uint32_t g_slice_exit_dirty = 0; /* dirty_ram_is_dirty(exit_pc phys) */
|
||||
uint32_t g_slice_exit_in_text = 0; /* psx_game_address_in_text(exit_pc) */
|
||||
uint32_t g_slice_exit_want = 0; /* want_exit value at exit */
|
||||
|
||||
/* Persistent async-RFE resume PC (Tomba 2 frame-1997 fix). psx_check_interrupts latches
|
||||
* this from g_dirty_safe_resume_pc at each dirty-safe-point exception entry, so it holds
|
||||
@@ -109,6 +133,7 @@ uint32_t g_sentinel_reach_async = 0;
|
||||
* than psx_unknown_dispatch (which has its own fail-fast). Captures the dispatch
|
||||
* addr + interrupt-return context so the exact producing path is identifiable
|
||||
* without printf. Surfaced in freeze_check (pczero_*). */
|
||||
int g_cosim_dirty_pump_site = 0; /* PSX_COSIM diagnostic: current dirty IRQ pump site */
|
||||
int g_pczero_latched = 0;
|
||||
uint32_t g_pczero_addr = 0;
|
||||
uint32_t g_pczero_ra = 0;
|
||||
@@ -540,6 +565,46 @@ static int dirty_ram_word_looks_decodable(uint32_t insn) {
|
||||
}
|
||||
}
|
||||
|
||||
static int dirty_ram_same_pc(uint32_t a, uint32_t b) {
|
||||
return (((a ^ b) & 0x1FFFFFFFu) == 0);
|
||||
}
|
||||
|
||||
static int dirty_ram_pump_boundary(CPUState *cpu, uint32_t committed_pc, int site) {
|
||||
uint32_t prev_safe = g_dirty_safe_resume_pc;
|
||||
int prev_site = g_cosim_dirty_pump_site;
|
||||
|
||||
s_last_dirty_irq_pump_insns = g_dirty_ram_insns_run;
|
||||
if (!psx_interrupts_checked_at_current_cycle(committed_pc)) {
|
||||
g_dirty_safe_resume_pc = committed_pc;
|
||||
g_cosim_dirty_pump_site = site;
|
||||
psx_check_interrupts(cpu);
|
||||
g_dirty_safe_resume_pc = prev_safe;
|
||||
g_cosim_dirty_pump_site = prev_site;
|
||||
}
|
||||
|
||||
if (g_psx_call_bail) return 1;
|
||||
if (cpu->pc == 0u && committed_pc != 0u) {
|
||||
cpu->pc = committed_pc;
|
||||
g_async_rfe_fire_count++;
|
||||
}
|
||||
if (cpu->pc != 0u && !dirty_ram_same_pc(cpu->pc, committed_pc)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
cpu->pc = committed_pc;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int dirty_ram_finish_call_return(CPUState *cpu, uint32_t return_pc,
|
||||
uint32_t *next_pc_out) {
|
||||
uint32_t prev_pc = cpu->pc;
|
||||
cpu->pc = return_pc;
|
||||
if (dirty_ram_pump_boundary(cpu, return_pc, 5)) return 1;
|
||||
cpu->pc = prev_pc;
|
||||
*next_pc_out = return_pc;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int dispatch_nonlocal_call(CPUState *cpu, uint32_t target,
|
||||
uint32_t return_pc,
|
||||
uint32_t *next_pc_out) {
|
||||
@@ -549,8 +614,7 @@ static int dispatch_nonlocal_call(CPUState *cpu, uint32_t target,
|
||||
* unwind in progress surfaces with cpu->pc = the guest's true target. */
|
||||
if (g_psx_call_bail) return 1;
|
||||
if (cpu->pc != 0) return 1;
|
||||
*next_pc_out = return_pc;
|
||||
return 0;
|
||||
return dirty_ram_finish_call_return(cpu, return_pc, next_pc_out);
|
||||
}
|
||||
|
||||
/* ── Mixed interp<->compiled dispatch owner (host-stack recursion fix) ──────
|
||||
@@ -882,7 +946,9 @@ static int interp_enter_compiled(CPUState *cpu, uint32_t target) {
|
||||
return 1;
|
||||
}
|
||||
g_mixed_depth++;
|
||||
ls_func_enter(target, cpu);
|
||||
int r = psx_dispatch_game_compiled(cpu, target);
|
||||
ls_func_exit(target, cpu, r);
|
||||
g_mixed_depth--;
|
||||
return r;
|
||||
}
|
||||
@@ -1045,6 +1111,7 @@ static int exec_one(CPUState *cpu, uint32_t pc, uint32_t *next_pc_out) {
|
||||
case 0x08: { /* JR rs */
|
||||
uint32_t target = cpu->gpr[rs];
|
||||
exec_delay_slot(cpu, pc + 4);
|
||||
cosim_exec_one_transfer_hook(pc + 4);
|
||||
/* crossing (if target is compiled) is counted at the block-loop
|
||||
* tail-transfer site (interp_enter_compiled, §18) — not here, to
|
||||
* avoid double-counting J/JR-to-compiled. */
|
||||
@@ -1057,6 +1124,7 @@ static int exec_one(CPUState *cpu, uint32_t pc, uint32_t *next_pc_out) {
|
||||
cpu->gpr[rd ? rd : 31] = return_pc;
|
||||
cpu->gpr[0] = 0;
|
||||
exec_delay_slot(cpu, pc + 4);
|
||||
cosim_exec_one_transfer_hook(pc + 4);
|
||||
uint32_t site_sp = cpu->gpr[29]; /* call contract: sp at the call */
|
||||
xprobe_event(pc, XOP_JALR, XSITE_INTERP, target,
|
||||
fetch_word((pc + 4) & 0x1FFFFFFFu), site_sp, cpu->gpr[31], 1);
|
||||
@@ -1069,8 +1137,7 @@ static int exec_one(CPUState *cpu, uint32_t pc, uint32_t *next_pc_out) {
|
||||
if (rd == 0 || rd == 31) {
|
||||
if (psx_call_contract(cpu, return_pc, site_sp)) return 1;
|
||||
}
|
||||
*next_pc_out = return_pc;
|
||||
return 0;
|
||||
return dirty_ram_finish_call_return(cpu, return_pc, next_pc_out);
|
||||
}
|
||||
#endif
|
||||
/* Native overlay candidates get the SAME call contract as
|
||||
@@ -1087,8 +1154,7 @@ static int exec_one(CPUState *cpu, uint32_t pc, uint32_t *next_pc_out) {
|
||||
if (rd == 0 || rd == 31) {
|
||||
if (psx_call_contract(cpu, return_pc, site_sp)) return 1;
|
||||
}
|
||||
*next_pc_out = return_pc;
|
||||
return 0;
|
||||
return dirty_ram_finish_call_return(cpu, return_pc, next_pc_out);
|
||||
}
|
||||
}
|
||||
if (!is_local_dirty_target(target)) {
|
||||
@@ -1216,6 +1282,7 @@ static int exec_one(CPUState *cpu, uint32_t pc, uint32_t *next_pc_out) {
|
||||
case 0x02: { /* J target */
|
||||
uint32_t target = ((pc + 4) & 0xF0000000u) | (target26(insn) << 2);
|
||||
exec_delay_slot(cpu, pc + 4);
|
||||
cosim_exec_one_transfer_hook(pc + 4);
|
||||
/* crossing counted at the block-loop tail-transfer site (§18). */
|
||||
cpu->pc = target;
|
||||
return 1;
|
||||
@@ -1225,6 +1292,7 @@ static int exec_one(CPUState *cpu, uint32_t pc, uint32_t *next_pc_out) {
|
||||
uint32_t return_pc = pc + 8;
|
||||
cpu->gpr[31] = return_pc;
|
||||
exec_delay_slot(cpu, pc + 4);
|
||||
cosim_exec_one_transfer_hook(pc + 4);
|
||||
uint32_t site_sp = cpu->gpr[29]; /* call contract: sp at the call */
|
||||
xprobe_event(pc, XOP_JAL, XSITE_INTERP, target,
|
||||
fetch_word((pc + 4) & 0x1FFFFFFFu), site_sp, cpu->gpr[31], 1);
|
||||
@@ -1235,8 +1303,7 @@ static int exec_one(CPUState *cpu, uint32_t pc, uint32_t *next_pc_out) {
|
||||
if (g_psx_call_bail) return 1; /* wild unwind: cpu->pc = true target */
|
||||
if (cpu->pc != 0) return 1;
|
||||
if (psx_call_contract(cpu, return_pc, site_sp)) return 1;
|
||||
*next_pc_out = return_pc;
|
||||
return 0;
|
||||
return dirty_ram_finish_call_return(cpu, return_pc, next_pc_out);
|
||||
}
|
||||
#endif
|
||||
/* Native overlay candidates get the SAME call contract as statically-
|
||||
@@ -1252,8 +1319,7 @@ static int exec_one(CPUState *cpu, uint32_t pc, uint32_t *next_pc_out) {
|
||||
if (g_psx_call_bail) return 1;
|
||||
if (cpu->pc != 0) return 1;
|
||||
if (psx_call_contract(cpu, return_pc, site_sp)) return 1;
|
||||
*next_pc_out = return_pc;
|
||||
return 0;
|
||||
return dirty_ram_finish_call_return(cpu, return_pc, next_pc_out);
|
||||
}
|
||||
}
|
||||
if (!is_local_dirty_target(target)) {
|
||||
@@ -1268,24 +1334,28 @@ static int exec_one(CPUState *cpu, uint32_t pc, uint32_t *next_pc_out) {
|
||||
case 0x04: { /* BEQ rs, rt, simm */
|
||||
int taken = (cpu->gpr[rs] == cpu->gpr[rt]);
|
||||
exec_delay_slot(cpu, pc + 4);
|
||||
cosim_exec_one_transfer_hook(pc + 4);
|
||||
cpu->pc = taken ? (pc + 4 + (simm << 2)) : (pc + 8);
|
||||
return 1;
|
||||
}
|
||||
case 0x05: { /* BNE */
|
||||
int taken = (cpu->gpr[rs] != cpu->gpr[rt]);
|
||||
exec_delay_slot(cpu, pc + 4);
|
||||
cosim_exec_one_transfer_hook(pc + 4);
|
||||
cpu->pc = taken ? (pc + 4 + (simm << 2)) : (pc + 8);
|
||||
return 1;
|
||||
}
|
||||
case 0x06: { /* BLEZ */
|
||||
int taken = ((int32_t)cpu->gpr[rs] <= 0);
|
||||
exec_delay_slot(cpu, pc + 4);
|
||||
cosim_exec_one_transfer_hook(pc + 4);
|
||||
cpu->pc = taken ? (pc + 4 + (simm << 2)) : (pc + 8);
|
||||
return 1;
|
||||
}
|
||||
case 0x07: { /* BGTZ */
|
||||
int taken = ((int32_t)cpu->gpr[rs] > 0);
|
||||
exec_delay_slot(cpu, pc + 4);
|
||||
cosim_exec_one_transfer_hook(pc + 4);
|
||||
cpu->pc = taken ? (pc + 4 + (simm << 2)) : (pc + 8);
|
||||
return 1;
|
||||
}
|
||||
@@ -1301,6 +1371,7 @@ static int exec_one(CPUState *cpu, uint32_t pc, uint32_t *next_pc_out) {
|
||||
default: return abort_unsupported(pc, insn, "REGIMM rt");
|
||||
}
|
||||
exec_delay_slot(cpu, pc + 4);
|
||||
cosim_exec_one_transfer_hook(pc + 4);
|
||||
cpu->pc = taken ? (pc + 4 + (simm << 2)) : (pc + 8);
|
||||
return 1;
|
||||
}
|
||||
@@ -1588,14 +1659,15 @@ int dirty_ram_dispatch(CPUState* cpu, uint32_t addr, uint32_t stop_addr) {
|
||||
* g_dirty_interp_active at its longjmp landing (same contract as the inner
|
||||
* local-flow pump), so a deliver-via-longjmp here is safe. */
|
||||
if (r == 1) {
|
||||
static uint64_t s_last_pump_insns = 0;
|
||||
uint64_t now = g_dirty_ram_insns_run;
|
||||
uint64_t gap = now - s_last_pump_insns;
|
||||
uint64_t gap = now - s_last_dirty_irq_pump_insns;
|
||||
if (gap > g_dirty_pump_max_gap_insns) g_dirty_pump_max_gap_insns = gap;
|
||||
if (gap >= 4096u) {
|
||||
s_last_pump_insns = now;
|
||||
s_last_dirty_irq_pump_insns = now;
|
||||
g_dirty_pump_count++;
|
||||
uint32_t committed = cpu->pc; /* block already retired; this is next PC */
|
||||
int prev_site = g_cosim_dirty_pump_site;
|
||||
g_cosim_dirty_pump_site = 6;
|
||||
g_dirty_safe_resume_pc = committed;
|
||||
psx_check_interrupts(cpu); /* may exception-enter / longjmp */
|
||||
/* Frame-1997 fix: a game-driven ReturnFromException longjmp'd through the
|
||||
@@ -1610,6 +1682,7 @@ int dirty_ram_dispatch(CPUState* cpu, uint32_t addr, uint32_t stop_addr) {
|
||||
g_async_rfe_fire_count++;
|
||||
}
|
||||
g_dirty_safe_resume_pc = 0;
|
||||
g_cosim_dirty_pump_site = prev_site;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1674,18 +1747,68 @@ static void psx_run_precise(CPUState *cpu, uint32_t bcyc, int deadline_entry) {
|
||||
g_slice_last_block = pc;
|
||||
g_slice_last_first_pc = pc;
|
||||
g_slice_last_first_insn = fetch_word(pc & 0x1FFFFFFFu);
|
||||
g_slice_exit_pc = pc;
|
||||
g_slice_exit_reason = 0;
|
||||
g_slice_exit_iter = 0;
|
||||
g_slice_exit_dispatchable = 0;
|
||||
g_slice_exit_dirty = dirty_ram_is_dirty(pc & 0x1FFFFFFFu) ? 1u : 0u;
|
||||
#ifdef PSX_HAS_GAME_DISPATCH
|
||||
g_slice_exit_in_text = psx_game_address_in_text(pc) ? 1u : 0u;
|
||||
#else
|
||||
g_slice_exit_in_text = 0;
|
||||
#endif
|
||||
g_slice_exit_want = 0;
|
||||
int irq_taken = 0; /* one take per slice (avoid re-taking an unacked IRQ) */
|
||||
enum { MAX_PRECISE_INSNS = 200000 };
|
||||
for (int i = 0; i < MAX_PRECISE_INSNS; i++) {
|
||||
if (!irq_taken && precise_irq_deliverable(cpu)) {
|
||||
uint32_t committed = pc;
|
||||
extern uint32_t i_stat;
|
||||
g_slice_last_committed = committed;
|
||||
g_slice_last_istat = i_stat;
|
||||
g_slice_last_imask = i_mask;
|
||||
g_slice_last_sr = cpu->cop0[12];
|
||||
cpu->pc = committed;
|
||||
int prev_site = g_cosim_dirty_pump_site;
|
||||
g_cosim_dirty_pump_site = 7;
|
||||
g_dirty_safe_resume_pc = committed;
|
||||
s_last_dirty_irq_pump_insns = g_dirty_ram_insns_run;
|
||||
psx_check_interrupts(cpu);
|
||||
if (cpu->pc == 0u && committed != 0u) {
|
||||
cpu->pc = committed;
|
||||
g_async_rfe_fire_count++;
|
||||
}
|
||||
g_dirty_safe_resume_pc = 0;
|
||||
g_cosim_dirty_pump_site = prev_site;
|
||||
g_slice_irq_taken++;
|
||||
irq_taken = 1;
|
||||
pc = cpu->pc ? cpu->pc : committed;
|
||||
cpu->pc = pc;
|
||||
if (precise_pc_dispatchable(cpu->pc)) {
|
||||
g_slice_exit_reason = 1;
|
||||
g_slice_exit_iter = (uint32_t)i;
|
||||
g_slice_exit_want = 1;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
uint32_t next_pc = 0;
|
||||
g_unsupported_seen = 0;
|
||||
cosim_exec_one_begin();
|
||||
int transferred = exec_one(cpu, pc, &next_pc); /* charges its own interlock */
|
||||
g_dirty_ram_insns_run++;
|
||||
#ifdef PSX_COSIM
|
||||
if (!cosim_exec_one_did_hook()) { extern void cosim_instr(uint32_t); cosim_instr(pc); }
|
||||
#endif
|
||||
if (g_unsupported_seen) {
|
||||
/* Valid compiled code always decodes; if not, hand the committed PC to
|
||||
* the dispatcher rather than abort the slice. */
|
||||
g_unsupported_seen = 0;
|
||||
pc = transferred ? cpu->pc : next_pc;
|
||||
g_slice_exit_reason = 2;
|
||||
g_slice_exit_iter = (uint32_t)i;
|
||||
g_slice_exit_want = 0;
|
||||
break;
|
||||
}
|
||||
uint32_t committed = transferred ? cpu->pc : next_pc;
|
||||
@@ -1710,34 +1833,59 @@ static void psx_run_precise(CPUState *cpu, uint32_t bcyc, int deadline_entry) {
|
||||
g_slice_last_imask = i_mask;
|
||||
g_slice_last_sr = cpu->cop0[12];
|
||||
cpu->pc = committed;
|
||||
int prev_site = g_cosim_dirty_pump_site;
|
||||
g_cosim_dirty_pump_site = 7;
|
||||
g_dirty_safe_resume_pc = committed; /* real EPC for exception entry */
|
||||
s_last_dirty_irq_pump_insns = g_dirty_ram_insns_run;
|
||||
psx_check_interrupts(cpu); /* takes it; runs handler; restores GPRs */
|
||||
g_dirty_safe_resume_pc = 0;
|
||||
g_cosim_dirty_pump_site = prev_site;
|
||||
g_slice_irq_taken++;
|
||||
irq_taken = 1;
|
||||
cpu->pc = committed; /* resume the interrupted stream at the exact PC */
|
||||
committed = cpu->pc ? cpu->pc : committed;
|
||||
want_exit = 1;
|
||||
}
|
||||
|
||||
pc = committed;
|
||||
cpu->pc = committed;
|
||||
|
||||
if (g_psx_call_bail) break; /* wild unwind: hand cpu->pc to the dispatcher */
|
||||
if (g_psx_call_bail) {
|
||||
g_slice_exit_reason = 3;
|
||||
g_slice_exit_iter = (uint32_t)i;
|
||||
g_slice_exit_want = want_exit ? 1u : 0u;
|
||||
break; /* wild unwind: hand cpu->pc to the dispatcher */
|
||||
}
|
||||
|
||||
if (irq_taken) {
|
||||
want_exit = 1; /* after the take, leave as soon as it is safe */
|
||||
} else if (transferred) {
|
||||
if (!deadline_entry) want_exit = 1; /* side-effect block: one block */
|
||||
else if (cycles_to_next_event() > bcyc) want_exit = 1; /* imminent window passed */
|
||||
want_exit = 1;
|
||||
/* else still imminent — keep slicing across this boundary */
|
||||
}
|
||||
|
||||
/* Hand back ONLY at a dispatchable PC. Otherwise (mid-function clean text)
|
||||
* keep interpreting until one is reached. */
|
||||
if (want_exit && precise_pc_dispatchable(cpu->pc)) break;
|
||||
if (want_exit && precise_pc_dispatchable(cpu->pc)) {
|
||||
g_slice_exit_reason = 1;
|
||||
g_slice_exit_iter = (uint32_t)i;
|
||||
g_slice_exit_want = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (g_slice_exit_reason == 0) {
|
||||
g_slice_exit_reason = 4;
|
||||
g_slice_exit_iter = MAX_PRECISE_INSNS;
|
||||
}
|
||||
|
||||
cpu->pc = pc;
|
||||
g_slice_exit_pc = cpu->pc;
|
||||
g_slice_exit_dispatchable = precise_pc_dispatchable(cpu->pc) ? 1u : 0u;
|
||||
g_slice_exit_dirty = dirty_ram_is_dirty(cpu->pc & 0x1FFFFFFFu) ? 1u : 0u;
|
||||
#ifdef PSX_HAS_GAME_DISPATCH
|
||||
g_slice_exit_in_text = psx_game_address_in_text(cpu->pc) ? 1u : 0u;
|
||||
#else
|
||||
g_slice_exit_in_text = 0;
|
||||
#endif
|
||||
g_precise_mode = prev_precise;
|
||||
g_dirty_interp_active = prev_active;
|
||||
}
|
||||
@@ -1770,8 +1918,23 @@ int psx_slice_block(CPUState *cpu, uint32_t block_addr, uint32_t bcyc, int side_
|
||||
* exception handling anyway). Keeps re-entrancy structurally impossible. */
|
||||
if (g_precise_mode || psx_get_in_exception()) return 0;
|
||||
|
||||
static int s_slice_always = -1;
|
||||
static int s_slice_margin = -1;
|
||||
if (s_slice_always < 0) {
|
||||
const char *e = getenv("PSX_PRECISE_ALWAYS");
|
||||
s_slice_always = (e && e[0] == '1') ? 1 : 0;
|
||||
}
|
||||
if (s_slice_margin < 0) {
|
||||
const char *e = getenv("PSX_PRECISE_MARGIN");
|
||||
s_slice_margin = (e && *e) ? atoi(e) : 0;
|
||||
if (s_slice_margin < 0) s_slice_margin = 0;
|
||||
}
|
||||
|
||||
uint32_t deadline = cycles_to_next_event();
|
||||
uint32_t budget = bcyc + (uint32_t)s_slice_margin;
|
||||
if (budget < bcyc) budget = 0xFFFFFFFFu;
|
||||
int entry_deliverable = precise_irq_deliverable(cpu);
|
||||
int has_deadline = entry_deliverable || (cycles_to_next_event() <= bcyc);
|
||||
int has_deadline = s_slice_always || entry_deliverable || (deadline <= budget);
|
||||
if (!has_deadline && !side_effects) return 0; /* fast path: no event in this block */
|
||||
|
||||
g_slice_entry_deliverable = (uint32_t)entry_deliverable;
|
||||
@@ -1783,6 +1946,7 @@ int psx_slice_block(CPUState *cpu, uint32_t block_addr, uint32_t bcyc, int side_
|
||||
|
||||
static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_addr) {
|
||||
uint32_t phys = addr & 0x1FFFFFFFu;
|
||||
int clean_game_text_miss = 0;
|
||||
|
||||
if (addr == 0x80000048u) {
|
||||
g_sentinel_reach_dirty++;
|
||||
@@ -1815,7 +1979,14 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_
|
||||
* unchanged for non-overlaying games once their text is baselined). */
|
||||
if (!dirty_ram_is_dirty(phys)) {
|
||||
g_mixed_depth++;
|
||||
{ int _gc = psx_dispatch_game_compiled(cpu, addr); g_mixed_depth--; if (_gc) return 1; }
|
||||
{
|
||||
ls_func_enter(addr, cpu);
|
||||
int _gc = psx_dispatch_game_compiled(cpu, addr);
|
||||
ls_func_exit(addr, cpu, _gc);
|
||||
g_mixed_depth--;
|
||||
if (_gc) return 1;
|
||||
}
|
||||
clean_game_text_miss = psx_game_address_in_text(addr) ? 1 : 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1859,7 +2030,7 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_
|
||||
}
|
||||
#define OV_FPLOG_RET1() do { if (_ovfp) overlay_fp_log(addr, _in_regs, cpu, 0); return 1; } while (0)
|
||||
|
||||
if (!dirty_ram_is_dirty(phys)) return 0;
|
||||
if (!dirty_ram_is_dirty(phys) && !clean_game_text_miss) return 0;
|
||||
|
||||
/* Interp-pressure signal for variant-capture automation (step 2.8):
|
||||
* counts dispatches the interpreter actually handles inside a capture
|
||||
@@ -1930,10 +2101,14 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_
|
||||
g_ls_dirty_observe = 1;
|
||||
debug_server_cyc_observe(pc & 0x1FFFFFFFu);
|
||||
g_ls_dirty_observe = 0;
|
||||
#endif
|
||||
#ifdef PSX_COSIM
|
||||
{ extern void cosim_block(uint32_t); cosim_block(pc); }
|
||||
#endif
|
||||
uint32_t insn = fetch_word(pc & 0x1FFFFFFFu);
|
||||
uint32_t before_s0 = cpu->gpr[16];
|
||||
uint32_t before_ra = cpu->gpr[31];
|
||||
cosim_exec_one_begin();
|
||||
int transferred = exec_one(cpu, pc, &next_pc);
|
||||
/* $ra->1 corruption tripwire (confirm-first probe): did THIS overlay
|
||||
* instruction clobber $ra to 1? Latches once, cheap after. */
|
||||
@@ -1968,6 +2143,9 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_
|
||||
dirty_ram_log_instruction(cpu, pc, insn, before_s0, next_pc,
|
||||
transferred ? cpu->pc : next_pc,
|
||||
transferred);
|
||||
#ifdef PSX_COSIM
|
||||
if (!cosim_exec_one_did_hook()) { extern void cosim_instr(uint32_t); cosim_instr(pc); }
|
||||
#endif
|
||||
if (g_unsupported_seen) {
|
||||
if (insns_executed == 0) {
|
||||
/* Couldn't decode the first instruction. Most likely
|
||||
@@ -2010,6 +2188,13 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_
|
||||
OV_FPLOG_RET1();
|
||||
}
|
||||
uint32_t target = cpu->pc;
|
||||
if (target != 0u && dirty_ram_pump_boundary(cpu, target, 1)) {
|
||||
g_dirty_ram_blocks_run++;
|
||||
if (pc_entry) pc_entry->insns += (uint64_t)insns_executed;
|
||||
g_dirty_interp_chain_target = cpu->pc;
|
||||
OV_FPLOG_RET1();
|
||||
}
|
||||
target = cpu->pc;
|
||||
#ifdef PSX_HAS_GAME_DISPATCH
|
||||
if (target != 0) {
|
||||
/* §20 FIX — the long-run idle freeze. A guest TAIL-transfer (j/jr/
|
||||
@@ -2062,9 +2247,6 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_
|
||||
if ((insns_executed & 0xFFF) == 0) {
|
||||
debug_server_poll();
|
||||
debug_server_wait_if_paused();
|
||||
g_dirty_safe_resume_pc = cpu->pc; /* committed (==target) -> real-EPC */
|
||||
psx_check_interrupts(cpu);
|
||||
g_dirty_safe_resume_pc = 0;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -2079,6 +2261,12 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_
|
||||
* hazard as a transfer to stop_addr). */
|
||||
if (stop_addr != 0 && pc == stop_addr) {
|
||||
cpu->pc = pc;
|
||||
if (dirty_ram_pump_boundary(cpu, pc, 2)) {
|
||||
g_dirty_ram_blocks_run++;
|
||||
if (pc_entry) pc_entry->insns += (uint64_t)insns_executed;
|
||||
g_dirty_interp_chain_target = cpu->pc;
|
||||
OV_FPLOG_RET1();
|
||||
}
|
||||
g_dirty_ram_blocks_run++;
|
||||
if (pc_entry) pc_entry->insns += (uint64_t)insns_executed;
|
||||
g_dirty_interp_chain_target = pc;
|
||||
@@ -2089,6 +2277,12 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_
|
||||
uint32_t next_phys = pc & 0x1FFFFFFFu;
|
||||
if (!dirty_ram_is_dirty(next_phys)) {
|
||||
cpu->pc = pc;
|
||||
if (dirty_ram_pump_boundary(cpu, pc, 3)) {
|
||||
g_dirty_ram_blocks_run++;
|
||||
if (pc_entry) pc_entry->insns += (uint64_t)insns_executed;
|
||||
g_dirty_interp_chain_target = cpu->pc;
|
||||
OV_FPLOG_RET1();
|
||||
}
|
||||
g_dirty_ram_blocks_run++;
|
||||
if (pc_entry) pc_entry->insns += (uint64_t)insns_executed;
|
||||
g_dirty_interp_chain_target = pc;
|
||||
@@ -2104,16 +2298,23 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_
|
||||
if (pc_entry) pc_entry->insns += (uint64_t)insns_executed;
|
||||
g_dirty_interp_chain_target = pc;
|
||||
{
|
||||
uint32_t committed = cpu->pc;
|
||||
g_dirty_safe_resume_pc = committed;
|
||||
psx_check_interrupts(cpu);
|
||||
/* Frame-1997 fix (see outer pump): restore the committed PC if a game RFE
|
||||
* longjmp left cpu->pc=0, so the trampoline re-dispatches instead of exiting. */
|
||||
if (cpu->pc == 0u && committed != 0u) {
|
||||
cpu->pc = committed;
|
||||
g_async_rfe_fire_count++;
|
||||
uint64_t gap = g_dirty_ram_insns_run - s_last_dirty_irq_pump_insns;
|
||||
if (gap >= 4096u) {
|
||||
uint32_t committed = cpu->pc;
|
||||
int prev_site = g_cosim_dirty_pump_site;
|
||||
g_cosim_dirty_pump_site = 4;
|
||||
g_dirty_safe_resume_pc = committed;
|
||||
s_last_dirty_irq_pump_insns = g_dirty_ram_insns_run;
|
||||
psx_check_interrupts(cpu);
|
||||
/* Frame-1997 fix (see outer pump): restore the committed PC if a game RFE
|
||||
* longjmp left cpu->pc=0, so the trampoline re-dispatches instead of exiting. */
|
||||
if (cpu->pc == 0u && committed != 0u) {
|
||||
cpu->pc = committed;
|
||||
g_async_rfe_fire_count++;
|
||||
}
|
||||
g_dirty_safe_resume_pc = 0;
|
||||
g_cosim_dirty_pump_site = prev_site;
|
||||
}
|
||||
g_dirty_safe_resume_pc = 0;
|
||||
}
|
||||
OV_FPLOG_RET1();
|
||||
}
|
||||
@@ -2127,11 +2328,12 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_
|
||||
|
||||
int g_ls_mode = 0;
|
||||
int g_ls_replay_active = 0;
|
||||
int g_ls_suppress_record = 0;
|
||||
|
||||
static uint32_t s_ls_frame_lo = 0, s_ls_frame_hi = 0; /* hi==0 => disabled */
|
||||
|
||||
typedef struct { uint8_t is_write, size; uint32_t addr, val; } ls_op_t;
|
||||
enum { LS_TRACE_CAP = 8192 };
|
||||
enum { LS_TRACE_CAP = 65536 };
|
||||
static ls_op_t s_ls_trace[LS_TRACE_CAP];
|
||||
static int s_ls_trace_n = 0, s_ls_trace_idx = 0;
|
||||
static int s_ls_overflow = 0, s_ls_mismatch = 0;
|
||||
@@ -2161,8 +2363,45 @@ static ls_op_t s_ls_div_trace[48];
|
||||
static int s_ls_div_trace_n = 0, s_ls_div_idx = 0;
|
||||
|
||||
static int s_ls_record_only = 0; /* diagnostic: record but skip inline replay (perturbation test) */
|
||||
|
||||
static uint32_t s_lsf_frame_lo = 0, s_lsf_frame_hi = 0; /* hi==0 => disabled */
|
||||
static int s_lsf_record_only = 0;
|
||||
static int s_lsf_active = 0;
|
||||
static int s_lsf_recording = 0;
|
||||
static int s_lsf_saw_irq = 0;
|
||||
static CPUState s_lsf_R0;
|
||||
static uint32_t s_lsf_entry = 0, s_lsf_dispatch_entry = 0, s_lsf_frame = 0;
|
||||
|
||||
static struct {
|
||||
int found;
|
||||
int kind; /* see lsf_kind names in ls_get_func_json */
|
||||
uint32_t frame, entry, pc, addr;
|
||||
uint32_t exp, act;
|
||||
int detail;
|
||||
uint64_t segments_checked, skipped_irq, skipped_overflow;
|
||||
uint64_t skipped_unhandled, skipped_conflict, skipped_disabled;
|
||||
uint32_t trace_ops, replay_ops, steps;
|
||||
} s_lsf_div = {0};
|
||||
|
||||
static ls_op_t s_lsf_div_trace[48];
|
||||
static int s_lsf_div_trace_n = 0, s_lsf_div_idx = 0;
|
||||
|
||||
void ls_set_window(uint32_t lo, uint32_t hi) { s_ls_frame_lo = lo; s_ls_frame_hi = hi; }
|
||||
void ls_set_record_only(int on) { s_ls_record_only = on; }
|
||||
void ls_func_set_window(uint32_t lo, uint32_t hi) { s_lsf_frame_lo = lo; s_lsf_frame_hi = hi; }
|
||||
void ls_func_set_record_only(int on) { s_lsf_record_only = on; }
|
||||
|
||||
void ls_note_exception_entry(void) {
|
||||
if (s_lsf_active) s_lsf_saw_irq = 1;
|
||||
}
|
||||
|
||||
void ls_suppress_begin(void) {
|
||||
g_ls_suppress_record++;
|
||||
}
|
||||
|
||||
void ls_suppress_end(void) {
|
||||
if (g_ls_suppress_record > 0) g_ls_suppress_record--;
|
||||
}
|
||||
|
||||
uint32_t ls_read_hook(uint32_t addr, int size, uint32_t real_val) {
|
||||
if (g_ls_mode == 2) { /* replay: serve from trace */
|
||||
@@ -2277,7 +2516,222 @@ static void ls_replay_and_compare(CPUState *cpu, uint32_t frame, uint32_t target
|
||||
}
|
||||
}
|
||||
|
||||
static int ls_same_phys(uint32_t a, uint32_t b) {
|
||||
return (((a ^ b) & 0x1FFFFFFFu) == 0);
|
||||
}
|
||||
|
||||
static void lsf_copy_div_trace(void) {
|
||||
int n = s_ls_trace_n;
|
||||
if (n > 48) n = 48;
|
||||
for (int i = 0; i < n; i++) s_lsf_div_trace[i] = s_ls_trace[i];
|
||||
s_lsf_div_trace_n = n;
|
||||
s_lsf_div_idx = s_ls_trace_idx;
|
||||
}
|
||||
|
||||
static void lsf_latch(int kind, uint32_t frame, uint32_t entry, uint32_t pc,
|
||||
uint32_t addr, uint32_t exp, uint32_t act, int detail,
|
||||
uint32_t steps) {
|
||||
if (s_lsf_div.found) return;
|
||||
s_lsf_div.found = 1;
|
||||
s_lsf_div.kind = kind;
|
||||
s_lsf_div.frame = frame;
|
||||
s_lsf_div.entry = entry;
|
||||
s_lsf_div.pc = pc;
|
||||
s_lsf_div.addr = addr;
|
||||
s_lsf_div.exp = exp;
|
||||
s_lsf_div.act = act;
|
||||
s_lsf_div.detail = detail;
|
||||
s_lsf_div.trace_ops = (uint32_t)s_ls_trace_n;
|
||||
s_lsf_div.replay_ops = (uint32_t)s_ls_trace_idx;
|
||||
s_lsf_div.steps = steps;
|
||||
lsf_copy_div_trace();
|
||||
}
|
||||
|
||||
static void lsf_compare_regs(CPUState *rep, CPUState *nat, uint32_t frame,
|
||||
uint32_t entry, uint32_t steps) {
|
||||
if (!ls_same_phys(rep->pc, nat->pc)) {
|
||||
lsf_latch(4, frame, entry, rep->pc, 0, rep->pc, nat->pc, 0, steps);
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < 32; i++) {
|
||||
if (rep->gpr[i] != nat->gpr[i]) {
|
||||
lsf_latch(1, frame, entry, rep->pc, 0, rep->gpr[i], nat->gpr[i], i, steps);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (rep->hi != nat->hi) {
|
||||
lsf_latch(2, frame, entry, rep->pc, 0, rep->hi, nat->hi, 0, steps);
|
||||
return;
|
||||
}
|
||||
if (rep->lo != nat->lo) {
|
||||
lsf_latch(3, frame, entry, rep->pc, 0, rep->lo, nat->lo, 0, steps);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static void lsf_replay_and_compare(CPUState *native_post, uint32_t frame,
|
||||
uint32_t entry_pc) {
|
||||
CPUState rep = s_lsf_R0;
|
||||
uint32_t target = native_post->pc;
|
||||
rep.pc = entry_pc;
|
||||
|
||||
s_ls_trace_idx = 0;
|
||||
s_ls_mismatch = 0;
|
||||
s_ls_m_kind = 0;
|
||||
s_ls_replay_done = 0;
|
||||
|
||||
int sav_unsup_seen = g_unsupported_seen;
|
||||
uint32_t sav_unsup_pc = g_unsupported_pc;
|
||||
uint32_t sav_unsup_insn = g_unsupported_insn;
|
||||
const char *sav_unsup_reason = g_unsupported_reason;
|
||||
uint32_t sav_store_pc = g_debug_last_store_pc;
|
||||
uint32_t sav_func_addr = g_debug_current_func_addr;
|
||||
|
||||
g_ls_replay_active = 1;
|
||||
g_ls_mode = 2;
|
||||
|
||||
uint32_t pc = entry_pc;
|
||||
uint32_t steps = 0;
|
||||
int done = 0;
|
||||
enum { MAX_FUNC_REPLAY_INSNS = 250000 };
|
||||
for (;;) {
|
||||
if (steps > 0 && target != 0 && ls_same_phys(pc, target) &&
|
||||
s_ls_trace_idx == s_ls_trace_n) {
|
||||
done = 1;
|
||||
break;
|
||||
}
|
||||
if (steps >= MAX_FUNC_REPLAY_INSNS) {
|
||||
lsf_latch(10, frame, entry_pc, pc, 0, target, pc, 0, steps);
|
||||
break;
|
||||
}
|
||||
if (s_ls_mismatch || s_ls_replay_done) break;
|
||||
|
||||
uint32_t next_pc = 0;
|
||||
s_ls_cur_pc = pc;
|
||||
g_unsupported_seen = 0;
|
||||
int transferred = exec_one(&rep, pc, &next_pc);
|
||||
if (g_unsupported_seen) {
|
||||
lsf_latch(11, frame, entry_pc, pc, 0, 0, g_unsupported_insn, 0, steps);
|
||||
break;
|
||||
}
|
||||
pc = transferred ? rep.pc : next_pc;
|
||||
rep.pc = pc;
|
||||
steps++;
|
||||
}
|
||||
|
||||
g_ls_mode = 0;
|
||||
g_ls_replay_active = 0;
|
||||
g_unsupported_seen = sav_unsup_seen;
|
||||
g_unsupported_pc = sav_unsup_pc;
|
||||
g_unsupported_insn = sav_unsup_insn;
|
||||
g_unsupported_reason = sav_unsup_reason;
|
||||
g_debug_last_store_pc = sav_store_pc;
|
||||
g_debug_current_func_addr = sav_func_addr;
|
||||
|
||||
s_lsf_div.segments_checked++;
|
||||
|
||||
if (s_ls_mismatch) {
|
||||
int k = (s_ls_m_kind == 1) ? 6 : (s_ls_m_kind == 2) ? 7 :
|
||||
(s_ls_m_kind == 3) ? 5 : 8;
|
||||
lsf_latch(k, frame, entry_pc, s_ls_m_pc, s_ls_m_addr,
|
||||
s_ls_m_exp, s_ls_m_act, 0, steps);
|
||||
return;
|
||||
}
|
||||
if (s_ls_replay_done) {
|
||||
lsf_latch(8, frame, entry_pc, s_ls_cur_pc, 0, (uint32_t)s_ls_trace_n,
|
||||
(uint32_t)s_ls_trace_idx, 0, steps);
|
||||
return;
|
||||
}
|
||||
if (!done) {
|
||||
lsf_latch(10, frame, entry_pc, pc, 0, target, pc, 0, steps);
|
||||
return;
|
||||
}
|
||||
if (s_ls_trace_idx != s_ls_trace_n) {
|
||||
lsf_latch(9, frame, entry_pc, pc, 0, (uint32_t)s_ls_trace_n,
|
||||
(uint32_t)s_ls_trace_idx, 0, steps);
|
||||
return;
|
||||
}
|
||||
|
||||
lsf_compare_regs(&rep, native_post, frame, entry_pc, steps);
|
||||
}
|
||||
|
||||
void ls_func_enter(uint32_t entry_pc, CPUState *cpu) {
|
||||
if (s_lsf_frame_hi == 0 || s_lsf_div.found || !cpu) return;
|
||||
if (g_ls_replay_active || g_ls_mode != 0 || s_lsf_active) {
|
||||
s_lsf_div.skipped_conflict++;
|
||||
return;
|
||||
}
|
||||
if (psx_get_in_exception()) {
|
||||
s_lsf_div.skipped_irq++;
|
||||
return;
|
||||
}
|
||||
uint32_t frame = (uint32_t)s_frame_count;
|
||||
if (frame < s_lsf_frame_lo || frame > s_lsf_frame_hi) {
|
||||
s_lsf_div.skipped_disabled++;
|
||||
return;
|
||||
}
|
||||
uint32_t phys = entry_pc & 0x1FFFFFFFu;
|
||||
if (phys < 0x00010000u || phys >= 0x00200000u) return;
|
||||
|
||||
s_lsf_dispatch_entry = entry_pc;
|
||||
s_lsf_entry = entry_pc;
|
||||
s_lsf_frame = frame;
|
||||
s_lsf_saw_irq = 0;
|
||||
s_lsf_active = 1;
|
||||
s_lsf_recording = 0;
|
||||
}
|
||||
|
||||
void ls_func_exit(uint32_t entry_pc, CPUState *cpu, int handled) {
|
||||
if (!s_lsf_active) return;
|
||||
if (s_lsf_dispatch_entry != entry_pc) {
|
||||
g_ls_mode = 0;
|
||||
s_lsf_active = 0;
|
||||
s_lsf_recording = 0;
|
||||
s_lsf_div.skipped_conflict++;
|
||||
return;
|
||||
}
|
||||
g_ls_mode = 0;
|
||||
s_lsf_active = 0;
|
||||
if (!s_lsf_recording) {
|
||||
s_lsf_div.skipped_unhandled++;
|
||||
return;
|
||||
}
|
||||
s_lsf_recording = 0;
|
||||
|
||||
if (!handled || !cpu) {
|
||||
s_lsf_div.skipped_unhandled++;
|
||||
return;
|
||||
}
|
||||
if (s_lsf_saw_irq) {
|
||||
s_lsf_div.skipped_irq++;
|
||||
return;
|
||||
}
|
||||
if (s_ls_overflow) {
|
||||
s_lsf_div.skipped_overflow++;
|
||||
return;
|
||||
}
|
||||
if (s_lsf_record_only) {
|
||||
s_lsf_div.segments_checked++;
|
||||
return;
|
||||
}
|
||||
lsf_replay_and_compare(cpu, s_lsf_frame, s_lsf_entry);
|
||||
}
|
||||
|
||||
void ls_at_leader(uint32_t leader_phys, CPUState *cpu) {
|
||||
if (s_lsf_active) {
|
||||
if (!s_lsf_recording && cpu && !s_lsf_div.found && g_ls_mode == 0) {
|
||||
s_lsf_R0 = *cpu;
|
||||
s_lsf_entry = leader_phys | 0x80000000u;
|
||||
s_ls_trace_n = 0;
|
||||
s_ls_trace_idx = 0;
|
||||
s_ls_overflow = 0;
|
||||
s_ls_mismatch = 0;
|
||||
s_ls_m_kind = 0;
|
||||
s_lsf_recording = 1;
|
||||
g_ls_mode = 1;
|
||||
}
|
||||
return; /* function-scope recording owns g_ls_mode */
|
||||
}
|
||||
if (s_ls_frame_hi == 0 || s_ls_div.found || !cpu) return;
|
||||
if (g_ls_mode == 2) return; /* re-entrancy guard (shouldn't happen) */
|
||||
uint32_t frame = (uint32_t)s_frame_count;
|
||||
@@ -2330,3 +2784,38 @@ int ls_get_diverge_json(char *buf, int buflen) {
|
||||
p += snprintf(buf + p, buflen - p, "]}");
|
||||
return p;
|
||||
}
|
||||
|
||||
int ls_get_func_json(char *buf, int buflen) {
|
||||
static const char *kn[] = {
|
||||
"none","gpr","hi","lo","pc","write-val","read-addr","write-addr",
|
||||
"trace-exhausted","trace-leftover","path-cap","unsupported"
|
||||
};
|
||||
int k = (s_lsf_div.kind >= 0 && s_lsf_div.kind <= 11) ? s_lsf_div.kind : 0;
|
||||
int p = snprintf(buf, buflen,
|
||||
"{\"found\":%d,\"kind\":\"%s\",\"frame\":%u,\"entry\":\"0x%08X\","
|
||||
"\"pc\":\"0x%08X\",\"addr\":\"0x%08X\","
|
||||
"\"interp_expected\":\"0x%08X\",\"compiled_actual\":\"0x%08X\","
|
||||
"\"reg\":%d,\"segments_checked\":%llu,\"skipped_irq\":%llu,"
|
||||
"\"skipped_overflow\":%llu,\"skipped_unhandled\":%llu,"
|
||||
"\"skipped_conflict\":%llu,\"skipped_disabled\":%llu,"
|
||||
"\"window\":[%u,%u],\"trace_ops\":%u,\"replay_ops\":%u,"
|
||||
"\"steps\":%u,\"div_idx\":%d,\"trace\":[",
|
||||
s_lsf_div.found, kn[k], s_lsf_div.frame, s_lsf_div.entry,
|
||||
s_lsf_div.pc, s_lsf_div.addr, s_lsf_div.exp, s_lsf_div.act,
|
||||
s_lsf_div.detail, (unsigned long long)s_lsf_div.segments_checked,
|
||||
(unsigned long long)s_lsf_div.skipped_irq,
|
||||
(unsigned long long)s_lsf_div.skipped_overflow,
|
||||
(unsigned long long)s_lsf_div.skipped_unhandled,
|
||||
(unsigned long long)s_lsf_div.skipped_conflict,
|
||||
(unsigned long long)s_lsf_div.skipped_disabled,
|
||||
s_lsf_frame_lo, s_lsf_frame_hi, s_lsf_div.trace_ops,
|
||||
s_lsf_div.replay_ops, s_lsf_div.steps, s_lsf_div_idx);
|
||||
for (int i = 0; i < s_lsf_div_trace_n && p < buflen - 48; i++) {
|
||||
p += snprintf(buf + p, buflen - p, "%s\"%c%d:%08X=%08X\"",
|
||||
i ? "," : "", s_lsf_div_trace[i].is_write ? 'W' : 'R',
|
||||
s_lsf_div_trace[i].size, s_lsf_div_trace[i].addr,
|
||||
s_lsf_div_trace[i].val);
|
||||
}
|
||||
p += snprintf(buf + p, buflen - p, "]}");
|
||||
return p;
|
||||
}
|
||||
|
||||
@@ -3098,6 +3098,26 @@ void gpu_write_gp1(uint32_t val) {
|
||||
X(vram_write_col) X(vram_write_row) X(vram_write_remaining) \
|
||||
X(vram_read_active) X(vram_read_x) X(vram_read_y) X(vram_read_w) X(vram_read_h) \
|
||||
X(vram_read_col) X(vram_read_row)
|
||||
#define GPU_COSIM_SNAP_FIELDS(X) \
|
||||
X(texpage_x) X(texpage_y) X(semi_transparency) X(texpage_colors) \
|
||||
X(dither_enabled) X(draw_to_display) X(texture_disable) X(texture_window_value) \
|
||||
X(set_mask_bit) X(check_mask_bit) \
|
||||
X(interlace_field) X(reverse_flag) \
|
||||
X(draw_area_left) X(draw_area_top) X(draw_area_right) X(draw_area_bottom) \
|
||||
X(draw_offset_x) X(draw_offset_y) \
|
||||
X(hres1) X(hres2) X(vres) X(video_mode) X(display_depth) X(vertical_interlace) \
|
||||
X(display_disabled) X(irq1_flag) X(dma_direction) X(lcf) \
|
||||
X(display_area_x) X(display_area_y) \
|
||||
X(h_display_x1) X(h_display_x2) X(v_display_y1) X(v_display_y2) \
|
||||
X(gpuread_latch) \
|
||||
X(gp0_state) \
|
||||
X(gp0_cmd_buf) X(gp0_words_collected) X(gp0_words_needed) \
|
||||
X(polyline_color) X(polyline_prev_x) X(polyline_prev_y) X(polyline_prev_c) \
|
||||
X(polyline_semi_trans) X(polyline_has_prev) \
|
||||
X(vram_write_x) X(vram_write_y) X(vram_write_w) X(vram_write_h) \
|
||||
X(vram_write_col) X(vram_write_row) X(vram_write_remaining) \
|
||||
X(vram_read_active) X(vram_read_x) X(vram_read_y) X(vram_read_w) X(vram_read_h) \
|
||||
X(vram_read_col) X(vram_read_row)
|
||||
uint32_t gpu_snapshot_bytes(void){ uint32_t n=0;
|
||||
#define X(f) n += (uint32_t)sizeof(f);
|
||||
GPU_SNAP_FIELDS(X)
|
||||
@@ -3108,6 +3128,55 @@ void gpu_snapshot_write(uint8_t *p){
|
||||
GPU_SNAP_FIELDS(X)
|
||||
#undef X
|
||||
}
|
||||
uint32_t gpu_cosim_snapshot_bytes(void){ uint32_t n=0;
|
||||
#define X(f) n += (uint32_t)sizeof(f);
|
||||
GPU_COSIM_SNAP_FIELDS(X)
|
||||
#undef X
|
||||
return n; }
|
||||
void gpu_cosim_snapshot_write(uint8_t *p){
|
||||
#define X(f) memcpy(p,&(f),sizeof(f)); p+=sizeof(f);
|
||||
GPU_COSIM_SNAP_FIELDS(X)
|
||||
#undef X
|
||||
}
|
||||
void gpu_cosim_dump(char *out, int cap) {
|
||||
if (!out || cap <= 0) return;
|
||||
char *p = out;
|
||||
size_t rem = (size_t)cap;
|
||||
#define APPEND(fmt, ...) do { \
|
||||
if (rem <= 1) break; \
|
||||
int w = snprintf(p, rem, fmt, __VA_ARGS__); \
|
||||
if (w < 0) { out[0] = 0; return; } \
|
||||
if ((size_t)w >= rem) { p += rem - 1; rem = 1; } \
|
||||
else { p += (size_t)w; rem -= (size_t)w; } \
|
||||
} while (0)
|
||||
#define X(f) APPEND(" %s %llx", #f, (unsigned long long)(uint32_t)(f));
|
||||
X(texpage_x) X(texpage_y) X(semi_transparency) X(texpage_colors)
|
||||
X(dither_enabled) X(draw_to_display) X(texture_disable) X(texture_window_value)
|
||||
X(set_mask_bit) X(check_mask_bit)
|
||||
X(interlace_field) X(reverse_flag)
|
||||
X(draw_area_left) X(draw_area_top) X(draw_area_right) X(draw_area_bottom)
|
||||
X(draw_offset_x) X(draw_offset_y)
|
||||
X(hres1) X(hres2) X(vres) X(video_mode) X(display_depth) X(vertical_interlace)
|
||||
X(display_disabled) X(irq1_flag) X(dma_direction) X(lcf)
|
||||
X(display_area_x) X(display_area_y)
|
||||
X(h_display_x1) X(h_display_x2) X(v_display_y1) X(v_display_y2)
|
||||
X(gpuread_latch)
|
||||
X(gp0_state)
|
||||
for (int i = 0; i < 16; i++) {
|
||||
APPEND(" gp0_cmd_buf%d %08x", i, gp0_cmd_buf[i]);
|
||||
}
|
||||
X(gp0_words_collected) X(gp0_words_needed)
|
||||
X(gp0_next_source_addr) X(gp0_cmd_source_addr)
|
||||
X(polyline_color) X(polyline_prev_x) X(polyline_prev_y) X(polyline_prev_c)
|
||||
X(polyline_semi_trans) X(polyline_has_prev)
|
||||
X(vram_write_x) X(vram_write_y) X(vram_write_w) X(vram_write_h)
|
||||
X(vram_write_col) X(vram_write_row) X(vram_write_remaining)
|
||||
X(vram_read_active) X(vram_read_x) X(vram_read_y) X(vram_read_w) X(vram_read_h)
|
||||
X(vram_read_col) X(vram_read_row)
|
||||
#undef X
|
||||
APPEND("%s", "\n");
|
||||
#undef APPEND
|
||||
}
|
||||
int gpu_snapshot_read(const uint8_t *p, uint32_t len){ if(len!=gpu_snapshot_bytes()) return 0;
|
||||
#define X(f) memcpy(&(f),p,sizeof(f)); p+=sizeof(f);
|
||||
GPU_SNAP_FIELDS(X)
|
||||
|
||||
@@ -902,6 +902,10 @@ static void gte_load_data(PSXRecomp::GTE::GTEState& gte, CPUState* cpu) {
|
||||
gte_mtc2(>e, i, cpu->gte_data[i]);
|
||||
}
|
||||
for (int i = 0; i < 32; i++) gte_ctc2(>e, i, cpu->gte_ctrl[i]);
|
||||
/* Loading emulator state is not a guest CTC2 write. CTC2 to FLAG masks
|
||||
* bit 31, but a previously computed FLAG error bit must survive reads and
|
||||
* unrelated control-register writes. */
|
||||
gte.FLAG = cpu->gte_ctrl[31];
|
||||
}
|
||||
|
||||
extern "C" uint32_t gte_read_data(CPUState* cpu, uint8_t reg) {
|
||||
|
||||
+303
-75
@@ -34,6 +34,7 @@
|
||||
#include "cpu_state.h"
|
||||
#include "debug_server.h"
|
||||
#include "event_ring.h"
|
||||
#include "lockstep.h"
|
||||
#include "psx_cycles.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -43,6 +44,7 @@
|
||||
* static BIOS exception handler is NOT interp code, so we clear it around the
|
||||
* handler dispatch (see psx_check_interrupts). */
|
||||
extern int g_dirty_interp_active;
|
||||
extern uint32_t g_dirty_safe_resume_pc;
|
||||
|
||||
/* IRQ-delivery context ring (MMX6 VSync-vs-CD-DMA hunt; dumped via `irqctx_ring`). */
|
||||
#define IRQCTX_RING_CAP 64u
|
||||
@@ -102,16 +104,218 @@ void psx_irq_raise(uint32_t bit, uint32_t detail)
|
||||
/* Dispatch counter for vblank scheduling. */
|
||||
#define VBLANK_INTERVAL 50000 /* legacy: dispatch-count fallback (unused for VBlank gating now) */
|
||||
#define VBLANK_CYCLES 564480u /* 33.8688 MHz / 60 Hz — real PSX NTSC VBlank period */
|
||||
#define VBLANK_DEFER_STALE_CYCLES (VBLANK_CYCLES * 10ull)
|
||||
static uint32_t dispatch_count;
|
||||
static uint64_t total_checks;
|
||||
static uint32_t cycles_since_vblank; /* incremented by interrupts_advance_cycles */
|
||||
|
||||
void interrupts_advance_cycles(uint32_t cycles) {
|
||||
cycles_since_vblank += cycles;
|
||||
}
|
||||
extern uint64_t g_vblank_raise_count;
|
||||
|
||||
/* Reentrancy guard: prevent interrupt handler from triggering interrupts. */
|
||||
static int in_exception;
|
||||
static int post_exception_cooldown;
|
||||
|
||||
static uint32_t last_sio_seq_seen;
|
||||
static uint64_t last_sio_progress_cycle;
|
||||
|
||||
#ifdef PSX_COSIM
|
||||
#define COSIM_IRQ_RING_CAP 4096u
|
||||
typedef struct {
|
||||
uint64_t seq;
|
||||
uint64_t cycle;
|
||||
uint32_t kind;
|
||||
uint32_t cpu_pc;
|
||||
uint32_t take_pc;
|
||||
uint32_t dirty_resume_pc;
|
||||
uint32_t compiled_resume_pc;
|
||||
uint32_t epc;
|
||||
uint32_t sr_before;
|
||||
uint32_t sr_after;
|
||||
uint32_t cause;
|
||||
uint32_t istat;
|
||||
uint32_t imask;
|
||||
uint32_t func;
|
||||
uint32_t block;
|
||||
uint32_t native;
|
||||
int32_t cooldown;
|
||||
int32_t dirty_site;
|
||||
} CosimIrqEntry;
|
||||
static CosimIrqEntry s_cosim_irq_ring[COSIM_IRQ_RING_CAP];
|
||||
static uint64_t s_cosim_irq_seq;
|
||||
extern int g_cosim_dirty_pump_site;
|
||||
extern uint32_t g_debug_current_func_addr;
|
||||
extern uint32_t cosim_last_block(void);
|
||||
extern uint32_t overlay_loader_get_inprogress(void);
|
||||
|
||||
static void cosim_irq_note(CPUState *cpu,
|
||||
uint32_t kind,
|
||||
uint32_t take_pc,
|
||||
uint32_t dirty_resume_pc,
|
||||
uint32_t compiled_resume_pc,
|
||||
uint32_t sr_before)
|
||||
{
|
||||
CosimIrqEntry *e = &s_cosim_irq_ring[s_cosim_irq_seq & (COSIM_IRQ_RING_CAP - 1u)];
|
||||
e->seq = s_cosim_irq_seq++;
|
||||
e->cycle = psx_get_cycle_count();
|
||||
e->kind = kind;
|
||||
e->cpu_pc = cpu ? cpu->pc : 0;
|
||||
e->take_pc = take_pc;
|
||||
e->dirty_resume_pc = dirty_resume_pc;
|
||||
e->compiled_resume_pc = compiled_resume_pc;
|
||||
e->epc = cpu ? cpu->cop0[COP0_EPC] : 0;
|
||||
e->sr_before = sr_before;
|
||||
e->sr_after = cpu ? cpu->cop0[COP0_SR] : 0;
|
||||
e->cause = cpu ? cpu->cop0[COP0_CAUSE] : 0;
|
||||
e->istat = i_stat;
|
||||
e->imask = i_mask;
|
||||
e->func = g_debug_current_func_addr;
|
||||
e->block = cosim_last_block();
|
||||
e->native = overlay_loader_get_inprogress();
|
||||
e->cooldown = post_exception_cooldown;
|
||||
e->dirty_site = g_cosim_dirty_pump_site;
|
||||
}
|
||||
|
||||
void interrupts_cosim_irq_dump(char *out, int cap)
|
||||
{
|
||||
if (!out || cap <= 0) return;
|
||||
char *p = out;
|
||||
size_t rem = (size_t)cap;
|
||||
uint64_t total = s_cosim_irq_seq < COSIM_IRQ_RING_CAP ? s_cosim_irq_seq : COSIM_IRQ_RING_CAP;
|
||||
uint64_t count = total;
|
||||
if (count > 16u) count = 16u;
|
||||
int w = snprintf(p, rem, "irqtrace count %llu",
|
||||
(unsigned long long)count);
|
||||
if (w < 0 || (size_t)w >= rem) { out[cap - 1] = 0; return; }
|
||||
p += w; rem -= (size_t)w;
|
||||
for (uint64_t i = 0; i < count && rem > 1; i++) {
|
||||
uint64_t seq = s_cosim_irq_seq - count + i;
|
||||
CosimIrqEntry *e = &s_cosim_irq_ring[seq & (COSIM_IRQ_RING_CAP - 1u)];
|
||||
w = snprintf(p, rem,
|
||||
" ; seq %llu kind %u cyc %llu pc %08x take %08x dirty %08x compiled %08x epc %08x sr0 %08x sr1 %08x cause %08x istat %08x imask %08x func %08x block %08x native %08x cool %d site %d",
|
||||
(unsigned long long)e->seq,
|
||||
e->kind,
|
||||
(unsigned long long)e->cycle,
|
||||
e->cpu_pc, e->take_pc, e->dirty_resume_pc,
|
||||
e->compiled_resume_pc, e->epc, e->sr_before,
|
||||
e->sr_after, e->cause, e->istat, e->imask,
|
||||
e->func, e->block, e->native,
|
||||
e->cooldown, e->dirty_site);
|
||||
if (w < 0 || (size_t)w >= rem) { break; }
|
||||
p += w; rem -= (size_t)w;
|
||||
}
|
||||
w = snprintf(p, rem, " | deliveries");
|
||||
if (w >= 0 && (size_t)w < rem) { p += w; rem -= (size_t)w; }
|
||||
uint64_t emitted = 0;
|
||||
for (uint64_t scanned = 0; scanned < total && emitted < 16u && rem > 1; scanned++) {
|
||||
uint64_t seq = s_cosim_irq_seq - 1u - scanned;
|
||||
CosimIrqEntry *e = &s_cosim_irq_ring[seq & (COSIM_IRQ_RING_CAP - 1u)];
|
||||
if (e->kind != 1u) continue;
|
||||
w = snprintf(p, rem,
|
||||
" ; seq %llu cyc %llu pc %08x take %08x dirty %08x compiled %08x epc %08x sr0 %08x sr1 %08x cause %08x istat %08x imask %08x func %08x block %08x native %08x cool %d site %d",
|
||||
(unsigned long long)e->seq,
|
||||
(unsigned long long)e->cycle,
|
||||
e->cpu_pc, e->take_pc, e->dirty_resume_pc,
|
||||
e->compiled_resume_pc, e->epc, e->sr_before,
|
||||
e->sr_after, e->cause, e->istat, e->imask,
|
||||
e->func, e->block, e->native,
|
||||
e->cooldown, e->dirty_site);
|
||||
if (w < 0 || (size_t)w >= rem) { break; }
|
||||
p += w; rem -= (size_t)w;
|
||||
emitted++;
|
||||
}
|
||||
uint64_t latest_delivery = UINT64_MAX;
|
||||
for (uint64_t scanned = 0; scanned < total; scanned++) {
|
||||
uint64_t seq = s_cosim_irq_seq - 1u - scanned;
|
||||
CosimIrqEntry *e = &s_cosim_irq_ring[seq & (COSIM_IRQ_RING_CAP - 1u)];
|
||||
if (e->kind == 1u) { latest_delivery = e->seq; break; }
|
||||
}
|
||||
if (latest_delivery != UINT64_MAX && rem > 1) {
|
||||
w = snprintf(p, rem, " | pre_delivery");
|
||||
if (w >= 0 && (size_t)w < rem) { p += w; rem -= (size_t)w; }
|
||||
uint64_t first = (latest_delivery >= 15u) ? (latest_delivery - 15u) : 0u;
|
||||
for (uint64_t seq = first; seq <= latest_delivery && rem > 1; seq++) {
|
||||
CosimIrqEntry *e = &s_cosim_irq_ring[seq & (COSIM_IRQ_RING_CAP - 1u)];
|
||||
w = snprintf(p, rem,
|
||||
" ; seq %llu kind %u cyc %llu pc %08x take %08x dirty %08x compiled %08x epc %08x sr0 %08x sr1 %08x cause %08x istat %08x imask %08x func %08x block %08x native %08x cool %d site %d",
|
||||
(unsigned long long)e->seq,
|
||||
e->kind,
|
||||
(unsigned long long)e->cycle,
|
||||
e->cpu_pc, e->take_pc, e->dirty_resume_pc,
|
||||
e->compiled_resume_pc, e->epc, e->sr_before,
|
||||
e->sr_after, e->cause, e->istat, e->imask,
|
||||
e->func, e->block, e->native,
|
||||
e->cooldown, e->dirty_site);
|
||||
if (w < 0 || (size_t)w >= rem) { break; }
|
||||
p += w; rem -= (size_t)w;
|
||||
}
|
||||
}
|
||||
if (rem >= 2) {
|
||||
p[0] = '\n';
|
||||
p[1] = 0;
|
||||
} else if (cap == 1) {
|
||||
out[0] = 0;
|
||||
} else {
|
||||
out[cap - 2] = '\n';
|
||||
out[cap - 1] = 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
static void note_sio_progress_cycle(void) {
|
||||
uint32_t cur_sio_seq = sio_get_seq();
|
||||
if (cur_sio_seq != last_sio_seq_seen) {
|
||||
last_sio_seq_seen = cur_sio_seq;
|
||||
last_sio_progress_cycle = psx_get_cycle_count();
|
||||
}
|
||||
}
|
||||
|
||||
static int should_defer_vblank_for_sio(void) {
|
||||
if (!sio_card_protocol_active()) return 0;
|
||||
uint64_t now = psx_get_cycle_count();
|
||||
uint64_t since_progress = now >= last_sio_progress_cycle
|
||||
? now - last_sio_progress_cycle
|
||||
: 0;
|
||||
return since_progress < VBLANK_DEFER_STALE_CYCLES;
|
||||
}
|
||||
|
||||
static void fire_vblank_edge(void) {
|
||||
/* Subtract one VBlank period rather than reset to 0 so cycle overshoot
|
||||
* carries forward. Prevents long-running blocks from rounding multiple
|
||||
* VBlanks together. */
|
||||
cycles_since_vblank -= VBLANK_CYCLES;
|
||||
dispatch_count = 0;
|
||||
/* DEQUEUE: this VBlank fired. ENQUEUE: next VBlank scheduled one period out. */
|
||||
event_ring_record_aux(EV_DEQ, (uint8_t)SRC_VBLANK,
|
||||
(uint32_t)psx_get_cycle_count());
|
||||
event_ring_record_aux(EV_ENQ, (uint8_t)SRC_VBLANK,
|
||||
(uint32_t)(psx_get_cycle_count() + VBLANK_CYCLES));
|
||||
psx_irq_raise(IRQ_VBLANK, 0);
|
||||
g_vblank_raise_count++;
|
||||
event_ring_record(EV_ISTAT_RAISE, IRQ_VBLANK);
|
||||
gpu_vblank_tick(); /* Toggle LCF (GPUSTAT bit 31) */
|
||||
#ifndef PSX_ENABLE_BLOCK_CYCLES
|
||||
timers_tick(33868); /* ~1 NTSC frame worth of cycles */
|
||||
cdrom_tick(); /* Process pending CDROM responses */
|
||||
#endif
|
||||
}
|
||||
|
||||
void interrupts_service_scheduled_events(void) {
|
||||
note_sio_progress_cycle();
|
||||
if (in_exception) return;
|
||||
while (cycles_since_vblank >= VBLANK_CYCLES) {
|
||||
if (should_defer_vblank_for_sio()) return;
|
||||
fire_vblank_edge();
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t interrupts_cycles_to_vblank(void) {
|
||||
if (cycles_since_vblank >= VBLANK_CYCLES) return 0;
|
||||
return VBLANK_CYCLES - cycles_since_vblank;
|
||||
}
|
||||
|
||||
void interrupts_advance_cycles(uint32_t cycles) {
|
||||
cycles_since_vblank += cycles;
|
||||
interrupts_service_scheduled_events();
|
||||
}
|
||||
|
||||
/* Diagnostic: total times the exception handler was entered (in_exception
|
||||
* transitioned 0->1). Diff this between two snapshots to measure handler
|
||||
@@ -131,8 +335,6 @@ static uint64_t exception_reentry_blocks;
|
||||
* pending interrupt can re-fire. Without this, unhandled interrupts cause
|
||||
* a livelock: the handler runs, doesn't clear I_STAT, returns, and the
|
||||
* very next psx_check_interrupts re-enters immediately. */
|
||||
static int post_exception_cooldown;
|
||||
|
||||
/* IRQ raise/deliver/ack telemetry (Tomba 2 exception-reentry-storm diagnosis).
|
||||
* Always-on, surfaced in the freeze heartbeat. Distinguishes:
|
||||
* raise≈deliver≈ack≈1/frame → healthy
|
||||
@@ -181,9 +383,38 @@ extern int g_psx_dispatch_depth;
|
||||
* the synchronous host window, dirty_ram_dispatch can resume at this guest PC
|
||||
* instead of treating the sentinel as pc=0 termination. */
|
||||
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;
|
||||
|
||||
static int same_guest_pc(uint32_t a, uint32_t b) {
|
||||
return (((a ^ b) & 0x1FFFFFFFu) == 0);
|
||||
}
|
||||
|
||||
int psx_get_in_exception(void) { return in_exception; }
|
||||
|
||||
/* Co-sim (COSIM_ORACLE.md): fold the GENUINE guest-timing interrupt statics into the
|
||||
* state hash. Deliberately EXCLUDES total_checks / dispatch_count / post_exception_
|
||||
* cooldown / s_compiled_interrupt_resume_pc — those are counted in psx_check_interrupts
|
||||
* CALLS or are backend-internal, so they legitimately differ between the compiled and
|
||||
* interp backends by call frequency and would be a false first-divergence. The one
|
||||
* quantity that is a real guest-cycle timing value is cycles_since_vblank (drives the
|
||||
* VBLANK deadline); in_exception mirrors the architectural handler-active state. */
|
||||
uint64_t interrupts_cosim_hash(uint64_t h) {
|
||||
const uint64_t P = 1099511628211ULL;
|
||||
uint32_t v = cycles_since_vblank;
|
||||
for (int i = 0; i < 4; i++) { h ^= (uint8_t)(v >> (i*8)); h *= P; }
|
||||
uint64_t sp = last_sio_progress_cycle;
|
||||
for (int i = 0; i < 8; i++) { h ^= (uint8_t)(sp >> (i*8)); h *= P; }
|
||||
h ^= (uint8_t)in_exception; h *= P;
|
||||
return h;
|
||||
}
|
||||
/* Co-sim field dump: expose the exact irqctl-timing fields so a divergence can be
|
||||
* field-diffed (is it cycles_since_vblank = VBLANK-raise-timing, or in_exception, ...). */
|
||||
void interrupts_cosim_dump(uint32_t *csv, int *inexc) {
|
||||
if (csv) *csv = cycles_since_vblank;
|
||||
if (inexc) *inexc = in_exception;
|
||||
}
|
||||
|
||||
/* ===== Fix B: faithful exception-return escape state (see psx_runtime.h) ===== */
|
||||
int g_rfe_escape_pending = 0;
|
||||
int g_exc_escape_reason = PSX_EXC_ESCAPE_NONE;
|
||||
@@ -246,6 +477,9 @@ void interrupts_init(void) {
|
||||
post_exception_cooldown = 0;
|
||||
exception_entries_total = 0;
|
||||
exception_reentry_blocks = 0;
|
||||
cycles_since_vblank = 0;
|
||||
last_sio_seq_seen = sio_get_seq();
|
||||
last_sio_progress_cycle = psx_get_cycle_count();
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -325,6 +559,20 @@ uint32_t cycles_to_next_event(void) {
|
||||
}
|
||||
|
||||
void psx_check_interrupts(CPUState* cpu) {
|
||||
extern int g_ls_suppress_record;
|
||||
#define PSX_CHECK_INTERRUPTS_RETURN() do { if (g_ls_suppress_record > 0) g_ls_suppress_record--; return; } while (0)
|
||||
#ifdef PSX_COSIM
|
||||
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)
|
||||
#define COSIM_IRQ_NOTE(kind_) cosim_irq_note(cpu, (kind_), COSIM_IRQ_TAKE_PC(), g_dirty_safe_resume_pc, s_compiled_interrupt_resume_pc, cpu->cop0[COP0_SR])
|
||||
#endif
|
||||
{
|
||||
uint32_t check_pc = g_dirty_safe_resume_pc ? g_dirty_safe_resume_pc
|
||||
: s_compiled_interrupt_resume_pc;
|
||||
s_last_interrupt_check_pc = check_pc;
|
||||
s_last_interrupt_check_cycle = psx_get_cycle_count();
|
||||
}
|
||||
g_ls_suppress_record++;
|
||||
total_checks++;
|
||||
if ((total_checks & 0x3FFFu) == 0) {
|
||||
debug_server_poll();
|
||||
@@ -338,25 +586,9 @@ void psx_check_interrupts(CPUState* cpu) {
|
||||
* during the delay loop BEFORE the clear, and the BIOS never
|
||||
* sees it. */
|
||||
|
||||
/* VBlank / timer tick — only when NOT inside the exception handler.
|
||||
*
|
||||
* On real hardware, VBlank is a hardware signal tied to the CRT
|
||||
* scanline counter, not to instruction count. The handler runs
|
||||
* for a few hundred cycles at most. In our model, the handler
|
||||
* runs thousands of block leaders (each calling psx_check_interrupts),
|
||||
* so dispatch_count can exceed VBLANK_INTERVAL during a single
|
||||
* handler invocation, causing VBlanks to stack up and starve the
|
||||
* main code. Gating the tick on !in_exception prevents this. */
|
||||
/* Track SIO byte progress at every check (not just at VBlank time). */
|
||||
static uint32_t last_sio_seq_seen = 0;
|
||||
static uint64_t total_checks_at_progress = 0;
|
||||
{
|
||||
uint32_t cur_sio_seq = sio_get_seq();
|
||||
if (cur_sio_seq != last_sio_seq_seen) {
|
||||
last_sio_seq_seen = cur_sio_seq;
|
||||
total_checks_at_progress = total_checks;
|
||||
}
|
||||
}
|
||||
interrupts_service_scheduled_events();
|
||||
|
||||
/* Dispatch-loop maintenance only when NOT inside the exception handler. */
|
||||
if (!in_exception) {
|
||||
#if SIO_MODEL_CYCLE_PACED
|
||||
/* Builds without block-cycle accounting need a small dispatch-loop
|
||||
@@ -371,50 +603,6 @@ void psx_check_interrupts(CPUState* cpu) {
|
||||
#endif
|
||||
#endif
|
||||
dispatch_count++;
|
||||
/* VBlank pacing is now cycle-based (real PSX has VBlank at
|
||||
* VBLANK_CYCLES = 564480 = 33.8688 MHz / 60 Hz). Previously this
|
||||
* used dispatch_count >= 50000 which fired at ~5-6 cycles per
|
||||
* dispatch = ~300k cycles per VBlank — half of real PSX, so
|
||||
* guest time ran at ~60% real rate. That broke FMV pacing
|
||||
* (TombaRecomp ISSUES.md #4: FMV at 6.7 fps vs 15 fps target). */
|
||||
if (cycles_since_vblank >= VBLANK_CYCLES) {
|
||||
/* Defer VBlank while a card SIO transaction is mid-flight.
|
||||
* On real hardware a 140-byte sector read finishes (~4.5ms)
|
||||
* well before the next VBlank (16.67ms). Our dispatch-count
|
||||
* pacing fires VBlank during the read window, and the BIOS
|
||||
* VBlank pad poll injects 0x01 onto the SIO bus mid-card
|
||||
* read, aborting the transaction via CTRL.RESET.
|
||||
*
|
||||
* Defer as long as the card protocol is making progress
|
||||
* (an SIO byte was exchanged in the last VBLANK_DEFER_STALE
|
||||
* dispatch ticks). If no byte for that long, treat the
|
||||
* protocol as stuck and force the VBlank to prevent deadlock. */
|
||||
#define VBLANK_DEFER_STALE 500000 /* ~10 frames; covers inter-byte gaps in card protocol */
|
||||
int card_active = sio_card_protocol_active();
|
||||
uint64_t since_progress = total_checks - total_checks_at_progress;
|
||||
int progress_stale = since_progress >= VBLANK_DEFER_STALE;
|
||||
if (!card_active || progress_stale) {
|
||||
/* Subtract one VBlank period rather than reset to 0 so
|
||||
* cycle overshoot carries forward. Prevents long-running
|
||||
* blocks from rounding multiple VBlanks together. */
|
||||
cycles_since_vblank -= VBLANK_CYCLES;
|
||||
dispatch_count = 0;
|
||||
/* DEQUEUE: this VBlank fired. ENQUEUE: next VBlank scheduled
|
||||
* one period out. */
|
||||
event_ring_record_aux(EV_DEQ, (uint8_t)SRC_VBLANK,
|
||||
(uint32_t)psx_get_cycle_count());
|
||||
event_ring_record_aux(EV_ENQ, (uint8_t)SRC_VBLANK,
|
||||
(uint32_t)(psx_get_cycle_count() + VBLANK_CYCLES));
|
||||
psx_irq_raise(IRQ_VBLANK, 0);
|
||||
g_vblank_raise_count++;
|
||||
event_ring_record(EV_ISTAT_RAISE, IRQ_VBLANK);
|
||||
gpu_vblank_tick(); /* Toggle LCF (GPUSTAT bit 31) */
|
||||
#ifndef PSX_ENABLE_BLOCK_CYCLES
|
||||
timers_tick(33868); /* ~1 NTSC frame worth of cycles */
|
||||
cdrom_tick(); /* Process pending CDROM responses */
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Event ring: generic i_stat-edge backstop. Catches raises from sites we
|
||||
@@ -429,24 +617,42 @@ void psx_check_interrupts(CPUState* cpu) {
|
||||
}
|
||||
|
||||
/* Check if any interrupts are pending. */
|
||||
if ((i_stat & i_mask) == 0) { irq_record_outcome(EV_NONE, 0, 0); return; }
|
||||
if ((i_stat & i_mask) == 0) { irq_record_outcome(EV_NONE, 0, 0); PSX_CHECK_INTERRUPTS_RETURN(); }
|
||||
if (in_exception) {
|
||||
exception_reentry_blocks++;
|
||||
irq_record_outcome(EV_IRQ_GATE, GATE_IN_EXCEPTION, 0);
|
||||
return;
|
||||
#ifdef PSX_COSIM
|
||||
COSIM_IRQ_NOTE(2u);
|
||||
#endif
|
||||
PSX_CHECK_INTERRUPTS_RETURN();
|
||||
}
|
||||
|
||||
/* Post-exception cooldown: let at least one block execute after RFE. */
|
||||
if (post_exception_cooldown > 0) {
|
||||
post_exception_cooldown--;
|
||||
irq_record_outcome(EV_IRQ_GATE, GATE_COOLDOWN, 0);
|
||||
return;
|
||||
#ifdef PSX_COSIM
|
||||
COSIM_IRQ_NOTE(3u);
|
||||
#endif
|
||||
PSX_CHECK_INTERRUPTS_RETURN();
|
||||
}
|
||||
|
||||
/* Check COP0 SR: IEc (bit 0) must be set, and IM2 (bit 10) must be set. */
|
||||
uint32_t sr = cpu->cop0[COP0_SR];
|
||||
if (!(sr & 0x01)) { irq_record_outcome(EV_IRQ_GATE, GATE_SR_IE, 0); return; } /* Interrupts globally disabled */
|
||||
if (!(sr & (1 << 10))) { irq_record_outcome(EV_IRQ_GATE, GATE_SR_IM2, 0); return; } /* Hardware interrupt bit not enabled */
|
||||
if (!(sr & 0x01)) {
|
||||
irq_record_outcome(EV_IRQ_GATE, GATE_SR_IE, 0);
|
||||
#ifdef PSX_COSIM
|
||||
COSIM_IRQ_NOTE(4u);
|
||||
#endif
|
||||
PSX_CHECK_INTERRUPTS_RETURN();
|
||||
} /* Interrupts globally disabled */
|
||||
if (!(sr & (1 << 10))) {
|
||||
irq_record_outcome(EV_IRQ_GATE, GATE_SR_IM2, 0);
|
||||
#ifdef PSX_COSIM
|
||||
COSIM_IRQ_NOTE(5u);
|
||||
#endif
|
||||
PSX_CHECK_INTERRUPTS_RETURN();
|
||||
} /* Hardware interrupt bit not enabled */
|
||||
|
||||
/* Architectural take-PC = the resume PC (same selection the async-RFE block
|
||||
* below uses): the dirty-interp commits the exact interrupted instruction in
|
||||
@@ -481,6 +687,7 @@ void psx_check_interrupts(CPUState* cpu) {
|
||||
};
|
||||
g_irqctx_seq++;
|
||||
}
|
||||
ls_note_exception_entry();
|
||||
in_exception = 1;
|
||||
exception_entries_total++;
|
||||
uint32_t pre_handler_istat = i_stat; /* snapshot for cooldown decision */
|
||||
@@ -516,7 +723,6 @@ void psx_check_interrupts(CPUState* cpu) {
|
||||
* is no mid-function resume PC, so fall back to the legacy sentinel + saved_gpr path
|
||||
* for THIS delivery only (reason = LEGACY_SENTINEL gates the saved_gpr restore and
|
||||
* disarms the RFE-flag escape). */
|
||||
extern uint32_t g_dirty_safe_resume_pc;
|
||||
g_rfe_escape_pending = 0;
|
||||
{
|
||||
uint32_t real_pc = g_dirty_safe_resume_pc ? g_dirty_safe_resume_pc
|
||||
@@ -533,6 +739,10 @@ void psx_check_interrupts(CPUState* cpu) {
|
||||
g_exception_real_epc = sentinel;
|
||||
g_exc_escape_reason = PSX_EXC_ESCAPE_LEGACY_SENTINEL;
|
||||
}
|
||||
#ifdef PSX_COSIM
|
||||
cosim_irq_note(cpu, 1u, real_pc, g_dirty_safe_resume_pc,
|
||||
s_compiled_interrupt_resume_pc, sr);
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Save the interrupted code's full register state.
|
||||
@@ -701,6 +911,12 @@ void psx_check_interrupts(CPUState* cpu) {
|
||||
uint32_t sio_active = (claimed | (i_stat & i_mask)) & (1u << IRQ_SIO0);
|
||||
post_exception_cooldown = sio_active ? 0 : CLAIMED_PROGRESS_QUANTUM;
|
||||
}
|
||||
if (g_ls_suppress_record > 0) g_ls_suppress_record--;
|
||||
#ifdef PSX_COSIM
|
||||
#undef COSIM_IRQ_NOTE
|
||||
#undef COSIM_IRQ_TAKE_PC
|
||||
#endif
|
||||
#undef PSX_CHECK_INTERRUPTS_RETURN
|
||||
}
|
||||
|
||||
/* Compatibility shim: the ape-flavored generated code calls
|
||||
@@ -714,3 +930,15 @@ void psx_check_interrupts_at(CPUState* cpu, uint32_t resume_pc) {
|
||||
psx_check_interrupts(cpu);
|
||||
s_compiled_interrupt_resume_pc = prev;
|
||||
}
|
||||
|
||||
int psx_interrupts_checked_at_current_cycle(uint32_t resume_pc) {
|
||||
return s_last_interrupt_check_cycle == psx_get_cycle_count() &&
|
||||
same_guest_pc(s_last_interrupt_check_pc, resume_pc);
|
||||
}
|
||||
|
||||
void psx_check_interrupts_dispatch_entry(CPUState* cpu, uint32_t resume_pc) {
|
||||
if (psx_interrupts_checked_at_current_cycle(resume_pc)) {
|
||||
return;
|
||||
}
|
||||
psx_check_interrupts_at(cpu, resume_pc);
|
||||
}
|
||||
|
||||
+68
-29
@@ -94,6 +94,9 @@ extern "C" void timers_init(void);
|
||||
|
||||
/* interrupts.c */
|
||||
extern "C" void interrupts_init(void);
|
||||
#ifdef PSX_COSIM
|
||||
extern "C" void cosim_init(void); /* first-divergence oracle server (cosim.c) */
|
||||
#endif
|
||||
extern "C" uint32_t psx_read_word(uint32_t addr);
|
||||
extern "C" void psx_write_word(uint32_t addr, uint32_t val);
|
||||
extern "C" uint16_t psx_read_half(uint32_t addr);
|
||||
@@ -143,6 +146,7 @@ static int g_video_screen = 0; /* 0=raw,1=crt,2=composite,3=trinitr
|
||||
static int g_video_win_w = 1280; /* window width (height follows aspect) */
|
||||
static bool g_audio_spu_hq = false; /* SPU float-shadow (env overrides) */
|
||||
static int g_auto_skip_fmv = 0; /* skip FMVs the instant they're detected */
|
||||
static int g_headless = 0; /* debug/CI frontend: no SDL window/audio */
|
||||
/* FMV instant-skip via the game's OWN end-of-movie path. Tomba's MDEC player
|
||||
* (FUN_8001efe8) tears a movie down when the streamed frame number reaches that
|
||||
* movie's per-movie total minus 3; writing the current movie's total down to
|
||||
@@ -1293,6 +1297,15 @@ static void sample_pad_into_sio(int override) {
|
||||
}
|
||||
}
|
||||
|
||||
static void sample_headless_pad_into_sio(int override) {
|
||||
if (override >= 0) {
|
||||
sio_set_pad_state_slot(0, (uint16_t)override);
|
||||
return;
|
||||
}
|
||||
sio_set_pad_state_slot(0, 0xFFFFu);
|
||||
sio_set_pad_state_slot(1, 0xFFFFu);
|
||||
}
|
||||
|
||||
/* PSX native vblank cadence: NTSC ≈ 59.94 Hz. Wall-clock target keeps
|
||||
* audio sample generation (735 samples/vblank * 60 = 44100/sec) matched
|
||||
* to the SDL audio device drain rate, eliminating queue overflow drops
|
||||
@@ -1403,33 +1416,35 @@ static void sdl_vblank_present(void) {
|
||||
if (cp->poll_main) cp->poll_main();
|
||||
}
|
||||
|
||||
/* Pump SDL events to prevent window freeze. */
|
||||
SDL_Event ev;
|
||||
while (SDL_PollEvent(&ev)) {
|
||||
if (ev.type == SDL_QUIT) {
|
||||
psx_crash_trace_set_exit_origin("sdl_window_close");
|
||||
shutdown_runtime();
|
||||
std::exit(0);
|
||||
} else if (ev.type == SDL_CONTROLLERDEVICEADDED) {
|
||||
refresh_player_devices();
|
||||
} else if (ev.type == SDL_CONTROLLERDEVICEREMOVED) {
|
||||
if (ev.cdevice.which == g_players[0].instance ||
|
||||
ev.cdevice.which == g_players[1].instance) {
|
||||
close_controller();
|
||||
if (!g_headless) {
|
||||
/* Pump SDL events to prevent window freeze. */
|
||||
SDL_Event ev;
|
||||
while (SDL_PollEvent(&ev)) {
|
||||
if (ev.type == SDL_QUIT) {
|
||||
psx_crash_trace_set_exit_origin("sdl_window_close");
|
||||
shutdown_runtime();
|
||||
std::exit(0);
|
||||
} else if (ev.type == SDL_CONTROLLERDEVICEADDED) {
|
||||
refresh_player_devices();
|
||||
}
|
||||
} else if (ev.type == SDL_KEYDOWN) {
|
||||
/* Fullscreen toggle: F11, Alt+Enter, or Cmd/Ctrl+F.
|
||||
* FULLSCREEN_DESKTOP keeps the desktop resolution; the
|
||||
* renderer's logical size letterboxes the 640x480 image. */
|
||||
const Uint16 mod = ev.key.keysym.mod;
|
||||
if (ev.key.keysym.sym == SDLK_F11 ||
|
||||
(ev.key.keysym.sym == SDLK_RETURN && (mod & KMOD_ALT)) ||
|
||||
(ev.key.keysym.sym == SDLK_f && (mod & (KMOD_GUI | KMOD_CTRL)))) {
|
||||
Uint32 is_fs = SDL_GetWindowFlags(sdl_window) &
|
||||
SDL_WINDOW_FULLSCREEN_DESKTOP;
|
||||
SDL_SetWindowFullscreen(sdl_window,
|
||||
is_fs ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP);
|
||||
} else if (ev.type == SDL_CONTROLLERDEVICEREMOVED) {
|
||||
if (ev.cdevice.which == g_players[0].instance ||
|
||||
ev.cdevice.which == g_players[1].instance) {
|
||||
close_controller();
|
||||
refresh_player_devices();
|
||||
}
|
||||
} else if (ev.type == SDL_KEYDOWN) {
|
||||
/* Fullscreen toggle: F11, Alt+Enter, or Cmd/Ctrl+F.
|
||||
* FULLSCREEN_DESKTOP keeps the desktop resolution; the
|
||||
* renderer's logical size letterboxes the 640x480 image. */
|
||||
const Uint16 mod = ev.key.keysym.mod;
|
||||
if (ev.key.keysym.sym == SDLK_F11 ||
|
||||
(ev.key.keysym.sym == SDLK_RETURN && (mod & KMOD_ALT)) ||
|
||||
(ev.key.keysym.sym == SDLK_f && (mod & (KMOD_GUI | KMOD_CTRL)))) {
|
||||
Uint32 is_fs = SDL_GetWindowFlags(sdl_window) &
|
||||
SDL_WINDOW_FULLSCREEN_DESKTOP;
|
||||
SDL_SetWindowFullscreen(sdl_window,
|
||||
is_fs ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1439,7 +1454,8 @@ static void sdl_vblank_present(void) {
|
||||
* g_low_latency_input this early sample is re-done after the pacer wait
|
||||
* (below) for the interactive present path; it still covers the turbo /
|
||||
* FMV-skip paths that early-return before pacing. */
|
||||
sample_pad_into_sio(override);
|
||||
if (g_headless) sample_headless_pad_into_sio(override);
|
||||
else sample_pad_into_sio(override);
|
||||
|
||||
/* Latency ring: open this present cycle's slot, stamping when input was
|
||||
* sampled into SIO. Always-on; queried via the debug server "latency". */
|
||||
@@ -1516,6 +1532,8 @@ static void sdl_vblank_present(void) {
|
||||
sdl_audio_update(fmv_skip_active);
|
||||
#endif
|
||||
|
||||
if (g_headless) return;
|
||||
|
||||
/* TCP turbo is for automated validation and trace capture. It keeps the
|
||||
* simulation advancing and the debug server polling, but removes frontend
|
||||
* presentation and wall-clock pacing. */
|
||||
@@ -1837,6 +1855,7 @@ int main(int argc, char** argv) {
|
||||
* --renderer <name> override the renderer: software|opengl|vulkan
|
||||
* --launcher force the GUI launcher (overrides skip_launcher)
|
||||
* --no-launcher skip the GUI launcher (boot straight in)
|
||||
* --headless skip SDL window/audio; use TCP screenshots/state
|
||||
* <positional> deprecated alias for --bios
|
||||
* No --memcard-dir / --game-root flags: those are config-driven. */
|
||||
for (int i = 1; i < argc; i++) {
|
||||
@@ -1860,11 +1879,20 @@ int main(int argc, char** argv) {
|
||||
force_launcher = true;
|
||||
} else if (std::strcmp(argv[i], "--no-launcher") == 0) {
|
||||
force_no_launcher = true;
|
||||
} else if (std::strcmp(argv[i], "--headless") == 0) {
|
||||
g_headless = 1;
|
||||
force_no_launcher = true;
|
||||
} else if (argv[i][0] != '-') {
|
||||
bios_path = argv[i];
|
||||
bios_from_cli = true;
|
||||
}
|
||||
}
|
||||
if (const char *e = std::getenv("PSX_HEADLESS")) {
|
||||
if (e[0] && e[0] != '0') {
|
||||
g_headless = 1;
|
||||
force_no_launcher = true;
|
||||
}
|
||||
}
|
||||
|
||||
std::string default_game_config_storage;
|
||||
if (!game_config_path) {
|
||||
@@ -2456,6 +2484,9 @@ int main(int argc, char** argv) {
|
||||
debug_server_init(debug_port);
|
||||
#else
|
||||
(void)debug_port;
|
||||
#endif
|
||||
#ifdef PSX_COSIM
|
||||
cosim_init(); /* first-divergence oracle server */
|
||||
#endif
|
||||
/* Heartbeat always on — see freeze_heartbeat.c rationale. */
|
||||
freeze_heartbeat_start("psx-runtime");
|
||||
@@ -2464,6 +2495,9 @@ int main(int argc, char** argv) {
|
||||
if (game_entry_pc != 0)
|
||||
fntrace_set_game_range(game_entry_pc, 0);
|
||||
|
||||
if (g_headless) {
|
||||
std::fprintf(stdout, "psxrecomp: headless frontend enabled\n");
|
||||
} else {
|
||||
/* ---- SDL init ---- */
|
||||
/* Scale quality governs SDL's logical-size -> window scaling. Linear when
|
||||
* antialiasing is on so the (super)sampled frame stays smooth when the
|
||||
@@ -2637,6 +2671,7 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
SDL_SetTextureScaleMode(sdl_texture,
|
||||
g_video_aa ? SDL_ScaleModeLinear : SDL_ScaleModeNearest);
|
||||
}
|
||||
}
|
||||
|
||||
/* Register vblank presentation callback. */
|
||||
@@ -2897,9 +2932,13 @@ int main(int argc, char** argv) {
|
||||
|
||||
std::fprintf(stdout, "psxrecomp runtime: execution completed, PC=0x%08X\n", cpu.pc);
|
||||
{ extern uint64_t g_slice_fired, g_slice_irq_taken, g_dirty_ram_insns_run;
|
||||
std::fprintf(stdout, "psxrecomp runtime: [slice diag] slice_fired=%llu slice_irq_taken=%llu dirty_insns=%llu\n",
|
||||
extern uint32_t g_slice_exit_pc, g_slice_exit_reason, g_slice_exit_iter;
|
||||
extern uint32_t g_slice_exit_dispatchable, g_slice_exit_dirty, g_slice_exit_in_text, g_slice_exit_want;
|
||||
std::fprintf(stdout, "psxrecomp runtime: [slice diag] slice_fired=%llu slice_irq_taken=%llu dirty_insns=%llu exit_pc=0x%08X reason=%u iter=%u dispatchable=%u dirty=%u in_text=%u want=%u\n",
|
||||
(unsigned long long)g_slice_fired, (unsigned long long)g_slice_irq_taken,
|
||||
(unsigned long long)g_dirty_ram_insns_run); }
|
||||
(unsigned long long)g_dirty_ram_insns_run, g_slice_exit_pc, g_slice_exit_reason,
|
||||
g_slice_exit_iter, g_slice_exit_dispatchable, g_slice_exit_dirty,
|
||||
g_slice_exit_in_text, g_slice_exit_want); }
|
||||
|
||||
shutdown_runtime();
|
||||
if (g_gl_active) gl_renderer_shutdown();
|
||||
|
||||
+17
-6
@@ -728,11 +728,13 @@ static void mmio_write8(uint32_t addr, uint8_t val) {
|
||||
* those device-internal accesses. Interrupt-handler ops are excluded too
|
||||
* (psx_get_in_exception): the replay runs only the block, never the handler. */
|
||||
static int s_ls_op_active = 0;
|
||||
extern int g_ls_suppress_record;
|
||||
extern int g_dma_exec_depth;
|
||||
extern int psx_get_in_exception(void);
|
||||
static uint32_t psx_read_word_raw(uint32_t addr);
|
||||
uint32_t psx_read_word(uint32_t addr) {
|
||||
if (g_ls_mode == 2) return ls_read_hook(addr, 4, 0u);
|
||||
if (g_ls_mode != 1 || s_ls_op_active) return psx_read_word_raw(addr);
|
||||
if (g_ls_mode != 1 || s_ls_op_active || g_ls_suppress_record || g_dma_exec_depth > 0) return psx_read_word_raw(addr);
|
||||
s_ls_op_active = 1;
|
||||
uint32_t v = psx_read_word_raw(addr);
|
||||
s_ls_op_active = 0;
|
||||
@@ -817,7 +819,7 @@ static inline void d44_note(uint32_t phys, uint32_t old, uint32_t val) {
|
||||
static void psx_write_word_raw(uint32_t addr, uint32_t val);
|
||||
void psx_write_word(uint32_t addr, uint32_t val) {
|
||||
if (g_ls_mode == 2) { ls_write_hook(addr, 4, val); return; }
|
||||
if (g_ls_mode != 1 || s_ls_op_active) { psx_write_word_raw(addr, val); return; }
|
||||
if (g_ls_mode != 1 || s_ls_op_active || g_ls_suppress_record || g_dma_exec_depth > 0) { psx_write_word_raw(addr, val); return; }
|
||||
if (!psx_get_in_exception()) ls_write_hook(addr, 4, val);
|
||||
s_ls_op_active = 1;
|
||||
psx_write_word_raw(addr, val);
|
||||
@@ -840,6 +842,9 @@ static void psx_write_word_raw(uint32_t addr, uint32_t val) {
|
||||
card_data_writes_check(phys, val, 4);
|
||||
dirty_ram_mark_kernel_write(phys);
|
||||
overlay_watch_note_write(phys, 4);
|
||||
#ifdef PSX_COSIM
|
||||
{ extern void cosim_note_ram_write(uint32_t,uint32_t); cosim_note_ram_write(phys, 4); }
|
||||
#endif
|
||||
ram[phys] = (uint8_t)(val);
|
||||
ram[phys + 1] = (uint8_t)(val >> 8);
|
||||
ram[phys + 2] = (uint8_t)(val >> 16);
|
||||
@@ -876,7 +881,7 @@ static void psx_write_word_raw(uint32_t addr, uint32_t val) {
|
||||
static uint16_t psx_read_half_raw(uint32_t addr);
|
||||
uint16_t psx_read_half(uint32_t addr) {
|
||||
if (g_ls_mode == 2) return (uint16_t)ls_read_hook(addr, 2, 0u);
|
||||
if (g_ls_mode != 1 || s_ls_op_active) return psx_read_half_raw(addr);
|
||||
if (g_ls_mode != 1 || s_ls_op_active || g_ls_suppress_record || g_dma_exec_depth > 0) return psx_read_half_raw(addr);
|
||||
s_ls_op_active = 1;
|
||||
uint16_t v = psx_read_half_raw(addr);
|
||||
s_ls_op_active = 0;
|
||||
@@ -908,7 +913,7 @@ static uint16_t psx_read_half_raw(uint32_t addr) {
|
||||
static void psx_write_half_raw(uint32_t addr, uint16_t val);
|
||||
void psx_write_half(uint32_t addr, uint16_t val) {
|
||||
if (g_ls_mode == 2) { ls_write_hook(addr, 2, val); return; }
|
||||
if (g_ls_mode != 1 || s_ls_op_active) { psx_write_half_raw(addr, val); return; }
|
||||
if (g_ls_mode != 1 || s_ls_op_active || g_ls_suppress_record || g_dma_exec_depth > 0) { psx_write_half_raw(addr, val); return; }
|
||||
if (!psx_get_in_exception()) ls_write_hook(addr, 2, val);
|
||||
s_ls_op_active = 1;
|
||||
psx_write_half_raw(addr, val);
|
||||
@@ -925,6 +930,9 @@ static void psx_write_half_raw(uint32_t addr, uint16_t val) {
|
||||
card_data_writes_check(phys, (uint32_t)val, 2);
|
||||
dirty_ram_mark_kernel_write(phys);
|
||||
overlay_watch_note_write(phys, 2);
|
||||
#ifdef PSX_COSIM
|
||||
{ extern void cosim_note_ram_write(uint32_t,uint32_t); cosim_note_ram_write(phys, 2); }
|
||||
#endif
|
||||
ram[phys] = (uint8_t)(val);
|
||||
ram[phys + 1] = (uint8_t)(val >> 8);
|
||||
return;
|
||||
@@ -952,7 +960,7 @@ static void psx_write_half_raw(uint32_t addr, uint16_t val) {
|
||||
static uint8_t psx_read_byte_raw(uint32_t addr);
|
||||
uint8_t psx_read_byte(uint32_t addr) {
|
||||
if (g_ls_mode == 2) return (uint8_t)ls_read_hook(addr, 1, 0u);
|
||||
if (g_ls_mode != 1 || s_ls_op_active) return psx_read_byte_raw(addr);
|
||||
if (g_ls_mode != 1 || s_ls_op_active || g_ls_suppress_record || g_dma_exec_depth > 0) return psx_read_byte_raw(addr);
|
||||
s_ls_op_active = 1;
|
||||
uint8_t v = psx_read_byte_raw(addr);
|
||||
s_ls_op_active = 0;
|
||||
@@ -1115,7 +1123,7 @@ uint8_t psx_guest_read_byte(uint32_t addr) { return psx_read_byte(addr); }
|
||||
static void psx_write_byte_raw(uint32_t addr, uint8_t val);
|
||||
void psx_write_byte(uint32_t addr, uint8_t val) {
|
||||
if (g_ls_mode == 2) { ls_write_hook(addr, 1, val); return; }
|
||||
if (g_ls_mode != 1 || s_ls_op_active) { psx_write_byte_raw(addr, val); return; }
|
||||
if (g_ls_mode != 1 || s_ls_op_active || g_ls_suppress_record || g_dma_exec_depth > 0) { psx_write_byte_raw(addr, val); return; }
|
||||
if (!psx_get_in_exception()) ls_write_hook(addr, 1, val);
|
||||
s_ls_op_active = 1;
|
||||
psx_write_byte_raw(addr, val);
|
||||
@@ -1132,6 +1140,9 @@ static void psx_write_byte_raw(uint32_t addr, uint8_t val) {
|
||||
card_data_writes_check(phys, (uint32_t)val, 1);
|
||||
dirty_ram_mark_kernel_write(phys);
|
||||
overlay_watch_note_write(phys, 1);
|
||||
#ifdef PSX_COSIM
|
||||
{ extern void cosim_note_ram_write(uint32_t,uint32_t); cosim_note_ram_write(phys, 1); }
|
||||
#endif
|
||||
ram[phys] = val;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
#include "sio.h"
|
||||
#include "starvation_ring.h"
|
||||
#include "timers.h"
|
||||
#ifdef PSX_COSIM
|
||||
#include "cosim_state.h"
|
||||
#endif
|
||||
|
||||
uint64_t psx_cycle_count = 0;
|
||||
|
||||
@@ -43,13 +46,34 @@ static void advance_devices(uint32_t c) {
|
||||
void psx_advance_cycles(uint32_t cycles) {
|
||||
{ extern int g_ls_replay_active; if (g_ls_replay_active) return; } /* lockstep replay: no global cycle/device mutation */
|
||||
if (cycles == 0) return;
|
||||
if (g_event_step_conservative && cycles > 1u) {
|
||||
/* Fine-step so sub-block events fire in true cycle order. */
|
||||
for (uint32_t i = 0; i < cycles; i++) advance_devices(1u);
|
||||
} else {
|
||||
advance_devices(cycles);
|
||||
uint32_t charged_cycles = cycles;
|
||||
#ifdef PSX_COSIM
|
||||
/* First-divergence oracle: the guest-cycle counter is the ONLY clock both backends
|
||||
* share identically, so it is the alignment point for the full-state hash. */
|
||||
cosim_tick();
|
||||
#endif
|
||||
interrupts_service_scheduled_events();
|
||||
while (cycles > 0) {
|
||||
uint32_t step = cycles;
|
||||
if (g_event_step_conservative && step > 1u) {
|
||||
step = 1u;
|
||||
} else {
|
||||
uint32_t to_vblank = interrupts_cycles_to_vblank();
|
||||
if (to_vblank > 0 && to_vblank < step) step = to_vblank;
|
||||
#ifdef PSX_COSIM
|
||||
uint32_t to_cp = cosim_cycles_to_next_checkpoint();
|
||||
if (to_cp > 0 && to_cp < step) step = to_cp;
|
||||
#endif
|
||||
}
|
||||
if (step == 0) step = cycles;
|
||||
advance_devices(step);
|
||||
cycles -= step;
|
||||
interrupts_service_scheduled_events();
|
||||
#ifdef PSX_COSIM
|
||||
cosim_tick();
|
||||
#endif
|
||||
}
|
||||
s_watchdog_throttle += cycles;
|
||||
s_watchdog_throttle += charged_cycles;
|
||||
if (s_watchdog_throttle >= 65536u) {
|
||||
s_watchdog_throttle = 0;
|
||||
starvation_watchdog_check();
|
||||
@@ -58,7 +82,7 @@ void psx_advance_cycles(uint32_t cycles) {
|
||||
* localize a busy-wait loop, sparse enough to not flood the 16K ring
|
||||
* during normal SIO traffic (~3000 samples/sec vs >10K SIO events/sec
|
||||
* during card transactions). */
|
||||
s_pc_sample_throttle += cycles;
|
||||
s_pc_sample_throttle += charged_cycles;
|
||||
if (s_pc_sample_throttle >= 1048576u) {
|
||||
s_pc_sample_throttle = 0;
|
||||
starvation_ring_pc_sample();
|
||||
|
||||
+289
@@ -0,0 +1,289 @@
|
||||
#!/usr/bin/env python3
|
||||
"""cosim.py — first-divergence co-simulation coordinator. See COSIM_ORACLE.md.
|
||||
|
||||
Launches two clean psx-cosim instances (each its own complete deterministic PSX),
|
||||
advances BOTH to the same guest-cycle checkpoints, and compares their full-state chain
|
||||
hashes. The first checkpoint whose chains differ brackets the first divergence; the
|
||||
per-subsystem `sub` hashes + the ring `window` say WHAT and WHERE.
|
||||
|
||||
The two instances are NOT interleaved on one timeline — they are two independent
|
||||
deterministic runs, sampled at matched guest cycles. Validity rests ENTIRELY on
|
||||
determinism, which is why you MUST pass the gates first:
|
||||
|
||||
# GATE 1 — determinism/hashing: two of the SAME backend must NEVER diverge.
|
||||
python cosim.py --a compiled --b compiled --stride 65536 --max 1500000000
|
||||
python cosim.py --a interp --b interp --stride 65536 --max 1500000000
|
||||
# GATE 4 — injected fault must halt at the right field:
|
||||
python cosim.py --a compiled --b compiled --inject-at 200000000 --inject ram:100000:1
|
||||
|
||||
# THE RUN (only after gates pass):
|
||||
python cosim.py --a compiled --b interp --stride 65536 --max 1500000000
|
||||
"""
|
||||
import socket, subprocess, os, sys, time, argparse
|
||||
|
||||
EXE = r"F:\Projects\psxrecomp\MegaManX6Recomp\build-cosim\psx-cosim.exe"
|
||||
CWD = r"F:\Projects\psxrecomp\MegaManX6Recomp\build-cosim"
|
||||
GAME = r"F:\Projects\psxrecomp\MegaManX6Recomp\game.toml"
|
||||
LOGDIR = os.path.join(CWD, "cosim-logs")
|
||||
|
||||
def tail_file(path, max_bytes=8192):
|
||||
try:
|
||||
with open(path, "rb") as f:
|
||||
f.seek(0, os.SEEK_END)
|
||||
size = f.tell()
|
||||
f.seek(max(0, size - max_bytes), os.SEEK_SET)
|
||||
return f.read().decode(errors="replace")
|
||||
except Exception as e:
|
||||
return f"<could not read {path}: {e}>"
|
||||
|
||||
def launch(mode, port, stride, start_cycle):
|
||||
os.makedirs(LOGDIR, exist_ok=True)
|
||||
env = dict(os.environ)
|
||||
env["PSX_HEADLESS"] = "1"
|
||||
env["PSX_COSIM_PORT"] = str(port)
|
||||
env["PSX_COSIM_STRIDE"] = str(stride)
|
||||
# The starvation watchdog is driven by debug-server heartbeats, while cosim
|
||||
# can spend seconds fast-forwarding without polling that server. Treat it as
|
||||
# a GUI/debugger watchdog, not a cosim oracle signal.
|
||||
env.setdefault("PSX_STARVATION_TIMEOUT_US", "0")
|
||||
if start_cycle:
|
||||
env["PSX_COSIM_START_CYCLE"] = str(start_cycle)
|
||||
if mode == "interp":
|
||||
env["PSX_FORCE_INTERP"] = "1"
|
||||
log_path = os.path.join(LOGDIR, f"cosim_{mode}_{port}_{os.getpid()}.log")
|
||||
log_file = open(log_path, "wb")
|
||||
p = subprocess.Popen([EXE, "--headless", "--no-launcher", "--game", GAME],
|
||||
cwd=CWD, env=env,
|
||||
stdout=log_file, stderr=subprocess.STDOUT,
|
||||
creationflags=0x00000200)
|
||||
p._cosim_log_path = log_path
|
||||
p._cosim_log_file = log_file
|
||||
if os.environ.get("PSX_COSIM_BELOW_NORMAL") == "1":
|
||||
try:
|
||||
import ctypes
|
||||
h = ctypes.windll.kernel32.OpenProcess(0x0400, False, p.pid)
|
||||
ctypes.windll.kernel32.SetPriorityClass(h, 0x00004000) # BELOW_NORMAL
|
||||
except Exception:
|
||||
pass
|
||||
return p
|
||||
|
||||
def connect(port, timeout=40):
|
||||
t0 = time.time()
|
||||
while time.time() - t0 < timeout:
|
||||
try:
|
||||
s = socket.socket(); s.settimeout(5); s.connect(("127.0.0.1", port))
|
||||
return s
|
||||
except Exception:
|
||||
time.sleep(0.5)
|
||||
raise RuntimeError(f"cosim port {port} never came up")
|
||||
|
||||
def cmd(s, line, timeout=600):
|
||||
s.settimeout(timeout)
|
||||
s.sendall((line + "\n").encode())
|
||||
buf = b""
|
||||
while b"\n" not in buf:
|
||||
d = s.recv(65536)
|
||||
if not d: break
|
||||
buf += d
|
||||
return buf.decode(errors="replace").strip()
|
||||
|
||||
def close_proc_log(p):
|
||||
try:
|
||||
p._cosim_log_file.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def report_child_failure(label, proc, cp, exc):
|
||||
code = proc.poll()
|
||||
print(f"[{label} reset] at local cp {cp}: poll={code} error={exc}", flush=True)
|
||||
path = getattr(proc, "_cosim_log_path", "")
|
||||
if path:
|
||||
print(f"--- {label} log tail: {path} ---", flush=True)
|
||||
print(tail_file(path), flush=True)
|
||||
|
||||
def wait_parked(sa, sb, timeout=600):
|
||||
t0 = time.time()
|
||||
last = ("", "")
|
||||
while time.time() - t0 < timeout:
|
||||
ra = kv(cmd(sa, "status", timeout=120))
|
||||
rb = kv(cmd(sb, "status", timeout=120))
|
||||
last = (ra, rb)
|
||||
if ra.get("parked") == "1" and rb.get("parked") == "1":
|
||||
return ra, rb
|
||||
time.sleep(0.05)
|
||||
raise RuntimeError(f"cosim did not park before timeout: A={last[0]} B={last[1]}")
|
||||
|
||||
def parse_cpu(resp):
|
||||
"""'pc XX hi XX ... r0 XX ... c0 XX ... mdts XX gtets XX ...' -> {field: val}"""
|
||||
t = resp.split()
|
||||
return {t[i]: t[i+1] for i in range(0, len(t)-1, 2)}
|
||||
|
||||
def kv(resp):
|
||||
"""parse a reply into {key: next_token} for every token that is followed by
|
||||
another token. Robust to a leading bare status word (e.g. 'parked cp N cycle M
|
||||
chain HEX'): we scan ALL adjacent pairs, so 'chain' -> HEX is always captured
|
||||
regardless of the leading word's parity. (The earlier stride-2 parser misaligned
|
||||
on the leading 'parked'/'timeout' word and returned chain=None for BOTH sides,
|
||||
making every compare None==None == 'equal' — a silent blind spot. Gate 4 exists
|
||||
to catch exactly this.)"""
|
||||
t = resp.split()
|
||||
d = {}
|
||||
for i in range(len(t) - 1):
|
||||
d.setdefault(t[i], t[i+1])
|
||||
return d
|
||||
|
||||
def dump_window(s, n=24):
|
||||
s.settimeout(30); s.sendall(f"window {n}\n".encode()); buf = b""
|
||||
while b"\nend\n" not in buf and b"\nend" not in (buf[-6:] if len(buf)>=6 else buf):
|
||||
d = s.recv(65536)
|
||||
if not d: break
|
||||
buf += d
|
||||
if buf.endswith(b"end\n") or buf.endswith(b"end"): break
|
||||
return buf.decode(errors="replace")
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--a", default="compiled", choices=["compiled", "interp"])
|
||||
ap.add_argument("--b", default="interp", choices=["compiled", "interp"])
|
||||
ap.add_argument("--stride", type=int, default=65536)
|
||||
ap.add_argument("--max", type=int, default=1_500_000_000) # ~2650 frames
|
||||
ap.add_argument("--porta", type=int, default=4600)
|
||||
ap.add_argument("--portb", type=int, default=4601)
|
||||
ap.add_argument("--inject-at", type=int, default=0)
|
||||
ap.add_argument("--inject", default="") # e.g. ram:100000:1 or reg:2:1
|
||||
ap.add_argument("--start-cycle", type=int, default=0,
|
||||
help="free-run to this absolute guest cycle before checkpointing")
|
||||
ap.add_argument("--cpudiff-at-cp", type=int, default=0,
|
||||
help="step both to this checkpoint and field-diff the CPU dump")
|
||||
args = ap.parse_args()
|
||||
|
||||
if args.cpudiff_at_cp:
|
||||
print(f"launch A={args.a}:{args.porta} B={args.b}:{args.portb} "
|
||||
f"stride={args.stride} start={args.start_cycle} cpudiff={args.cpudiff_at_cp}",
|
||||
flush=True)
|
||||
pa = launch(args.a, args.porta, args.stride, args.start_cycle); pb = launch(args.b, args.portb, args.stride, args.start_cycle)
|
||||
try:
|
||||
sa = connect(args.porta); sb = connect(args.portb)
|
||||
wait_parked(sa, sb)
|
||||
n = args.cpudiff_at_cp - 1 # both start parked at cp 1
|
||||
for _ in range(max(0, n)):
|
||||
cmd(sa, "step 1", timeout=1200)
|
||||
cmd(sb, "step 1", timeout=1200)
|
||||
da = parse_cpu(cmd(sa, "cpu"))
|
||||
db = parse_cpu(cmd(sb, "cpu"))
|
||||
print(f"CPU field-diff at cp {args.cpudiff_at_cp} (A={args.a} B={args.b}):", flush=True)
|
||||
diffs = [k for k in da if da.get(k) != db.get(k)]
|
||||
if not diffs:
|
||||
print(" (no CPU field differs)", flush=True)
|
||||
for k in diffs:
|
||||
print(f" cpu.{k}: A={da.get(k)} B={db.get(k)}", flush=True)
|
||||
ga = parse_cpu(cmd(sa, "gte"))
|
||||
gb = parse_cpu(cmd(sb, "gte"))
|
||||
print("GTE field-diff:", flush=True)
|
||||
gdiffs = [k for k in ga if ga.get(k) != gb.get(k)]
|
||||
if not gdiffs:
|
||||
print(" (no GTE field differs)", flush=True)
|
||||
for k in gdiffs:
|
||||
print(f" gte.{k}: A={ga.get(k)} B={gb.get(k)}", flush=True)
|
||||
# device-timing fields (behind irqctl/gpu sub-hashes)
|
||||
va = parse_cpu(cmd(sa, "dev")); vb = parse_cpu(cmd(sb, "dev"))
|
||||
print("DEV field-diff (irqctl/gpu timing):", flush=True)
|
||||
ddiffs = [k for k in va if va.get(k) != vb.get(k)]
|
||||
if not ddiffs:
|
||||
print(" (no dev field differs)", flush=True)
|
||||
for k in ddiffs:
|
||||
print(f" dev.{k}: A={va.get(k)} B={vb.get(k)}", flush=True)
|
||||
xa = parse_cpu(cmd(sa, "gpu")); xb = parse_cpu(cmd(sb, "gpu"))
|
||||
print("GPU field-diff:", flush=True)
|
||||
xdiffs = [k for k in xa if xa.get(k) != xb.get(k)]
|
||||
if not xdiffs:
|
||||
print(" (no GPU field differs)", flush=True)
|
||||
for k in xdiffs:
|
||||
print(f" gpu.{k}: A={xa.get(k)} B={xb.get(k)}", flush=True)
|
||||
print("IRQ trace A:", flush=True)
|
||||
print(" " + cmd(sa, "irqtrace"), flush=True)
|
||||
print("IRQ trace B:", flush=True)
|
||||
print(" " + cmd(sb, "irqtrace"), flush=True)
|
||||
finally:
|
||||
for p in (pa, pb):
|
||||
try:
|
||||
if p.poll() is None: p.terminate()
|
||||
except Exception: pass
|
||||
close_proc_log(p)
|
||||
return
|
||||
|
||||
print(f"launch A={args.a}:{args.porta} B={args.b}:{args.portb} stride={args.stride} start={args.start_cycle}", flush=True)
|
||||
pa = launch(args.a, args.porta, args.stride, args.start_cycle); pb = launch(args.b, args.portb, args.stride, args.start_cycle)
|
||||
try:
|
||||
sa = connect(args.porta); sb = connect(args.portb)
|
||||
ia, ib = wait_parked(sa, sb)
|
||||
if args.max <= args.start_cycle:
|
||||
print(f"--max must be greater than --start-cycle ({args.start_cycle})", flush=True)
|
||||
return
|
||||
max_cp = (args.max - args.start_cycle) // args.stride
|
||||
print(f"both up; stepping {max_cp} checkpoints (stride {args.stride})", flush=True)
|
||||
print(f"initial park A cycle {ia.get('cycle')} cp {ia.get('cp')} B cycle {ib.get('cycle')} cp {ib.get('cp')}", flush=True)
|
||||
|
||||
# Both guests are parked at checkpoint 1 immediately after launch (deterministic
|
||||
# cycle boundary). We advance BOTH one checkpoint per iteration and compare the
|
||||
# cumulative chain hash. First mismatch = first divergence.
|
||||
injected = not args.inject
|
||||
cp = 0
|
||||
while cp < max_cp:
|
||||
# Gate-4 injection into B just before the checkpoint that crosses inject_at.
|
||||
if not injected and args.start_cycle + (cp + 1) * args.stride >= args.inject_at:
|
||||
kind, a, b = args.inject.split(":")
|
||||
print(cmd(sb, f"inject {kind} {int(a)} {int(b)}"), flush=True)
|
||||
print(f"[inject] {args.inject} into B before cp {cp+1}", flush=True)
|
||||
injected = True
|
||||
|
||||
try:
|
||||
ra = kv(cmd(sa, "step 1"))
|
||||
except Exception as e:
|
||||
report_child_failure("A", pa, cp, e)
|
||||
return
|
||||
try:
|
||||
rb = kv(cmd(sb, "step 1"))
|
||||
except Exception as e:
|
||||
report_child_failure("B", pb, cp, e)
|
||||
return
|
||||
if pa.poll() is not None or pb.poll() is not None:
|
||||
print(f"[exit] a={pa.poll()} b={pb.poll()} at cp {cp}", flush=True); break
|
||||
ca, cb = ra.get("chain"), rb.get("chain")
|
||||
if ca is None or cb is None:
|
||||
print(f"[FATAL] could not parse chain — tool is BLIND, aborting.\n"
|
||||
f" A: {ra}\n B: {rb}", flush=True); return
|
||||
cyc_a, cyc_b = ra.get("cycle"), rb.get("cycle")
|
||||
if cyc_a != cyc_b:
|
||||
print(f"[WARN] cycle skew A={cyc_a} B={cyc_b} at cp {ra.get('cp')} — "
|
||||
f"the two runs are NOT parking at the same cycle (harness "
|
||||
f"nondeterminism, not a guest divergence). Investigate before trusting.",
|
||||
flush=True)
|
||||
if ca != cb:
|
||||
print(f"\n*** FIRST DIVERGENCE at checkpoint cp={ra.get('cp')} "
|
||||
f"cycle~{cyc_a} (frame~{int(cyc_a)//566000}) ***", flush=True)
|
||||
print(f" A chain={ca} B chain={cb}", flush=True)
|
||||
print(" --- A subhash ---\n " + cmd(sa, "sub"), flush=True)
|
||||
print(" --- B subhash ---\n " + cmd(sb, "sub"), flush=True)
|
||||
print(" (the FIRST subsystem hash that differs is where it split)", flush=True)
|
||||
print(" --- A window ---\n" + dump_window(sa, 16), flush=True)
|
||||
print(" --- B window ---\n" + dump_window(sb, 16), flush=True)
|
||||
return
|
||||
cp += 1
|
||||
if cp % 256 == 0:
|
||||
print(f" ok cp {cp} cycle {cyc_a} (frame~{int(cyc_a)//566000}) chain {ca}", flush=True)
|
||||
|
||||
print("no divergence within --max (or a process exited).", flush=True)
|
||||
if args.inject:
|
||||
print("NOTE: injection run — a clean 'no divergence' means the tool MISSED the "
|
||||
"fault (gate-4 FAIL); it should have stopped at the injected checkpoint.", flush=True)
|
||||
finally:
|
||||
for p in (pa, pb):
|
||||
try:
|
||||
if p.poll() is None: p.terminate()
|
||||
except Exception:
|
||||
pass
|
||||
close_proc_log(p)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user