diff --git a/runtime/include/pgxp.h b/runtime/include/pgxp.h index cedac6da..aff0f216 100644 --- a/runtime/include/pgxp.h +++ b/runtime/include/pgxp.h @@ -280,6 +280,13 @@ void pgxp_note_nclip(int disagree, int corrected); /* SWC2 site: copy the GTE register shadow (regs 12..15) to the RAM shadow. */ void pgxp_store_gte_reg(uint32_t addr, uint8_t reg); +/* After a title's original store: retain a projected SXY's precision/depth + * when only clip/outcode bits outside the GPU's two 11-bit fields changed. + * The caller supplies the current GTE word and actual stored word. Changed + * coordinates, clamping, stale sources and unmapped destinations fail closed. + * Host shadow only; no guest memory/register write. */ +int pgxp_store_flagged_gte_sxy(uint32_t addr, uint8_t reg, + uint32_t source_word, uint32_t stored_word); /* Address-keyed precise-word lookup (perspective texturing path). Returns * nonzero when the tracked word matches `packed` AND carries a depth. */ diff --git a/runtime/src/pgxp.cpp b/runtime/src/pgxp.cpp index fdcc4fc2..ee6a6286 100644 --- a/runtime/src/pgxp.cpp +++ b/runtime/src/pgxp.cpp @@ -999,6 +999,17 @@ static inline int half_exact(const PGXPValue *pv, uint32_t value, int hi_half) { } /* A saturated projection shadow sits beyond the clamped word; it is not * the value of that half and must not be carried as one. */ + if (s_preserve_projection) { + const int32_t field = (int32_t)((uint32_t)half << 21) >> 21; + const int64_t d = (int64_t)v16 - (int64_t)half * 65536; + // Clip metadata uses bits 13..15. Bits 11/12 must still be the + // coordinate's sign fill; a real wrapped coordinate stays strict. + const uint16_t sign_fill = field < 0 ? 0x1800u : 0; + if (field > -0x400 && field < 0x3FF && + (uint16_t(half) & 0x1800u) == sign_fill) + return d > -(int64_t)PGXP_PPP_AGREE_BELOW * 65536 && + d < (int64_t)PGXP_PPP_AGREE_ABOVE * 65536; + } return (v16 >> 16) == half; } @@ -1646,9 +1657,9 @@ extern "C" void pgxp_gte_reg_written(int reg, uint32_t value) { * packet half, `half` the raw 16-bit half in the word. Exact (integer part * equals the native parse) unless preserve-projection is on; then the precise * position may sit a bounded distance from it (PGXP_PPP_AGREE_*), except - * when the half is at or beyond the GTE saturation limits: there the guest - * integer is a clamp that says nothing about the vertex, or (beyond them) a - * CPU-modified word the GPU's 11-bit parse wraps. */ + * when the decoded coordinate is saturated or the word is a genuinely + * wrapped coordinate. Known outcode bits 13..15 alone are not saturation; + * bits 11/12 must still match the decoded coordinate's sign fill. */ static inline int pgxp_agrees(int32_t p16, int32_t native, int16_t half, int derived) { const int64_t d = (int64_t)p16 - (int64_t)native * 65536; @@ -1656,7 +1667,9 @@ static inline int pgxp_agrees(int32_t p16, int32_t native, int16_t half, * own arithmetic chose, so the value may sit one pixel either side of * the truncation window - still bounded, still the same vertex. */ if (derived) return d > -65536 && d < 2 * 65536; - if (!s_preserve_projection || half <= -0x400 || half >= 0x3FF) + const uint16_t sign_fill = native < 0 ? 0x1800u : 0; + if (!s_preserve_projection || native <= -0x400 || native >= 0x3FF || + (uint16_t(half) & 0x1800u) != sign_fill) return (p16 >> 16) == native; return d > -(int64_t)PGXP_PPP_AGREE_BELOW * 65536 && d < (int64_t)PGXP_PPP_AGREE_ABOVE * 65536; @@ -1815,6 +1828,27 @@ extern "C" void pgxp_store_gte_reg(uint32_t addr, uint8_t reg) { *dst = *src; } +extern "C" int pgxp_store_flagged_gte_sxy(uint32_t addr, uint8_t reg, + uint32_t source_word, uint32_t stored_word) { + if (!g_pgxp_active || (addr & 3u) || reg < 12 || reg > 15 || + ((source_word ^ stored_word) & 0x07FF07FFu)) return 0; + const PGXPValue* src = &s_gte[reg]; + if (src->gen != s_gen || src->value != source_word || !src->z || + (src->flags & (PGXP_F_VXY | PGXP_F_VZ)) != (PGXP_F_VXY | PGXP_F_VZ)) return 0; + const int64_t x = (int64_t)src->x16 + + ((int64_t)(int16_t)stored_word - (int16_t)source_word) * 65536; + const int64_t y = (int64_t)src->y16 + + ((int64_t)(int16_t)(stored_word >> 16) - (int16_t)(source_word >> 16)) * 65536; + if (x < INT32_MIN || x > INT32_MAX || y < INT32_MIN || y > INT32_MAX) return 0; + PGXPValue* dst = pgxp_ptr_w(addr); + if (!dst) return 0; + *dst = *src; + dst->value = stored_word; + dst->x16 = (int32_t)x; + dst->y16 = (int32_t)y; + return 1; +} + extern "C" int pgxp_debug_shadow(int space, uint32_t key, int *live, uint32_t *value, uint32_t *flags, int32_t *x16, int32_t *y16, uint16_t *z) { diff --git a/runtime/tests/test_pgxp.cpp b/runtime/tests/test_pgxp.cpp index 3bbc8775..1e08bc5a 100644 --- a/runtime/tests/test_pgxp.cpp +++ b/runtime/tests/test_pgxp.cpp @@ -239,12 +239,16 @@ int main(void) { * The flag ops rewrite bits 14/15 and 30/31 of the word, never the 11-bit * fields GP0 decodes, so the vertex (and its depth) must survive - in * BOTH tiers, since bitwise carries are exact. --- */ - for (int mode = 0; mode < 2; mode++) { + for (int exact = 0; exact < 2; exact++) { + pgxp_set_preserve_projection(exact); + const int32_t expected_x = X16 - (exact ? 65536 : 0); + const int32_t expected_y = Y16 + (exact ? 65536 : 0); + for (int mode = 0; mode < 2; mode++) { pgxp_set_cpu_mode(mode); const uint32_t T2 = 10, T4 = 12, T5 = 13, T9 = 25, A3 = 7; const uint32_t bounds = (240u << 16) | 128u; /* x >= 128 -> flag */ const uint32_t m14 = 0xBFFFBFFFu, m15 = 0x7FFF7FFFu; - pgxp_gte_push_sxy(X16, Y16, SZ3, PACKED); + pgxp_gte_push_sxy(expected_x, expected_y, SZ3, PACKED); psx_pgxp_cop2(nullptr, MFC2(T2, 14), PACKED, 0); psx_pgxp_load(nullptr, LW(1, T4), 0x80180000u, bounds); /* untracked */ psx_pgxp_alu(nullptr, LUI(T5, 0xBFFF), 0xBFFF0000u, 0, 0); @@ -266,11 +270,13 @@ int main(void) { psx_pgxp_store(nullptr, SW(1, T2), ADDR_B, w); int32_t x, y; uint16_t z; CHECK(lookup(ADDR_B, w, 160, 80, &x, &y, &z) == PGXP_SRC_DATAFLOW); - CHECK(x == X16 && y == Y16 && z == SZ3); + CHECK(x == expected_x && y == expected_y && z == SZ3); uint16_t wz = 0; CHECK(pgxp_load_precise_word(ADDR_B, w, nullptr, nullptr, &wz) == 1); CHECK(wz == SZ3); } + } + pgxp_set_preserve_projection(0); pgxp_set_cpu_mode(0); /* --- quad from a projected-vertex table (Spider-Man 0x8007C5D4): diff --git a/runtime/tests/test_pgxp_word_shadow.cpp b/runtime/tests/test_pgxp_word_shadow.cpp index e649e8c0..74d640fc 100644 --- a/runtime/tests/test_pgxp_word_shadow.cpp +++ b/runtime/tests/test_pgxp_word_shadow.cpp @@ -188,6 +188,31 @@ int main() { check_attributes(arena, capture(Arena, Packed)); pgxp_invalidate_all(); CHECK(!capture(Arena, Packed).valid); + // Neversoft subdivision unpacks/re-packs SXY and adds screen outcodes. + // Reattach validated position/depth; exact projection may cross the + // original integer boundary while the guest packet remains unchanged. + pgxp_set_preserve_projection(1); + pgxp_set_tolerance(-1.0f); + const uint32_t Flagged = Packed | 0x80004000u; + pgxp_gte_push_sxy((8<<16)-16384, (12<<16)+73728, 300, Packed); + CHECK(pgxp_store_flagged_gte_sxy(Arena, 14, Packed, Flagged)); + int32_t precise_x=0, precise_y=0; + CHECK(pgxp_get_precise_vertex(Arena, Flagged, 8, 12, &precise_x, &precise_y, &depth) == PGXP_SRC_DATAFLOW); + CHECK(precise_x == (8<<16)-16384 && precise_y == (12<<16)+73728 && depth == 300); + CHECK(pgxp_load_precise_word(Arena, Flagged, nullptr, nullptr, &depth) && depth == 300); + const auto flagged = capture(Arena, Flagged); + CHECK(!pgxp_store_flagged_gte_sxy(Arena, 14, Packed + 1, Flagged)); + CHECK(!pgxp_store_flagged_gte_sxy(Arena, 14, Packed, Flagged ^ 1)); + CHECK(!pgxp_store_flagged_gte_sxy(Arena + 1, 14, Packed, Flagged)); + CHECK(!pgxp_store_flagged_gte_sxy(0x1F801814u, 14, Packed, Flagged)); + CHECK(!pgxp_store_flagged_gte_sxy(Arena, 11, Packed, Flagged)); + check_attributes(flagged, capture(Arena, Flagged)); + pgxp_checkpoint_begin(); + CHECK(pgxp_store_flagged_gte_sxy(Arena, 14, Packed, Packed)); + pgxp_checkpoint_rollback(); + check_attributes(flagged, capture(Arena, Flagged)); + pgxp_invalidate_all(); + CHECK(!pgxp_store_flagged_gte_sxy(Arena, 14, Packed, Flagged)); gpu_bytes = 0; // pages retained on the host are not guest allocations CHECK(!pgxp_capture_word_shadow(Arena, Packed, &refused)); std::printf("PGXP packet shadow checks: %s\n",failures?"FAIL":"PASS");