mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 10:19:47 -04:00
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:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user