diff --git a/runtime/include/cpu_state.h b/runtime/include/cpu_state.h index 5d1713bc..6595d2d0 100644 --- a/runtime/include/cpu_state.h +++ b/runtime/include/cpu_state.h @@ -220,6 +220,49 @@ extern uint32_t gte_geometry_correction_hits(void); extern void gte_geometry_correction_stats(uint32_t *lookups, uint32_t *hits, uint32_t *miss_unrecorded, uint32_t *miss_ambiguous); +/* Exact-sign NCLIP coverage: precise = all three SXY shadows live and + * word-validated; fallback = integer sign decided (the widescreen thin-face + * flicker regime); corrected = sign disagreements actually fixed. */ +extern void gte_nclip_precise_stats(uint64_t *hits, uint64_t *fallbacks, + uint64_t *corrected); + +/* pc=0 escape journal (traps.c). Every runtime site that publishes a null PC + * (the "continue the native chain below" convention) or rescues one records + * an entry; when the convention breaks — a published 0 reaches the top-level + * trampoline and the run dies with "execution completed, PC=0" — the ring + * tail names the exact publisher and its context. Publishes are rare, so the + * always-on cost is a few stores; this works in play builds where the + * per-dispatch fntrace ring is compiled out (the exit trace was blind). */ +enum { + PSX_PC0J_TRAP_CHANGE_SELF = 1, /* a: target_tcb */ + PSX_PC0J_TRAP_FIBER_SWITCH = 2, + PSX_PC0J_TRAP_CRIT_ENTER = 3, /* a: sr */ + PSX_PC0J_TRAP_CRIT_EXIT = 4, /* a: sr */ + PSX_PC0J_TRAP_NULL_TARGET = 5, /* a: addr (0) */ + PSX_PC0J_TRAP_SENTINEL = 6, /* a: addr; in-exception longjmp path */ + PSX_PC0J_TRAP_ASYNC_RFE = 7, /* a: resume pc (rescue, not a 0) */ + PSX_PC0J_TRAP_E10_NOOP = 8, /* a: addr */ + PSX_PC0J_TRAP_STALE_MISS = 9, /* a: addr */ + PSX_PC0J_IRQ_SAME_THREAD = 10, /* a: would-be resume; d: escape reason */ + PSX_PC0J_IRQ_RESCUE = 11, /* a: published resume; d: reason */ + PSX_PC0J_DIRTY_SENTINEL = 12, /* d: bit0 in_exception, bit1 async-armed */ + PSX_PC0J_DIRTY_UNSUPPORTED = 13, /* a: unsupported pc; d: reason */ +}; +typedef struct Pc0JournalEntry { + uint64_t seq; + uint32_t site; /* PSX_PC0J_* */ + uint32_t frame; /* present frame count at publish */ + int32_t depth; /* g_psx_dispatch_depth at publish */ + uint32_t a; /* site-specific (see enum) */ + uint32_t b; /* $ra at publish */ + uint32_t c; /* COP0 EPC at publish */ + uint32_t d; /* site-specific extra */ +} Pc0JournalEntry; +#define PSX_PC0J_CAP 64 +extern Pc0JournalEntry g_pc0j_ring[PSX_PC0J_CAP]; +extern uint64_t g_pc0j_seq; +extern void psx_pc0_journal_note(uint32_t site, CPUState *cpu, + uint32_t a, uint32_t d); /* PGXP dataflow-shadowing hook macros (PGXP_LOAD/STORE/ALU/MULDIV/COP2). * The emitter writes them unconditionally; they expand to real calls only diff --git a/runtime/include/pgxp.h b/runtime/include/pgxp.h index db419e91..f41ddb61 100644 --- a/runtime/include/pgxp.h +++ b/runtime/include/pgxp.h @@ -58,6 +58,11 @@ void pgxp_gte_push_sxy(int32_t x16, int32_t y16, uint16_t sz3, uint32_t packed); /* Current SXY FIFO shadow (index 0..3 selects GTE data regs 12..15). * Returns nonzero when the shadow is live and carries X/Y precision. */ int pgxp_get_gte_sxy(uint32_t index, int32_t *x16, int32_t *y16); +/* Validate-on-read variant: check!=0 additionally requires the shadow's packed + * word to equal expect (the live guest SXY word) - the engine's single safety + * invariant, which sign-consumers must not skip. */ +int pgxp_get_gte_sxy_checked(uint32_t index, uint32_t expect, int check, + int32_t *x16, int32_t *y16); /* Guest write to a GTE data register outside gte_execute (MTC2/CTC2 handled * by psx_pgxp_cop2; this is for direct gte_write_data paths): reg 15 performs @@ -98,6 +103,7 @@ typedef struct PGXPStats { uint64_t w_valid; /* lookups that also carried a usable depth */ uint64_t produced; /* RTPS/RTPT projections pushed into shadows */ uint64_t swc2_stores; /* GTE reg shadows copied to RAM shadows */ + uint64_t invalidations; /* generation bumps (each kills all shadows) */ } PGXPStats; void pgxp_get_stats(PGXPStats *out); diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index f4affc18..386fcd52 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -5213,6 +5213,11 @@ static void handle_geom_correction(int id, const char *json) * triangles that reach the predicate. */ uint64_t tc_att = 0, tc_arm = 0, tc_off = 0, tc_nosrc = 0, tc_noz = 0; gpu_texture_correction_stats(&tc_att, &tc_arm, &tc_off, &tc_nosrc, &tc_noz); + /* Exact-sign NCLIP coverage: fallbacks are frames' worth of faces decided + * by the integer sign (the flicker regime); invalidations counts shadow + * generation bumps (each one blinks the whole arena's provenance). */ + uint64_t nc_hit = 0, nc_fb = 0, nc_corr = 0; + gte_nclip_precise_stats(&nc_hit, &nc_fb, &nc_corr); send_fmt("{\"id\":%d,\"ok\":true," "\"geometry_correction\":%d," "\"geometry_vertex_hits\":%u," @@ -5224,7 +5229,10 @@ static void handle_geom_correction(int id, const char *json) "\"lookups\":%llu,\"dataflow_hit\":%llu,\"fallback_hit\":%llu," "\"native\":%llu,\"value_mismatch\":%llu,\"trunc_reject\":%llu," "\"tolerance_reject\":%llu,\"w_valid\":%llu," - "\"produced\":%llu,\"swc2_stores\":%llu}}", + "\"produced\":%llu,\"swc2_stores\":%llu," + "\"invalidations\":%llu}," + "\"nclip\":{\"precise\":%llu,\"fallback\":%llu," + "\"corrected\":%llu}}", id, gte_geometry_correction_enabled(), (unsigned)hits, @@ -5243,7 +5251,10 @@ static void handle_geom_correction(int id, const char *json) (unsigned long long)ps.tolerance_reject, (unsigned long long)ps.w_valid, (unsigned long long)ps.produced, - (unsigned long long)ps.swc2_stores); + (unsigned long long)ps.swc2_stores, + (unsigned long long)ps.invalidations, + (unsigned long long)nc_hit, (unsigned long long)nc_fb, + (unsigned long long)nc_corr); } /* pgxp — live-tune the value-propagation engine for one-toggle isolation runs diff --git a/runtime/src/gte.cpp b/runtime/src/gte.cpp index 351078ea..0c29be74 100644 --- a/runtime/src/gte.cpp +++ b/runtime/src/gte.cpp @@ -932,6 +932,19 @@ void gte_rtpt(GTEState* gte, uint32_t instr) { // --------------------------------------------------------------------------- // NCLIP (0x06) — Normal Clipping (2D cross product for backface culling) // --------------------------------------------------------------------------- +/* Exact-sign coverage counters (free-running; debug server geom_correction + * reply). A fallback means the integer sign decided — the regime where the + * widescreen X-squash rounding can flip a thin face for a frame. */ +static uint64_t s_nclip_precise_hits = 0; +static uint64_t s_nclip_fallbacks = 0; +static uint64_t s_nclip_corrected = 0; +extern "C" void gte_nclip_precise_stats(uint64_t *hits, uint64_t *fallbacks, + uint64_t *corrected) { + if (hits) *hits = s_nclip_precise_hits; + if (fallbacks) *fallbacks = s_nclip_fallbacks; + if (corrected) *corrected = s_nclip_corrected; +} + void gte_nclip(GTEState* gte, uint32_t instr) { gte->FLAG = 0; int32_t sx0 = static_cast(gte->SXY[0] & 0xFFFF); @@ -955,17 +968,22 @@ void gte_nclip(GTEState* gte, uint32_t instr) { * substitute is honest about the face's true orientation. */ int32_t px0, py0, px1, py1, px2, py2; if (gpu_ws_precise_nclip_enabled() && - pgxp_get_gte_sxy(0, &px0, &py0) && - pgxp_get_gte_sxy(1, &px1, &py1) && - pgxp_get_gte_sxy(2, &px2, &py2)) { + !s_gte_replay_sandbox && s_speculative_depth == 0 && + pgxp_get_gte_sxy_checked(0, gte->SXY[0], 1, &px0, &py0) && + pgxp_get_gte_sxy_checked(1, gte->SXY[1], 1, &px1, &py1) && + pgxp_get_gte_sxy_checked(2, gte->SXY[2], 1, &px2, &py2)) { + s_nclip_precise_hits++; const double fx0 = px0 / 65536.0, fy0 = py0 / 65536.0; const double fx1 = px1 / 65536.0, fy1 = py1 / 65536.0; const double fx2 = px2 / 65536.0, fy2 = py2 / 65536.0; const double cross = fx0 * (fy1 - fy2) + fx1 * (fy2 - fy0) + fx2 * (fy0 - fy1); - if (cross > 0.0 && out <= 0) out = 1; - else if (cross < 0.0 && out >= 0) out = -1; + if (cross > 0.0 && out <= 0) { out = 1; s_nclip_corrected++; } + else if (cross < 0.0 && out >= 0) { out = -1; s_nclip_corrected++; } + } else if (gpu_ws_precise_nclip_enabled() && + !s_gte_replay_sandbox && s_speculative_depth == 0) { + s_nclip_fallbacks++; } gte->MAC0 = out; gte->set_error_flag(); @@ -1565,11 +1583,19 @@ extern "C" int gte_replay_side_effects_begin(void) { if (s_gte_replay_sandbox) return 0; s_gte_replay_saved_caller_ra = s_gte_caller_ra; s_gte_replay_sandbox = 1; + /* Replay passes must be shadow-silent: the record pass already wrote the + * authoritative shadows, and a replay's memory hooks re-validating stale + * FIFO shadows against different vertices drop flags and overwrite live + * slots (arena-wide provenance kill => one-frame integer-sign NCLIP + * blinks under overlay diff mode). Suppression makes every psx_pgxp_* + * hook a no-op for the bracket, like the speculative validation passes. */ + pgxp_suppress_begin(); return 1; } extern "C" void gte_replay_side_effects_end(void) { using namespace PSXRecomp::GTE; if (!s_gte_replay_sandbox) return; + pgxp_suppress_end(); s_gte_caller_ra = s_gte_replay_saved_caller_ra; s_gte_replay_sandbox = 0; } diff --git a/runtime/src/pgxp.cpp b/runtime/src/pgxp.cpp index 8c6bc86d..80932146 100644 --- a/runtime/src/pgxp.cpp +++ b/runtime/src/pgxp.cpp @@ -93,6 +93,7 @@ static PGXPStats s_stats; extern "C" void pgxp_invalidate_all(void) { if (s_suppress != 0) { s_deferred_invalidate = 1; return; } + s_stats.invalidations++; if (++s_gen == 0) { /* generation wrapped: physically clear so stale slots can't revive */ if (s_ram) std::memset(s_ram, 0, s_ram_words * sizeof(PGXPValue)); @@ -104,6 +105,7 @@ extern "C" void pgxp_invalidate_all(void) { } extern "C" void pgxp_set_enabled(int enabled) { + int resized = 0; if (enabled) { /* Cover the REAL guest RAM, not the stock 2 MB. With the 8 MB mod the * extension is real memory, and a 2 MB-wrapped shadow aliased every @@ -129,11 +131,17 @@ extern "C" void pgxp_set_enabled(int enabled) { std::free(s_ram); s_ram = fresh; s_ram_words = want; + resized = 1; } else if (!s_ram) { enabled = 0; /* fail closed: stay faithful */ } } } + /* Idempotence: a redundant re-derive (config apply, correction toggle, + * debug-server A/B) must not cost a generation bump - every bump kills + * all live shadows and blinks one frame of exact-sign NCLIP coverage. */ + if (s_enabled == (enabled ? 1 : 0) && !resized) + return; s_enabled = enabled ? 1 : 0; pgxp_invalidate_all(); recompute_active(); @@ -724,10 +732,23 @@ extern "C" void pgxp_gte_push_sxy(int32_t x16, int32_t y16, uint16_t sz3, } extern "C" int pgxp_get_gte_sxy(uint32_t index, int32_t *x16, int32_t *y16) { + return pgxp_get_gte_sxy_checked(index, 0u, 0, x16, y16); +} + +/* Validate-on-read variant: the engine's single safety invariant is that a + * shadow is believed only while its packed word matches the live guest word. + * gte_nclip was the one consumer skipping that check, so any staleness class + * (e.g. an SXYP MTC2 shifting the real FIFO while shadows 12/13 keep + * describing pre-shift words) could invent a silently WRONG winding sign + * instead of an honest integer fallback. check!=0 enforces the compare. */ +extern "C" int pgxp_get_gte_sxy_checked(uint32_t index, uint32_t expect, + int check, int32_t *x16, int32_t *y16) { if (index >= 4) return 0; const PGXPValue *pv = &s_gte[12 + index]; if (!pv_live(pv) || (pv->flags & PGXP_F_VXY) != PGXP_F_VXY) return 0; + if (check && pv->value != expect) + return 0; if (x16) *x16 = pv->x16; if (y16) *y16 = pv->y16; return 1;