From 4b0e5bd1db0487e507d84d49fcb6d96e9c73fb10 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Fri, 2 Oct 2026 19:08:13 -0700 Subject: [PATCH] GPU packet keys keep the enhancement aperture apart from the main RAM it aliases The UI prepass re-read aperture node payloads through the RAM-folded key, so THPS2's relocated prim arena discarded the HUD squash every frame. Co-Authored-By: Claude Opus 5.5 (1M context) --- runtime/include/mod_memory.h | 17 ++++++ runtime/include/ws_primitive_roles.h | 6 +- runtime/src/gpu.c | 17 +++--- runtime/src/memory.c | 10 +++- runtime/tests/gpu_exec_stubs.inc | 15 ++++- runtime/tests/test_gpu_gp0_history_guard.py | 1 + .../tests/test_ws_auto_ui_untextured_exec.c | 58 +++++++++++++++++++ 7 files changed, 109 insertions(+), 15 deletions(-) diff --git a/runtime/include/mod_memory.h b/runtime/include/mod_memory.h index 93c8de19..bab68684 100644 --- a/runtime/include/mod_memory.h +++ b/runtime/include/mod_memory.h @@ -49,6 +49,23 @@ static inline uint32_t psx_mod_gpu_dma_resolve_address_for( return psx_ram_canonical_offset(canonical) & ~3u; } +/* + * Identity of a GPU packet word at `address` (a CPU pointer or a DMA tag): + * the allocated enhancement aperture keeps its own physical address, main + * RAM folds to its word-aligned live offset. Every source-address key (UI + * prepass nodes and items, packet guards, title roles) uses this, so a packet + * in the aperture never shares a key with, or reads back, the main RAM its + * low bits alias. + */ +static inline uint32_t psx_gpu_packet_key_for(uint32_t address, uint32_t used) { + uint32_t canonical = address & 0x1FFFFFFCu; + if (psx_mod_gpu_dma_aperture_offset_for( + canonical, 4u, used, (uint32_t *)0)) + return canonical; + return psx_ram_canonical_offset(canonical) & ~3u; +} + +uint32_t psx_gpu_packet_key(uint32_t address); uint32_t psx_mod_gpu_dma_memory_alloc(uint32_t size, uint32_t alignment); uint32_t psx_mod_gpu_dma_resolve_address(uint32_t address); uint32_t psx_mod_memory_alloc(uint32_t size, uint32_t alignment); diff --git a/runtime/include/ws_primitive_roles.h b/runtime/include/ws_primitive_roles.h index e3ba25a0..f13103fb 100644 --- a/runtime/include/ws_primitive_roles.h +++ b/runtime/include/ws_primitive_roles.h @@ -2,7 +2,7 @@ #define PSX_WS_PRIMITIVE_ROLES_H #include #include -#include "psx_memory.h" +#include "mod_memory.h" /* Title-supplied packet identity, independent of draw order and sprite tags. * Keys name PsyQ P_TAG headers; GPU lookup names the following command word. */ @@ -15,7 +15,7 @@ typedef struct { static inline WsPrimitiveRole* ws_role_write(WsPrimitiveRole* tags, uint32_t primitive, uint32_t now) { - uint32_t key = psx_ram_canonical_offset(primitive) & ~3u; + uint32_t key = psx_gpu_packet_key(primitive); if (!key) return NULL; uint32_t slot = (key >> 2) & (WS_ROLE_BUCKETS - 1u), victim = slot; for (unsigned i = 0; i < 4; ++i) { @@ -36,7 +36,7 @@ static inline WsPrimitiveRole* ws_role_write(WsPrimitiveRole* tags, static inline const WsPrimitiveRole* ws_role_read(const WsPrimitiveRole* tags, uint32_t source, uint32_t now) { if (source == 0xFFFFFFFFu) return NULL; - uint32_t key = psx_ram_canonical_offset(source - 4u) & ~3u; + uint32_t key = psx_gpu_packet_key(source - 4u); if (!key) return NULL; uint32_t slot = (key >> 2) & (WS_ROLE_BUCKETS - 1u); for (unsigned i = 0; i < 4; ++i) { diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index 98836849..13b37bf2 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -53,9 +53,10 @@ #include #include "gpu_timeline.h" -/* Word-aligned main-RAM key for a primitive/OT address through the live - * geometry (retail: the DMAC's 0x1FFFFC fold). */ -#define GPU_RAM_KEY(a) (psx_ram_canonical_offset(a) & ~3u) +/* Packet identity of a primitive/OT address: main RAM through the live + * geometry (retail: the DMAC's 0x1FFFFC fold), the enhancement aperture by its + * own address (mod_memory.h). */ +#define GPU_RAM_KEY(a) psx_gpu_packet_key(a) extern uint16_t psx_read_half(uint32_t addr); extern uint8_t psx_read_byte(uint32_t addr); @@ -2466,7 +2467,7 @@ void gpu_ws_tag_background_prim(uint32_t prim) { } WsPrepassPacketGuard guard = ws_prepass_packet_guard(words, count); ws_hud_anchor_insert(ws_background_tags, WS_HUD_ANCHOR_TABLE_SIZE, - (prim + 4u) & 0x1FFFFCu, 0, &guard, + GPU_RAM_KEY((prim + 4u)), 0, &guard, (uint32_t)s_frame_count); ws_background_tags_used = 1; ws_background_tag_frame = (uint32_t)s_frame_count; @@ -6474,8 +6475,8 @@ void gpu_ws_validate_linked_list_header(uint32_t addr, uint32_t header) { void gpu_ws_validate_linked_list_node(uint32_t addr, uint32_t num_words) { if (ws_ui_prepass_count == 0) return; - uint32_t resolved = - GPU_RAM_KEY(psx_mod_gpu_dma_resolve_address(addr)); + const uint32_t node_addr = psx_mod_gpu_dma_resolve_address(addr); + const uint32_t resolved = GPU_RAM_KEY(node_addr); const WsUiPrepassNode *node = NULL; for (uint32_t i = 0; i < ws_ui_prepass_node_count; i++) { if (ws_ui_prepass_nodes[i].addr == resolved) { @@ -6489,9 +6490,11 @@ void gpu_ws_validate_linked_list_node(uint32_t addr, uint32_t num_words) { return; } + /* Re-read the payload where the walk reads it (the node's own memory), + * never through the key. */ uint32_t payload[255]; uint32_t first_addr = - psx_mod_gpu_dma_resolve_address(resolved + 4u); + psx_mod_gpu_dma_resolve_address(node_addr + 4u); for (uint32_t i = 0; i < num_words; i++) { payload[i] = psx_read_word(psx_mod_gpu_dma_resolve_address( first_addr + i * 4u)); diff --git a/runtime/src/memory.c b/runtime/src/memory.c index 98189ebf..a3ba149c 100644 --- a/runtime/src/memory.c +++ b/runtime/src/memory.c @@ -144,6 +144,10 @@ uint32_t psx_mod_gpu_dma_resolve_address(uint32_t address) { address, mod_gpu_dma_memory_used); } +uint32_t psx_gpu_packet_key(uint32_t address) { + return psx_gpu_packet_key_for(address, mod_gpu_dma_memory_used); +} + /* Exposed for inlined main-RAM load helpers in psx_cyc.h (VLC/decode hot path). */ uint8_t *g_psx_ram = ram; /* PSX_LOAD_DELAY gate (default on). −1 = unread; 0/1 after first resolve. */ @@ -1355,9 +1359,9 @@ static void mmio_write32(uint32_t addr, uint32_t val) { * 0x80030000, so ROM 0xBFC38B1C executes at 0x80050B1C * (scph1001_relocated_store). */ if (debug_cpu_ptr && scph1001_relocated_store(0x80050B1Cu, 0xBFC38B1Cu)) { - /* Word-aligned RAM key through the live geometry (retail: the - * 0x1FFFFC fold, identical to the DMA/GPU source keys). */ - src = psx_ram_canonical_offset(debug_cpu_ptr->gpr[4] - 4u) & ~3u; + /* The same packet key as the DMA/GPU source keys (retail: the + * 0x1FFFFC fold). */ + src = psx_gpu_packet_key(debug_cpu_ptr->gpr[4] - 4u); } gpu_set_gp0_source(src); gpu_write_gp0(val); diff --git a/runtime/tests/gpu_exec_stubs.inc b/runtime/tests/gpu_exec_stubs.inc index dbcb8e01..c4807742 100644 --- a/runtime/tests/gpu_exec_stubs.inc +++ b/runtime/tests/gpu_exec_stubs.inc @@ -54,6 +54,10 @@ int mod_texture_packet_precision(uint32_t source, float q[3], float xy[6]) { } static uint32_t test_ram[0x00200000u / 4u]; +/* The enhancement GPU-DMA aperture (mod_memory.h): a test allocates it by + * setting test_aperture_used; reads and keys then follow the real rules. */ +static uint32_t test_aperture[0x00020000u / 4u]; +static uint32_t test_aperture_used; static struct { int calls; @@ -100,7 +104,9 @@ static inline void gpu_exec_record_triangle(int x0, int y0, int x1, int y1, } uint32_t psx_read_word(uint32_t addr) { - uint32_t phys = addr & 0x1FFFFFFFu; + uint32_t phys = addr & 0x1FFFFFFFu, off; + if (psx_mod_gpu_dma_aperture_offset_for(phys, 4u, test_aperture_used, &off)) + return test_aperture[off / 4u]; if (phys <= 0x00200000u - 4u) return test_ram[phys / 4u]; return 0; @@ -243,7 +249,12 @@ void color_lut_map555(const ColorLut *lut, uint16_t bgr555, if (g) *g = (uint8_t)(((bgr555 >> 5) & 0x1Fu) << 3); if (b) *b = (uint8_t)(((bgr555 >> 10) & 0x1Fu) << 3); } -uint32_t psx_mod_gpu_dma_resolve_address(uint32_t address) { return address; } +uint32_t psx_mod_gpu_dma_resolve_address(uint32_t address) { + return psx_mod_gpu_dma_resolve_address_for(address, test_aperture_used); +} +uint32_t psx_gpu_packet_key(uint32_t address) { + return psx_gpu_packet_key_for(address, test_aperture_used); +} void text_xlate_vram_upload(int x, int y, int w, int h) { (void)x; (void)y; (void)w; (void)h; } diff --git a/runtime/tests/test_gpu_gp0_history_guard.py b/runtime/tests/test_gpu_gp0_history_guard.py index cf6132a9..990a0bed 100644 --- a/runtime/tests/test_gpu_gp0_history_guard.py +++ b/runtime/tests/test_gpu_gp0_history_guard.py @@ -250,6 +250,7 @@ void text_xlate_vram_upload(uint16_t x, uint16_t y, uint16_t w, uint16_t h) int ws_cull_should_keep(uint32_t addr) { (void)addr; return 1; } int ws_ui_group_should_keep(uint32_t addr) { (void)addr; return 1; } uint32_t psx_mod_gpu_dma_resolve_address(uint32_t address) { return address; } +uint32_t psx_gpu_packet_key(uint32_t address) { return address & 0x001FFFFCu; } void ws_ui_group_assign(WsUiGroupItem *items, size_t count, int32_t display_width, int dense_menu, int in_place) { (void)items; (void)count; (void)display_width; (void)dense_menu; (void)in_place; } diff --git a/runtime/tests/test_ws_auto_ui_untextured_exec.c b/runtime/tests/test_ws_auto_ui_untextured_exec.c index 2dc60a80..3172f817 100644 --- a/runtime/tests/test_ws_auto_ui_untextured_exec.c +++ b/runtime/tests/test_ws_auto_ui_untextured_exec.c @@ -107,6 +107,31 @@ static void add_rear_pieces(const RearPiece *pieces, int count) { test_ram[NODE_GAP / 4u] = NODE_RING; } +/* The same HUD list in the enhancement GPU-DMA aperture (THPS2's draw-distance + * prim arena), with unrelated words in the main RAM its low bits alias. */ +#define AP_BASE 0x00800000u +static void put_node_ap(uint32_t addr, uint32_t next, const uint32_t *words, + uint32_t count) { + const uint32_t off = (addr - AP_BASE) / 4u; + test_aperture[off] = (count << 24) | (next & 0xFFFFFFu); + for (uint32_t i = 0; i < count; i++) + test_aperture[off + 1u + i] = words[i]; +} +static void move_hud_to_aperture(void) { + const uint32_t nodes[3] = {NODE_RING, NODE_FILL, NODE_FLAT}; + test_aperture_used = sizeof test_aperture; + memset(test_aperture, 0, sizeof test_aperture); + for (int i = 0; i < 3; i++) { + const uint32_t h = test_ram[nodes[i] / 4u], n = h >> 24; + const uint32_t next = (h & 0xFFFFFFu) == 0xFFFFFFu + ? 0xFFFFFFu : AP_BASE + (h & 0xFFFFFFu); + put_node_ap(AP_BASE + nodes[i], next, &test_ram[nodes[i] / 4u + 1u], n); + } + test_ram[OT_HEAD / 4u] = AP_BASE + NODE_RING; + for (uint32_t a = NODE_RING; a < NODE_FLAT + 0x40u; a += 4u) + test_ram[a / 4u] = 0xDEAD0000u | a; +} + static void load_packet(uint32_t node, uint32_t count) { for (uint32_t i = 0; i < count; i++) gp0_cmd_buf[i] = test_ram[node / 4u + 1u + i]; @@ -473,6 +498,39 @@ int main(void) { assert(gpu_exec_triangles.min_x == 50 && gpu_exec_triangles.max_x == 140); } + /* A list in the aperture validates against its own words at DMA time: + * the prepass survives the walk and the fill squashes as in RAM. A RAM + * node at the aliased offset is a different node. */ + reset_state(1); + build_hud(60, 128, 64, 120); + move_hud_to_aperture(); + gpu_ws_prepass_linked_list(OT_HEAD); + assert(ws_ui_prepass_count == 3); + { + const uint32_t nodes[3] = {NODE_RING, NODE_FILL, NODE_FLAT}; + const uint32_t stale = ws_ui_reject.stale; + for (int i = 0; i < 3; i++) { + const uint32_t a = AP_BASE + nodes[i]; + const uint32_t h = test_aperture[(a - AP_BASE) / 4u]; + gpu_ws_validate_linked_list_header(a, h); + gpu_ws_validate_linked_list_node(a, h >> 24); + } + assert(ws_ui_reject.stale == stale && ws_ui_prepass_count == 3); + gpu_exec_reset_triangles(); + for (uint32_t i = 0; i < 8; i++) + gp0_cmd_buf[i] = test_aperture[(NODE_FILL + 4u) / 4u + i]; + gp0_cmd_source_addr = AP_BASE + NODE_FILL + 4u; + gp0_words_needed = 8; + gp0_exec_shaded_quad(); + const int32_t centre = 60 + (128 - 60) / 2; + assert(gpu_exec_triangles.min_x == ws_scale_about(64, centre)); + assert(gpu_exec_triangles.max_x == ws_scale_about(120, centre)); + assert(GPU_RAM_KEY(AP_BASE + NODE_FILL) != GPU_RAM_KEY(NODE_FILL)); + gpu_ws_validate_linked_list_header(NODE_FILL, test_ram[NODE_FILL / 4u]); + assert(ws_ui_reject.stale == stale + 1 && ws_ui_stale_why[2] > 0); + } + test_aperture_used = 0; + puts("ws_auto_ui_untextured_exec_test: PASS"); return 0; }