mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-27 18:52:11 -04:00
5da57aa478
Extends the two-process parity ring so each row carries, per watch word, the exact store PC + guest cycle + frame + (approx) writing TCB of the most recent write to that word. parity_trace_note_write() maintains the table from BOTH processes' write paths (native: memory.c psx_write_* with g_debug_last_store_pc; Beetle: beetle_libretro.cpp wtrace_callback with the store pc). Both parity_dump serializers emit the new wwpc/wwcy/wwf/wwt arrays (identical wire format), and parity_diff.py prints a side-by-side WATCH PROVENANCE block at the divergence. This turns "the gate-input value diverged" into "who/when produced it" -- the instrument ChatGPT recommended for the MMX6 over-fast wedge. First result (MMX6 cutscene->gameplay, watch 0x80071088/0x800CD3F8/0x8006D9AC/ 0x800CD3FC/0x800CD404/0x800200F4): the cutscene-setup func_8001CB3C (writer pc 0x8001CB7C of countdown 0x800CD3FC) runs ~99.7M guest cycles / ~176 frames EARLIER on native (cyc 998,597,937 frame 1769) than Beetle (cyc 1,098,296,890 frame 1942) -- same writer PC, fired early. On Beetle the state struct 0x800CD3F8 is never touched by game code (stays BIOS-init 0 -> setter path); on native it advances. The over-fast wait is localized one level above the state machine, at whatever schedules func_8001CB3C. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
113 lines
5.5 KiB
C
113 lines
5.5 KiB
C
/* parity_trace.h — general two-process control-flow parity trace.
|
|
*
|
|
* Purpose: end the "somewhere upstream" blindness. A thread/TCB-scoped,
|
|
* freeze-on-trigger ring with an IDENTICAL struct + JSON wire format on BOTH
|
|
* psx-runtime (code under test) and psx-beetle (independent oracle), so a
|
|
* generic sequence-diff tool (tools/parity_diff.py) can locate the FIRST logical
|
|
* divergence between the two timelines — not a frame diff (PRINCIPLES.md "find
|
|
* the first divergence" + "is the value WRONG or is the behavior MISSING").
|
|
*
|
|
* Model (CLAUDE.md ring-buffer doctrine): armed from boot (so the pre-divergence
|
|
* window is always covered — never arm-then-time), records one row per relevant
|
|
* control event while current_tcb == watched_tcb (or all TCBs if watched==0),
|
|
* and LATCHES (freezes) the moment a configured trigger target is dispatched, so
|
|
* the lead-up to the wedge survives instead of being evicted by the wedge loop.
|
|
*
|
|
* Generality: point it at any watched TCB + freeze trigger + up to six watch-word
|
|
* addresses. The MMX6 cutscene→gameplay freeze is the first user, but nothing
|
|
* here is MMX6-specific — it is the reusable substrate for a functional-parity
|
|
* audit between recomp and oracle.
|
|
*/
|
|
#ifndef PARITY_TRACE_H
|
|
#define PARITY_TRACE_H
|
|
|
|
#include <stdint.h>
|
|
|
|
#ifdef __cplusplus
|
|
extern "C" {
|
|
#endif
|
|
|
|
typedef enum {
|
|
PARITY_KIND_DISPATCH = 0, /* psx_dispatch iteration (per-leader) */
|
|
PARITY_KIND_CHANGETHREAD, /* syscall-3 ChangeThread decision point */
|
|
PARITY_KIND_RFE, /* ReturnFromException */
|
|
PARITY_KIND_YIELD, /* scheduler cross-thread switch committed */
|
|
PARITY_KIND_SAVE, /* context saved to a TCB */
|
|
PARITY_KIND_RESTORE, /* context restored from a TCB */
|
|
PARITY_KIND_TRIGGER, /* the freeze trigger fired (final row) */
|
|
} parity_kind_t;
|
|
|
|
#define PARITY_WATCH_MAX 6
|
|
|
|
typedef struct {
|
|
uint64_t seq;
|
|
uint32_t frame;
|
|
uint32_t kind;
|
|
uint64_t cycle; /* absolute guest CPU cycles since boot (the deterministic
|
|
* ruler; native psx_get_cycle_count / Beetle
|
|
* beetle_core_get_guest_cycles — directly comparable) */
|
|
uint32_t current_tcb;
|
|
uint32_t pc;
|
|
uint32_t ra;
|
|
uint32_t sp;
|
|
uint32_t epc; /* resume PC read from watched TCB (epc slot) */
|
|
uint32_t tcb_state; /* state/status word read from watched TCB */
|
|
uint32_t target; /* dispatch / switch target */
|
|
uint32_t watch[PARITY_WATCH_MAX];
|
|
/* Per-watch-word LAST-WRITER provenance (set by parity_trace_note_write):
|
|
* the exact store PC, guest cycle, frame, and (approx) writing TCB of the
|
|
* most recent write to each watch word. This is what turns "the value
|
|
* diverged" into "who/when produced the divergent value" — diff these across
|
|
* native vs Beetle to name the early producer of a gate input. */
|
|
uint32_t watch_wpc[PARITY_WATCH_MAX];
|
|
uint64_t watch_wcycle[PARITY_WATCH_MAX];
|
|
uint32_t watch_wframe[PARITY_WATCH_MAX];
|
|
uint32_t watch_wtcb[PARITY_WATCH_MAX];
|
|
} ParityEntry;
|
|
|
|
/* Each process supplies its own RAM reader (recomp: cpu->read_word; Beetle:
|
|
* PS_CPU peek). ctx is passed back verbatim. */
|
|
typedef uint32_t (*parity_read_word_fn)(void* ctx, uint32_t addr);
|
|
|
|
/* Configure. watched_tcb==0 => record all TCBs. trigger_target==0 => no freeze.
|
|
* tcb_epc_off / tcb_state_off are watched-TCB-relative byte offsets (PSX BIOS:
|
|
* epc slot = tcb+0x88, state word = tcb+0x00). watch_addrs/watch_count: up to
|
|
* PARITY_WATCH_MAX guest word addresses snapshotted on every row. Does not arm. */
|
|
void parity_trace_config(uint32_t watched_tcb, uint32_t trigger_target,
|
|
uint32_t tcb_epc_off, uint32_t tcb_state_off,
|
|
const uint32_t* watch_addrs, int watch_count);
|
|
void parity_trace_arm(int on); /* on=1 begins recording (idempotent) */
|
|
void parity_trace_reset(void); /* clear ring + unfreeze (keeps config) */
|
|
int parity_trace_is_armed(void); /* hot-path gate: cheap one-branch check */
|
|
int parity_trace_is_frozen(void);
|
|
|
|
/* Record one event. read_word+ctx source current_tcb (via TCBH 0x108), the
|
|
* watched-TCB epc/state, and the watch words. No-op unless armed, not frozen,
|
|
* and (watched==0 || current_tcb==watched). Latches on target==trigger. */
|
|
void parity_trace_record(parity_kind_t kind, uint32_t pc, uint32_t ra,
|
|
uint32_t sp, uint32_t target,
|
|
parity_read_word_fn read_word, void* ctx);
|
|
|
|
/* Note a guest memory write so the per-watch-word last-writer table stays
|
|
* current. addr may be guest-virtual or physical (masked internally); width is
|
|
* the access size in bytes (1/2/4); writer_pc is the EXACT store instruction PC
|
|
* (native: g_debug_last_store_pc; Beetle: the store pc from the wtrace hook).
|
|
* Updates a watch slot iff [addr,addr+width) overlaps that watch word. No-op
|
|
* unless armed and not frozen. Called from BOTH processes' write paths so the
|
|
* provenance is directly comparable. The writing TCB is taken from the most
|
|
* recent recorded control event (a cheap, accurate-in-practice approximation —
|
|
* the writer is the running thread). */
|
|
void parity_trace_note_write(uint32_t addr, uint32_t width, uint32_t writer_pc);
|
|
|
|
/* Readback (matches the beetle_fntrace_get pattern): copies up to max_rows
|
|
* NEWEST entries, oldest-first, into out[]; returns the count copied. */
|
|
uint32_t parity_trace_get(ParityEntry* out, uint32_t max_rows);
|
|
uint64_t parity_trace_total(void);
|
|
const char* parity_kind_str(uint32_t kind);
|
|
|
|
#ifdef __cplusplus
|
|
}
|
|
#endif
|
|
|
|
#endif /* PARITY_TRACE_H */
|