Files
ARMSX2/pcsx2/ee_divtrace.h
Brian DegenhardtandClaude 5458d12ca1 eerunner: VU0-aware fingerprints, divergence census, frame-local RAM diff
Three stepdiff triage upgrades built during the Jak 3 artifact hunt:

- EERUNNER_VU0FP=1 mixes VU0 macro-visible state (VF00-31, ACC, the
  STATUS/MAC/CLIP flag VIs) into the divtrace fingerprint and captures it
  in FullSnap/DiffFullSnaps, so the zoom breaks at the EE block whose COP2
  macro emission first produced divergent VU0 state instead of only when
  it later lands in a GPR or memory (the default fingerprint surfaced the
  Jak 3 fMax vertex flood at an unrelated memcpy). VF is memory-resident
  at every block boundary per the COP2 VF-cache seam policy, so the JIT
  block-prologue sample site reads consistent state. Opt-in: the benign
  filters were tuned without VU0 fields, and mVU-vs-interp has by-design
  NaN/clamp corners.

- EERUNNER_CENSUS=1 stops the zoom hard-stopping at the first non-benign
  divergence: each data/control site is reported compactly, ResyncAfter
  walks past TRANSIENT ones (DMA-phase staging buffers, poll-phase
  control-flow splits that reconverge), and the walk stops at the first
  PERSISTENT site. Persistent control-flow sites also dump the JIT-side
  guest regs at the split entry (busy-poll operands), the polled word when
  it resolves to RAM, and both sides' frame-end SPR channel registers.

- The stepdiff candidate-real branch now re-runs the frame once per mode
  from the same checkpoint and byte-diffs EE RAM+scratch (ReportMemDiff,
  max_show now a parameter): with a clean interp control every differing
  byte is a real JIT store difference, at frame-local granularity.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-19 21:02:39 -07:00

149 lines
6.9 KiB
C++

// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
// ee_divtrace — EE (R5900) JIT-vs-interpreter divergence trace.
//
// Records a stream of architectural fingerprints from a deterministic
// full-system run so that an interpreter "golden" run and a JIT run, replayed
// from the same savestate, can be diffed OFFLINE (never live / in lockstep).
// The shared clock cpuRegs.cycle plus cpuRegs.pc is the alignment key between the two runs.
//
// Three-level funnel, all bounded in memory (pcsx2-eerunner drives it):
// 1. per-FRAME — runner hashes FingerprintCpu()+HashMemory() once per frame
// over the whole run; cheap; localizes the divergent frame.
// 2. per-OP — within the one divergent frame, the capture sites append a
// Sample (cycle, pc, fp) per executed op (interp, dense) and
// per block dispatch (JIT, sparse). Aligned by (cycle, pc).
// 3. full-snap — a small window (ConfigureFullWindow) around the first
// divergent op stores whole cpuRegisters+fpuRegisters for the
// register-level report.
//
// Capture sites:
// pcsx2/Interpreter.cpp — interp, after execI() (dense)
// pcsx2/arm64/iR5900-arm64.cpp — JIT, C-hook at the block dispatcher (sparse)
//
// fp hashes EXACTLY the fields DiffEe (harness/StateSnapshot.cpp) compares —
// GPR (both doublewords), HI/LO, CP0 except {1 Random, 9 Count, 11 Compare},
// FPR, FPU ACC, pc, sa — so a fingerprint mismatch is a genuine architectural
// divergence, not dispatcher-bookkeeping noise.
#include "common/Pcsx2Defs.h"
#include "R5900.h"
#include <atomic>
#include <vector>
namespace ee_divtrace
{
// One architectural sample, ~24 bytes.
struct Sample
{
u64 cycle; // cpuRegs.cycle at the sample
u64 fp; // FingerprintCpu()
u32 pc; // cpuRegs.pc at the sample (next instr / block entry)
u32 _pad;
};
// Full architectural snapshot — stored only inside the detail window.
// vu0_vf holds VF00..VF31 then ACC (33 quads); the three flag VIs follow.
// Captured unconditionally (cheap memcpy); only the FINGERPRINT inclusion
// is gated on g_vu0_include so alignment behavior is opt-in.
struct FullSnap
{
cpuRegisters cpu;
fpuRegisters fpu;
alignas(16) u8 vu0_vf[33 * 16];
u32 vu0_vi_status;
u32 vu0_vi_mac;
u32 vu0_vi_clip;
u32 _pad0;
u64 cycle;
u32 pc;
u32 _pad;
};
// Gates the per-op/per-block capture sites. Read on the hot path, so the
// site is `if (g_enabled.load(relaxed)) RecordSample();`. The runner sets
// it true only for the single frame being finely traced.
extern std::atomic<bool> g_enabled;
// Current pass's sample stream (interp run, then JIT run — separate runs).
extern std::vector<Sample> g_stream;
// Detail-window full snapshots; g_snaps[idx - g_full_lo] for idx in window.
extern std::vector<FullSnap> g_snaps;
extern u32 g_full_lo;
extern u32 g_full_hi;
// When set, recRecompile emits a block-entry hook into every EE block
// prologue (pcsx2-eerunner triage build). Read at codegen time, so it must
// be set before the recompiler is initialized / reset. Default false →
// production recompiles emit nothing. Checked in pcsx2/arm64/iR5900-arm64.cpp.
extern bool g_emit_block_hook;
// When set, FingerprintCpu() omits the FPU register file + ACC from the hash,
// so FP-register divergences do not break the alignment walk. Used to hunt a
// NON-FP (integer / control-flow) EE-jit divergence when the FP path is known
// benign (e.g. Burnout 3: the hang persists with the EE-FPU fully converged to
// the interp, so the cond_b off-by-0xC0 is an integer/pointer bug, and the
// pervasive 1-ULP div.s noise just masks it). The eerunner sets this from
// EERUNNER_NOFP; the Main.cpp diff helpers honor it too for consistent reports.
extern bool g_fp_exclude;
// When set, FingerprintCpu() ALSO mixes VU0 macro-visible state (VF00-31,
// ACC, and the STATUS/MAC/CLIP flag VIs) into the hash, so the alignment
// walk breaks at the first EE block whose COP2 macro emission produced
// divergent VU0 state — instead of only when that state later lands in a
// GPR or memory (the Jak 3 blindness: the funnel surfaced a downstream
// memcpy of fMax-flooded vertex data, not the VU0-macro producer). VF is
// memory-resident at every block boundary (the COP2 VF cache seam policy
// flushes at block tails), so the JIT block-prologue sample site reads
// consistent state. Off by default: the benign-divergence filters were
// tuned without VU0 fields, and mVU-vs-interp has its own by-design NaN /
// clamp corners that could add noise classes. eerunner sets this from
// EERUNNER_VU0FP.
extern bool g_vu0_include;
// When set, the interpreter's SYSCALL handler skips FlushCache (0x64) /
// iFlushCache (0x68) — returning without raising the syscall exception —
// exactly as the JIT's recSYSCALL does when $v1 is a known const. This keeps
// the golden interp timeline in lockstep with the JIT across that ABI-benign
// divergence (the JIT skip leaves caller-saved v0/v1/at/t0/t1 + EPC and the
// BIOS exception-frame memory untouched; running the real handler in interp
// only would desync both register state AND the kernel stack, masking real
// codegen bugs downstream). Set by pcsx2-eerunner; default false → production
// interp runs the real handler. Read in pcsx2/R5900OpcodeImpl.cpp SYSCALL().
extern bool g_skip_flushcache_syscall;
// Hash live cpuRegs+fpuRegs over exactly DiffEe's compared field set.
u64 FingerprintCpu();
// Append one Sample (cycle, pc, fp) from live cpuRegs, plus a FullSnap when
// the new index is in the detail window. `pc` is passed explicitly because
// the JIT block hook knows its startpc at compile time but cpuRegs.pc is
// not reliable on a statically-linked entry. The caller (JIT site) must
// flush its pinned cycle register to cpuRegs.cycle first.
void RecordSample(u32 pc);
// xxh3 of EE main RAM (32 MB) + scratchpad (16 KB) — frame memory fingerprint.
u64 HashMemory();
// Clear g_stream; zero the used portion of the detail window.
void Reset();
// Set the detail window to [lo, lo+len); (re)allocates g_snaps to `len`.
void ConfigureFullWindow(u32 lo, u32 len);
// Reserve g_stream capacity (samples) up front so the fine pass doesn't
// reallocate mid-frame. Called by the runner before a traced frame.
void ReserveStream(size_t samples);
} // namespace ee_divtrace
// C-callable JIT block-entry hook (plain symbol for armEmitCall). Emitted into
// every EE block prologue when ee_divtrace::g_emit_block_hook is set. Checks
// g_enabled and records a sample for block `startpc`. cpuRegs.cycle must
// already have been flushed from the pinned cycle register by the caller.
extern "C" void ee_divtrace_jit_block_hook(u32 startpc);