From 4ff88fcee8b26b8ab1730f0d405c9a2ff4cb997f Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Sun, 11 Oct 2026 00:22:59 -0700 Subject: [PATCH] Preserve CPU near-clip projections and crossing quad winding --- runtime/include/mod_plugins.h | 12 ++++ runtime/src/gpu.c | 6 +- runtime/src/gte.cpp | 46 ++++++++++++++- runtime/src/pgxp.cpp | 25 ++++++++ runtime/tests/test_gte_register_access.cpp | 65 +++++++++++++++++++++ runtime/tests/test_native_wide_projection.c | 15 +++++ 6 files changed, 165 insertions(+), 4 deletions(-) diff --git a/runtime/include/mod_plugins.h b/runtime/include/mod_plugins.h index 819db0a6..8bea0e67 100644 --- a/runtime/include/mod_plugins.h +++ b/runtime/include/mod_plugins.h @@ -283,6 +283,18 @@ void psx_mod_anchor_hud_primitive(uint32_t primitive, int edge); * Both services are render-only, word guarded and inert at native 4:3. */ void psx_mod_tag_screen_mask_quad(uint32_t primitive); void psx_mod_tag_radial_screen_mask_quad(uint32_t primitive, float scale); +/* A verified CPU near-clip producer may publish its unclamped screen point. + * The original aligned RAM word must equal the producer's +/-1023 clamp; + * the complete word is rechecked through cpu->read_word. Metadata follows + * ordinary tracked loads/stores and is invalidated by every later writer. + * This records presentation provenance only; RAM and CPU state are untouched. */ +int psx_mod_record_cpu_projected_vertex(const struct CPUState* cpu, + uint32_t address, uint32_t expected, int32_t x, int32_t y, + uint16_t depth, uint16_t near_depth); +/* A verified quad renderer may prove its fourth source corner lies in front + * when the three current NCLIP corners are behind the near plane. This only + * supplements the current full-word winding proof; guest registers stay stock. */ +int psx_mod_extend_nclip_for_quad_vertex(const struct CPUState* cpu, uint32_t vertex); /* Exclude a known world packet from screen-space backdrop stretching, even * if it sorts before the first shaded polygon. Zero clears a recycled tag. */ void psx_mod_tag_world_primitive(uint32_t primitive, int is_world); diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index cfbe5184..d0b641e1 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -5239,7 +5239,11 @@ static int native_wide_projective_draw(void) { PGXPProjection p; if (!pgxp_load_projection(gp0_cmd_source_addr+4*index,word,&p)) return 0; int32_t x,y; parse_vertex(word,&x,&y); - needs_clip |= p.zread_word || (vertex&3u) || physical<0x10000u || + physical>0x200000u-8u || !gpu_ws_precise_nclip_enabled() || + !gpu_ws_projective_clipping_supported() || !pgxp_projection_tracking() || + s_gte_replay_sandbox || s_speculative_depth || + s_nclip_last_generation!=s_geom_generation || + (int32_t)cpu->gte_data[24]!=s_nclip_last_native) return 0; + PGXPProjection p[3]; + for (unsigned i=0;i<3;++i) { + if (!pgxp_get_gte_projection(i,cpu->gte_data[12+i],&p[i]) || + !std::isfinite(p[i].x) || !std::isfinite(p[i].y) || + !std::isfinite(p[i].z) || !std::isfinite(p[i].near_z) || + p[i].near_z<=0 || p[i].z>=p[i].near_z) return 0; + } + const uint32_t xy=cpu->read_word(vertex), zw=cpu->read_word(vertex+4u); + const int16_t v[3]={(int16_t)xy,(int16_t)(xy>>16),(int16_t)zw}; + const int16_t r[3]={(int16_t)cpu->gte_ctrl[3],(int16_t)(cpu->gte_ctrl[3]>>16), + (int16_t)cpu->gte_ctrl[4]}; + const int64_t mac3=(int64_t)(int32_t)cpu->gte_ctrl[7]*4096 + + (int64_t)r[0]*v[0]+(int64_t)r[1]*v[1]+(int64_t)r[2]*v[2]; + /* Fail closed when the fourth transform would overflow the GTE MAC lane. */ + if (mac3<-(1ll<<43) || mac3>=(1ll<<43) || + (double)(mac3>>12)> 16; const int y = exact_y[i] >> 16; - checked &= x > -4096 && x < 4096 && y > -4096 && y < 4096; + checked &= exact_x[i] > -4096 * 65536 && exact_x[i] < 4096 * 65536 - 1 && + exact_y[i] > -4096 * 65536 && exact_y[i] < 4096 * 65536 - 1; checked &= gte->SZ[i + 1] >= gte_h_scaled(gte) / 2 && gte->SZ[i + 1] != 0; if (raw_x[i] == 1023 && x > 1023) saturated = true; else if (raw_x[i] == -1024 && x < -1024) saturated = true; @@ -1539,15 +1573,21 @@ void gte_nclip(GTEState* gte, uint32_t instr) { if (gpu_ws_precise_nclip_enabled() && gpu_ws_projective_clipping_supported() && pgxp_projection_tracking() && !s_gte_replay_sandbox && s_speculative_depth == 0) { PGXPProjection p[3]; - bool valid = true, crosses = false, front = false; + bool valid = true, crosses = false, front = false, saturated = false; for (unsigned i=0;i<3;i++) { if (!pgxp_get_gte_projection(i, gte->SXY[i], &p[i])) { valid=false; break; } valid &= std::isfinite(p[i].x) && std::isfinite(p[i].y) && std::isfinite(p[i].z) && std::isfinite(p[i].near_z) && p[i].near_z > 0; crosses |= p[i].z < p[i].near_z; front |= p[i].z >= p[i].near_z; + const int x = (int16_t)(gte->SXY[i] & 0xffffu); + const int y = (int16_t)(gte->SXY[i] >> 16); + saturated |= x == -1024 || x == 1023 || y == -1024 || y == 1023; } - if (valid && crosses && front) { + /* Close, forward-facing terrain can exceed the bounded 16.16 + * transport without crossing the camera. Its clamped SXY winding + * is equally unreliable. The full homogeneous source stays valid. */ + if (valid && (crosses || saturated) && front) { const double determinant = (double)p[0].x * ((double)p[1].y*p[2].z-(double)p[1].z*p[2].y) - (double)p[0].y * ((double)p[1].x*p[2].z-(double)p[1].z*p[2].x) + diff --git a/runtime/src/pgxp.cpp b/runtime/src/pgxp.cpp index b37936fd..e2c11775 100644 --- a/runtime/src/pgxp.cpp +++ b/runtime/src/pgxp.cpp @@ -1481,6 +1481,31 @@ extern "C" void pgxp_gte_set_projection(const PGXPProjection *projection) { s_gte[14].flags |= PGXP_F_PROJECTION; s_gte[15] = s_gte[14]; } + +extern "C" int psx_mod_record_cpu_projected_vertex(const CPUState* cpu, + uint32_t address, uint32_t expected, int32_t x, int32_t y, + uint16_t depth, uint16_t near_depth) { + const uint32_t physical = address & 0x1fffffffu; + if (!g_pgxp_active || !s_projection_tracking || !cpu || !cpu->read_word || + (address & 3u) || physical < 0x10000u || + physical > psx_ram_live_bytes() - 4u || !depth || !near_depth || + depth < near_depth || x < -1048576 || x > 1048576 || + y < -1048576 || y > 1048576) return 0; + const int32_t cx = x < -1023 ? -1023 : x > 1023 ? 1023 : x; + const int32_t cy = y < -1023 ? -1023 : y > 1023 ? 1023 : y; + const uint32_t clamped = (uint16_t)cx | ((uint32_t)(uint16_t)cy << 16); + if (expected != clamped || cpu->read_word(address) != expected) return 0; + PGXPValue* pv = pgxp_ptr(address); + if (!pv) return 0; + pv->value = expected; pv->gen = s_gen; + /* The ordinary precision tier remains at the original CPU coordinates. + * Only the explicit full projection carries values outside those rails. */ + pv->x16 = cx * 65536; pv->y16 = cy * 65536; pv->z = depth; + pv->flags = PGXP_F_VXY | PGXP_F_VZ | PGXP_F_PROJECTION; + pv->projection = {(float)((double)x * depth), (float)((double)y * depth), + (float)depth, (float)near_depth}; + return 1; +} static int projection_read(const PGXPValue *pv, uint32_t packed, PGXPProjection *out) { const uint16_t flags = PGXP_F_VXY | PGXP_F_PROJECTION; if (!s_enabled || !s_projection_tracking || !pv || pv->gen != s_gen || pv->value != packed || diff --git a/runtime/tests/test_gte_register_access.cpp b/runtime/tests/test_gte_register_access.cpp index 92844d68..39cb3c40 100644 --- a/runtime/tests/test_gte_register_access.cpp +++ b/runtime/tests/test_gte_register_access.cpp @@ -62,6 +62,13 @@ extern "C" void gte_test_execute_reference(CPUState *cpu, uint32_t cmd); extern "C" int gpu_ws_present_native_43(void) { return 0; } static int g_test_precise_nclip_enabled; static int g_test_projective_supported = 1; +static uint32_t g_test_cpu_projection_word; +static uint32_t g_test_quad_vertex[2]; +static uint32_t cpu_projection_read(uint32_t address) { + if (address==0x80019000u) return g_test_quad_vertex[0]; + if (address==0x80019004u) return g_test_quad_vertex[1]; + return g_test_cpu_projection_word; +} extern "C" int gpu_ws_projective_clipping_supported(void) { return g_test_projective_supported; } extern "C" int gpu_ws_precise_nclip_enabled(void) { return g_test_precise_nclip_enabled; @@ -823,9 +830,42 @@ int test_saturated_nclip_keeps_architectural_result() { if (psx_mod_projected_triangle_clipping_supported(&cpu)) return fail_value("background clipping refuses unsupported renderer",0,0x30,0,0,1); g_test_projective_supported=1; + /* All corners remain forward, but their screen projections exceed the + * 16.16 transport window and collapse onto one hardware corner. */ + cpu.gte_data[0]=8000u|(8000u<<16); cpu.gte_data[1]=300; + cpu.gte_data[2]=9000u|(7000u<<16); cpu.gte_data[3]=300; + cpu.gte_data[4]=10000u|(9000u<<16); cpu.gte_data[5]=300; + gte_execute(&cpu,0x80030u); + gte_execute(&cpu,6); + if (cpu.gte_data[24]!=0 || !gte_nclip_native_wide_sign(0,&sign) || sign!=1) + return fail_value("forward winding beyond fixed-point transport",0,6,0,1,sign); + g_test_projective_supported=0; + gte_execute(&cpu,6); + if (gte_nclip_native_wide_sign(0,&sign)) + return fail_value("unbounded winding requires host clipper",0,6,0,0,1); + g_test_projective_supported=1; cpu.gte_data[12]^=1u; if (psx_mod_projected_triangle_clipping_supported(&cpu)) return fail_value("background clipping requires all original SXY words",0,0x30,0,0,1); + /* The first triangle of a crossing quad may be wholly behind. Only its + * actual fourth source vertex can establish a front component. */ + cpu.gte_data[0]=0;cpu.gte_data[1]=(uint16_t)-300; + cpu.gte_data[2]=100;cpu.gte_data[3]=(uint16_t)-300; + cpu.gte_data[4]=100u<<16;cpu.gte_data[5]=(uint16_t)-300; + cpu.read_word=cpu_projection_read;g_test_quad_vertex[0]=0;g_test_quad_vertex[1]=300; + gte_execute(&cpu,0x80030u);gte_execute(&cpu,6); + CHECK(!gte_nclip_native_wide_sign((int32_t)cpu.gte_data[24],&sign)); + const CPUState quad_before=cpu; + CHECK(psx_mod_extend_nclip_for_quad_vertex(&cpu,0x80019000u)); + CHECK(gte_nclip_native_wide_sign((int32_t)cpu.gte_data[24],&sign) && sign==-1); + CHECK(!std::memcmp(&quad_before,&cpu,sizeof cpu)); + gte_execute(&cpu,6);g_test_quad_vertex[1]=(uint16_t)-300; + CHECK(!psx_mod_extend_nclip_for_quad_vertex(&cpu,0x80019000u)); + CHECK(!gte_nclip_native_wide_sign((int32_t)cpu.gte_data[24],&sign)); + g_test_quad_vertex[1]=300;g_test_projective_supported=0; + CHECK(!psx_mod_extend_nclip_for_quad_vertex(&cpu,0x80019000u));g_test_projective_supported=1; + CHECK(!psx_mod_extend_nclip_for_quad_vertex(&cpu,0x80019001u)); + cpu.gte_data[12]^=1u;CHECK(!psx_mod_extend_nclip_for_quad_vertex(&cpu,0x80019000u)); pgxp_set_projection_tracking(old_tracking); g_test_precise_nclip_enabled = 0; return 0; @@ -1438,6 +1478,31 @@ int main() { } gte_set_fov_scale(1,1); pgxp_set_projection_tracking(0); + /* Explicit CPU-generated intersections retain their original word while + * carrying full unclamped projection through ordinary memory transport. */ + pgxp_set_enabled(1); pgxp_set_projection_tracking(1); + CPUState clipped_cpu{}; clipped_cpu.read_word=cpu_projection_read; + const uint32_t clipped_addr=0x80018000u, copied_addr=clipped_addr+4; + const uint32_t clipped_word=(uint16_t)-1023|((uint32_t)(uint16_t)1023<<16); + g_test_cpu_projection_word=clipped_word; + CHECK(psx_mod_record_cpu_projected_vertex(&clipped_cpu,clipped_addr,clipped_word,-6000,8000,320,320)); + PGXPProjection cp; + CHECK(pgxp_load_projection(clipped_addr,clipped_word,&cp)); + CHECK(cp.x==-6000.f*320.f && cp.y==8000.f*320.f && cp.z==320 && cp.near_z==320); + psx_pgxp_load(&clipped_cpu,0x8c020000u,clipped_addr,clipped_word); + psx_pgxp_store(&clipped_cpu,0xac020000u,copied_addr,clipped_word); + CHECK(pgxp_load_projection(copied_addr,clipped_word,&cp)); + CHECK(!pgxp_load_projection(copied_addr,clipped_word^1,&cp)); + CHECK(!psx_mod_record_cpu_projected_vertex(&clipped_cpu,clipped_addr+1,clipped_word,-6000,8000,320,320)); + CHECK(!psx_mod_record_cpu_projected_vertex(&clipped_cpu,clipped_addr,clipped_word,-200,8000,320,320)); + CHECK(!psx_mod_record_cpu_projected_vertex(&clipped_cpu,clipped_addr,clipped_word,-6000,8000,319,320)); + CHECK(!psx_mod_record_cpu_projected_vertex(&clipped_cpu,clipped_addr,clipped_word,-1048577,8000,320,320)); + g_test_cpu_projection_word^=1; + CHECK(!psx_mod_record_cpu_projected_vertex(&clipped_cpu,clipped_addr,clipped_word,-6000,8000,320,320)); + pgxp_invalidate_word(copied_addr);CHECK(!pgxp_load_projection(copied_addr,clipped_word,&cp)); + pgxp_invalidate_all();CHECK(!pgxp_load_projection(clipped_addr,clipped_word,&cp)); + pgxp_set_projection_tracking(0); + std::puts("PASS: CPU near intersections preserve full-word projection and writer invalidation"); std::puts("PASS: canonical GTE register helpers match GTEState transfer oracle"); return 0; } diff --git a/runtime/tests/test_native_wide_projection.c b/runtime/tests/test_native_wide_projection.c index e11b399b..a82ee66f 100644 --- a/runtime/tests/test_native_wide_projection.c +++ b/runtime/tests/test_native_wide_projection.c @@ -165,6 +165,21 @@ int main(void) { fixture_projective_supported=1;gp0_cmd_source_addr=UINT32_MAX; assert(!native_wide_projective_draw()); assert(fixture_projective_draws==2); + /* The retail CPU clipper uses -1023, with an explicit full projection + * supplied by its verified producer. It must reach normal host clipping. */ + fixture_projective_draws=0;gp0_cmd_source_addr=0x10004u; + for(unsigned i=0;i<4;++i) { + unsigned k=1+3*i; + gp0_cmd_buf[k]=pack_vertex(i==1?-1023:100+i*30,40+i*20); + fixture_homogeneous[i].addr=gp0_cmd_source_addr+4*k; + fixture_homogeneous[i].word=gp0_cmd_buf[k]; + fixture_homogeneous[i].p=(PGXPProjection){(i==1?-6000.f:100.f+i*30.f)*500.f, + (40.f+i*20.f)*500.f,500,320};fixture_homogeneous[i].valid=1; + } + assert(native_wide_projective_draw() && fixture_projective_draws==2); + assert(fixture_projective_vertices[1].x==-6000.f*500.f); + fixture_homogeneous[3].valid=0;assert(!native_wide_projective_draw()); + assert(fixture_projective_draws==2); puts("native_wide_projection_test: PASS"); return 0; }