Both fixes come from the first real run against a live pair.
A column where one side records thousands of writes and the other records ZERO
sailed through the global coverage check, which only compares total write
counts. It then reported a confident "FIRST DIVERGENCE" for scratchpad, where
the truth was that the oracle had never observed a single scratchpad write
(0 against psx-runtime's 719264). That is a coverage hole, and calling it a
divergence would send someone looking for a guest fault that does not exist.
Each column now checks its own counts and says NOT COMPARABLE instead.
Divergence on the very first frame of the window is not a finding either: it
means the fork happened BEFORE the window, and the frame named is just wherever
the window happened to start. It now says so and suggests widening.
The global ratio band was 0.5x..2.0x, and a live pair sat at 1.98x -- the
oracle's CPU-store hook does not see DMA-written RAM -- squeaking through as
"comparable". Tightened to 0.67x..1.5x.
Working, verified live: the oracle records fingerprints in interpreter mode,
and the guest cycle clocks agree to 0.31% drift across a 150-frame window,
which is what makes cycle-based alignment a sharp test rather than a guess.
Still open: the oracle sees no scratchpad writes at all, and its main-RAM write
count runs well under psx-runtime's. Until that coverage gap closes the tool
will keep refusing to name a divergence frame, which is the correct behaviour.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three things stood between the fingerprint and being usable, all found by
running it against a live pair rather than by reading it.
DebugConn.frame() asked `ping` for the frame number. psx-runtime intercepts
ping in a lock-free fast path that never reaches the command dispatcher, so its
reply carries only {ok, pong, io_thread} -- no frame. Every native run
therefore reported frame 0, silently, and anything aligning two emulators by
frame number was comparing against zero. This is the "native frame 0" that has
been showing up in gpu_parity output all along. The oracle DOES put a frame in
its ping, which is what made it look like an oracle quirk rather than a
native-side blind spot. frame() now asks the `frame` command, which both
answer, and falls back to ping.
frame_fingerprint replies are now paged (64 frames per call). A whole ring in
one request is over a megabyte, and the oracle's socket truncates anything past
~16 KB, after which the read blocks forever waiting for a newline that never
comes. Paging here is not an optimisation; it is the difference between working
and hanging.
Added an alignment gate ahead of the coverage gate. A frame number means
"frames since THIS emulator booted", so overlapping frame numbers prove
nothing: start one thirty seconds after the other and both report a frame 500
that are half a minute apart. A diff across that names a divergence at the
first frame compared, every time, and it looks exactly like a real finding.
The gate compares guest cycle counts and refuses when they disagree. Measured
on a live pair, the two agree on cycles-per-frame to 0.3% (564480 vs 566204),
so this is a sharp test, and it correctly refused a mismatched pair on its
first live run.
The oracle now seeds ExecutionMode = Interpreter. The store hook lives in the
interpreter's write path; a recompiler oracle inlines stores, never reaches it,
and would compare clean against anything. A false "no divergence" is worse than
a slow reference.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Gives the DuckStation oracle the same per-frame write fingerprint psxrecomp
already had, so the two can be compared by "which frame did execution fork"
rather than by pictures. Comparing pictures needs the two aligned to the same
instant, which nothing short of deterministic replay achieves; the fingerprint
is cumulative, so frame N's hash answers whether the whole write history up to
frame N matched, and phase does not enter into it.
Ported byte for byte from debug_server.c -- same FNV-1a constants, same JSON
field names, same decision to keep main RAM, store-PC, device registers, and
scratchpad in SEPARATE columns. Folding device churn into the RAM hash is what
hid a real first divergence before: the RAM hash sees only the aftermath of a
device-interaction fork, so it points frames too late.
Two traps are closed rather than left for the reader:
* The store hook lives in the interpreter's write path (hooked in
WriteMemoryByte/HalfWord/Word so swl/swr/sc are covered, not at the
sb/sh/sw cases). A recompiler build inlines stores, never reaches it, and
would record almost nothing -- so the reply carries "interpreter" and
fingerprint_diff.py refuses to report on a build that is not in that mode.
* Values are truncated to the width actually written before hashing, matching
what memory.c hands fp_record_write. Hashing the untruncated register would
make every byte store disagree and every frame "diverge" for no guest
reason.
fingerprint_diff.py compares write COUNTS before it compares hashes and refuses
to name a divergence frame when they do not track. The oracle's hook does not
see DMA-written RAM; a mismatch from that is indistinguishable from a real fork,
and a confident wrong answer would send you hunting one that is not there.
Unverified: hash equality between the two has not been demonstrated on a live
pair yet -- the oracle needs a rebuild first.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>