Honor explicit HUD anchors in projection rendering

This commit is contained in:
Matthew Stanley
2026-10-06 21:49:56 -07:00
parent 8f95b59999
commit 20b0ea04bc
5 changed files with 156 additions and 10 deletions
+24
View File
@@ -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.
+2 -1
View File
@@ -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);
+6 -2
View File
@@ -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
+45 -7
View File
@@ -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) ||
@@ -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. */