diff --git a/runtime/include/gte_nclip_stats.h b/runtime/include/gte_nclip_stats.h index 2812f5b6..e4b65c2a 100644 --- a/runtime/include/gte_nclip_stats.h +++ b/runtime/include/gte_nclip_stats.h @@ -48,6 +48,14 @@ void gte_nclip_stats_reset(void); * guest just read from MAC0), else the native sign. */ int32_t gte_nclip_exact_sign(int32_t native_mac0, uint32_t pc); +/* Opt-in thin-face rescue for exact-word BLEZ consumers. Unlike the separate + * horizontal-saturation policy, this only changes an architectural zero to a + * validated positive winding. Registration is host-side; the existing overlay + * ws_nclip_branch callback and its ABI remain unchanged. */ +void psx_mod_set_native_wide_nclip_zero_sites(const uint32_t *addresses, + const uint32_t *expected, int count); +int gte_nclip_zero_positive(int32_t native_mac0, uint32_t pc); + /* Kill switch for the exact-site consumer (default on): off makes every exact * site test the native sign, for same-build/same-state A/B and diagnosis. */ void gte_nclip_exact_set_enabled(int on); diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index 3ef0a3f1..7eb4d9ca 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -26,6 +26,7 @@ #include "crash_trace.h" #include "debug_server.h" #include "cpu_state.h" +#include "gte_nclip_stats.h" #include "event_ring.h" #include "psx_video_timing.h" #include "color_lut.h" @@ -964,8 +965,25 @@ int psx_ws_masked_reject_site(uint32_t pc, uint32_t instr, static WsMaskedRejectSite ws_nclip_sites[WS_EXPLICIT_CULL_SITES_MAX]; static uint8_t ws_nclip_previous[WS_EXPLICIT_CULL_SITES_MAX]; static int ws_nclip_site_count; +static WsMaskedRejectSite ws_nclip_zero_sites[WS_EXPLICIT_CULL_SITES_MAX]; +static int ws_nclip_zero_site_count; static uint64_t ws_nclip_rescues; uint64_t gpu_ws_native_wide_nclip_rescues(void) { return ws_nclip_rescues; } +void psx_mod_set_native_wide_nclip_zero_sites(const uint32_t* addresses, + const uint32_t* expected, int count) { + if (count < 0 || count > WS_EXPLICIT_CULL_SITES_MAX || + (count && (!addresses || !expected))) abort(); + for (int i = 0; i < count; ++i) { + /* Only BLEZ rs,reject is proved here; positive/negative nonzero + * winding and all other branch shapes retain their native result. */ + if ((addresses[i] & 3u) || expected[i] >> 26 != 6u || + ((expected[i] >> 16) & 31u) != 0u || + ((expected[i] >> 21) & 31u) == 0u) abort(); + ws_nclip_zero_sites[i].address = addresses[i] & 0x1FFFFFFFu; + ws_nclip_zero_sites[i].expected = expected[i]; + } + ws_nclip_zero_site_count = count; +} void psx_mod_set_native_wide_nclip_sites(const uint32_t* addresses, const uint32_t* expected, int count) { if (count < 0 || count > WS_EXPLICIT_CULL_SITES_MAX) abort(); @@ -993,6 +1011,15 @@ extern int gte_nclip_native_wide_sign(int32_t mac0, int* sign); extern int gte_nclip_native_wide_previous_sign(int32_t mac0, int* sign); int psx_ws_nclip_branch(uint32_t pc, uint32_t instr, int32_t mac0, int vanilla) { if (psx_ws_x_margin() <= 0 || !gpu_ws_precise_nclip_enabled()) return vanilla; + for (int i = 0; i < ws_nclip_zero_site_count; ++i) { + if (ws_nclip_zero_sites[i].address != (pc & 0x1FFFFFFFu) || + ws_nclip_zero_sites[i].expected != instr) continue; + if (mac0 == 0 && gte_nclip_zero_positive(mac0, pc)) { + ++ws_nclip_rescues; + return 0; + } + return vanilla; + } for (int i = 0; i < ws_nclip_site_count; ++i) { if (ws_nclip_sites[i].address != (pc & 0x1FFFFFFFu) || ws_nclip_sites[i].expected != instr) continue; diff --git a/runtime/src/gte.cpp b/runtime/src/gte.cpp index 58d4e981..297e53e2 100644 --- a/runtime/src/gte.cpp +++ b/runtime/src/gte.cpp @@ -1189,6 +1189,7 @@ static int32_t s_nclip_last_native = 0; static int8_t s_nclip_last_precise_sign = 0; static bool s_nclip_last_precise_valid = false; static bool s_nclip_last_horizontal_saturated = false; +static bool s_nclip_last_zero_safe = false; static uint32_t s_nclip_last_generation; static int32_t s_nclip_previous_native; static int8_t s_nclip_previous_precise_sign; @@ -1197,6 +1198,7 @@ static bool s_nclip_previous_horizontal_saturated; static uint32_t s_nclip_previous_generation; static void gte_nclip_precision_invalidate() { s_nclip_last_precise_valid = false; + s_nclip_last_zero_safe = false; s_nclip_previous_precise_valid = false; } extern "C" void gte_nclip_precise_stats(uint64_t *hits, uint64_t *fallbacks, @@ -1270,6 +1272,26 @@ extern "C" int gte_nclip_native_wide_sign(int32_t native_mac0, int* sign) { *sign = s_nclip_last_precise_sign; return 1; } +extern "C" int gte_nclip_zero_positive(int32_t native_mac0, uint32_t pc) { + const bool usable = native_mac0 == 0 && s_nclip_last_native == 0 && + s_nclip_exact_enabled && gpu_ws_precise_nclip_enabled() && + s_nclip_last_precise_valid && s_nclip_last_zero_safe && + s_nclip_last_generation == s_geom_generation && + !s_gte_replay_sandbox && s_speculative_depth == 0 && + !psx_overlay_shadow_diff_active(); + const bool rescue = usable && s_nclip_last_precise_sign > 0; + if (!s_gte_replay_sandbox && s_speculative_depth == 0) { + if (GteNclipSiteStat *e = nclip_find(s_nclip_site, pc, + &GteNclipSiteStat::pc, + &s_nclip_site_dropped)) { + e->evals++; + if (!usable) e->fallbacks++; + else if (rescue) e->flips++; + e->last_frame = (uint32_t)s_frame_count; + } + } + return rescue; +} /* Some guarded quad consumers save two MAC0 values before branching. Their * first branch must use the first command's provenance, even when both native * results are equal. This is explicitly selected by the title, never searched @@ -1335,6 +1357,7 @@ void gte_nclip(GTEState* gte, uint32_t instr) { s_nclip_last_native = out; s_nclip_last_precise_valid = false; s_nclip_last_horizontal_saturated = false; + s_nclip_last_zero_safe = false; /* Compute an exact 16.16 determinant for configured branch consumers, but * preserve native guest-visible MAC0 for every architectural reader. */ int32_t px0, py0, px1, py1, px2, py2; @@ -1357,16 +1380,24 @@ void gte_nclip(GTEState* gte, uint32_t instr) { * the matching saturation rail. Ordinary subpixel winding stays stock. */ const int raw_x[3] = {sx0, sx1, sx2}, raw_y[3] = {sy0, sy1, sy2}; const int32_t exact_x[3] = {px0, px1, px2}, exact_y[3] = {py0, py1, py2}; - bool checked = true, saturated = false; + bool checked = true, saturated = false, zero_safe = true; for (int i = 0; i < 3; ++i) { const int x = exact_x[i] >> 16; checked &= (exact_y[i] >> 16) == raw_y[i] && x > -4096 && x < 4096; checked &= gte->SZ[i + 1] >= gte_h_scaled(gte) / 2 && gte->SZ[i + 1] != 0; + zero_safe &= raw_x[i] > -1024 && raw_x[i] < 1023 && + raw_y[i] > -1024 && raw_y[i] < 1023 && + 2u * gte->SZ[i + 1] > gte_h_scaled(gte); if (raw_x[i] == 1023 && x > 1023) saturated = true; else if (raw_x[i] == -1024 && x < -1024) saturated = true; else checked &= x == raw_x[i]; } s_nclip_last_horizontal_saturated = checked && saturated; + /* A separate, explicitly registered BLEZ policy may recover a thin + * face rounded to zero. Do not extend it to clamp/near-plane cases or + * nonzero architectural winding, and retain the checked FIFO/depth + * provenance used above. */ + s_nclip_last_zero_safe = out == 0 && checked && !saturated && zero_safe; const bool disagree = (cross > 0 && out <= 0) || (cross < 0 && out >= 0); if (disagree) s_nclip_disagreements++; @@ -2196,6 +2227,9 @@ static void gte_export_cpu_state(CPUState* cpu, static void gte_run_command(PSXRecomp::GTE::GTEState* gte, uint32_t cmd) { using namespace PSXRecomp::GTE; + /* A zero-rescue consumer must immediately follow its own NCLIP, not a + * later GTE command that happens to leave another zero in MAC0. */ + s_nclip_last_zero_safe = false; switch (cmd & 0x3Fu) { case 0x01: gte_rtps(gte, cmd); break; case 0x06: gte_nclip(gte, cmd); break; @@ -2482,6 +2516,7 @@ extern "C" uint32_t gte_read_ctrl(CPUState* cpu, uint8_t reg) { extern "C" void gte_write_data(CPUState* cpu, uint8_t reg, uint32_t val) { if (reg >= 32) return; + PSXRecomp::GTE::s_nclip_last_zero_safe = false; gte_cpu_canonicalize_backing(cpu); switch (reg) { case 7: case 16: case 17: case 18: case 19: @@ -2548,6 +2583,7 @@ extern "C" void gte_write_data(CPUState* cpu, uint8_t reg, uint32_t val) { extern "C" void gte_write_ctrl(CPUState* cpu, uint8_t reg, uint32_t val) { if (reg >= 32) return; + PSXRecomp::GTE::s_nclip_last_zero_safe = false; gte_cpu_canonicalize_backing(cpu); switch (reg) { case 4: case 12: case 20: case 26: case 27: case 29: case 30: diff --git a/runtime/tests/gpu_exec_stubs.inc b/runtime/tests/gpu_exec_stubs.inc index c4807742..739d12fd 100644 --- a/runtime/tests/gpu_exec_stubs.inc +++ b/runtime/tests/gpu_exec_stubs.inc @@ -415,6 +415,9 @@ int gte_nclip_native_wide_sign(int32_t mac0, int* sign) { int gte_nclip_native_wide_previous_sign(int32_t mac0, int* sign) { (void)mac0; (void)sign; return 0; } +int gte_nclip_zero_positive(int32_t mac0, uint32_t pc) { + (void)mac0; (void)pc; return 0; +} static int fixture_triangle_recovered; void gl_renderer_note_wide_triangle_recovery(int enabled) { fixture_triangle_recovered=enabled; diff --git a/runtime/tests/test_gte_register_access.cpp b/runtime/tests/test_gte_register_access.cpp index 0ed00e40..235e78fb 100644 --- a/runtime/tests/test_gte_register_access.cpp +++ b/runtime/tests/test_gte_register_access.cpp @@ -66,6 +66,8 @@ extern "C" int gpu_ws_precise_nclip_enabled(void) { return g_test_precise_nclip_enabled; } extern "C" void gpu_pgxp_rederive_enable(void) {} +/* This fixture has ordinary RAM only; no optional GPU DMA arena is allocated. */ +extern "C" int psx_mod_gpu_dma_memory_contains(uint32_t, uint32_t) { return 0; } static int g_test_shadow_diff = 0; extern "C" int psx_overlay_shadow_diff_active(void) { return g_test_shadow_diff; } extern "C" void psx_ws_note_gte_project(int) {} @@ -773,6 +775,7 @@ int test_saturated_nclip_keeps_architectural_result() { if (cpu.gte_data[24] != 0 || !gte_nclip_native_wide_sign(0, &sign) || sign != 1 || gte_nclip_native_wide_sign(1, &sign)) return fail_value("saturated winding rescue preserves native zero MAC0",0,6,0,0,cpu.gte_data[24]); + CHECK(!gte_nclip_zero_positive(0, 0x80011008u)); /* Both quad MAC0 values can saturate to the same zero. Keep each exact * sign separately: the preceding triangle points forward, latest back. */ for (unsigned i=0;i<3;i++) { @@ -805,6 +808,74 @@ int test_saturated_nclip_keeps_architectural_result() { return 0; } +int test_zero_nclip_requires_current_safe_projection() { + CPUState cpu{}; + gte_precision_tracking_set(1); + g_test_precise_nclip_enabled = 1; + pgxp_set_culling(0); + gte_nclip_exact_set_enabled(1); + /* Captured terrain quad801ACC44, first three vertices. Both first vertices + * quantize to (221,45), although their precise face has positive area. */ + const int16_t rt[] = {3518,0,-2096,-375,4029,-629,2061,732,3460}; + for (unsigned i = 0; i < 4; ++i) + cpu.gte_ctrl[i] = (uint16_t)rt[2*i] | ((uint32_t)(uint16_t)rt[2*i+1] << 16); + cpu.gte_ctrl[4] = (uint32_t)(int32_t)rt[8]; + cpu.gte_ctrl[5] = (uint32_t)-4521; cpu.gte_ctrl[6] = 2122; + cpu.gte_ctrl[7] = (uint32_t)-5132; + cpu.gte_ctrl[24] = 160u << 16; cpu.gte_ctrl[25] = 120u << 16; + cpu.gte_ctrl[26] = 350; + const int16_t v[3][3] = {{9970,-1315,6223},{9938,-1315,6172},{9970,-894,6223}}; + auto project = [&](bool reverse = false) { + for (unsigned i = 0; i < 3; ++i) { + unsigned j = reverse ? 2-i : i; + cpu.gte_data[2*i] = (uint16_t)v[j][0] | ((uint32_t)(uint16_t)v[j][1] << 16); + cpu.gte_data[2*i+1] = (uint32_t)(int32_t)v[j][2]; + } + gte_execute(&cpu, 0x00280030u); + gte_execute(&cpu, 6); + }; + const uint32_t site = 0x8013FF14u; + gte_nclip_stats_reset(); + project(); + CHECK(cpu.gte_data[24] == 0); + CHECK(gte_nclip_zero_positive(0, site)); + GteNclipSiteStat stat{}; + CHECK(gte_nclip_site_stats(&stat, 1) == 1 && stat.pc == site && + stat.evals == 1 && stat.flips == 1 && stat.fallbacks == 0); + CHECK(!gte_nclip_zero_positive(-1, site) && !gte_nclip_zero_positive(1, site)); + CHECK(cpu.gte_data[24] == 0); + gte_nclip_exact_set_enabled(0); + CHECK(!gte_nclip_zero_positive(0, site)); + gte_nclip_exact_set_enabled(1); + project(true); + CHECK(cpu.gte_data[24] == 0 && !gte_nclip_zero_positive(0, site)); + project(); + gte_execute(&cpu, 0x2D); + CHECK(!gte_nclip_zero_positive(0, site)); + project(); + gte_write_data(&cpu, 24, 0); + CHECK(!gte_nclip_zero_positive(0, site)); + project(); + gte_write_ctrl(&cpu, 26, 350); + CHECK(!gte_nclip_zero_positive(0, site)); + project(); + cpu.gte_data[17] = 10; + gte_execute(&cpu, 6); + CHECK(!gte_nclip_zero_positive(0, site)); + project(); + gte_precision_speculative_begin(); + CHECK(!gte_nclip_zero_positive(0, site)); + gte_precision_speculative_end(); + gte_precision_timeline_invalidate(); + CHECK(!gte_nclip_zero_positive(0, site)); + project(); + gte_test_seed_precise_projection(0, cpu.gte_data[12] ^ 1u, 221 << 16, 45 << 16, 4906); + gte_execute(&cpu, 6); + CHECK(!gte_nclip_zero_positive(0, site)); + g_test_precise_nclip_enabled = 0; + return 0; +} + /* PGXP precise culling (docs/ENHANCEMENTS.md G1.12): NCLIP's MAC0 takes the * exact sign only while geometry correction and culling are armed, only when * the three SXY shadows are believed, and never in a compared pass. */ @@ -1366,6 +1437,7 @@ int main() { if (int rc = test_precise_sxy_invalidation()) return rc; if (int rc = test_precise_nclip_is_title_scoped()) return rc; if (int rc = test_saturated_nclip_keeps_architectural_result()) return rc; + if (int rc = test_zero_nclip_requires_current_safe_projection()) return rc; if (int rc = test_precision_speculative_transaction()) return rc; if (int rc = test_pgxp_probe_does_not_count_geometry_lookup()) return rc; if (int rc = test_preserve_projection_is_shadow_only()) return rc; diff --git a/runtime/tests/test_ws_cull_edge_interp.c b/runtime/tests/test_ws_cull_edge_interp.c index 34d843f3..83dfe816 100644 --- a/runtime/tests/test_ws_cull_edge_interp.c +++ b/runtime/tests/test_ws_cull_edge_interp.c @@ -74,6 +74,9 @@ int mdec_recently_active(uint32_t f) { (void)f; return 0; } void gte_execute(CPUState *cpu, uint32_t cmd) { (void)cpu; (void)cmd; } static int precise_sign_valid, precise_sign; static int previous_sign_valid, previous_sign; +int gte_nclip_zero_positive(int32_t mac0, uint32_t pc) { + (void)mac0; (void)pc; return 0; +} int gte_nclip_native_wide_sign(int32_t mac0, int* sign) { (void)mac0; *sign = precise_sign; return precise_sign_valid; }