pgxp/gte: exact-sign NCLIP hardening — validate-on-read, shadow-silent replay, idempotent enable

Root-causes the transient 1-frame sign-corner flicker at wide aspects
(sign faces blink when the exact-sign NCLIP path silently loses its
shadows for one frame):

- gte_nclip was the only shadow consumer skipping the engine's single
  safety invariant (packed-word validation). A stale FIFO shadow (e.g.
  after an SXYP MTC2 shifts the real FIFO but not shadows 12/13) could
  invent a silently WRONG winding sign. New pgxp_get_gte_sxy_checked
  validates the shadow's word against the live SXY register; staleness
  now demotes to the honest integer fallback.
- Overlay shadow-diff REPLAY passes ran the pgxp memory hooks live
  (record pass's stale FIFO shadows re-validated against different
  vertices -> flags dropped, live slots overwritten -> arena-wide
  provenance kill = one-frame integer-sign blink, plus false
  SHDIFF_GTE_DATA divergences permanently demoting shards to interp).
  gte_replay_side_effects_begin/end now bracket pgxp_suppress so replay
  is shadow-silent, like the speculative validation passes. The nclip
  precise path is additionally gated off inside the sandbox and
  speculative passes.
- pgxp_set_enabled is now idempotent: a redundant re-derive (config
  apply, correction toggle, debug A/B) no longer costs a generation
  bump, which killed every live shadow and blinked one frame of
  exact-sign coverage.
- Observability: free-running NCLIP precise/fallback/corrected counters
  and a PGXPStats.invalidations counter (bumped at the generation-bump
  site), both exposed in the geom_correction debug reply.

All changes are inert at 4:3 (ws_precise_nclip gate) and touch
host-only state exclusively.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
tetrisgm
2026-08-19 10:39:10 -07:00
parent d9f31e1efc
commit ada331a401
5 changed files with 114 additions and 7 deletions
+43
View File
@@ -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
+6
View File
@@ -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);
+13 -2
View File
@@ -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
+31 -5
View File
@@ -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<int16_t>(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;
}
+21
View File
@@ -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;