From 20b0ea04bcc87642a2eac3e2059de1004fb1ee33 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Tue, 6 Oct 2026 21:49:56 -0700 Subject: [PATCH] Honor explicit HUD anchors in projection rendering --- docs/PARITY_CAMPAIGN.md | 24 ++++++ runtime/include/gpu.h | 3 +- runtime/include/mod_plugins.h | 8 +- runtime/src/gpu.c | 52 ++++++++++-- .../tests/test_ws_auto_ui_untextured_exec.c | 79 +++++++++++++++++++ 5 files changed, 156 insertions(+), 10 deletions(-) diff --git a/docs/PARITY_CAMPAIGN.md b/docs/PARITY_CAMPAIGN.md index c2880426..d5045bb1 100644 --- a/docs/PARITY_CAMPAIGN.md +++ b/docs/PARITY_CAMPAIGN.md @@ -4,6 +4,30 @@ Owner-approved scope, 2026-10-06. Tracking: `beads-eio.3.277`; shared execution work: `beads-wuhs`. Implementation is in progress. Source presence, a successful build, a short gameplay check, and owner acceptance are distinct milestones. +## First review pass + +Owner steering after the status check: prioritize runnable candidates and human +validation. Use build/configuration checks and focused tests for risky changes; +defer broad automated gameplay and performance matrices until owner feedback. +Windows review candidates come first in the build queue, with native Linux +artifacts following. Missing enhancements and known defects stay explicit. + +For each candidate, provide its launcher/path and this short playtest checklist: + +1. Launch, start/load, and play for a few minutes. +2. Check ordinary and ultrawide aspect settings: scene edges, HUD and dialogue. +3. Move the camera: look for jitter, seams, missing geometry and bad textures. +4. Pause/resume, change areas and revisit; note stalls and load times. +5. Check audio and existing multiplayer where applicable. + +The shared guarded HUD anchor now also supports projection-and-stretch mode. +Title code identifies completed screen-space packets; a common edge/centre +anchor can span separate DMA lists. Native-wide translation, packet guards, +expiry and native 4:3 behavior remain covered by focused execution fixtures. +This addresses the mechanism behind Ape's separate Status backing/text batches; +its title binding and actual visual result still need review. Tracking: +`beads-eio.3.125` and `beads-uutk`. + ## Acceptance target - Enhanced OpenGL is the normal renderer, with 1080p internal resolution. diff --git a/runtime/include/gpu.h b/runtime/include/gpu.h index b596394d..43f99c2c 100644 --- a/runtime/include/gpu.h +++ b/runtime/include/gpu.h @@ -451,7 +451,8 @@ void gpu_ws_set_nw_hud_corners(int on); void gpu_ws_tag_hud_primitive(uint32_t primitive, int edge); void gpu_ws_tag_world_primitive(uint32_t primitive, int is_world); void gpu_ws_set_adaptive_backdrop_preload(int enabled); -/* Explicit native-wide HUD packet anchor from a trusted title plugin. +/* Explicit screen-space HUD packet anchor from a trusted title plugin. + * Native-wide translates; projection-and-stretch scales about the anchor. * `prim` is the address of the PsyQ P_TAG word; the drawn command starts at * prim+4. anchor: -1 = left, 0 = center, +1 = right. */ void gpu_ws_tag_hud_prim(uint32_t prim, int anchor); diff --git a/runtime/include/mod_plugins.h b/runtime/include/mod_plugins.h index 228b735b..a7e0bc61 100644 --- a/runtime/include/mod_plugins.h +++ b/runtime/include/mod_plugins.h @@ -238,8 +238,12 @@ void psx_mod_set_native_wide_nclip_previous_site(uint32_t address, uint32_t expe void psx_mod_tag_hud_primitive(uint32_t primitive, int edge); /* Packet-guarded screen-space anchor: -1 left, +1 right, 0 stays centred. * Unlike the legacy role tag above, zero explicitly protects centred text - * from automatic layout transforms. Word immediately before the GP0 colour - * command; for a compound E1+SPRT packet this is its E1 word (P_TAG+4). */ + * from automatic layout transforms. Native-wide translates the group; + * projection-and-stretch squashes around the selected screen anchor. This + * works across separate DMA lists and is inert at native 4:3. Title code + * owns classification and must tag each completed packet. Pass the word + * immediately before the GP0 colour command; for a compound E1+SPRT packet + * this is its E1 word (P_TAG+4). */ void psx_mod_anchor_hud_primitive(uint32_t primitive, int edge); /* Screen-space masks identified by their title's producer, P_TAG addresses. * Flat panels extend only their exterior boundaries. Radial flat/Gouraud diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index dbd827d0..2887699d 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -2416,7 +2416,7 @@ void gpu_ws_tag_hud_prim(uint32_t prim, int anchor) { uint32_t command_addr = prim + 4u; uint32_t words[12]; uint32_t word_count = 0; - if (!ws_native_wide_configured() || + if (!(ws_native_wide_configured() || ws_configured()) || (prim & 3u) != 0 || prim > UINT32_MAX - 4u || !ws_hud_command_words(command_addr, words, &word_count)) { ws_hud_tag_stats.tag_rejected++; @@ -2547,9 +2547,8 @@ static int ws_nw_radial_mask_transform(int32_t x[4], int32_t y[4]) { return 1; } -static int ws_nw_explicit_hud_delta(int32_t *out_delta) { - if (out_delta) *out_delta = 0; - if (!ws_native_wide_active() || gp0_cmd_source_addr == 0xFFFFFFFFu) +static int ws_explicit_hud_anchor(int *out_anchor) { + if (gp0_cmd_source_addr == 0xFFFFFFFFu) return 0; if (gp0_words_needed <= 0 || gp0_words_needed > 12) return 0; @@ -2563,9 +2562,36 @@ static int ws_nw_explicit_hud_delta(int32_t *out_delta) { ws_tag_stats_note_lookup(&ws_hud_tag_stats, result); if (result != WS_HUD_ANCHOR_HIT) return 0; - int32_t off = ws_nw_offset(); + if (out_anchor) *out_anchor = anchor; + return 1; +} + +static int ws_nw_explicit_hud_delta(int32_t *out_delta) { + if (out_delta) *out_delta = 0; + if (!ws_native_wide_active()) return 0; + int anchor; + if (!ws_explicit_hud_anchor(&anchor)) return 0; if (out_delta) - *out_delta = ws_hud_anchor_native_delta(1, off, anchor); + *out_delta = ws_hud_anchor_native_delta(1, ws_nw_offset(), anchor); + return 1; +} + +/* A title can identify one widget whose pieces arrive in separate DMA lists. + * Per-list auto grouping cannot see that ownership. Use the same guarded + * edge/centre role as native-wide, expressed as a squash pivot here. */ +static int ws_projection_hud_anchor(int32_t *out_anchor) { + if (!ws_active()) return 0; + int edge; + if (!ws_explicit_hud_anchor(&edge)) return 0; + if (out_anchor) + *out_anchor = ws_disp_x() + (edge < 0 ? 0 : edge > 0 ? ws_disp_w() : ws_disp_w() / 2); + return 1; +} + +static int ws_projection_hud_transform(int32_t *x, int count) { + int32_t anchor; + if (!ws_projection_hud_anchor(&anchor)) return 0; + for (int i = 0; i < count; i++) x[i] = ws_scale_about(x[i], anchor); return 1; } @@ -2574,6 +2600,9 @@ static int ws_nw_explicit_hud_delta(int32_t *out_delta) { * a direct hit too in case a tag site passes the colour-word address. */ static int ws_tagged_anchor(int32_t *out_ax) { if (!ws_active() || gp0_cmd_source_addr == 0xFFFFFFFFu) return 0; + /* Explicit screen-space ownership wins over a recycled billboard tag. + * The regular UI transform below will apply it exactly once. */ + if (ws_projection_hud_anchor(NULL)) return 0; uint32_t now = (uint32_t)s_frame_count; for (int variant = 0; variant < 2; variant++) { uint32_t key = GPU_RAM_KEY((gp0_cmd_source_addr - (variant ? 0u : 4u))); @@ -2896,6 +2925,7 @@ static int ws_axis_aligned_quad(const int32_t vx[4], const int32_t vy[4]) { * quad): squashed with its widget when the prepass admitted it as lying * inside that widget (ws_ui_prepass_finish). */ static int ws_auto_ui_transform_part(int32_t *vx, int n) { + if (ws_projection_hud_transform(vx, n)) return 1; if (ws_nw_explicit_hud_delta(NULL) || psx_ws_prim_is_tagged()) return 0; int32_t anchor; if (!ws_auto_ui_anchor(&anchor)) return 0; @@ -2906,6 +2936,7 @@ static int ws_auto_ui_transform_part(int32_t *vx, int n) { } static int ws_auto_ui_transform_quad(int32_t vx[4], const int32_t vy[4]) { + if (ws_projection_hud_transform(vx, 4)) return 1; if (ws_nw_explicit_hud_delta(NULL) || psx_ws_prim_is_tagged()) return 0; if (!ws_axis_aligned_quad(vx, vy)) return ws_auto_ui_transform_part(vx, 4); @@ -2931,7 +2962,14 @@ static int ws_auto_ui_transform_quad(int32_t vx[4], const int32_t vy[4]) { } static int ws_auto_ui_transform_rect(int32_t *x, int32_t y, int *w, int h) { - if (!x || !w || *w <= 0 || ws_nw_explicit_hud_delta(NULL) || psx_ws_prim_is_tagged()) + if (!x || !w || *w <= 0) return 0; + int32_t explicit_anchor; + if (ws_projection_hud_anchor(&explicit_anchor)) { + *x = ws_scale_about(*x, explicit_anchor); + *w = (int)ws_scale_len(*w); + return 1; + } + if (ws_nw_explicit_hud_delta(NULL) || psx_ws_prim_is_tagged()) return 0; int32_t X = ws_disp_x(), W = ws_disp_w(), H = ws_disp_h(); if ((*x <= X && *x + *w >= X + W && y <= 0 && y + h >= H) || diff --git a/runtime/tests/test_ws_auto_ui_untextured_exec.c b/runtime/tests/test_ws_auto_ui_untextured_exec.c index 572ad517..efa5a0fe 100644 --- a/runtime/tests/test_ws_auto_ui_untextured_exec.c +++ b/runtime/tests/test_ws_auto_ui_untextured_exec.c @@ -190,11 +190,90 @@ static void test_wall_behind_front_layer(int triangle) { assert(ws_auto_ui_transform_count == 0); } +static void test_explicit_projection_widget(void) { + const uint32_t tile = 0x20000u, glyph = 0x21000u; + reset_state(0); + hres1 = 0; hres2 = 1; /* Captured Ape display: 384x240. */ + h_display_x2 = h_display_x1 + 384u * 7u; + ws_hud_sprt = 0; + gpu_ws_set_auto_ui_squash(0); /* Explicit ownership is independent. */ + ws_hud_anchor_clear(ws_hud_anchor_tags, WS_HUD_ANCHOR_TABLE_SIZE); + memset(test_ram, 0, sizeof(test_ram)); + /* Actual Status backing tile shape, followed by a separate glyph list. + * Replacing the prepass must not lose the earlier list's widget anchor. */ + const uint32_t backing[] = {0x76606060u, 0x00C60146u, 0x77ABB090u}; + const uint32_t text[] = {0x7D808080u, pack_vertex(54, 64), 0x7C5D9800u}; + put_node(tile, 0xFFFFFFu, backing, 3); + put_node(glyph, 0xFFFFFFu, text, 3); + gpu_ws_tag_hud_prim(tile, 0); + gpu_ws_tag_hud_prim(glyph, 0); + test_ram[OT_HEAD / 4u] = glyph; + gpu_ws_prepass_linked_list(OT_HEAD); + load_packet(tile, 3); + last_scaled_rect.calls = 0; + gp0_exec_textured_8x8(); + assert(last_scaled_rect.calls == 1); + assert(last_scaled_rect.x == ws_scale_about(326, 192)); + assert(last_scaled_rect.w == 3); + assert(last_scaled_rect.u1 - last_scaled_rect.u0 == 8); + + /* A stale legacy billboard classification cannot double-transform an + * explicitly owned screen-space packet. */ + WsTag *legacy = &ws_tags[(glyph >> 2) & (WS_TAG_BUCKETS - 1)]; + legacy->key = glyph; legacy->stamp = (uint32_t)s_frame_count; + legacy->anchor_x = 54; + for (int edge = -1; edge <= 1; edge++) { + gpu_ws_tag_hud_prim(glyph, edge); + load_packet(glyph, 3); + last_scaled_rect.calls = 0; + gp0_exec_textured_16x16(); + assert(last_scaled_rect.calls == 1); + assert(last_scaled_rect.x == ws_scale_about(54, (edge + 1) * 192)); + assert(last_scaled_rect.w == 6); + } + memset(ws_tags, 0, sizeof(ws_tags)); + + /* Complete packet guards, frame expiry and native 4:3 identity. */ + load_packet(glyph, 3); + gp0_cmd_buf[2] ^= 1u; + last_textured_rect.calls = last_scaled_rect.calls = 0; + gp0_exec_textured_16x16(); + assert(last_textured_rect.calls == 1 && last_textured_rect.x == 54); + assert(last_scaled_rect.calls == 0); + s_frame_count += 3; + load_packet(glyph, 3); + last_textured_rect.calls = last_scaled_rect.calls = 0; + gp0_exec_textured_16x16(); + assert(last_textured_rect.calls == 1 && last_textured_rect.x == 54); + assert(last_scaled_rect.calls == 0); + gpu_ws_tag_hud_prim(glyph, 0); + ws_xnum = ws_xden = 1; + last_textured_rect.calls = last_scaled_rect.calls = 0; + gp0_exec_textured_16x16(); + assert(last_textured_rect.calls == 1 && last_textured_rect.x == 54); + assert(last_scaled_rect.calls == 0); + + /* Explicitly identified panels do not depend on automatic size/rank + * heuristics; untagged world/fades still use the existing conservative gate. */ + ws_xnum = 3; ws_xden = 8; + const uint32_t panel[] = {0x28202020u, pack_vertex(30, 30), + pack_vertex(334, 30), pack_vertex(30, 206), pack_vertex(334, 206)}; + put_node(tile, 0xFFFFFFu, panel, 5); + gpu_ws_tag_hud_prim(tile, 0); + load_packet(tile, 5); + gpu_exec_reset_triangles(); + gp0_exec_mono_quad(); + assert(gpu_exec_triangles.min_x == ws_scale_about(30, 192)); + assert(gpu_exec_triangles.max_x == ws_scale_about(334, 192)); + ws_hud_anchor_clear(ws_hud_anchor_tags, WS_HUD_ANCHOR_TABLE_SIZE); +} + int main(void) { int gmin, gmax, fmin, fmax; test_wall_behind_front_layer(0); test_wall_behind_front_layer(1); + test_explicit_projection_widget(); /* A real HUD remains eligible when its last drawing layer is followed * by an empty OT bucket, as in Ape's memory-card menu. */