diff --git a/runtime/include/pgxp.h b/runtime/include/pgxp.h index 8b5d4455..271a0a0e 100644 --- a/runtime/include/pgxp.h +++ b/runtime/include/pgxp.h @@ -62,7 +62,8 @@ int pgxp_position_fallback(void); * near geometry. Guest-visible SXY, MAC0 and FLAG are unchanged either way. * Because that position can then differ from the guest integer by more than * the fraction, truncation agreement becomes a window (PGXP_PPP_AGREE_*) and - * a vertex at the GTE's saturation limit must still agree exactly. One + * unclamped IR projections need an intact, value-validated GTE word; halves + * and cache fallbacks keep the strict saturation checks. One * consumer is not visual: [widescreen] precise_nclip takes its branch sign * from these shadows, so in a title that sets both, that (already * non-faithful) widescreen sign follows the exact projection too. */ @@ -91,6 +92,7 @@ int pgxp_preserve_projection(void); * when none). Returns 1 with the position in x16/y16 when the vertex * qualifies (sf=1, IR1/IR2 not saturated, SZ3 not saturated and nonzero, and * no divide overflow: H < 2*SZ3), else 0 and the caller keeps the IR path. */ +/* Return 2 for an opt-in IR-unclamped projection, 1 for ordinary precision. */ int pgxp_project_precise(int64_t mac1, int64_t mac2, int64_t mac3, int shift, int32_t ir1, int32_t ir2, uint32_t sz3, uint32_t h, int32_t ofx, int32_t ofy, int64_t x_num, int64_t x_den, @@ -164,6 +166,8 @@ void pgxp_suppress_end(void); /* Called at the RTPS/RTPT projection with the pre-truncation 16.16 screen * coordinates, the projected depth (SZ3), and the packed SXY word the guest * sees. Shifts the shadow FIFO exactly like push_sxy (regs 12..15). */ +/* Tag only immediately after push_sxy of a qualified unclamped projection. */ +void pgxp_gte_mark_unclamped(void); void pgxp_gte_push_sxy(int32_t x16, int32_t y16, uint16_t sz3, uint32_t packed); /* Host-only homogeneous screen projection, before SZ/SXY/divider saturation. diff --git a/runtime/src/gte.cpp b/runtime/src/gte.cpp index 59b7adc4..91d3faac 100644 --- a/runtime/src/gte.cpp +++ b/runtime/src/gte.cpp @@ -1232,6 +1232,7 @@ void gte_rtps_internal(GTEState* gte, int16_t* V, bool setMac0, uint32_t instr) int64_t cx16 = sx16 < -kLim ? -kLim : (sx16 > kLim - 1 ? kLim - 1 : sx16); int64_t cy16 = sy16 < -kLim ? -kLim : (sy16 > kLim - 1 ? kLim - 1 : sy16); int32_t px16 = (int32_t)cx16, py16 = (int32_t)cy16; + bool unclamped_projection = false; /* Preserve projection precision (G1.11): shadow the exact projection * instead when the vertex qualifies and it lies inside the GPU's * truncation-agreement window; outside it the IR path's shadow is @@ -1241,15 +1242,18 @@ void gte_rtps_internal(GTEState* gte, int16_t* V, bool setMac0, uint32_t instr) * passes), where the shadow would be dropped anyway. */ if (!proj_override && pgxp_preserve_projection() && pgxp_active()) { int32_t ex16, ey16; - if (pgxp_project_precise(mac1, mac2, mac3, shift, gte->IR1, gte->IR2, - gte->SZ[3], gte_h_scaled(gte), gte->OFX, gte->OFY, - x_num, x_den, &ex16, &ey16) && - pgxp_ppp_accept(ex16, ey16, (uint32_t)gte->SXY[2])) { + const int precise = pgxp_project_precise(mac1, mac2, mac3, shift, + gte->IR1, gte->IR2, gte->SZ[3], gte_h_scaled(gte), + gte->OFX, gte->OFY, x_num, x_den, &ex16, &ey16); + if (precise == 2 || (precise == 1 && + pgxp_ppp_accept(ex16, ey16, (uint32_t)gte->SXY[2]))) { + unclamped_projection = precise == 2; px16 = ex16; py16 = ey16; } } pgxp_gte_push_sxy(px16, py16, gte->SZ[3], (uint32_t)gte->SXY[2]); + if (unclamped_projection) pgxp_gte_mark_unclamped(); if (!proj_override && pgxp_projection_tracking() && shift == 12 && !lm) { const double z = (double)(mac3 >> 12); const double h_proj = (double)gte_h_scaled(gte); diff --git a/runtime/src/pgxp.cpp b/runtime/src/pgxp.cpp index df92faf0..720e77d6 100644 --- a/runtime/src/pgxp.cpp +++ b/runtime/src/pgxp.cpp @@ -63,6 +63,7 @@ enum { * precise value may sit up to one pixel below or two above it. */ PGXP_F_DX = 1u << 4, PGXP_F_DY = 1u << 5, + PGXP_F_UNCLAMPED = 1u << 6, /* exact GTE projection; intact word only */ PGXP_F_VXY = PGXP_F_VX | PGXP_F_VY, }; @@ -551,7 +552,7 @@ static inline int pv_live(const PGXPValue *pv) { static inline void pv_validate(PGXPValue *pv, uint32_t actual) { if (pv->gen != s_gen) return; uint32_t diff = pv->value ^ actual; - if (diff) pv->flags &= (uint16_t)~PGXP_F_PROJECTION; + if (diff) pv->flags &= (uint16_t)~(PGXP_F_PROJECTION | PGXP_F_UNCLAMPED); if ((pv->flags & PGXP_F_VX) && (diff & 0x0000FFFFu)) pv->flags &= (uint16_t)~(PGXP_F_VX | PGXP_F_DX | PGXP_F_VZ); if ((pv->flags & PGXP_F_VY) && (diff & 0xFFFF0000u)) @@ -768,7 +769,7 @@ extern "C" void psx_pgxp_store(struct CPUState *cpu, uint32_t instr, if (hi_half) dst->value = (dst->value & 0x0000FFFFu) | (half << 16); else dst->value = (dst->value & 0xFFFF0000u) | half; dst->flags &= (uint16_t)~((hi_half ? (PGXP_F_VY | PGXP_F_DY) : (PGXP_F_VX | PGXP_F_DX)) | - PGXP_F_VZ | PGXP_F_PROJECTION); + PGXP_F_VZ | PGXP_F_PROJECTION | PGXP_F_UNCLAMPED); dst->z = 0; int ok = 0, derived = 0; int32_t v16 = 0; @@ -797,7 +798,7 @@ extern "C" void psx_pgxp_store(struct CPUState *cpu, uint32_t instr, dst->value = (dst->value & ~(0xFFu << shift)) | ((value & 0xFFu) << shift); dst->flags &= (uint16_t)~(((addr & 2u) ? (PGXP_F_VY | PGXP_F_DY) : (PGXP_F_VX | PGXP_F_DX)) | - PGXP_F_VZ | PGXP_F_PROJECTION); + PGXP_F_VZ | PGXP_F_PROJECTION | PGXP_F_UNCLAMPED); dst->z = 0; return; } @@ -1504,11 +1505,20 @@ extern "C" int pgxp_project_precise(int64_t mac1, int64_t mac2, int64_t mac3, int64_t x_num, int64_t x_den, int32_t *x16, int32_t *y16) { /* Qualify exactly the vertices whose guest projection is the plain - * IR * (H / SZ3) of the continuous one: sf=1, no IR1/IR2 clamp (the - * stored IR equals MAC >> 12), SZ3 unclamped and nonzero, and no UNR + * IR * (H / SZ3) of the continuous one: sf=1, verified IR1/IR2 conversion, SZ3 unclamped and nonzero, and no UNR * divide overflow (the GTE saturates the quotient when H >= 2*SZ3). */ if (shift != 12 || x_den <= 0) return 0; - if ((mac1 >> 12) != (int64_t)ir1 || (mac2 >> 12) != (int64_t)ir2) return 0; + const int64_t ix = mac1 >> 12, iy = mac2 >> 12; + const bool unclamped = ix != ir1 || iy != ir2; + if (unclamped) { + /* Only the opt-in exact projection may bypass IR saturation. Reject + * wrapped accumulators, lm=1 sign clamps and every other mismatch. */ + const int64_t lo = -(INT64_C(1) << 43), hi = (INT64_C(1) << 43) - 1; + const int64_t cx = ix < -32768 ? -32768 : ix > 32767 ? 32767 : ix; + const int64_t cy = iy < -32768 ? -32768 : iy > 32767 ? 32767 : iy; + if (!s_preserve_projection || mac1 < lo || mac1 > hi || + mac2 < lo || mac2 > hi || cx != ir1 || cy != ir2) return 0; + } const int64_t z_int = mac3 >> 12; if (z_int <= 0 || z_int > 0xFFFF || (uint32_t)z_int != sz3) return 0; if ((int64_t)h >= 2 * z_int) return 0; @@ -1528,7 +1538,7 @@ extern "C" int pgxp_project_precise(int64_t mac1, int64_t mac2, int64_t mac3, if (fy > lim - 1.0) fy = lim - 1.0; *x16 = (int32_t)std::floor(fx); *y16 = (int32_t)std::floor(fy); - return 1; + return unclamped ? 2 : 1; } /* Defined below with the GPU consumer. */ @@ -1562,6 +1572,12 @@ extern "C" void pgxp_gte_push_sxy(int32_t x16, int32_t y16, uint16_t sz3, s_gte[15] = *pv; /* SXYP mirrors SXY2 */ } +extern "C" void pgxp_gte_mark_unclamped(void) { + if (!g_pgxp_active) return; + s_gte[14].flags |= PGXP_F_UNCLAMPED; + s_gte[15] = s_gte[14]; +} + 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); } @@ -1695,8 +1711,12 @@ static inline int pgxp_accept(int32_t px, int32_t py, int32_t int_x, int32_t int_y, uint32_t word, uint16_t flags) { const int dx_derived = (flags & PGXP_F_DX) != 0; const int dy_derived = (flags & PGXP_F_DY) != 0; - if (!pgxp_agrees(px, int_x, (int16_t)(word & 0xFFFFu), dx_derived) || - !pgxp_agrees(py, int_y, (int16_t)(word >> 16), dy_derived)) + /* This tag can only ride an intact, value-validated GTE word. Arithmetic + * constructs new flags and loses it; the position-cache tier never has it. + * Clamped IR is an integer hardware limit, not the enhanced visual vertex. */ + if (!(s_preserve_projection && (flags & PGXP_F_UNCLAMPED)) && + (!pgxp_agrees(px, int_x, (int16_t)(word & 0xFFFFu), dx_derived) || + !pgxp_agrees(py, int_y, (int16_t)(word >> 16), dy_derived))) return 1; if (s_tolerance >= 0.0f) { if (pgxp_moved(px, int_x, dx_derived) > s_tolerance || diff --git a/runtime/tests/test_pgxp.cpp b/runtime/tests/test_pgxp.cpp index 6128e68f..202b9f13 100644 --- a/runtime/tests/test_pgxp.cpp +++ b/runtime/tests/test_pgxp.cpp @@ -622,6 +622,36 @@ int main(void) { &x, &y) == 1); } + /* Far-world projection: exact dataflow may recover IR saturation, while + * stale words, lm sign clamps and half writes must stay rejected. */ + { + const int saved_preserve = pgxp_preserve_projection(); + pgxp_set_preserve_projection(1); + pgxp_set_tolerance(-1.0f); + int32_t x, y; + CHECK(pgxp_project_precise(INT64_C(40000)*4096, INT64_C(-35000)*4096, + INT64_C(60000)*4096, 12, 32767, -32768, 60000, 300, + 160<<16, 120<<16, 1, 1, &x, &y) == 2); + CHECK(x == 360*65536 && y == -55*65536); + const uint32_t packed = (uint16_t)323 | ((uint32_t)(uint16_t)-44 << 16); + pgxp_gte_push_sxy(x, y, 60000, packed); + pgxp_gte_mark_unclamped(); + pgxp_store_gte_reg(0x80005000, 14); + int32_t px, py; uint16_t z; + CHECK(pgxp_get_precise_vertex(0x80005000, packed, 323, -44, &px, &py, &z)); + CHECK(px == x && py == y && z == 60000); + CHECK(!pgxp_get_precise_vertex(0x80005000, packed^1u, 322, -44, &px, &py, &z)); + psx_pgxp_cop2(nullptr, MFC2(2, 14), packed, 0); + psx_pgxp_store(nullptr, SH(1, 2), 0x80005000, packed); + CHECK(!pgxp_get_precise_vertex(0x80005000, packed, 323, -44, &px, &py, &z)); + CHECK(pgxp_project_precise(-4096, 0, INT64_C(60000)*4096, 12, 0, 0, + 60000, 300, 0, 0, 1, 1, &x, &y) == 0); + pgxp_set_preserve_projection(0); + CHECK(pgxp_project_precise(INT64_C(40000)*4096, 0, INT64_C(60000)*4096, + 12, 32767, 0, 60000, 300, 0, 0, 1, 1, &x, &y) == 0); + pgxp_set_preserve_projection(saved_preserve); + } + /* --- probe: the same decision, not counted --- */ { produce_at(ADDR_A);