mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
tooling(parity): general two-process control-flow parity trace (native producer)
End the "somewhere upstream" blindness with a reusable first-divergence
instrument (PRINCIPLES.md "find the first divergence; is the value WRONG or the
behavior MISSING"). A TCB-scoped, freeze-on-trigger control-flow ring with an
identical struct + JSON wire format on both processes, so a generic sequence-diff
(tools/parity_diff.py) locates the first logical divergence between recomp and
oracle — not a frame diff.
- parity_trace.{h,c}: armed-from-boot ring, gated on current_tcb==watched_tcb,
latches on a configured trigger target so the pre-divergence window survives
the wedge loop. Up to 6 watch-word snapshots/row. Self-contained TU shared by
both binaries; frame source decoupled via parity_host_frame().
- fntrace.c: per-dispatch producer hook (cheap armed gate on the hot path).
- traps.c: YIELD control-event hook in the scheduler switch.
- main.cpp: env-armed from boot (PSX_PARITY_TRACE=1; TCB/trigger/watch tunable),
defaults target the MMX6 cutscene wedge.
- debug_server.c: parity_dump + parity_ctl commands.
- tools/parity_diff.py: pulls both rings, sequence-diffs by (kind,pc,target).
First use (MMX6 freeze) confirmed MISSING behavior, not a wrong value: native
thread1 is permanently parked at func_8002000C resume (TCB EPC 0x800200F4),
handshake flag 0x8006D9AC never set (0 across 16384 rows), setter func_8001344C
never runs; thread1 re-resumes a dead func_8002000C frame whose saved-ra slot was
reused -> jr 0x800CD3F8 wedge. Upstream context-staleness/scheduling root.
Beetle mirror producer + the cross-process diff are the next step.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
/* 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 cycle;
|
||||
uint32_t kind;
|
||||
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];
|
||||
} 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);
|
||||
|
||||
/* 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 */
|
||||
@@ -132,6 +132,7 @@ set(PSXRECOMP_RUNTIME_SOURCES
|
||||
${PSXRECOMP_ROOT}/runtime/src/debug_server.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/dirty_ram_interp.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/fntrace.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/parity_trace.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/boot_state.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/traps.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/crash_trace.c
|
||||
|
||||
@@ -2605,6 +2605,55 @@ static void handle_dirty_insn_dump_file(int id, const char *json)
|
||||
* argument-passing chains, and answer "who called X with what args"
|
||||
* across processes (psx-runtime port 4370, psx-beetle port 4380). */
|
||||
#include "fntrace.h"
|
||||
#include "parity_trace.h"
|
||||
|
||||
/* ---- parity_dump / parity_ctl: general two-process control-flow parity ring.
|
||||
* Mirrors the IDENTICAL command on psx-beetle so tools/parity_diff.py can pull
|
||||
* both timelines and align by logical sequence (PRINCIPLES.md first-divergence). */
|
||||
static void handle_parity_dump(int id, const char *json)
|
||||
{
|
||||
int count = json_get_int(json, "count", 8192);
|
||||
if (count < 1) count = 1;
|
||||
if (count > 16384) count = 16384;
|
||||
ParityEntry *e = (ParityEntry *)malloc(sizeof(ParityEntry) * (size_t)count);
|
||||
if (!e) { send_err(id, "oom"); return; }
|
||||
uint32_t got = parity_trace_get(e, (uint32_t)count);
|
||||
const size_t BUF_SZ = 12 * 1024 * 1024;
|
||||
char *out = (char *)malloc(BUF_SZ);
|
||||
if (!out) { free(e); send_err(id, "oom"); return; }
|
||||
size_t pos = 0;
|
||||
pos += snprintf(out + pos, BUF_SZ - pos,
|
||||
"{\"id\":%d,\"ok\":true,\"total\":%llu,\"armed\":%d,\"frozen\":%d,\"count\":%u,\"entries\":[",
|
||||
id, (unsigned long long)parity_trace_total(), parity_trace_is_armed(),
|
||||
parity_trace_is_frozen(), got);
|
||||
for (uint32_t i = 0; i < got; i++) {
|
||||
if (pos > BUF_SZ - 1024) break;
|
||||
ParityEntry *r = &e[i];
|
||||
pos += snprintf(out + pos, BUF_SZ - pos,
|
||||
"%s{\"seq\":%llu,\"frame\":%u,\"kind\":\"%s\",\"cur_tcb\":\"0x%08X\","
|
||||
"\"pc\":\"0x%08X\",\"ra\":\"0x%08X\",\"sp\":\"0x%08X\",\"epc\":\"0x%08X\","
|
||||
"\"state\":\"0x%08X\",\"target\":\"0x%08X\","
|
||||
"\"w\":[\"0x%08X\",\"0x%08X\",\"0x%08X\",\"0x%08X\",\"0x%08X\",\"0x%08X\"]}",
|
||||
i ? "," : "", (unsigned long long)r->seq, r->frame, parity_kind_str(r->kind),
|
||||
r->current_tcb, r->pc, r->ra, r->sp, r->epc, r->tcb_state, r->target,
|
||||
r->watch[0], r->watch[1], r->watch[2], r->watch[3], r->watch[4], r->watch[5]);
|
||||
}
|
||||
pos += snprintf(out + pos, BUF_SZ - pos, "]}\n");
|
||||
debug_server_send_line(out); free(out); free(e);
|
||||
}
|
||||
|
||||
static void handle_parity_ctl(int id, const char *json)
|
||||
{
|
||||
if (json_get_int(json, "reset", 0)) parity_trace_reset();
|
||||
long armv = json_get_int(json, "arm", -1);
|
||||
if (armv == 0 || armv == 1) parity_trace_arm((int)armv);
|
||||
char buf[160];
|
||||
snprintf(buf, sizeof buf,
|
||||
"{\"id\":%d,\"ok\":true,\"armed\":%d,\"frozen\":%d,\"total\":%llu}\n",
|
||||
id, parity_trace_is_armed(), parity_trace_is_frozen(),
|
||||
(unsigned long long)parity_trace_total());
|
||||
debug_server_send_line(buf);
|
||||
}
|
||||
|
||||
static void handle_fntrace_arm(int id, const char *json)
|
||||
{
|
||||
@@ -10251,6 +10300,8 @@ static void handle_vk_perf(int id, const char *json)
|
||||
|
||||
static const CmdEntry s_commands[] = {
|
||||
{ "ping", handle_ping },
|
||||
{ "parity_dump", handle_parity_dump },
|
||||
{ "parity_ctl", handle_parity_ctl },
|
||||
{ "latency", handle_latency },
|
||||
{ "vk_perf", handle_vk_perf },
|
||||
{ "game_options", handle_game_options },
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
/* fntrace.c — runtime side of psx_dispatch call ring. See fntrace.h. */
|
||||
|
||||
#include "fntrace.h"
|
||||
#include "parity_trace.h" /* general control-flow parity ring (native producer) */
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
/* RAM reader adapter for the parity trace (cpu->read_word takes only addr). */
|
||||
static uint32_t parity_rw_cb(void* ctx, uint32_t addr) {
|
||||
return ((CPUState*)ctx)->read_word(addr);
|
||||
}
|
||||
|
||||
/* Frame-source accessor for parity_trace.c (decoupled from s_frame_count type). */
|
||||
uint32_t parity_host_frame(void) { extern uint64_t s_frame_count; return (uint32_t)s_frame_count; }
|
||||
|
||||
FntraceEntry g_fntrace_ring[FNTRACE_RING_CAP];
|
||||
uint64_t g_fntrace_seq = 0;
|
||||
|
||||
@@ -46,6 +55,14 @@ static inline int armed_match(uint32_t target) {
|
||||
}
|
||||
|
||||
void fntrace_record(CPUState* cpu, uint32_t target) {
|
||||
/* General parity ring: one DISPATCH row per leader while current_tcb matches
|
||||
* the watched thread. Gated by the cheap armed flag so disarmed runs pay only
|
||||
* a single branch on this hot path. pc==target (the leader being entered). */
|
||||
if (parity_trace_is_armed()) {
|
||||
parity_trace_record(PARITY_KIND_DISPATCH, target, cpu->gpr[31],
|
||||
cpu->gpr[29], target, parity_rw_cb, cpu);
|
||||
}
|
||||
|
||||
/* Dispatch-level bail-source capture (Tomba 2 wild-return spin, runtime-only,
|
||||
* no BIOS regen). The generated psx_dispatch_impl calls us at the top of each
|
||||
* dispatch iteration. When a previous iteration bail-FLATTENED a compiled
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#include "cpu_state.h"
|
||||
#include "psx_scheduler.h" /* psx_scheduler_run — deterministic TCB scheduler */
|
||||
#include "parity_trace.h" /* general two-process control-flow parity ring */
|
||||
#include "psx_interpreter.h"
|
||||
#include "cdrom.h"
|
||||
#include "fntrace.h"
|
||||
@@ -2789,6 +2790,44 @@ int main(int argc, char** argv) {
|
||||
psx_hle_scheduler_enabled() ? "HLE (deterministic TCB)"
|
||||
: "LLE (host fibers)");
|
||||
std::fflush(stdout);
|
||||
|
||||
/* General control-flow parity trace (parity_trace.h). Armed from boot via
|
||||
* PSX_PARITY_TRACE=1 so the pre-divergence window is always covered (never
|
||||
* arm-then-time). Watched TCB / freeze trigger / watch words are env-tunable;
|
||||
* defaults target the MMX6 cutscene→gameplay wedge (thread1 0xA000E35C,
|
||||
* trigger dispatch 0x800CD3F8, watch the handshake flag + state struct). */
|
||||
if (const char* pt = std::getenv("PSX_PARITY_TRACE"); pt && pt[0] && pt[0] != '0') {
|
||||
auto envhex = [](const char* k, uint32_t dflt) -> uint32_t {
|
||||
const char* v = std::getenv(k);
|
||||
return (v && v[0]) ? (uint32_t)std::strtoul(v, nullptr, 0) : dflt;
|
||||
};
|
||||
uint32_t tcb = envhex("PSX_PARITY_TCB", 0xA000E35Cu);
|
||||
uint32_t trigger = envhex("PSX_PARITY_TRIGGER", 0x800CD3F8u);
|
||||
uint32_t watch[PARITY_WATCH_MAX] = {
|
||||
0x801FEB78u, /* thread1 stack saved-ra slot */
|
||||
0x8006D9ACu, /* handshake flag (setter target) */
|
||||
0x800CD3F8u, /* cutscene state struct: outer/sub */
|
||||
0x800CD3FCu, /* countdown */
|
||||
0x800CD404u, /* done flag region */
|
||||
0x800200F4u, /* func_8002000C resume point */
|
||||
};
|
||||
int wc = PARITY_WATCH_MAX;
|
||||
if (const char* wl = std::getenv("PSX_PARITY_WATCH"); wl && wl[0]) {
|
||||
wc = 0; std::string s(wl); size_t p = 0;
|
||||
while (p < s.size() && wc < PARITY_WATCH_MAX) {
|
||||
size_t c = s.find(',', p);
|
||||
std::string tok = s.substr(p, c == std::string::npos ? c : c - p);
|
||||
if (!tok.empty()) watch[wc++] = (uint32_t)std::strtoul(tok.c_str(), nullptr, 0);
|
||||
if (c == std::string::npos) break; p = c + 1;
|
||||
}
|
||||
}
|
||||
parity_trace_config(tcb, trigger, 0x88u, 0x00u, watch, wc);
|
||||
parity_trace_arm(1);
|
||||
std::fprintf(stdout, "psxrecomp: parity trace ARMED tcb=0x%08X trigger=0x%08X (%d watch)\n",
|
||||
tcb, trigger, wc);
|
||||
std::fflush(stdout);
|
||||
}
|
||||
|
||||
psx_scheduler_run(&cpu);
|
||||
#endif
|
||||
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
/* parity_trace.c — general two-process control-flow parity trace.
|
||||
* See parity_trace.h. Self-contained (no runtime internals) so the SAME TU
|
||||
* compiles into both psx-runtime and psx-beetle and they emit identical rows. */
|
||||
#include "parity_trace.h"
|
||||
#include <string.h>
|
||||
|
||||
#define PARITY_RING_CAP 16384u /* power of two; ~spans the cutscene window */
|
||||
|
||||
static ParityEntry s_ring[PARITY_RING_CAP];
|
||||
static uint64_t s_seq = 0;
|
||||
static uint32_t s_armed = 0;
|
||||
static uint32_t s_frozen = 0;
|
||||
|
||||
static uint32_t s_watched_tcb = 0;
|
||||
static uint32_t s_trigger = 0;
|
||||
static uint32_t s_epc_off = 0x88u;
|
||||
static uint32_t s_state_off = 0x00u;
|
||||
static uint32_t s_watch[PARITY_WATCH_MAX];
|
||||
static int s_watch_count = 0;
|
||||
|
||||
/* Frame stamp: each host process defines parity_host_frame() (recomp's
|
||||
* s_frame_count is uint64_t, Beetle's is uint32_t — an accessor decouples the
|
||||
* type/linkage so this single TU compiles into both). */
|
||||
extern uint32_t parity_host_frame(void);
|
||||
|
||||
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)
|
||||
{
|
||||
s_watched_tcb = watched_tcb;
|
||||
s_trigger = trigger_target;
|
||||
s_epc_off = tcb_epc_off;
|
||||
s_state_off = tcb_state_off;
|
||||
if (watch_count < 0) watch_count = 0;
|
||||
if (watch_count > PARITY_WATCH_MAX) watch_count = PARITY_WATCH_MAX;
|
||||
s_watch_count = watch_count;
|
||||
for (int i = 0; i < PARITY_WATCH_MAX; i++)
|
||||
s_watch[i] = (watch_addrs && i < watch_count) ? watch_addrs[i] : 0;
|
||||
}
|
||||
|
||||
void parity_trace_arm(int on) { s_armed = on ? 1u : 0u; }
|
||||
void parity_trace_reset(void) { s_seq = 0; s_frozen = 0; memset(s_ring, 0, sizeof(s_ring)); }
|
||||
int parity_trace_is_armed(void) { return (int)s_armed; }
|
||||
int parity_trace_is_frozen(void) { return (int)s_frozen; }
|
||||
uint64_t parity_trace_total(void) { return s_seq; }
|
||||
|
||||
const char* parity_kind_str(uint32_t kind)
|
||||
{
|
||||
switch (kind) {
|
||||
case PARITY_KIND_DISPATCH: return "dispatch";
|
||||
case PARITY_KIND_CHANGETHREAD: return "changethread";
|
||||
case PARITY_KIND_RFE: return "rfe";
|
||||
case PARITY_KIND_YIELD: return "yield";
|
||||
case PARITY_KIND_SAVE: return "save";
|
||||
case PARITY_KIND_RESTORE: return "restore";
|
||||
case PARITY_KIND_TRIGGER: return "trigger";
|
||||
default: return "?";
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (!s_armed || s_frozen || !read_word) return;
|
||||
|
||||
/* current TCB = *(*(0x108)) — the PSX kernel TCBH chain. */
|
||||
uint32_t tcbh = read_word(ctx, 0x00000108u);
|
||||
uint32_t current_tcb = tcbh ? read_word(ctx, tcbh) : 0u;
|
||||
|
||||
if (s_watched_tcb && current_tcb != s_watched_tcb) return;
|
||||
|
||||
ParityEntry* e = &s_ring[s_seq & (PARITY_RING_CAP - 1u)];
|
||||
e->seq = s_seq;
|
||||
e->frame = parity_host_frame();
|
||||
e->cycle = 0u; /* best-effort; populated by host hook if available */
|
||||
e->kind = (uint32_t)kind;
|
||||
e->current_tcb = current_tcb;
|
||||
e->pc = pc;
|
||||
e->ra = ra;
|
||||
e->sp = sp;
|
||||
e->target = target;
|
||||
e->epc = s_watched_tcb ? read_word(ctx, s_watched_tcb + s_epc_off) : 0u;
|
||||
e->tcb_state = s_watched_tcb ? read_word(ctx, s_watched_tcb + s_state_off) : 0u;
|
||||
for (int i = 0; i < PARITY_WATCH_MAX; i++)
|
||||
e->watch[i] = (i < s_watch_count && s_watch[i]) ? read_word(ctx, s_watch[i]) : 0u;
|
||||
s_seq++;
|
||||
|
||||
/* Latch on the trigger so the pre-divergence window survives the wedge. The
|
||||
* trigger row itself is recorded (above) before freezing. */
|
||||
if (s_trigger && target == s_trigger) {
|
||||
/* re-tag the just-written row as the trigger for easy diffing */
|
||||
e->kind = (uint32_t)PARITY_KIND_TRIGGER;
|
||||
s_frozen = 1;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t parity_trace_get(ParityEntry* out, uint32_t max_rows)
|
||||
{
|
||||
if (!out || max_rows == 0) return 0;
|
||||
uint64_t total = s_seq;
|
||||
uint32_t avail = (total < PARITY_RING_CAP) ? (uint32_t)total : PARITY_RING_CAP;
|
||||
uint32_t n = (avail < max_rows) ? avail : max_rows;
|
||||
uint64_t start = total - n;
|
||||
for (uint32_t i = 0; i < n; i++)
|
||||
out[i] = s_ring[(start + i) & (PARITY_RING_CAP - 1u)];
|
||||
return n;
|
||||
}
|
||||
@@ -14,6 +14,12 @@
|
||||
#include <setjmp.h>
|
||||
#include "psx_fiber.h" /* cross-platform fibers (Win32 fibers / POSIX ucontext) */
|
||||
#include "psx_scheduler.h" /* deterministic TCB scheduler carve-out (scaffolding) */
|
||||
#include "parity_trace.h" /* general two-process control-flow parity ring */
|
||||
|
||||
/* RAM reader adapter for the parity trace (cpu->read_word takes only addr). */
|
||||
static uint32_t traps_parity_rw(void* ctx, uint32_t addr) {
|
||||
return ((CPUState*)ctx)->read_word(addr);
|
||||
}
|
||||
|
||||
/* Forward declarations from interrupts.c */
|
||||
int psx_get_in_exception(void);
|
||||
@@ -623,6 +629,9 @@ static int psx_request_thread_switch(CPUState* cpu, uint32_t target_tcb)
|
||||
g_sched_escape.reason = PSX_RUN_YIELD_TO_TCB;
|
||||
sched_escape_ring_log(cpu, PSX_RUN_YIELD_TO_TCB, current_tcb, target_tcb, 0);
|
||||
debug_server_log_thread_event(8, cpu, current_tcb, target_tcb, 0);
|
||||
if (parity_trace_is_armed())
|
||||
parity_trace_record(PARITY_KIND_YIELD, cpu->pc, cpu->gpr[31],
|
||||
cpu->gpr[29], target_tcb, traps_parity_rw, cpu);
|
||||
longjmp(g_scheduler_jmpbuf, 1); /* unwind to psx_scheduler_run; never returns */
|
||||
return 1; /* unreachable */
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env python3
|
||||
"""parity_diff.py — generic two-process control-flow first-divergence finder.
|
||||
|
||||
Pulls the `parity_dump` ring from psx-runtime (code under test) and psx-beetle
|
||||
(independent oracle), aligns the two thread-scoped control-flow streams by
|
||||
LOGICAL SEQUENCE (not frame), and reports the FIRST point where they diverge —
|
||||
plus the watched state-word deltas at that point. This is the instrument that
|
||||
replaces "somewhere upstream" guessing (recomp-template PRINCIPLES.md: "find the
|
||||
first divergence, not the final visible bug").
|
||||
|
||||
Both sides emit the IDENTICAL row schema (parity_trace.h), so the diff is a plain
|
||||
sequence match on the (kind, pc, target) tuple. Each side is typically armed from
|
||||
boot and frozen on its own trigger (native: the wedge target; beetle: the
|
||||
expected-but-missing producer), so the common prefix aligns and the tail reveals
|
||||
the missing/extra/reordered behavior.
|
||||
|
||||
Usage:
|
||||
python tools/parity_diff.py [--native PORT] [--beetle PORT] [--key kind,pc,target]
|
||||
[--context N] [--count N]
|
||||
"""
|
||||
import argparse, json, socket, sys, difflib
|
||||
|
||||
def query(port, cmd, **kw):
|
||||
obj = {"cmd": cmd}; obj.update(kw)
|
||||
s = socket.create_connection(("127.0.0.1", port), timeout=10)
|
||||
try:
|
||||
s.sendall((json.dumps(obj) + "\n").encode())
|
||||
buf = b""
|
||||
while b"\n" not in buf:
|
||||
chunk = s.recv(1 << 20)
|
||||
if not chunk: break
|
||||
buf += chunk
|
||||
return json.loads(buf.decode(errors="replace").split("\n", 1)[0])
|
||||
finally:
|
||||
s.close()
|
||||
|
||||
def rows(port, count):
|
||||
r = query(port, "parity_dump", count=count)
|
||||
if not r.get("ok"):
|
||||
print(f"[port {port}] parity_dump failed: {r}", file=sys.stderr); sys.exit(2)
|
||||
return r
|
||||
|
||||
def keytuple(e, fields):
|
||||
return tuple(e.get(f, "") for f in fields)
|
||||
|
||||
def fmt(e):
|
||||
w = e.get("w", [])
|
||||
return (f"seq={e['seq']:>5} f={e['frame']:>5} {e['kind']:<12} "
|
||||
f"cur={e['cur_tcb']} pc={e['pc']} tgt={e['target']} "
|
||||
f"epc={e['epc']} st={e['state']} ra={e['ra']} sp={e['sp']} "
|
||||
f"flag={w[1] if len(w)>1 else '?'} struct={w[2] if len(w)>2 else '?'}")
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--native", type=int, default=4490)
|
||||
ap.add_argument("--beetle", type=int, default=4382)
|
||||
ap.add_argument("--count", type=int, default=16384)
|
||||
ap.add_argument("--key", default="kind,pc,target",
|
||||
help="comma fields forming the per-row alignment key")
|
||||
ap.add_argument("--context", type=int, default=12)
|
||||
args = ap.parse_args()
|
||||
fields = args.key.split(",")
|
||||
|
||||
nat = rows(args.native, args.count)
|
||||
bet = rows(args.beetle, args.count)
|
||||
ne, be = nat["entries"], bet["entries"]
|
||||
print(f"native(:{args.native}): {len(ne)} rows total={nat['total']} "
|
||||
f"armed={nat['armed']} frozen={nat['frozen']}")
|
||||
print(f"beetle(:{args.beetle}): {len(be)} rows total={bet['total']} "
|
||||
f"armed={bet['armed']} frozen={bet['frozen']}")
|
||||
if not ne or not be:
|
||||
print("\nOne side has no rows — arm both from boot and reach the window.")
|
||||
return
|
||||
|
||||
nk = [keytuple(e, fields) for e in ne]
|
||||
bk = [keytuple(e, fields) for e in be]
|
||||
sm = difflib.SequenceMatcher(a=nk, b=bk, autojunk=False)
|
||||
blocks = sm.get_matching_blocks()
|
||||
|
||||
# First divergence = end of the first matching block that does not extend
|
||||
# to the end of both streams.
|
||||
first = None
|
||||
for blk in blocks:
|
||||
end_a, end_b = blk.a + blk.size, blk.b + blk.size
|
||||
if blk.size and (end_a < len(nk) or end_b < len(bk)):
|
||||
first = (end_a, end_b); break
|
||||
if first is None:
|
||||
print("\nNo divergence in the compared window (streams match to the end).")
|
||||
return
|
||||
ia, ib = first
|
||||
C = args.context
|
||||
print(f"\n===== FIRST DIVERGENCE (native idx {ia}, beetle idx {ib}) =====")
|
||||
print("--- last common rows (native) ---")
|
||||
for e in ne[max(0, ia - C):ia]:
|
||||
print(" ", fmt(e))
|
||||
print(f"--- NATIVE diverges here (idx {ia}+) ---")
|
||||
for e in ne[ia:ia + C]:
|
||||
print(" N", fmt(e))
|
||||
print(f"--- ORACLE/BEETLE path here (idx {ib}+) ---")
|
||||
for e in be[ib:ib + C]:
|
||||
print(" B", fmt(e))
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user