mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-28 10:59:16 -04:00
4aebb349da
The cycle-keyed design hashed+parked at the first cosim_instr boundary AFTER the stride cycle was crossed; the two backends can reach that crossing at different retirement positions, so the oracle compared different architectural states (observed as a +5-cycle park skew and phantom CPU-register divergences mid-CPS). Checkpoints are now taken inside cosim_instr, keyed on the retirement count itself (a branch and its delay slot retire as one event in both backends). Checkpoint k = state after k*stride retirements in BOTH instances by construction. The guest-cycle clock stays inside the state hash, so real cycle-accounting drift is still detected -- now pinned to an exact instruction window instead of corrupting the alignment. cosim.py: stride/--max/--start-cycle now in retirement units; the cycle skew warning is now reported as a real timing divergence. launch() gives instance B its own exe (the mods root is exe-dir-relative, so a shared exe races on mods/state.toml publish regardless of cwd separation) and refuses a stale COSIM_CWD_B exe. Gates on CMR2 (SLUS-01222): compiled/compiled and interp/interp both clean over 100M retirements with identical cross-pair chains; injected 1-bit RAM fault detected at the next checkpoint.
389 lines
18 KiB
C
389 lines
18 KiB
C
#ifdef PSX_COSIM
|
|
/* cosim.c — first-divergence co-simulation engine + minimal TCP server.
|
|
* See COSIM_ORACLE.md. Active ONLY in the clean `psx-cosim` target (PSX_COSIM); the
|
|
* whole translation unit is empty otherwise, so it is safe in the shared source list.
|
|
*
|
|
* Model: the guest runs normally on the main thread. At every basic-block leader the
|
|
* emitted/interp hook calls cosim_block(pc): it stamps the block into a ring, folds a
|
|
* cumulative chain hash, and — when the coordinator has asked to stop at a target
|
|
* sequence — PARKS the guest thread there so the coordinator can read a quiescent
|
|
* machine over TCP, compare, and resume. Two processes (compiled + interp) driven in
|
|
* lockstep by tools/cosim.py converge on the first divergent block, then drill.
|
|
*
|
|
* Determinism is a hard requirement (both runs must be bit-identical until the real
|
|
* divergence). The clean build runs headless, single-threaded guest, software renderer,
|
|
* no host-time throttle. The compiled-vs-compiled gate proves it.
|
|
*/
|
|
#include "cosim_state.h"
|
|
#include <stdint.h>
|
|
#include <string.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
|
|
#ifdef _WIN32
|
|
#include <winsock2.h>
|
|
#include <ws2tcpip.h>
|
|
#include <windows.h>
|
|
typedef SOCKET sock_t;
|
|
#define COSIM_SLEEP(ms) Sleep(ms)
|
|
#else
|
|
#include <sys/socket.h>
|
|
#include <netinet/in.h>
|
|
#include <unistd.h>
|
|
#include <pthread.h>
|
|
typedef int sock_t;
|
|
#define COSIM_SLEEP(ms) usleep((ms)*1000)
|
|
#endif
|
|
|
|
extern uint32_t i_stat, i_mask;
|
|
extern uint64_t psx_cycle_count;
|
|
|
|
/* EXPERIMENTAL input-driven cosim: held pad state applied every frame by the headless
|
|
* sampler (main.cpp). 0xFFFF = all released (PSX pad active-low). Set via `setpad`.
|
|
* If two instances desync under input, this path is proven nondeterministic. */
|
|
volatile int g_cosim_pad_hold[2] = { 0xFFFF, 0xFFFF };
|
|
|
|
#define RING_N 8192u /* bounded reporting window (power of two) */
|
|
typedef struct { uint64_t cp; uint32_t pc; uint64_t hash; uint32_t istat, imask; uint64_t cycle; } Entry;
|
|
static Entry g_ring[RING_N];
|
|
static uint64_t g_cp = 0; /* checkpoints crossed so far */
|
|
static uint64_t g_chain = 1469598103934665603ULL; /* cumulative FNV over checkpoints */
|
|
static uint64_t g_stride = 4096; /* retired instructions per checkpoint */
|
|
static uint64_t g_next_cp = 0; /* icount at which to take the next checkpoint */
|
|
static uint32_t g_last_leader_pc = 0; /* set by cosim_block, reported at checkpoints */
|
|
static uint64_t g_icount = 0; /* retirement events (cosim_instr calls) */
|
|
|
|
/* lockstep control (written by TCP thread, read by guest thread).
|
|
* The guest parks at EVERY checkpoint boundary (a deterministic guest cycle = multiple
|
|
* of stride) and only advances when the coordinator grants budget via `step N`. It does
|
|
* NOT free-run: the earlier "free-run until you notice an async stop cycle" design was
|
|
* racy — two processes noticed the flag at different wall-times and parked at different
|
|
* cycles (a HARNESS nondeterminism, not a guest one). Parking at fixed checkpoint
|
|
* boundaries makes both processes stop at identical cycles by construction. */
|
|
static volatile int64_t g_run_budget = 0; /* checkpoints the guest may cross before parking */
|
|
static volatile int g_parked = 0;
|
|
static volatile int g_go_token = 0; /* bump to wake a parked guest to re-check budget */
|
|
|
|
static inline uint64_t fold(uint64_t h, uint64_t v) {
|
|
for (int i = 0; i < 8; i++) { h ^= (uint8_t)(v >> (i*8)); h *= 1099511628211ULL; }
|
|
return h;
|
|
}
|
|
|
|
/* Block-leader hook (emitted under PSX_COSIM + called from the interp loop). Cheap:
|
|
* just stashes the last leader PC for human-readable reporting — the actual compare is
|
|
* the cycle-keyed checkpoint below, which aligns across both backends. */
|
|
void cosim_block(uint32_t pc) { g_last_leader_pc = pc; }
|
|
uint32_t cosim_last_block(void) { return g_last_leader_pc; }
|
|
|
|
uint32_t cosim_cycles_to_next_checkpoint(void) {
|
|
return 0; /* checkpoints are icount-keyed; no cycle sub-step cap needed */
|
|
}
|
|
|
|
/* Instruction-count-keyed checkpoint — taken inside cosim_instr, which both
|
|
* backends call exactly once per retirement event (a branch and its delay slot
|
|
* retire as ONE event in both the dirty-RAM interp and the emitted code). That
|
|
* makes checkpoint k "the state after k*stride retirements" by construction, so
|
|
* the two instances always hash the SAME architectural position. The guest-cycle
|
|
* clock is part of the hash (cosim_state.c), so any cycle-accounting drift shows
|
|
* up as a chain divergence AT an exact instruction instead of silently skewing
|
|
* where the two instances park (the old cycle-keyed design parked at the first
|
|
* instruction boundary after the stride cycle, which the two backends could
|
|
* reach at different retirement positions — phantom divergences). */
|
|
static void cosim_record_checkpoint(uint64_t icount, uint32_t pc) {
|
|
uint64_t h = cosim_state_hash(NULL);
|
|
uint64_t cp = ++g_cp;
|
|
g_chain = fold(fold(g_chain, icount), h);
|
|
Entry *e = &g_ring[cp & (RING_N - 1u)];
|
|
e->cp = cp; e->pc = pc; e->hash = h;
|
|
e->istat = i_stat; e->imask = i_mask; e->cycle = psx_cycle_count;
|
|
|
|
/* deterministic park: consume one checkpoint of budget, else block for `step`.
|
|
* The guest ALWAYS stops here (a fixed icount boundary), never at a wall-time point. */
|
|
if (g_run_budget > 0) { g_run_budget--; return; }
|
|
g_parked = 1;
|
|
while (g_run_budget <= 0) {
|
|
int tok = g_go_token;
|
|
while (g_go_token == tok && g_run_budget <= 0) COSIM_SLEEP(1);
|
|
}
|
|
g_parked = 0;
|
|
g_run_budget--;
|
|
}
|
|
|
|
void cosim_tick(void) {
|
|
/* Retained for the psx_advance_cycles call sites; checkpointing moved to
|
|
* the icount key in cosim_instr (see cosim_record_checkpoint comment). */
|
|
}
|
|
|
|
void cosim_instr(uint32_t pc) {
|
|
g_last_leader_pc = pc;
|
|
g_icount++;
|
|
if (g_cp == 0 && g_next_cp == 0) {
|
|
/* Initial park at the very first retirement: the coordinator gains
|
|
* control before the guest free-runs (unless PSX_COSIM_START_CYCLE
|
|
* set a free-run target, which makes g_next_cp nonzero). */
|
|
cosim_record_checkpoint(g_icount, pc);
|
|
g_next_cp = g_icount + (g_stride ? g_stride : 1);
|
|
return;
|
|
}
|
|
if (g_icount >= g_next_cp) {
|
|
cosim_record_checkpoint(g_icount, pc);
|
|
g_next_cp = g_icount + (g_stride ? g_stride : 1);
|
|
}
|
|
}
|
|
|
|
/* ---------------- minimal TCP command server (own thread) ---------------- */
|
|
/* Line protocol, whitespace-separated. Replies one line.
|
|
* seq -> "seq <n> chain <hex> parked <0|1>"
|
|
* runto <n> -> set stop=n, resume; blocks until parked at n (or exit)
|
|
* chain -> "chain <hex> seq <n>"
|
|
* hash -> "hash <hex> pc <hex> istat <hex> imask <hex> cyc <n>"
|
|
* sub -> per-subsystem hashes of the current state
|
|
* window <n> -> last n ring rows (bounded to RING_N)
|
|
* inject ram <phys> <xor> -> gate-4 fault into RAM
|
|
* inject reg <idx> <xor> -> gate-4 fault into a CPU reg (idx 0..31, 32=hi,33=lo)
|
|
* reset -> reset incremental hash state
|
|
*/
|
|
static void handle_line(sock_t s, char *line);
|
|
|
|
static int send_line(sock_t s, const char *buf) {
|
|
size_t n = strlen(buf);
|
|
size_t off = 0;
|
|
while (off < n) {
|
|
int sent = send(s, buf + off, (int)(n - off), 0);
|
|
if (sent <= 0) return 0;
|
|
off += (size_t)sent;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
static void server_loop(unsigned short port) {
|
|
sock_t ls = socket(AF_INET, SOCK_STREAM, 0);
|
|
if (ls == (sock_t)-1) return;
|
|
int yes = 1;
|
|
setsockopt(ls, SOL_SOCKET, SO_REUSEADDR, (const char*)&yes, sizeof yes);
|
|
struct sockaddr_in a; memset(&a, 0, sizeof a);
|
|
a.sin_family = AF_INET; a.sin_addr.s_addr = htonl(INADDR_LOOPBACK); a.sin_port = htons(port);
|
|
if (bind(ls, (struct sockaddr*)&a, sizeof a) != 0) return;
|
|
listen(ls, 4);
|
|
for (;;) {
|
|
sock_t c = accept(ls, NULL, NULL);
|
|
if (c == (sock_t)-1) { COSIM_SLEEP(5); continue; }
|
|
char buf[512]; int used = 0;
|
|
for (;;) {
|
|
int r = recv(c, buf + used, (int)sizeof(buf) - 1 - used, 0);
|
|
if (r <= 0) break;
|
|
used += r; buf[used] = 0;
|
|
char *nl;
|
|
while ((nl = strchr(buf, '\n')) != NULL) {
|
|
*nl = 0;
|
|
char *cr = strchr(buf, '\r'); if (cr) *cr = 0;
|
|
handle_line(c, buf);
|
|
size_t rest = strlen(nl + 1);
|
|
memmove(buf, nl + 1, rest + 1);
|
|
used = (int)rest;
|
|
}
|
|
if (used >= (int)sizeof(buf) - 1) used = 0; /* overlong line: drop */
|
|
}
|
|
#ifdef _WIN32
|
|
closesocket(c);
|
|
#else
|
|
close(c);
|
|
#endif
|
|
}
|
|
}
|
|
|
|
static void handle_line(sock_t s, char *line) {
|
|
char out[1024];
|
|
char cmd[32] = {0};
|
|
if (sscanf(line, "%31s", cmd) != 1) { send_line(s, "err empty\n"); return; }
|
|
|
|
if (!strcmp(cmd, "status")) {
|
|
snprintf(out, sizeof out, "cp %llu cycle %llu icnt %llu chain %016llx stride %llu parked %d\n",
|
|
(unsigned long long)g_cp, (unsigned long long)psx_cycle_count,
|
|
(unsigned long long)g_icount,
|
|
(unsigned long long)g_chain, (unsigned long long)g_stride, g_parked);
|
|
send_line(s, out); return;
|
|
}
|
|
if (!strcmp(cmd, "chain")) {
|
|
snprintf(out, sizeof out, "chain %016llx cp %llu cycle %llu\n",
|
|
(unsigned long long)g_chain, (unsigned long long)g_cp,
|
|
(unsigned long long)psx_cycle_count);
|
|
send_line(s, out); return;
|
|
}
|
|
if (!strcmp(cmd, "stride")) {
|
|
/* Stride fixes the checkpoint cycle boundaries; set ONCE before stepping so both
|
|
* processes checkpoint at identical cycles. Changing it mid-run is refused. */
|
|
unsigned long long n = 0;
|
|
if (sscanf(line, "%*s %llu", &n) != 1 || n == 0) { send_line(s, "err stride\n"); return; }
|
|
if (g_cp != 0) { send_line(s, "err stride-after-start\n"); return; }
|
|
g_stride = (uint64_t)n; g_next_cp = 0; send_line(s, "ok\n"); return;
|
|
}
|
|
if (!strcmp(cmd, "step")) { /* step <N-checkpoints>; parks at cp+N */
|
|
unsigned long long n = 1;
|
|
sscanf(line, "%*s %llu", &n);
|
|
g_run_budget += (int64_t)n;
|
|
g_go_token++; /* wake the parked guest */
|
|
/* wait until it parks again (crossed N checkpoints), or the guest exits */
|
|
int spins = 0;
|
|
while (g_run_budget > 0 && spins < 1200000) { COSIM_SLEEP(1); spins++; }
|
|
/* small settle so g_parked/g_chain reflect the checkpoint just recorded */
|
|
int s2 = 0; while (!g_parked && g_run_budget <= 0 && s2 < 2000) { COSIM_SLEEP(1); s2++; }
|
|
snprintf(out, sizeof out, "%s cp %llu cycle %llu icnt %llu chain %016llx\n",
|
|
g_parked ? "parked" : "running",
|
|
(unsigned long long)g_cp, (unsigned long long)psx_cycle_count,
|
|
(unsigned long long)g_icount,
|
|
(unsigned long long)g_chain);
|
|
send_line(s, out); return;
|
|
}
|
|
if (!strcmp(cmd, "hash")) {
|
|
Entry *e = &g_ring[g_cp & (RING_N - 1u)];
|
|
snprintf(out, sizeof out, "hash %016llx pc %08x istat %08x imask %08x cyc %llu\n",
|
|
(unsigned long long)e->hash, e->pc, e->istat, e->imask,
|
|
(unsigned long long)e->cycle);
|
|
send_line(s, out); return;
|
|
}
|
|
if (!strcmp(cmd, "sub")) {
|
|
CosimSubHashes h; cosim_state_hash(&h);
|
|
snprintf(out, sizeof out,
|
|
"cpu %016llx irqctl %016llx ram %016llx scratch %016llx vram %016llx "
|
|
"gpu %016llx spu %016llx cdrom %016llx dma %016llx sio %016llx "
|
|
"timers %016llx clock %016llx dirty %016llx\n",
|
|
(unsigned long long)h.cpu,(unsigned long long)h.irqctl,(unsigned long long)h.ram,
|
|
(unsigned long long)h.scratch,(unsigned long long)h.vram,(unsigned long long)h.gpu,
|
|
(unsigned long long)h.spu,(unsigned long long)h.cdrom,(unsigned long long)h.dma,
|
|
(unsigned long long)h.sio,(unsigned long long)h.timers,(unsigned long long)h.clock,
|
|
(unsigned long long)h.dirty);
|
|
send_line(s, out); return;
|
|
}
|
|
if (!strcmp(cmd, "window")) {
|
|
unsigned long long n = 32;
|
|
sscanf(line, "%*s %llu", &n);
|
|
if (n > RING_N) n = RING_N;
|
|
if (n > g_cp) n = g_cp;
|
|
for (uint64_t i = 0; i < n; i++) {
|
|
uint64_t cp = g_cp - (n - 1 - i);
|
|
Entry *e = &g_ring[cp & (RING_N - 1u)];
|
|
snprintf(out, sizeof out, "row cp %llu pc %08x hash %016llx istat %08x cyc %llu\n",
|
|
(unsigned long long)e->cp, e->pc, (unsigned long long)e->hash,
|
|
e->istat, (unsigned long long)e->cycle);
|
|
send_line(s, out);
|
|
}
|
|
send_line(s, "end\n"); return;
|
|
}
|
|
if (!strcmp(cmd, "inject")) {
|
|
char what[8] = {0}; unsigned long a=0, b=0;
|
|
if (sscanf(line, "%*s %7s %lu %lu", what, &a, &b) == 3) {
|
|
if (!strcmp(what, "ram")) { cosim_inject_ram((uint32_t)a, (uint8_t)b); send_line(s, "ok\n"); return; }
|
|
if (!strcmp(what, "reg")) { cosim_inject_reg((int)a, (uint32_t)b); send_line(s, "ok\n"); return; }
|
|
}
|
|
send_line(s, "err inject\n"); return;
|
|
}
|
|
if (!strcmp(cmd, "cpu")) {
|
|
/* Full CPU field dump so the coordinator can field-diff a cpu-subsystem
|
|
* divergence (which register / micro-state field actually split). */
|
|
extern CPUState *debug_cpu_ptr;
|
|
CPUState *c = debug_cpu_ptr;
|
|
if (!c) { send_line(s, "err no-cpu\n"); return; }
|
|
char *p = out; size_t rem = sizeof out;
|
|
int w = snprintf(p, rem, "pc %08x hi %08x lo %08x", c->pc, c->hi, c->lo);
|
|
p += w; rem -= w;
|
|
for (int i = 0; i < 32; i++) { w = snprintf(p, rem, " r%d %08x", i, c->gpr[i]); p+=w; rem-=w; }
|
|
for (int i = 0; i < 16; i++) { w = snprintf(p, rem, " c%d %08x", i, c->cop0[i]); p+=w; rem-=w; }
|
|
w = snprintf(p, rem, " mdts %llx gtets %llx rfudge %02x rabw %02x ldw %02x ldabs %08x\n",
|
|
(unsigned long long)c->muldiv_ts_done, (unsigned long long)c->gte_ts_done,
|
|
c->read_fudge, c->read_absorb_which, c->ld_which_t, c->ld_absorb);
|
|
send_line(s, out); return;
|
|
}
|
|
if (!strcmp(cmd, "gte")) {
|
|
extern CPUState *debug_cpu_ptr;
|
|
CPUState *c = debug_cpu_ptr;
|
|
if (!c) { send_line(s, "err no-cpu\n"); return; }
|
|
char big[4096];
|
|
char *p = big; size_t rem = sizeof big;
|
|
for (int i = 0; i < 32 && rem > 1; i++) {
|
|
uint32_t val = c->gte_data[i];
|
|
if ((i >= 8 && i <= 11) || i == 15 || i == 28 || i == 29 || i == 31) {
|
|
val = gte_read_data(c, (uint8_t)i);
|
|
}
|
|
int w = snprintf(p, rem, " gd%d %08x", i, val);
|
|
if (w < 0 || (size_t)w >= rem) { p += rem - 1; rem = 1; break; }
|
|
p += w; rem -= (size_t)w;
|
|
}
|
|
for (int i = 0; i < 32 && rem > 1; i++) {
|
|
int w = snprintf(p, rem, " gc%d %08x", i, c->gte_ctrl[i]);
|
|
if (w < 0 || (size_t)w >= rem) { p += rem - 1; rem = 1; break; }
|
|
p += w; rem -= (size_t)w;
|
|
}
|
|
snprintf(p, rem, "\n");
|
|
send_line(s, big); return;
|
|
}
|
|
if (!strcmp(cmd, "dev")) {
|
|
/* Device-timing field dump: the fields behind the `irqctl` and `gpu`
|
|
* sub-hashes, so a device divergence can be field-diffed (esp.
|
|
* cycles_since_vblank = VBLANK-raise-timing, and GPUSTAT). */
|
|
extern void interrupts_cosim_dump(uint32_t*, int*);
|
|
extern uint32_t gpu_read_gpustat(void);
|
|
uint32_t csv = 0; int inexc = 0; interrupts_cosim_dump(&csv, &inexc);
|
|
snprintf(out, sizeof out,
|
|
"csv %08x inexc %d imask %08x istat %08x gpustat %08x\n",
|
|
csv, inexc, i_mask, i_stat, gpu_read_gpustat());
|
|
send_line(s, out); return;
|
|
}
|
|
if (!strcmp(cmd, "gpu")) {
|
|
extern void gpu_cosim_dump(char *out, int cap);
|
|
char big[4096];
|
|
gpu_cosim_dump(big, (int)sizeof big);
|
|
send_line(s, big); return;
|
|
}
|
|
if (!strcmp(cmd, "irqtrace")) {
|
|
extern void interrupts_cosim_irq_dump(char *out, int cap);
|
|
char big[32768];
|
|
interrupts_cosim_irq_dump(big, (int)sizeof big);
|
|
send_line(s, big); return;
|
|
}
|
|
if (!strcmp(cmd, "setpad")) {
|
|
/* EXPERIMENTAL input-driven cosim: set the HELD pad state (applied every frame).
|
|
* Coordinator issues the SAME value to both instances while parked at a
|
|
* checkpoint, so input reaches both at the same guest cycle. `setpad <slot> <hex>`
|
|
* (slot 0/1; 0xFFFF = released, active-low). Desync under this => nondeterministic. */
|
|
unsigned slot = 0, val = 0xFFFFu;
|
|
if (sscanf(line, "%*s %u %x", &slot, &val) >= 1 && slot < 2) {
|
|
g_cosim_pad_hold[slot] = (int)(val & 0xFFFFu); send_line(s, "ok\n"); return;
|
|
}
|
|
send_line(s, "err setpad\n"); return;
|
|
}
|
|
if (!strcmp(cmd, "reset")) { cosim_state_reset(); send_line(s, "ok\n"); return; }
|
|
send_line(s, "err unknown\n");
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
static DWORD WINAPI server_thread(LPVOID p) { server_loop((unsigned short)(uintptr_t)p); return 0; }
|
|
#else
|
|
static void *server_thread(void *p) { server_loop((unsigned short)(uintptr_t)p); return NULL; }
|
|
#endif
|
|
|
|
/* Call once at startup (from the clean-build main, before the guest runs). Port from
|
|
* env PSX_COSIM_PORT (default 4600). */
|
|
void cosim_init(void) {
|
|
cosim_state_reset();
|
|
unsigned short port = 4600;
|
|
const char *e = getenv("PSX_COSIM_PORT");
|
|
if (e && *e) port = (unsigned short)atoi(e);
|
|
/* Stride fixed at launch (env) so the checkpoint icount boundaries are identical
|
|
* in both processes before either runs a single instruction — no set-stride race.
|
|
* Both stride and start are in RETIRED INSTRUCTIONS (icount), not guest cycles. */
|
|
const char *st = getenv("PSX_COSIM_STRIDE");
|
|
if (st && *st) { unsigned long long v = strtoull(st, 0, 10); if (v) g_stride = v; }
|
|
const char *sc = getenv("PSX_COSIM_START_CYCLE");
|
|
if (sc && *sc) { unsigned long long v = strtoull(sc, 0, 10); g_next_cp = (uint64_t)v; }
|
|
#ifdef _WIN32
|
|
WSADATA w; WSAStartup(MAKEWORD(2,2), &w);
|
|
CreateThread(NULL, 0, server_thread, (LPVOID)(uintptr_t)port, 0, NULL);
|
|
#else
|
|
pthread_t t; pthread_create(&t, NULL, server_thread, (void*)(uintptr_t)port);
|
|
#endif
|
|
fprintf(stdout, "cosim: first-divergence oracle listening on 127.0.0.1:%u\n", port);
|
|
}
|
|
|
|
#endif /* PSX_COSIM */
|