Files
Shokunin 255d05e8e4 debug_server: judge frame_fingerprint on guest-visible facts
frame_fingerprint's wr column is an ordered hash over main-RAM writes.
DMA and device writes (MDEC-out, CD, SPU, GPU->RAM) are recorded in host
device-service order. Native shards flush cycles at every store barrier,
while batched interpreted and static code services devices after the
block. So identical guest writes gave a different wr forever after.

- ws: an order-independent sum of a splitmix64 term per (addr, val) over
  main RAM. Together with wc it is the main-RAM judge column. wr and pc
  stay as ordered locators.
- qc: fingerprint-eligible writes that FMV-quiet kept out. If a straddled
  write falls into a quiet frame in one run only, the cumulative columns
  never agree again, and qc is where that shows.
- The accumulator moves to runtime/include/frame_fingerprint.h so the
  semantics are unit-tested (frame_fingerprint_test).
- The JSON buffer is sized from the real maximum entry length (284 B),
  which the old 224 B per entry could overflow.

docs/TCP_COMMANDS.md documents the column roles, the one-frame snapshot
straddle and the FMV-quiet caveat (set PSX_DEBUG_FMV_QUIET=0 for A/B
identity runs). Existing columns keep their meaning. Nothing outside
debug-tools builds changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 18:30:24 -07:00

114 lines
4.7 KiB
C

#ifndef FRAME_FINGERPRINT_H
#define FRAME_FINGERPRINT_H
/* Per-frame guest-write fingerprint accumulator behind the debug server's
* `frame_fingerprint` command (debug_server.c snapshots it once per frame).
* Header-only so the hash semantics are unit-tested without the server
* (runtime/tests/test_frame_fingerprint.c).
*
* Two kinds of column, because an A/B run needs both:
*
* JUDGE columns say whether two runs did the same guest-visible things:
* wcount + wsum main RAM: count and ORDER-INDEPENDENT sum of (phys,val)
* sp_*, mmio_* scratchpad / device registers: CPU-only traffic, so the
* ordered hash is program order (includes the store PC)
* quiet_count writes FMV-quiet kept out of every column
*
* LOCATOR columns help find WHERE two runs parted, but can differ while
* guest state is identical:
* wr_hash, pc_hash ORDERED rolling hashes over main RAM. DMA/device
* writes (MDEC-out, CD, SPU, GPU->RAM) land in host
* device-service order, which depends on how often a
* backend flushes cycles, so the same multiset of writes
* can arrive interleaved differently with CPU stores.
* A device write also carries the last CPU store PC.
*/
#include <stdint.h>
#define PSX_FP_FNV_SEED 1469598103934665603ULL
#define PSX_FP_FNV_PRIME 1099511628211ULL
#define PSX_FP_SCRATCH_LO 0x1F800000u
#define PSX_FP_SCRATCH_HI 0x1F8003FFu
typedef struct PsxFrameFingerprint {
uint64_t wr_hash; /* locator: ordered (phys,val), main RAM */
uint64_t pc_hash; /* locator: ordered store PC, main RAM */
uint64_t wcount; /* judge: main-RAM write count */
uint64_t wsum; /* judge: order-independent sum over main RAM */
uint64_t mmio_hash; /* judge: ordered (addr,val,pc), device registers */
uint64_t mmio_count;
uint64_t sp_hash; /* judge: ordered (phys,val,pc), scratchpad */
uint64_t sp_count;
uint64_t quiet_count; /* judge: fingerprint-eligible writes FMV-quiet skipped */
} PsxFrameFingerprint;
#define PSX_FRAME_FINGERPRINT_INIT \
{ PSX_FP_FNV_SEED, PSX_FP_FNV_SEED, 0, 0, PSX_FP_FNV_SEED, 0, \
PSX_FP_FNV_SEED, 0, 0 }
/* One write's contribution to wsum: the splitmix64 finalizer of (phys,val).
* It is a bijection on the 64-bit key, so distinct (phys,val) pairs never
* share a term, and a value moved between two addresses changes the sum.
* Summing (mod 2^64) makes the column blind to order only. */
static inline uint64_t psx_fp_write_term(uint32_t phys, uint32_t val)
{
uint64_t z = (((uint64_t)phys << 32) | val) + 0x9E3779B97F4A7C15ULL;
z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ULL;
z = (z ^ (z >> 27)) * 0x94D049BB133111EBULL;
return z ^ (z >> 31);
}
/* Main RAM (phys < ram_live_bytes) and scratchpad feed the fingerprint;
* anything else a RAM write path reports is ignored. */
static inline int psx_fp_write_eligible(uint32_t phys, uint32_t ram_live_bytes)
{
return phys < ram_live_bytes ||
(phys >= PSX_FP_SCRATCH_LO && phys <= PSX_FP_SCRATCH_HI);
}
static inline void psx_fp_record_write(PsxFrameFingerprint *fp, uint32_t phys,
uint32_t val, uint32_t pc,
uint32_t ram_live_bytes)
{
if (phys >= PSX_FP_SCRATCH_LO && phys <= PSX_FP_SCRATCH_HI) {
uint64_t s = fp->sp_hash;
s = (s ^ (uint64_t)phys) * PSX_FP_FNV_PRIME;
s = (s ^ (uint64_t)val) * PSX_FP_FNV_PRIME;
s = (s ^ (uint64_t)pc) * PSX_FP_FNV_PRIME;
fp->sp_hash = s;
fp->sp_count++;
return;
}
if (phys >= ram_live_bytes) return;
uint64_t h = fp->wr_hash;
h = (h ^ (uint64_t)phys) * PSX_FP_FNV_PRIME;
h = (h ^ (uint64_t)val) * PSX_FP_FNV_PRIME;
fp->wr_hash = h;
fp->pc_hash = (fp->pc_hash ^ (uint64_t)pc) * PSX_FP_FNV_PRIME;
fp->wcount++;
fp->wsum += psx_fp_write_term(phys, val);
}
static inline void psx_fp_record_mmio(PsxFrameFingerprint *fp, uint32_t addr,
uint32_t val, uint32_t pc)
{
uint64_t h = fp->mmio_hash;
h = (h ^ (uint64_t)addr) * PSX_FP_FNV_PRIME;
h = (h ^ (uint64_t)val) * PSX_FP_FNV_PRIME;
h = (h ^ (uint64_t)pc) * PSX_FP_FNV_PRIME;
fp->mmio_hash = h;
fp->mmio_count++;
}
/* A write FMV-quiet kept out of the fingerprint. Counting it makes the
* suppression visible: two runs whose quiet_count differs dropped different
* writes, so their cumulative columns stop being comparable from there on. */
static inline void psx_fp_note_quiet(PsxFrameFingerprint *fp)
{
fp->quiet_count++;
}
#endif /* FRAME_FINGERPRINT_H */