diff --git a/runtime/src/dirty_ram_interp.c b/runtime/src/dirty_ram_interp.c index 641b3d73..82213e5d 100644 --- a/runtime/src/dirty_ram_interp.c +++ b/runtime/src/dirty_ram_interp.c @@ -2811,6 +2811,17 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_ * pages overwritten by a runtime overlay (Tomba 2), not just [FLOOR, RAM). */ int allow_local_dirty_flow = phys_is_overlay_flow_region(phys); + /* Backend-invariant mod_function_entry hooks: generated code fires + * psx_mod_function_entry at listed function entries, but a mod-patched + * (dirty) page runs HERE instead and would silently skip them. Fire the + * same hook on interp dispatch so the contract does not depend on which + * backend executes the page. The runtime filters by exact address, so + * this is a short scan over the (tiny) registered-plugin list. */ + { + extern void psx_mod_function_entry(CPUState* cpu, uint32_t address); + psx_mod_function_entry(cpu, addr); + } + /* Per-PC entry counter (visible via dirty_ram_stats). */ DirtyRamPcEntry *pc_entry = pc_table_get_or_insert(phys); if (pc_entry) pc_entry->hits++; diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index 9f2dc6fc..d3a7ab12 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -219,7 +219,13 @@ static int ws_gameplay_state_value_count = 0; * this many consecutive frames (save/options/memory-card) — reverts to 4:3. */ #define WS_GTE_GAME_MODE_HYSTERESIS 45u void gpu_ws_set_gte_game_mode(int on) { ws_gte_game_mode_cfg = on ? 1 : 0; } -void gpu_ws_set_precise_nclip(int on) { ws_precise_nclip_cfg = on ? 1 : 0; } +void gpu_pgxp_rederive_enable(void); +void gpu_ws_set_precise_nclip(int on) { + ws_precise_nclip_cfg = on ? 1 : 0; + /* Precise NCLIP and clamp rescue consume PGXP dataflow shadows even when + * both correction features are off — re-derive the engine arm. */ + gpu_pgxp_rederive_enable(); +} int gpu_ws_precise_nclip_enabled(void) { return ws_precise_nclip_cfg && ws_active(); } void gpu_ws_set_gameplay_state_gate(uint32_t addr, const uint32_t *values, int nvalues) { @@ -3386,12 +3392,19 @@ static int s_texture_correction_enabled = 0; extern int gte_precision_load_word(uint32_t addr, uint32_t packed, int32_t *x16, int32_t *y16, uint16_t *z); +/* The PGXP dataflow engine feeds the two correction features AND the + * widescreen precision consumers (precise NCLIP, clamp rescue). Arm it while + * any of them is on. Every setter re-derives through here so init order and + * runtime toggles (debug server) can never leave the engine stale. */ +void gpu_pgxp_rederive_enable(void) { + pgxp_set_enabled(s_texture_correction_enabled || + gte_geometry_correction_enabled() || + ws_precise_nclip_cfg); +} + void gpu_texture_correction_set(int enabled) { s_texture_correction_enabled = enabled ? 1 : 0; - /* The PGXP dataflow engine feeds BOTH corrections; arm it while either - * is on (geometry correction is toggled in gte.cpp, so re-derive here). */ - pgxp_set_enabled(s_texture_correction_enabled || - gte_geometry_correction_enabled()); + gpu_pgxp_rederive_enable(); } int gpu_texture_correction_enabled(void) { @@ -3436,10 +3449,62 @@ int gpu_pgxp_census_dump(uint32_t *frame_out, uint32_t *tris, uint32_t *clean, return n; } +/* Clamp rescue (widescreen precision, rides the precise-NCLIP opt-in): a + * vertex parked exactly on a GTE saturation rail (+/-1024 screen units) is + * hardware-mangled — its true projection is somewhere far off-frame, and + * rasterizing the clamped position folds that error INTO the frame as the + * classic PS1 "polygon spike" (a sliver streaking across the screen). 4:3 + * framing plus CRT overscan hid most of these; a wide aspect stares right at + * them. When the address-keyed dataflow shadow knows the true sub-pixel + * position (clamped to +/-4096 px at the producer), substitute it for railed + * vertices only — every in-range vertex stays native, so normal geometry is + * bit-identical and shared edges cannot crack. */ +static int ws_clamp_rescue_vertex(uint32_t addr, uint32_t word, + int32_t raw_x, int32_t raw_y, + int32_t *px, int32_t *py) { + if (raw_x != -1024 && raw_x != 1023 && raw_y != -1024 && raw_y != 1023) + return 0; + uint16_t sz; + if (pgxp_get_precise_vertex(addr, word, raw_x, raw_y, px, py, &sz) + != PGXP_SRC_DATAFLOW) + return 0; + /* Only substitute when the shadow actually differs — a legit vertex that + * genuinely sits on the rail keeps its native position. */ + return ((*px >> 16) != raw_x) || ((*py >> 16) != raw_y); +} + static void prepare_precise_triangle(int i0, int i1, int i2, const int32_t vx[3], const int32_t vy[3]) { gr_set_perspective_triangle(0, 0.0f, 0.0f, 0.0f); if (!gte_geometry_correction_enabled()) { + if (gpu_ws_precise_nclip_enabled() && + gp0_cmd_source_addr != 0xFFFFFFFFu) { + const int idx[3] = { i0, i1, i2 }; + int32_t fx[3], fy[3]; + int any_rescued = 0; + for (int i = 0; i < 3; i++) { + uint32_t word = gp0_cmd_buf[idx[i]]; + int32_t raw_x, raw_y; + parse_vertex(word, &raw_x, &raw_y); + uint32_t addr = gp0_cmd_source_addr + (uint32_t)idx[i] * 4u; + int32_t px, py; + if (ws_clamp_rescue_vertex(addr, word, raw_x, raw_y, &px, &py)) { + any_rescued = 1; + fx[i] = (int32_t)((int64_t)px + + (int64_t)(vx[i] - raw_x) * 65536); + fy[i] = (int32_t)((int64_t)py + + (int64_t)(vy[i] - raw_y) * 65536); + } else { + fx[i] = vx[i] << 16; + fy[i] = vy[i] << 16; + } + } + if (any_rescued) { + gr_set_precise_triangle(1, fx[0],fy[0], fx[1],fy[1], + fx[2],fy[2]); + return; + } + } gr_set_precise_triangle(0, 0,0, 0,0, 0,0); return; } diff --git a/runtime/src/gte.cpp b/runtime/src/gte.cpp index 4ef73575..351078ea 100644 --- a/runtime/src/gte.cpp +++ b/runtime/src/gte.cpp @@ -312,6 +312,8 @@ static inline int64_t geom_slot(uint32_t packed) { return (int64_t)(y + GEOM_BIAS) * GEOM_AXIS + (x + GEOM_BIAS); } +extern "C" void gpu_pgxp_rederive_enable(void); + extern "C" void gte_geometry_correction_set(int enabled) { s_geom_enabled = enabled ? 1 : 0; s_geom_hits = 0; @@ -324,6 +326,7 @@ extern "C" void gte_geometry_correction_set(int enabled) { if (!s_geom_cache) s_geom_enabled = 0; /* fail closed: stay faithful */ } gte_geom_generation_advance(); + gpu_pgxp_rederive_enable(); } /* Lookup census for the debug server: attempted, hit, and the two miss classes. @@ -888,9 +891,17 @@ void gte_rtps_internal(GTEState* gte, int16_t* V, bool setMac0, uint32_t instr) int64_t sx = sx16 >> 16; int64_t sy = sy16 >> 16; gte->push_sxy(sx, sy); - if (!s_gte_replay_sandbox) - pgxp_gte_push_sxy((int32_t)sx16, (int32_t)sy16, gte->SZ[3], + if (!s_gte_replay_sandbox) { + /* Shadow the pre-truncation position, clamped to +/-4096 px so an + * extreme projection blowup can't wrap the 16.16 int32 and land a + * garbage shadow back inside the frame. Well past the +/-1024 + * hardware rails either way, so direction survives for clamp rescue. */ + const int64_t kLim = (int64_t)4096 << 16; + int64_t cx16 = sx16 < -kLim ? -kLim : (sx16 > kLim - 1 ? kLim - 1 : sx16); + int64_t cy16 = sy16 < -kLim ? -kLim : (sy16 > kLim - 1 ? kLim - 1 : sy16); + pgxp_gte_push_sxy((int32_t)cx16, (int32_t)cy16, gte->SZ[3], (uint32_t)gte->SXY[2]); + } geom_note((uint32_t)gte->SXY[2], sx16, sy16); // Step 5: Depth cueing (MAC0/IR0) — only for last vertex of RTPT or RTPS @@ -929,23 +940,34 @@ void gte_nclip(GTEState* gte, uint32_t instr) { int32_t sy1 = static_cast(gte->SXY[1] >> 16); int32_t sx2 = static_cast(gte->SXY[2] & 0xFFFF); int32_t sy2 = static_cast(gte->SXY[2] >> 16); - int32_t px0 = 0, py0 = 0, px1 = 0, py1 = 0, px2 = 0, py2 = 0; - 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)) { - sx0 = px0 >> 16; - sy0 = py0 >> 16; - sx1 = px1 >> 16; - sy1 = py1 >> 16; - sx2 = px2 >> 16; - sy2 = py2 >> 16; - } int64_t mac0 = (int64_t)sx0 * (sy1 - sy2) + (int64_t)sx1 * (sy2 - sy0) + (int64_t)sx2 * (sy0 - sy1); gte->check_mac0_overflow(mac0); - gte->MAC0 = static_cast(mac0); + int32_t out = static_cast(mac0); + /* Widescreen precise NCLIP: the X-squash scales winding areas by a + * positive factor, but integer SXY rounding flips the sign of thin + * glancing faces (trackside signs vanishing / flickering only at wide + * aspects). When the full sub-pixel shadow of all three vertices is + * live, take the SIGN from the exact cross product; keep the hardware + * magnitude so NCLIP-magnitude consumers (LOD heuristics) are untouched. + * A sign disagreement replaces a near-zero hardware value, so the +/-1 + * 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)) { + 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; + } + gte->MAC0 = out; gte->set_error_flag(); }