mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
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>
This commit is contained in:
+57
-2
@@ -181,6 +181,61 @@ re-dispatches the guest's true target. Counters in
|
||||
|
||||
---
|
||||
|
||||
## `frame_fingerprint` — per-frame guest-write fingerprint (native only)
|
||||
|
||||
Cumulative write hashes, snapshotted at every VBlank into a 32768-frame ring
|
||||
(`runtime/include/frame_fingerprint.h`). Diff two runs of the same seeded
|
||||
input, such as native overlay shards against the interpreter or two builds, to
|
||||
find the first frame where guest behaviour forks. Then arm
|
||||
`record_frame` on that frame in both runs and compare the two ordered logs.
|
||||
|
||||
- `{"cmd":"frame_fingerprint","count":1024,"frame_lo":N,"frame_hi":M}`: all
|
||||
parameters are optional. Entries come back oldest first.
|
||||
|
||||
| Column | Role | Covers |
|
||||
|---|---|---|
|
||||
| `cyc` | judge | guest cycle counter at the snapshot |
|
||||
| `wc`, `ws` | judge | main-RAM write count, and an order-independent sum over `(addr, value)` |
|
||||
| `mmio`, `mc` | judge | device-register writes: ordered hash over `(addr, value, store PC)`, and a count |
|
||||
| `sp`, `sc` | judge | scratchpad writes: ordered hash over `(addr, value, store PC)`, and a count |
|
||||
| `qc` | judge | writes that FMV-quiet kept out of every other column |
|
||||
| `wr`, `pc` | locator | main-RAM writes: ordered hash over `(addr, value)`, and an ordered hash over store PCs |
|
||||
|
||||
**Judge on the judge columns.** Two runs that behaved the same agree on all of
|
||||
them at every frame. `wr` and `pc` can differ even when guest state is
|
||||
identical. DMA and device writes to RAM (MDEC-out, CD, SPU, GPU→RAM) are
|
||||
recorded in the order the host services the device. A native shard flushes
|
||||
cycles at every store barrier, but batched interpreted and static code
|
||||
services the device after the block. As a result, the same writes can
|
||||
interleave differently with CPU stores. A device write also takes whatever
|
||||
CPU store PC came last. Use `wr` and `pc` only to narrow down a fork once a
|
||||
judge column has found it.
|
||||
|
||||
**Store PCs are exact on every backend.** Static code and the interpreter set
|
||||
`g_debug_last_store_pc` themselves. Native overlay shards write the runtime's
|
||||
copy through the ABI v24 `last_store_pc` pointer. Before v24, shards kept a
|
||||
private copy, so `pc`, `mmio` and `sp` named an older store for every overlay
|
||||
store.
|
||||
|
||||
**One-frame straddles are not divergences.** Batched code services devices up
|
||||
to a basic block late. So a device write due at, for example, VBlank + 1 cycle
|
||||
can land on the other side of the snapshot in one run. `ws` and `wc` then
|
||||
differ for that single frame, with `wc` off by the number of straddled writes,
|
||||
and agree again at the next one. In a 12000-frame R4 A/B of native shards
|
||||
against the interpreter, 497 frames straddled by exactly one write, and every
|
||||
one re-converged on the next frame.
|
||||
|
||||
**Turn FMV-quiet off for identity runs.** `PSX_DEBUG_FMV_QUIET` is on unless it
|
||||
is set to `0`. While the MDEC has decoded recently, it stops write recording,
|
||||
and those frames' writes add only to `qc`. If a straddled write falls into a
|
||||
quiet frame in one run and not the other, `ws`, `wc` and `qc` never agree
|
||||
again. `wc + qc` still agrees, which tells this case apart from a real fork.
|
||||
For A/B identity, set `PSX_DEBUG_FMV_QUIET=0` in both runs.
|
||||
|
||||
`ws` is a multiset sum. It cannot see two writes to one address arriving in
|
||||
the opposite order, even though the final RAM differs. Such a fork shows up in
|
||||
later writes, `cyc`, or `read_ram`.
|
||||
|
||||
## `bios_info` — linked recompiled-BIOS identity (native only)
|
||||
|
||||
Reports which BIOS image this build's recompiled C was generated from
|
||||
@@ -338,7 +393,7 @@ The TCP server is the canonical instrumentation surface. Rule 3 in `CLAUDE.md` i
|
||||
|
||||
**316 commands registered** — 303 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`).
|
||||
|
||||
54 of 316 have prose above; **262 are index-only**. An index-only command still works — it just has no description here yet. Send it `{"cmd":"<name>"}` and read the reply, or find its `handle_*` function in the server source.
|
||||
55 of 316 have prose above; **261 are index-only**. An index-only command still works — it just has no description here yet. Send it `{"cmd":"<name>"}` and read the reply, or find its `handle_*` function in the server source.
|
||||
|
||||
Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this block has drifted from the code.
|
||||
|
||||
@@ -451,7 +506,7 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc
|
||||
| `fntrace_reset` | | ✓ | |
|
||||
| `fntrace_unfiltered` | | ✓ | |
|
||||
| `frame` | ✓ | | ✓ |
|
||||
| `frame_fingerprint` | ✓ | | |
|
||||
| `frame_fingerprint` | ✓ | | ✓ |
|
||||
| `frame_perf` | ✓ | | |
|
||||
| `frame_range` | ✓ | ✓ | ✓ |
|
||||
| `frame_timeseries` | ✓ | ✓ | ✓ |
|
||||
|
||||
@@ -710,6 +710,11 @@ if(BUILD_TESTING)
|
||||
add_test(NAME overlay_store_pc_forwarding_test
|
||||
COMMAND overlay_store_pc_forwarding_test)
|
||||
|
||||
# frame_fingerprint judge columns are blind to device-write service order.
|
||||
add_executable(frame_fingerprint_test tests/test_frame_fingerprint.c)
|
||||
target_include_directories(frame_fingerprint_test PRIVATE include)
|
||||
add_test(NAME frame_fingerprint_test COMMAND frame_fingerprint_test)
|
||||
|
||||
add_executable(psx_cycle_event_boundaries_test
|
||||
tests/test_psx_cycle_event_boundaries.c
|
||||
src/psx_cycles.c)
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
#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 */
|
||||
+40
-73
@@ -49,6 +49,7 @@
|
||||
#include "gpu_gl_renderer.h"
|
||||
#include "lockstep.h"
|
||||
#include "guest_tty.h"
|
||||
#include "frame_fingerprint.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -153,58 +154,26 @@ int debug_server_fmv_quiet(void)
|
||||
uint64_t s_frame_count = 0;
|
||||
|
||||
/* ---- Layer-1 first-divergence per-frame fingerprint ----------------------
|
||||
* Cumulative, ORDER-DEPENDENT rolling hashes over MAIN-RAM guest writes.
|
||||
* The point: native and dirty-interp are the SAME deterministic program run
|
||||
* two ways; they must produce an identical sequence of (addr,val,store_pc)
|
||||
* guest-RAM writes until a codegen/timing bug forks them. A guest RAM write
|
||||
* (and the PC issuing it) is pure guest semantics — granularity-invariant
|
||||
* across native vs interp (unlike host block/dispatch counts). We accumulate
|
||||
* two rolling hashes and snapshot them once per guest frame. Diff two runs'
|
||||
* per-frame columns: the FIRST frame whose hash differs is the first-divergence
|
||||
* frame, found in O(1) instead of O(n) function guesses. wr_hash vs pc_hash
|
||||
* classifies the fork: pc differs but wr matches => same writes via a different
|
||||
* control path; wr differs => actual state divergence. Reusable for any title.
|
||||
* Hashed over live main RAM (phys < live RAM end) only — game state lives there; MMIO/
|
||||
* scratchpad churn (device polling) would add benign cross-backend noise. */
|
||||
uint64_t g_fp_wr_hash = 1469598103934665603ULL; /* FNV-1a-style seed (main RAM) */
|
||||
uint64_t g_fp_pc_hash = 1469598103934665603ULL; /* store-PC path sig (main RAM) */
|
||||
uint64_t g_fp_write_count = 0;
|
||||
uint64_t g_fp_mmio_hash = 1469598103934665603ULL; /* SEPARATE: device-register writes */
|
||||
uint64_t g_fp_mmio_count = 0;
|
||||
uint64_t g_fp_sp_hash = 1469598103934665603ULL; /* SEPARATE: scratchpad (0x1F8000xx) writes */
|
||||
uint64_t g_fp_sp_count = 0;
|
||||
* Cumulative hashes over guest writes, snapshotted once per guest frame.
|
||||
* Native and dirty-interp are the SAME deterministic program run two ways;
|
||||
* diff two runs' per-frame columns and the FIRST frame that differs is the
|
||||
* first-divergence frame, found in O(1) instead of O(n) function guesses.
|
||||
* Column semantics (which ones judge guest state, which only locate a fork)
|
||||
* live in frame_fingerprint.h and docs/TCP_COMMANDS.md. Reusable for any
|
||||
* title. */
|
||||
static PsxFrameFingerprint s_fp = PSX_FRAME_FINGERPRINT_INIT;
|
||||
#define FP_RING_CAP 32768
|
||||
typedef struct { uint32_t frame; uint64_t wr_hash; uint64_t pc_hash; uint64_t wcount;
|
||||
uint64_t mmio_hash; uint64_t mmio_count;
|
||||
uint64_t sp_hash; uint64_t sp_count; uint64_t cyc; } FpEntry;
|
||||
typedef struct { uint32_t frame; PsxFrameFingerprint fp; uint64_t cyc; } FpEntry;
|
||||
static PSX_BSS FpEntry s_fp_ring[FP_RING_CAP];
|
||||
static uint32_t s_fp_head = 0;
|
||||
static uint64_t s_fp_total = 0;
|
||||
|
||||
/* Record a guest WRITE into the per-frame fingerprint. Live main RAM
|
||||
* feeds the proven wr/pc hashes. Scratchpad (0x1F800000..0x1F8003FF) feeds a
|
||||
* SEPARATE sp_hash — it was previously dropped entirely (the blind spot that
|
||||
* hid a possible pre-1823 scratchpad-state fork), but folding it into wr_hash
|
||||
* would pollute the main-RAM signal with benign device-poll churn, so it gets
|
||||
* its own classified column instead. */
|
||||
/* Record a guest WRITE into the per-frame fingerprint. Live main RAM feeds
|
||||
* the wr/pc/wc/ws columns; scratchpad (0x1F800000..0x1F8003FF) feeds its own
|
||||
* sp column so device-poll churn there cannot pollute the main-RAM signal. */
|
||||
static inline void fp_record_write(uint32_t phys, uint32_t val, uint32_t pc)
|
||||
{
|
||||
if (phys >= 0x1F800000u && phys <= 0x1F8003FFu) { /* scratchpad — separate hash */
|
||||
uint64_t s = g_fp_sp_hash;
|
||||
s = (s ^ (uint64_t)phys) * 1099511628211ULL;
|
||||
s = (s ^ (uint64_t)val) * 1099511628211ULL;
|
||||
s = (s ^ (uint64_t)pc) * 1099511628211ULL;
|
||||
g_fp_sp_hash = s;
|
||||
g_fp_sp_count++;
|
||||
return;
|
||||
}
|
||||
if (phys >= psx_ram_live_bytes()) return; /* main RAM only */
|
||||
uint64_t h = g_fp_wr_hash;
|
||||
h = (h ^ (uint64_t)phys) * 1099511628211ULL;
|
||||
h = (h ^ (uint64_t)val) * 1099511628211ULL;
|
||||
g_fp_wr_hash = h;
|
||||
g_fp_pc_hash = (g_fp_pc_hash ^ (uint64_t)pc) * 1099511628211ULL;
|
||||
g_fp_write_count++;
|
||||
psx_fp_record_write(&s_fp, phys, val, pc, psx_ram_live_bytes());
|
||||
}
|
||||
|
||||
/* MMIO/device-register write signature — separate hash so the diff can tell a
|
||||
@@ -213,25 +182,14 @@ static inline void fp_record_write(uint32_t phys, uint32_t val, uint32_t pc)
|
||||
* only sees the RAM AFTERMATH of a CD/IRQ-register divergence. */
|
||||
static inline void fp_record_mmio(uint32_t addr, uint32_t val, uint32_t pc)
|
||||
{
|
||||
uint64_t h = g_fp_mmio_hash;
|
||||
h = (h ^ (uint64_t)addr) * 1099511628211ULL;
|
||||
h = (h ^ (uint64_t)val) * 1099511628211ULL;
|
||||
h = (h ^ (uint64_t)pc) * 1099511628211ULL;
|
||||
g_fp_mmio_hash = h;
|
||||
g_fp_mmio_count++;
|
||||
psx_fp_record_mmio(&s_fp, addr, val, pc);
|
||||
}
|
||||
|
||||
static void fp_snapshot(uint32_t frame)
|
||||
{
|
||||
FpEntry *e = &s_fp_ring[s_fp_head];
|
||||
e->frame = frame;
|
||||
e->wr_hash = g_fp_wr_hash;
|
||||
e->pc_hash = g_fp_pc_hash;
|
||||
e->wcount = g_fp_write_count;
|
||||
e->mmio_hash = g_fp_mmio_hash;
|
||||
e->mmio_count = g_fp_mmio_count;
|
||||
e->sp_hash = g_fp_sp_hash;
|
||||
e->sp_count = g_fp_sp_count;
|
||||
e->frame = frame;
|
||||
e->fp = s_fp;
|
||||
{ extern uint64_t psx_get_cycle_count(void); e->cyc = psx_get_cycle_count(); }
|
||||
s_fp_head = (s_fp_head + 1) % FP_RING_CAP;
|
||||
s_fp_total++;
|
||||
@@ -5023,10 +4981,13 @@ static void handle_frame(int id, const char *json)
|
||||
|
||||
/* Layer-1 first-divergence: dump the per-frame write fingerprint ring.
|
||||
* Params: count (default 1024), frame_lo / frame_hi (optional inclusive
|
||||
* filter). Entries are oldest-first within the window. Diff the wr/pc columns
|
||||
* of two runs (native vs interp/oracle): the first frame whose wr or pc differs
|
||||
* is the first-divergence frame. Small integer fields only — no large/ragged
|
||||
* filter). Entries are oldest-first within the window. Judge two runs
|
||||
* (native vs interp/oracle) on cyc/wc/ws/mmio/mc/sp/sc/qc; wr/pc are ordered
|
||||
* locators (frame_fingerprint.h). Small integer fields only — no large/ragged
|
||||
* payload, so it never trips the trace-dump JSON/eviction problems. */
|
||||
/* Longest entry the format below can produce: every hash at 16 hex digits and
|
||||
* every counter at 20 decimal digits comes to 284 bytes with the separator. */
|
||||
#define FP_JSON_ENTRY_MAX 320
|
||||
static void handle_frame_fingerprint(int id, const char *json)
|
||||
{
|
||||
int count = json_get_int(json, "count", 1024);
|
||||
@@ -5038,7 +4999,7 @@ static void handle_frame_fingerprint(int id, const char *json)
|
||||
uint32_t avail = (s_fp_total < FP_RING_CAP) ? (uint32_t)s_fp_total : FP_RING_CAP;
|
||||
uint32_t start = (s_fp_total < FP_RING_CAP) ? 0 : s_fp_head;
|
||||
|
||||
size_t BUF = 512 + (size_t)count * 224;
|
||||
size_t BUF = 512 + (size_t)count * FP_JSON_ENTRY_MAX;
|
||||
char *out = (char *)malloc(BUF);
|
||||
if (!out) { send_err(id, "oom"); return; }
|
||||
size_t pos = 0;
|
||||
@@ -5052,16 +5013,19 @@ static void handle_frame_fingerprint(int id, const char *json)
|
||||
if (fhi >= 0 && (int)e->frame > fhi) continue;
|
||||
pos += snprintf(out + pos, BUF - pos,
|
||||
"%s{\"frame\":%u,\"wr\":\"0x%016llx\",\"pc\":\"0x%016llx\",\"wc\":%llu,"
|
||||
"\"ws\":\"0x%016llx\","
|
||||
"\"mmio\":\"0x%016llx\",\"mc\":%llu,"
|
||||
"\"sp\":\"0x%016llx\",\"sc\":%llu,\"cyc\":%llu}",
|
||||
"\"sp\":\"0x%016llx\",\"sc\":%llu,\"qc\":%llu,\"cyc\":%llu}",
|
||||
emitted ? "," : "", e->frame,
|
||||
(unsigned long long)e->wr_hash,
|
||||
(unsigned long long)e->pc_hash,
|
||||
(unsigned long long)e->wcount,
|
||||
(unsigned long long)e->mmio_hash,
|
||||
(unsigned long long)e->mmio_count,
|
||||
(unsigned long long)e->sp_hash,
|
||||
(unsigned long long)e->sp_count,
|
||||
(unsigned long long)e->fp.wr_hash,
|
||||
(unsigned long long)e->fp.pc_hash,
|
||||
(unsigned long long)e->fp.wcount,
|
||||
(unsigned long long)e->fp.wsum,
|
||||
(unsigned long long)e->fp.mmio_hash,
|
||||
(unsigned long long)e->fp.mmio_count,
|
||||
(unsigned long long)e->fp.sp_hash,
|
||||
(unsigned long long)e->fp.sp_count,
|
||||
(unsigned long long)e->fp.quiet_count,
|
||||
(unsigned long long)e->cyc);
|
||||
emitted++;
|
||||
}
|
||||
@@ -9621,7 +9585,10 @@ void debug_server_trace_write_check(uint32_t phys, uint32_t old_val,
|
||||
(void)phys; (void)old_val; (void)new_val; (void)width;
|
||||
return;
|
||||
#endif
|
||||
if (s_fmv_quiet) return;
|
||||
if (s_fmv_quiet) {
|
||||
if (psx_fp_write_eligible(phys, psx_ram_live_bytes())) psx_fp_note_quiet(&s_fp);
|
||||
return;
|
||||
}
|
||||
if (is_card_critical_addr(phys)) card_trace_record(phys, old_val, new_val, width);
|
||||
fp_record_write(phys, new_val, g_debug_last_store_pc);
|
||||
{
|
||||
@@ -9650,7 +9617,7 @@ void debug_server_trace_mmio_write(uint32_t addr, uint32_t val, uint8_t width)
|
||||
(void)addr; (void)val; (void)width;
|
||||
return;
|
||||
#endif
|
||||
if (s_fmv_quiet) return;
|
||||
if (s_fmv_quiet) { psx_fp_note_quiet(&s_fp); return; }
|
||||
/* First-divergence fingerprint + frame recorder also see device writes. */
|
||||
fp_record_mmio(addr, val, g_debug_last_store_pc);
|
||||
rec_event(REC_KIND_MMIO_W, addr, val, g_debug_last_store_pc,
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
/* frame_fingerprint.h: the judge columns (wc/ws, sp, mmio, qc) must agree for
|
||||
* two runs that performed the same guest writes, however a backend interleaved
|
||||
* device writes with CPU stores, and must still tell different writes apart.
|
||||
* The ordered wr column is the locator that is allowed to differ.
|
||||
*
|
||||
* The streams below model the R4 (SLUS-00797) case that motivated ws: a native
|
||||
* overlay shard flushes cycles at every store barrier, so MDEC-out DMA words
|
||||
* land between its CPU stores, while batched interpreted/static code services
|
||||
* the same DMA after the block. */
|
||||
#include <assert.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "frame_fingerprint.h"
|
||||
|
||||
#define LIVE_RAM 0x00200000u
|
||||
|
||||
typedef struct { uint32_t phys, val, pc; } Write;
|
||||
|
||||
static void replay(PsxFrameFingerprint *fp, const Write *w, int n)
|
||||
{
|
||||
for (int i = 0; i < n; i++)
|
||||
psx_fp_record_write(fp, w[i].phys, w[i].val, w[i].pc, LIVE_RAM);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
/* CPU stores A, B; DMA words C, D (DMA writes carry whatever the last CPU
|
||||
* store PC was when the device was serviced). */
|
||||
const Write shard[] = {
|
||||
{ 0x00010000u, 1u, 0x80065800u },
|
||||
{ 0x000F0000u, 3u, 0x80065800u }, /* DMA, serviced at the barrier */
|
||||
{ 0x00010004u, 2u, 0x80065804u },
|
||||
{ 0x000F0004u, 4u, 0x80065804u }, /* DMA */
|
||||
};
|
||||
const Write batched[] = {
|
||||
{ 0x00010000u, 1u, 0x80065800u },
|
||||
{ 0x00010004u, 2u, 0x80065804u },
|
||||
{ 0x000F0000u, 3u, 0x80065804u }, /* DMA, serviced after the block */
|
||||
{ 0x000F0004u, 4u, 0x80065804u },
|
||||
};
|
||||
PsxFrameFingerprint a = PSX_FRAME_FINGERPRINT_INIT;
|
||||
PsxFrameFingerprint b = PSX_FRAME_FINGERPRINT_INIT;
|
||||
replay(&a, shard, 4);
|
||||
replay(&b, batched, 4);
|
||||
assert(a.wcount == 4 && b.wcount == 4);
|
||||
assert(a.wsum == b.wsum); /* judge: same guest writes */
|
||||
assert(a.wr_hash != b.wr_hash); /* locator: service order differs */
|
||||
assert(a.pc_hash != b.pc_hash);
|
||||
|
||||
/* ws still separates different writes: a changed value, and the same two
|
||||
* values swapped between two addresses. */
|
||||
{
|
||||
const Write changed[] = {
|
||||
{ 0x00010000u, 1u, 0 }, { 0x00010004u, 5u, 0 },
|
||||
{ 0x000F0000u, 3u, 0 }, { 0x000F0004u, 4u, 0 },
|
||||
};
|
||||
const Write swapped[] = {
|
||||
{ 0x00010000u, 2u, 0 }, { 0x00010004u, 1u, 0 },
|
||||
{ 0x000F0000u, 3u, 0 }, { 0x000F0004u, 4u, 0 },
|
||||
};
|
||||
PsxFrameFingerprint c = PSX_FRAME_FINGERPRINT_INIT;
|
||||
PsxFrameFingerprint d = PSX_FRAME_FINGERPRINT_INIT;
|
||||
replay(&c, changed, 4);
|
||||
replay(&d, swapped, 4);
|
||||
assert(c.wsum != b.wsum);
|
||||
assert(d.wsum != b.wsum);
|
||||
}
|
||||
|
||||
/* A write that straddles a frame snapshot: one backend services it before
|
||||
* VBlank, the other after. The cumulative ws/wc differ for that one frame
|
||||
* and agree again at the next snapshot. */
|
||||
{
|
||||
PsxFrameFingerprint early = PSX_FRAME_FINGERPRINT_INIT;
|
||||
PsxFrameFingerprint late = PSX_FRAME_FINGERPRINT_INIT;
|
||||
replay(&early, batched, 3); /* frame N: DMA word C in */
|
||||
replay(&late, batched, 2); /* frame N: DMA word C late */
|
||||
PsxFrameFingerprint snap_early = early, snap_late = late;
|
||||
assert(snap_early.wsum != snap_late.wsum);
|
||||
assert(snap_early.wcount == snap_late.wcount + 1);
|
||||
replay(&early, batched + 3, 1); /* frame N+1 */
|
||||
replay(&late, batched + 2, 2);
|
||||
assert(early.wsum == late.wsum && early.wcount == late.wcount);
|
||||
}
|
||||
|
||||
/* FMV-quiet: a suppressed write changes nothing but qc. If the straddled
|
||||
* write above falls into a quiet frame for one run only, ws never agrees
|
||||
* again, and qc is where that shows. */
|
||||
{
|
||||
PsxFrameFingerprint recorded = PSX_FRAME_FINGERPRINT_INIT;
|
||||
PsxFrameFingerprint quiet = PSX_FRAME_FINGERPRINT_INIT;
|
||||
replay(&recorded, batched, 4);
|
||||
replay(&quiet, batched, 2);
|
||||
replay(&quiet, batched + 3, 1);
|
||||
assert(psx_fp_write_eligible(batched[2].phys, LIVE_RAM));
|
||||
psx_fp_note_quiet(&quiet);
|
||||
assert(quiet.quiet_count == 1 && recorded.quiet_count == 0);
|
||||
assert(quiet.wsum != recorded.wsum);
|
||||
assert(quiet.wcount + quiet.quiet_count == recorded.wcount);
|
||||
/* Only writes the fingerprint would have recorded are eligible. */
|
||||
assert(psx_fp_write_eligible(0x1F800010u, LIVE_RAM));
|
||||
assert(!psx_fp_write_eligible(LIVE_RAM, LIVE_RAM));
|
||||
assert(!psx_fp_write_eligible(0x1F801810u, LIVE_RAM));
|
||||
}
|
||||
|
||||
/* Scratchpad and MMIO keep their own ordered columns (program order, so the
|
||||
* store PC is part of the signature); main RAM beyond the live size is
|
||||
* ignored. */
|
||||
{
|
||||
PsxFrameFingerprint fp = PSX_FRAME_FINGERPRINT_INIT;
|
||||
const PsxFrameFingerprint seed = PSX_FRAME_FINGERPRINT_INIT;
|
||||
psx_fp_record_write(&fp, LIVE_RAM, 7u, 0x80010000u, LIVE_RAM);
|
||||
assert(fp.wcount == 0 && fp.wsum == 0 && fp.wr_hash == seed.wr_hash);
|
||||
psx_fp_record_write(&fp, 0x1F800010u, 7u, 0x80010000u, LIVE_RAM);
|
||||
assert(fp.sp_count == 1 && fp.wcount == 0 && fp.wsum == 0);
|
||||
uint64_t sp = fp.sp_hash;
|
||||
PsxFrameFingerprint other_pc = PSX_FRAME_FINGERPRINT_INIT;
|
||||
psx_fp_record_write(&other_pc, 0x1F800010u, 7u, 0x80010004u, LIVE_RAM);
|
||||
assert(other_pc.sp_hash != sp);
|
||||
psx_fp_record_mmio(&fp, 0x1F801070u, 1u, 0x80010008u);
|
||||
assert(fp.mmio_count == 1 && fp.mmio_hash != seed.mmio_hash);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user