From 5b8d39e5bb6fb49f0341f7900923c5e456da4db0 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Wed, 7 Oct 2026 16:52:26 -0700 Subject: [PATCH] Track PGXP geometry and texture depth in allocated GPU primitive buffers --- runtime/CMakeLists.txt | 5 +++ runtime/src/pgxp.cpp | 58 ++++++++++++++++++------- runtime/tests/pgxp_test_mod_memory.cpp | 5 +++ runtime/tests/test_pgxp_word_shadow.cpp | 49 +++++++++++++++++++++ 4 files changed, 102 insertions(+), 15 deletions(-) create mode 100644 runtime/tests/pgxp_test_mod_memory.cpp diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index c6de56a0..119a55f1 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -550,6 +550,11 @@ if(BUILD_TESTING) endif() add_test(NAME pgxp_session_test COMMAND pgxp_session_test) + foreach(_pgxp_isolated_test + gte_register_access_test gte_hle_contract_test pgxp_test pgxp_session_test) + target_sources(${_pgxp_isolated_test} PRIVATE tests/pgxp_test_mod_memory.cpp) + endforeach() + add_executable(psx_instr_cost_test tests/test_psx_instr_cost.cpp) target_include_directories(psx_instr_cost_test PRIVATE include) add_test(NAME psx_instr_cost_test COMMAND psx_instr_cost_test) diff --git a/runtime/src/pgxp.cpp b/runtime/src/pgxp.cpp index a160e913..fdcc4fc2 100644 --- a/runtime/src/pgxp.cpp +++ b/runtime/src/pgxp.cpp @@ -35,6 +35,7 @@ #include "pgxp_hooks.h" #include "cpu_state.h" #include "psx_memory.h" +#include "mod_memory.h" #include #include @@ -81,6 +82,12 @@ struct PGXPValue { #define PGXP_REG_LO 33 static PGXPValue *s_ram = nullptr; /* lazily allocated, 72 MiB VA */ +// Extended draw buffers live outside the main-RAM decode window. Give their +// allocated words separate shadows, in stable pages created only on writes. +// Read misses never allocate, and a page cannot move while a checkpoint uses it. +#define PGXP_GPU_PAGE_WORDS 1024u +#define PGXP_GPU_WORDS (PSX_MOD_GPU_DMA_APERTURE_SIZE >> 2) +static PGXPValue *s_gpu_pages[PGXP_GPU_WORDS / PGXP_GPU_PAGE_WORDS]; static PGXPValue s_scratch[PGXP_SCRATCH_WORDS]; static PGXPValue s_gpr[34]; /* 32 GPRs + HI + LO */ static PGXPValue s_gte[32]; /* GTE data registers */ @@ -137,6 +144,8 @@ extern "C" void pgxp_invalidate_all(void) { ck_wrapped(); /* generation wrapped: physically clear so stale slots can't revive */ if (s_ram) std::memset(s_ram, 0, PGXP_RAM_WORDS * sizeof(PGXPValue)); + for (PGXPValue* page : s_gpu_pages) + if (page) std::memset(page, 0, PGXP_GPU_PAGE_WORDS * sizeof(PGXPValue)); std::memset(s_scratch, 0, sizeof(s_scratch)); std::memset(s_gpr, 0, sizeof(s_gpr)); std::memset(s_gte, 0, sizeof(s_gte)); @@ -315,12 +324,21 @@ extern "C" uint64_t pgxp_store_ring(const PGXPStoreRecord **ring, uint32_t *cap) /* ------------------------------------------------------------------------- */ /* Guest address -> shadow slot, or NULL for BIOS/MMIO/KSEG2 (untrackable). */ -static inline PGXPValue *pgxp_ptr(uint32_t addr) { +static inline PGXPValue *pgxp_ptr(uint32_t addr, bool create = false) { uint32_t m = addr & 0x1FFFFFFFu; if (m < PSX_MAIN_RAM_WINDOW_BYTES) /* RAM + its mirrors (live map) */ return s_ram ? &s_ram[psx_ram_canonical_offset(m) >> 2] : nullptr; if ((m & 0xFFFFFC00u) == 0x1F800000u) /* scratchpad */ return &s_scratch[(m & 0x3FCu) >> 2]; + if (addr < 0xC0000000u && m >= PSX_MOD_GPU_DMA_APERTURE_BASE && + m < PSX_MOD_GPU_DMA_APERTURE_BASE + PSX_MOD_GPU_DMA_APERTURE_SIZE && + psx_mod_gpu_dma_memory_contains(addr & ~3u, 4u)) { + const uint32_t word = (m - PSX_MOD_GPU_DMA_APERTURE_BASE) >> 2; + PGXPValue*& page = s_gpu_pages[word / PGXP_GPU_PAGE_WORDS]; + if (!page && create) + page = static_cast(std::calloc(PGXP_GPU_PAGE_WORDS, sizeof(PGXPValue))); + return page ? &page[word % PGXP_GPU_PAGE_WORDS] : nullptr; + } return nullptr; } @@ -339,6 +357,7 @@ static inline PGXPValue *pgxp_ptr(uint32_t addr) { * pass, fails closed: rollback invalidates everything. */ struct PGXPJournalEntry { PGXPValue *slot; + size_t index; PGXPValue old; }; @@ -353,21 +372,25 @@ static PGXPScalar s_ck_gpr_s[34], s_ck_gte_s[32], s_ck_gtc_s[32]; static uint32_t s_ck_gen = 0, s_ck_suppress = 0; static int s_ck_deferred = 0; -static inline size_t ck_index(const PGXPValue *pv) { - if (pv >= s_scratch && pv < s_scratch + PGXP_SCRATCH_WORDS) - return (size_t)PGXP_RAM_WORDS + (size_t)(pv - s_scratch); - return (size_t)(pv - s_ram); +static inline size_t ck_index(uint32_t addr) { + const uint32_t physical = addr & 0x1FFFFFFFu; + if (physical < PSX_MAIN_RAM_WINDOW_BYTES) + return psx_ram_canonical_offset(physical) >> 2; + if ((physical & 0xFFFFFC00u) == 0x1F800000u) + return (size_t)PGXP_RAM_WORDS + ((physical & 0x3FCu) >> 2); + return (size_t)PGXP_RAM_WORDS + PGXP_SCRATCH_WORDS + + ((physical - PSX_MOD_GPU_DMA_APERTURE_BASE) >> 2); } /* Journal a RAM / scratchpad slot before its first mutation in a pass. */ -static inline void ck_note(PGXPValue *pv) { +static inline void ck_note(PGXPValue *pv, uint32_t addr) { if (s_ck_depth == 0 || !pv) return; - if (s_ram != s_ck_ram && !(pv >= s_scratch && - pv < s_scratch + PGXP_SCRATCH_WORDS)) { + if (s_ck_lossy || !s_ck_bits) { s_ck_lossy = 1; return; } + if (s_ram != s_ck_ram && (addr & 0x1FFFFFFFu) < PSX_MAIN_RAM_WINDOW_BYTES) { s_ck_lossy = 1; /* shadow RAM appeared mid-pass */ return; } - size_t i = ck_index(pv); + size_t i = ck_index(addr); uint8_t bit = (uint8_t)(1u << (i & 7u)); if (s_ck_bits[i >> 3] & bit) return; if (s_ck_n == s_ck_cap) { @@ -380,6 +403,7 @@ static inline void ck_note(PGXPValue *pv) { } s_ck_bits[i >> 3] |= bit; s_ck_log[s_ck_n].slot = pv; + s_ck_log[s_ck_n].index = i; s_ck_log[s_ck_n].old = *pv; s_ck_n++; } @@ -391,8 +415,8 @@ static void ck_wrapped(void) { static int ck_in_pass(void) { return s_ck_depth != 0; } static inline PGXPValue *pgxp_ptr_w(uint32_t addr) { - PGXPValue *pv = pgxp_ptr(addr); - ck_note(pv); + PGXPValue *pv = pgxp_ptr(addr, true); + ck_note(pv, addr); return pv; } @@ -407,6 +431,10 @@ static int packet_shadow_address(uint32_t addr, uint32_t *canonical) { *canonical = physical; return 1; } + if (psx_mod_gpu_dma_memory_contains(addr, 4u)) { + *canonical = physical; + return 1; + } return 0; } @@ -440,10 +468,10 @@ static int restore_word_shadow(uint32_t addr, uint32_t expected, in->value != expected || in->valid > 1u || in->source_generation != s_ck_gen) return 0; if (in->valid && !s_enabled) return 0; - PGXPValue *destination = pgxp_ptr(addr); + PGXPValue *destination = pgxp_ptr(addr, in->valid != 0u); // With PGXP disabled/unallocated, there is already no RAM shadow to clear. if (!destination) return in->valid == 0u; - ck_note(destination); + ck_note(destination, addr); if (s_ck_lossy) return 0; PGXPValue restored{}; if (in->valid) { @@ -474,7 +502,7 @@ extern "C" void pgxp_checkpoint_begin(void) { if (s_ck_depth++ != 0) return; /* the outermost pass journals */ if (!s_ck_bits) { s_ck_bits = (uint8_t *)std::calloc( - ((size_t)PGXP_RAM_WORDS + PGXP_SCRATCH_WORDS + 7u) / 8u, 1); + ((size_t)PGXP_RAM_WORDS + PGXP_SCRATCH_WORDS + PGXP_GPU_WORDS + 7u) / 8u, 1); if (!s_ck_bits) s_ck_lossy = 1; } s_ck_ram = s_ram; @@ -496,7 +524,7 @@ extern "C" void pgxp_checkpoint_rollback(void) { for (size_t i = s_ck_n; i-- > 0;) { PGXPJournalEntry *e = &s_ck_log[i]; *e->slot = e->old; - size_t k = ck_index(e->slot); + size_t k = e->index; s_ck_bits[k >> 3] &= (uint8_t)~(1u << (k & 7u)); } s_ck_n = 0; diff --git a/runtime/tests/pgxp_test_mod_memory.cpp b/runtime/tests/pgxp_test_mod_memory.cpp new file mode 100644 index 00000000..b0303969 --- /dev/null +++ b/runtime/tests/pgxp_test_mod_memory.cpp @@ -0,0 +1,5 @@ +#include + +// These isolated instruction/session fixtures have no allocated mod memory. +// The packet-shadow fixture supplies its own bounded, allocated aperture. +extern "C" int psx_mod_gpu_dma_memory_contains(uint32_t, uint32_t) { return 0; } diff --git a/runtime/tests/test_pgxp_word_shadow.cpp b/runtime/tests/test_pgxp_word_shadow.cpp index 9d421b5a..e649e8c0 100644 --- a/runtime/tests/test_pgxp_word_shadow.cpp +++ b/runtime/tests/test_pgxp_word_shadow.cpp @@ -1,11 +1,16 @@ #include "pgxp.h" #include "pgxp_hooks.h" +#include "mod_memory.h" #include #include #include extern "C" int gte_geometry_correction_lookup(uint32_t,int32_t*,int32_t*) {return 0;} extern "C" int gte_geometry_correction_lookup_probe(uint32_t,int32_t*,int32_t*) {return 0;} +static uint32_t gpu_bytes = 0; +extern "C" int psx_mod_gpu_dma_memory_contains(uint32_t address, uint32_t bytes) { + return psx_mod_gpu_dma_aperture_offset_for(address, bytes, gpu_bytes, nullptr); +} static int failures=0; #define CHECK(c) do {if(!(c)){std::fprintf(stderr,"FAIL line %d: %s\n",__LINE__,#c);++failures;}}while(0) constexpr uint32_t Address=0x80001000u,Other=0x80002000u,Packed=0x000C0008u; @@ -141,6 +146,50 @@ int main() { auto relocated_scratch=scratch;relocated_scratch.address=Address&0x1FFFFFFFu; check_attributes(relocated_scratch,capture(Address,Packed)); pgxp_checkpoint_rollback(); + // Expanded primitive storage must retain the exact same XYZ/depth and + // projection as RAM, without aliasing the retail RAM word under its tag. + const uint32_t Arena = PSX_MOD_GPU_DMA_GUEST_BASE + 0x1000u; + const uint32_t ArenaOther = Arena + 0x1000u; + gpu_bytes = 0x4000u; + pgxp_invalidate_all(); + seed(Address, Packed, (8<<16)+16384); + seed(Arena, Packed, (8<<16)+32768); + const auto ram = capture(Address, Packed); + const auto arena = capture(Arena, Packed); + CHECK(arena.valid && arena.address == 0x00801000u); + CHECK(arena.x16 != ram.x16); + check_attributes(arena, capture(Arena & 0x1FFFFFFFu, Packed)); + check_attributes(arena, capture(Arena | 0x20000000u, Packed)); + uint16_t depth = 0; + CHECK(pgxp_load_precise_word(Arena, Packed, nullptr, nullptr, &depth)); + CHECK(depth == 300); + CHECK(!pgxp_load_precise_word(Arena, Packed + 1, nullptr, nullptr, &depth)); + PGXPWordShadow refused{}; + CHECK(!pgxp_capture_word_shadow(PSX_MOD_GPU_DMA_GUEST_BASE + gpu_bytes, Packed, &refused)); + CHECK(!pgxp_capture_word_shadow(0xBF801000u, Packed, &refused)); + CHECK(!pgxp_capture_word_shadow(0xC0801000u, Packed, &refused)); + const auto empty_arena = capture(ArenaOther, Packed); + CHECK(!empty_arena.valid); + pgxp_checkpoint_begin(); + seed(Arena, Packed, (8<<16)+49152); + // This page is allocated for the first time inside the checkpoint. + CHECK(pgxp_restore_relocated_word_shadow(ArenaOther, Packed, &arena)); + auto relocated_arena = arena; relocated_arena.address = 0x00802000u; + check_attributes(relocated_arena, capture(ArenaOther, Packed)); + check_attributes(ram, capture(Address, Packed)); + pgxp_checkpoint_rollback(); + check_attributes(arena, capture(Arena, Packed)); + check_attributes(empty_arena, capture(ArenaOther, Packed)); + check_attributes(ram, capture(Address, Packed)); + pgxp_checkpoint_begin(); pgxp_invalidate_all(); + CHECK(pgxp_restore_word_shadow(Arena, Packed, &arena)); + check_attributes(arena, capture(Arena, Packed)); + pgxp_checkpoint_rollback(); + check_attributes(arena, capture(Arena, Packed)); + pgxp_invalidate_all(); + CHECK(!capture(Arena, Packed).valid); + 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"); return failures?1:0; }