From bcda320f9c38ffe5884ae85f143c64af4e1d489e Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Sat, 24 Oct 2026 14:12:26 -0700 Subject: [PATCH] Anchor GTE-projected HUD pieces by translation; expose recent-GTE-projection query Co-Authored-By: Claude Sonnet 5.5 --- runtime/CMakeLists.txt | 5 ++ runtime/include/gpu.h | 12 ++++ runtime/include/mod_plugins.h | 11 +++ runtime/include/ws_gte_sxy.h | 59 ++++++++++++++++ runtime/include/ws_hud_anchor.h | 62 +++++++++++++--- runtime/src/gpu.c | 82 +++++++++++++++++----- runtime/src/gte.cpp | 3 + runtime/src/mod_runtime.cpp | 6 ++ runtime/tests/test_gte_register_access.cpp | 1 + runtime/tests/test_ws_gte_sxy.c | 44 ++++++++++++ runtime/tests/test_ws_hud_anchor.c | 34 +++++++++ 11 files changed, 292 insertions(+), 27 deletions(-) create mode 100644 runtime/include/ws_gte_sxy.h create mode 100644 runtime/tests/test_ws_gte_sxy.c diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 59dcc656..00d68318 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -1027,6 +1027,11 @@ if(BUILD_TESTING) target_include_directories(ws_hud_anchor_test PRIVATE include) add_test(NAME ws_hud_anchor_test COMMAND ws_hud_anchor_test) + add_executable(ws_gte_sxy_test + tests/test_ws_gte_sxy.c) + target_include_directories(ws_gte_sxy_test PRIVATE include) + add_test(NAME ws_gte_sxy_test COMMAND ws_gte_sxy_test) + add_executable(gpu_textured_dot_nw_shift_exec_test tests/test_gpu_textured_dot_nw_shift_exec.c src/psx_video_timing.c diff --git a/runtime/include/gpu.h b/runtime/include/gpu.h index 08550135..c7ad881f 100644 --- a/runtime/include/gpu.h +++ b/runtime/include/gpu.h @@ -518,6 +518,18 @@ void gpu_ws_set_adaptive_backdrop_preload(int enabled); * `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); +/* Same, for a primitive whose X the game took from GTE-projected screen + * coordinates, which already carry the widescreen X squash about the display + * centre. Projection-and-stretch translates it to the edge instead of scaling + * it again; native-wide behaves as for gpu_ws_tag_hud_prim. */ +void gpu_ws_tag_hud_prim_projected(uint32_t prim, int anchor); +/* GTE-projection provenance under the widescreen squash (ws_gte_sxy.h): the + * GTE reports each SXY it produces; a title plugin asks whether a packet + * vertex is one of them, i.e. whether the game built that primitive from + * projected geometry (its X is already squashed) rather than authored + * screen coordinates. Recorded only while the squash is engaged. */ +void gpu_ws_note_gte_sxy(int32_t x, int32_t y); +int gpu_ws_gte_sxy_recent(int32_t x, int32_t y); /* Stretch a trusted authored background primitive about the display center in * the native-wide output only (GL/SW). prim+4 is the command address, including * for a command embedded inside a longer DMA packet. Only opaque polygons and diff --git a/runtime/include/mod_plugins.h b/runtime/include/mod_plugins.h index 671b41fe..9ad2122b 100644 --- a/runtime/include/mod_plugins.h +++ b/runtime/include/mod_plugins.h @@ -360,6 +360,17 @@ void psx_mod_tag_hud_primitive(uint32_t primitive, int edge); * 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); +/* As psx_mod_anchor_hud_primitive, for a packet whose X coordinates came out + * of the GTE (so they already carry the widescreen squash about the display + * centre, and the guest wrote them verbatim). Projection-and-stretch moves the + * piece to the selected edge by translation, keeping its proportions; scaling + * it about the edge again would squash it twice. */ +void psx_mod_anchor_hud_primitive_projected(uint32_t primitive, int edge); +/* Did the GTE project a vertex at screen (x, y) in this or the previous + * vblank? Under the widescreen squash a primitive whose every vertex answers + * yes was built from projected geometry (already squashed); one that does not + * carries authored screen coordinates. Always 0 while the squash is off. */ +int psx_mod_gte_sxy_recent(int32_t x, int32_t y); /* Screen-space masks identified by their title's producer, P_TAG addresses. * Flat panels extend only their exterior boundaries. Radial flat/Gouraud * quads scale around the display centre for an aspect-aware iris transition. diff --git a/runtime/include/ws_gte_sxy.h b/runtime/include/ws_gte_sxy.h new file mode 100644 index 00000000..a37f1a33 --- /dev/null +++ b/runtime/include/ws_gte_sxy.h @@ -0,0 +1,59 @@ +#ifndef PSXRECOMP_WS_GTE_SXY_H +#define PSXRECOMP_WS_GTE_SXY_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Which screen positions did the GTE project recently? + * + * Under projection-and-stretch widescreen the GTE squashes every projected X + * about the display centre. A title that builds HUD from GTE-projected + * geometry (vector digits and lettering run through its model renderer) then + * writes already-squashed X into its packets, whereas screen-space HUD packets + * carry authored X. The two need different corrections, and the packet alone + * does not say which it is. The GTE stamps every SXY it produces here with the + * vblank it happened in; a packet vertex that matches a recent stamp was + * projected. + * + * "Recent" is WS_GTE_SXY_WINDOW vblanks: games commonly rebuild their packets + * every few vblanks and resubmit the same ordering table in between (Army Men + * rebuilds every fourth), so a retained packet is still younger than its + * projection plus one rebuild period. The cell grid is 1024 x 512, covering + * the signed 11-bit GP0 vertex range. A hit is evidence, not proof: with ~1000 + * projections a frame the chance that a whole polygon matches by accident is + * negligible, so require every vertex of a primitive to hit. */ +#define WS_GTE_SXY_W 1024u +#define WS_GTE_SXY_H 512u +#define WS_GTE_SXY_CELLS (WS_GTE_SXY_W * WS_GTE_SXY_H) +#define WS_GTE_SXY_WINDOW 8u + +typedef struct { + uint16_t stamp[WS_GTE_SXY_CELLS]; /* 0x8000 | vblank (15 bits); 0 = never */ +} WsGteSxy; + +static inline uint32_t ws_gte_sxy_cell(int32_t x, int32_t y) { + return (((uint32_t)(y + 256) & (WS_GTE_SXY_H - 1u)) * WS_GTE_SXY_W) + + ((uint32_t)(x + 512) & (WS_GTE_SXY_W - 1u)); +} + +static inline void ws_gte_sxy_note(WsGteSxy *s, uint32_t frame, + int32_t x, int32_t y) { + s->stamp[ws_gte_sxy_cell(x, y)] = (uint16_t)(0x8000u | (frame & 0x7FFFu)); +} + +static inline int ws_gte_sxy_recent(const WsGteSxy *s, uint32_t frame, + int32_t x, int32_t y) { + const uint16_t st = s->stamp[ws_gte_sxy_cell(x, y)]; + if (!st) return 0; + return ((frame - st) & 0x7FFFu) <= WS_GTE_SXY_WINDOW; +} + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/runtime/include/ws_hud_anchor.h b/runtime/include/ws_hud_anchor.h index 40480f23..ce5d2773 100644 --- a/runtime/include/ws_hud_anchor.h +++ b/runtime/include/ws_hud_anchor.h @@ -20,6 +20,11 @@ typedef struct { uint32_t frame; int8_t anchor; uint8_t used; + /* The primitive's X came out of the GTE and already carries the + * widescreen squash about the display centre (see ws_gte_sxy.h). The + * projection path then translates it to the anchor instead of scaling it + * about the anchor, and never lets automatic UI layout squash it again. */ + uint8_t projected; WsPrepassPacketGuard guard; } WsHudAnchorTag; @@ -54,10 +59,12 @@ static inline void ws_hud_anchor_clear(WsHudAnchorTag *tags, tags[i].used = 0; } -static inline void ws_hud_anchor_insert(WsHudAnchorTag *tags, uint32_t count, - uint32_t command_addr, int anchor, - const WsPrepassPacketGuard *guard, - uint32_t frame) { +static inline void ws_hud_anchor_insert_stored(WsHudAnchorTag *tags, + uint32_t count, + uint32_t command_addr, + int anchor, int projected, + const WsPrepassPacketGuard *guard, + uint32_t frame) { if (!tags || !count || !guard) return; uint32_t idx = ws_hud_anchor_slot(command_addr) & (count - 1u); uint32_t victim = idx; @@ -75,9 +82,29 @@ static inline void ws_hud_anchor_insert(WsHudAnchorTag *tags, uint32_t count, tags[victim].command_addr = command_addr; tags[victim].frame = frame; tags[victim].anchor = (int8_t)ws_hud_anchor_clamp(anchor); + tags[victim].projected = projected ? 1u : 0u; tags[victim].guard = *guard; } +static inline void ws_hud_anchor_insert(WsHudAnchorTag *tags, uint32_t count, + uint32_t command_addr, int anchor, + const WsPrepassPacketGuard *guard, + uint32_t frame) { + ws_hud_anchor_insert_stored(tags, count, command_addr, anchor, 0, guard, + frame); +} + +/* Tag a GTE-projected piece (see WsHudAnchorTag.projected). */ +static inline void ws_hud_anchor_insert_projected(WsHudAnchorTag *tags, + uint32_t count, + uint32_t command_addr, + int edge, + const WsPrepassPacketGuard *guard, + uint32_t frame) { + ws_hud_anchor_insert_stored(tags, count, command_addr, edge, 1, guard, + frame); +} + /* Lookup outcome, so callers can keep always-on counters that say WHY a tagged * primitive was or was not honoured (absent, expired, or packet rewritten). */ enum { @@ -87,13 +114,14 @@ enum { WS_HUD_ANCHOR_GUARD_MISMATCH = -2 }; -static inline int ws_hud_anchor_lookup_result(const WsHudAnchorTag *tags, - uint32_t count, - uint32_t command_addr, - const uint32_t *words, - uint32_t word_count, - uint32_t frame, - int *out_anchor) { +static inline int ws_hud_anchor_lookup_result_ex(const WsHudAnchorTag *tags, + uint32_t count, + uint32_t command_addr, + const uint32_t *words, + uint32_t word_count, + uint32_t frame, + int *out_anchor, + int *out_projected) { if (!tags || !count || !words) return WS_HUD_ANCHOR_ABSENT; uint32_t idx = ws_hud_anchor_slot(command_addr) & (count - 1u); for (uint32_t i = 0; i < WS_HUD_ANCHOR_PROBES && i < count; i++) { @@ -106,11 +134,23 @@ static inline int ws_hud_anchor_lookup_result(const WsHudAnchorTag *tags, if (!ws_prepass_packet_matches(&tag->guard, words, word_count)) return WS_HUD_ANCHOR_GUARD_MISMATCH; if (out_anchor) *out_anchor = tag->anchor; + if (out_projected) *out_projected = tag->projected; return WS_HUD_ANCHOR_HIT; } return WS_HUD_ANCHOR_ABSENT; } +static inline int ws_hud_anchor_lookup_result(const WsHudAnchorTag *tags, + uint32_t count, + uint32_t command_addr, + const uint32_t *words, + uint32_t word_count, + uint32_t frame, + int *out_anchor) { + return ws_hud_anchor_lookup_result_ex(tags, count, command_addr, words, + word_count, frame, out_anchor, 0); +} + static inline int ws_hud_anchor_lookup(const WsHudAnchorTag *tags, uint32_t count, uint32_t command_addr, diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index 2b1deaac..2733250f 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -43,6 +43,7 @@ #include "ws_backdrop_extent.h" #include "ws_prepass_guard.h" #include "ws_hud_anchor.h" +#include "ws_gte_sxy.h" #include "ws_repeat_rect.h" #include "ws_feedback_rect.h" #include "ws_screen_mask.h" @@ -2581,7 +2582,18 @@ static void ws_tag_stats_note_lookup(GpuWsTagStats *stats, int result) { } } -void gpu_ws_tag_hud_prim(uint32_t prim, int anchor) { +/* Positions the GTE projected this and the previous vblank (ws_gte_sxy.h). + * Only recorded while the squash is engaged: nothing else reads it. */ +static WsGteSxy ws_gte_sxy; +void gpu_ws_note_gte_sxy(int32_t x, int32_t y) { + if (!ws_active()) return; + ws_gte_sxy_note(&ws_gte_sxy, (uint32_t)s_frame_count, x, y); +} +int gpu_ws_gte_sxy_recent(int32_t x, int32_t y) { + return ws_gte_sxy_recent(&ws_gte_sxy, (uint32_t)s_frame_count, x, y); +} + +static void ws_tag_hud_prim(uint32_t prim, int anchor, int projected) { ws_hud_tag_stats.tag_calls++; uint32_t command_addr = prim + 4u; uint32_t words[12]; @@ -2595,11 +2607,23 @@ void gpu_ws_tag_hud_prim(uint32_t prim, int anchor) { WsPrepassPacketGuard guard = ws_prepass_packet_guard(words, word_count); - ws_hud_anchor_insert(ws_hud_anchor_tags, WS_HUD_ANCHOR_TABLE_SIZE, - GPU_RAM_KEY(command_addr), anchor, &guard, - (uint32_t)s_frame_count); + if (projected) + ws_hud_anchor_insert_projected(ws_hud_anchor_tags, + WS_HUD_ANCHOR_TABLE_SIZE, + GPU_RAM_KEY(command_addr), anchor, + &guard, (uint32_t)s_frame_count); + else + ws_hud_anchor_insert(ws_hud_anchor_tags, WS_HUD_ANCHOR_TABLE_SIZE, + GPU_RAM_KEY(command_addr), anchor, &guard, + (uint32_t)s_frame_count); ws_hud_tag_stats.last_tag_frame = (uint32_t)s_frame_count; } +void gpu_ws_tag_hud_prim(uint32_t prim, int anchor) { + ws_tag_hud_prim(prim, anchor, 0); +} +void gpu_ws_tag_hud_prim_projected(uint32_t prim, int anchor) { + ws_tag_hud_prim(prim, anchor, 1); +} void gpu_ws_tag_background_prim(uint32_t prim) { ws_background_tag_stats.tag_calls++; @@ -2730,7 +2754,7 @@ static int ws_nw_radial_mask_transform(int32_t x[4], int32_t y[4]) { return 1; } -static int ws_explicit_hud_anchor(int *out_anchor) { +static int ws_explicit_hud_anchor_ex(int *out_anchor, int *out_projected) { if (gp0_cmd_source_addr == 0xFFFFFFFFu) return 0; if (gp0_words_needed <= 0 || gp0_words_needed > 12) @@ -2739,17 +2763,21 @@ static int ws_explicit_hud_anchor(int *out_anchor) { if ((command_addr & 3u) || (command_addr >= psx_ram_live_bytes() && !psx_mod_gpu_dma_memory_contains(command_addr, 4u))) return 0; - int anchor = 0; - int result = ws_hud_anchor_lookup_result( + int anchor = 0, projected = 0; + int result = ws_hud_anchor_lookup_result_ex( ws_hud_anchor_tags, WS_HUD_ANCHOR_TABLE_SIZE, GPU_RAM_KEY(command_addr), gp0_cmd_buf, (uint32_t)gp0_words_needed, (uint32_t)s_frame_count, - &anchor); + &anchor, &projected); ws_tag_stats_note_lookup(&ws_hud_tag_stats, result); if (result != WS_HUD_ANCHOR_HIT) return 0; if (out_anchor) *out_anchor = anchor; + if (out_projected) *out_projected = projected; return 1; } +static int ws_explicit_hud_anchor(int *out_anchor) { + return ws_explicit_hud_anchor_ex(out_anchor, NULL); +} static int ws_nw_explicit_hud_delta(int32_t *out_delta) { if (out_delta) *out_delta = 0; @@ -2764,19 +2792,36 @@ static int ws_nw_explicit_hud_delta(int32_t *out_delta) { /* 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) { +static int ws_projection_hud_anchor_ex(int32_t *out_anchor, int *out_projected) { if (!ws_active()) return 0; - int edge; - if (!ws_explicit_hud_anchor(&edge)) return 0; + int edge, projected = 0; + if (!ws_explicit_hud_anchor_ex(&edge, &projected)) return 0; + if (out_projected) *out_projected = projected; 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_anchor(int32_t *out_anchor) { + return ws_projection_hud_anchor_ex(out_anchor, NULL); +} + +/* Shift that takes an X already squashed about the display centre to where + * scaling it about the anchor would have put it: (anchor - centre) * (1 - s). */ +static int32_t ws_projected_hud_shift(int32_t anchor) { + return ws_scale_about(0, anchor) - ws_scale_about(0, ws_disp_x() + ws_disp_w() / 2); +} + 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); + int projected = 0; + if (!ws_projection_hud_anchor_ex(&anchor, &projected)) return 0; + if (projected) { + const int32_t shift = ws_projected_hud_shift(anchor); + for (int i = 0; i < count; i++) x[i] += shift; + } else { + for (int i = 0; i < count; i++) x[i] = ws_scale_about(x[i], anchor); + } return 1; } @@ -3279,9 +3324,14 @@ static int ws_auto_ui_transform_quad(int32_t vx[4], int32_t vy[4]) { static int ws_auto_ui_transform_rect(int32_t *x, int32_t *y, int *w, int *h) { if (!x || !y || !w || !h || *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); + int explicit_projected = 0; + if (ws_projection_hud_anchor_ex(&explicit_anchor, &explicit_projected)) { + if (explicit_projected) { + *x += ws_projected_hud_shift(explicit_anchor); + } else { + *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()) diff --git a/runtime/src/gte.cpp b/runtime/src/gte.cpp index 5d60ef36..4e81a5e4 100644 --- a/runtime/src/gte.cpp +++ b/runtime/src/gte.cpp @@ -17,6 +17,7 @@ extern "C" int psx_netplay_active(void); extern "C" uint32_t psx_read_word(uint32_t addr); extern "C" int gpu_ws_precise_nclip_enabled(void); +extern "C" void gpu_ws_note_gte_sxy(int32_t x, int32_t y); /* overlay_loader.c: nonzero while an overlay shadow diff (interpreter record * plus native replay of one candidate) is running. */ extern "C" int psx_overlay_shadow_diff_active(void); @@ -1259,6 +1260,8 @@ void gte_rtps_internal(GTEState* gte, int16_t* V, bool setMac0, uint32_t instr) int64_t sx = sx16 >> 16; int64_t sy = sy16 >> 16; gte->push_sxy(sx, sy); + gpu_ws_note_gte_sxy((int16_t)(gte->SXY[2] & 0xFFFF), + (int16_t)((uint32_t)gte->SXY[2] >> 16)); if (!s_gte_replay_sandbox) { /* Bound the 16.16 transport so an extreme projection cannot wrap an * int32 and masquerade as a valid on-screen shadow. */ diff --git a/runtime/src/mod_runtime.cpp b/runtime/src/mod_runtime.cpp index e2e09ba2..e17f0eec 100644 --- a/runtime/src/mod_runtime.cpp +++ b/runtime/src/mod_runtime.cpp @@ -3469,6 +3469,12 @@ extern "C" void psx_mod_tag_hud_primitive(uint32_t primitive, int edge) { extern "C" void psx_mod_anchor_hud_primitive(uint32_t primitive, int edge) { gpu_ws_tag_hud_prim(primitive, edge); } +extern "C" int psx_mod_gte_sxy_recent(int32_t x, int32_t y) { + return gpu_ws_gte_sxy_recent(x, y); +} +extern "C" void psx_mod_anchor_hud_primitive_projected(uint32_t primitive, int edge) { + gpu_ws_tag_hud_prim_projected(primitive, edge); +} extern "C" void psx_mod_tag_screen_mask_quad(uint32_t primitive) { gpu_ws_tag_screen_mask_quad(primitive); } diff --git a/runtime/tests/test_gte_register_access.cpp b/runtime/tests/test_gte_register_access.cpp index c8f1f279..999787a2 100644 --- a/runtime/tests/test_gte_register_access.cpp +++ b/runtime/tests/test_gte_register_access.cpp @@ -61,6 +61,7 @@ extern "C" void gte_test_execute_reference(CPUState *cpu, uint32_t cmd); /* gte.cpp runtime dependencies that are irrelevant to register-transfer tests. */ extern "C" int gpu_ws_present_native_43(void) { return 0; } +extern "C" void gpu_ws_note_gte_sxy(int32_t, int32_t) {} static int g_test_precise_nclip_enabled; static int g_test_render_projective; extern "C" int gpu_render_projective_enabled(void) { return g_test_render_projective; } diff --git a/runtime/tests/test_ws_gte_sxy.c b/runtime/tests/test_ws_gte_sxy.c new file mode 100644 index 00000000..c2154b1b --- /dev/null +++ b/runtime/tests/test_ws_gte_sxy.c @@ -0,0 +1,44 @@ +#include "ws_gte_sxy.h" + +#include +#include +#include + +int main(void) { + WsGteSxy *s = (WsGteSxy *)calloc(1, sizeof *s); + assert(s); + + /* Nothing recorded yet, including at the zero cell. */ + assert(!ws_gte_sxy_recent(s, 10u, 100, 50)); + assert(!ws_gte_sxy_recent(s, 0u, -512, -256)); + + ws_gte_sxy_note(s, 10u, 397, 58); + ws_gte_sxy_note(s, 10u, -20, -8); + assert(ws_gte_sxy_recent(s, 10u, 397, 58)); + assert(ws_gte_sxy_recent(s, 10u, -20, -8)); + assert(!ws_gte_sxy_recent(s, 10u, 398, 58)); + assert(!ws_gte_sxy_recent(s, 10u, 397, 59)); + + /* A packet built in one vblank and resubmitted for the next several + * (the ordering table is rebuilt every fourth vblank) still matches. */ + for (uint32_t f = 10u; f <= 10u + WS_GTE_SXY_WINDOW; ++f) + assert(ws_gte_sxy_recent(s, f, 397, 58)); + assert(!ws_gte_sxy_recent(s, 10u + WS_GTE_SXY_WINDOW + 1u, 397, 58)); + + /* A fresh projection of the same position refreshes it. */ + ws_gte_sxy_note(s, 16u, 397, 58); + assert(ws_gte_sxy_recent(s, 20u, 397, 58)); + + /* A reader behind the stamp (render thread lag) does not match. */ + assert(!ws_gte_sxy_recent(s, 3u, 397, 58)); + + /* The 15-bit stamp wraps without turning ancient entries into hits. */ + ws_gte_sxy_note(s, 32767u, 1, 1); + assert(ws_gte_sxy_recent(s, 32767u, 1, 1)); + assert(ws_gte_sxy_recent(s, 32768u + 2u, 1, 1)); + assert(!ws_gte_sxy_recent(s, 32768u + 100u, 1, 1)); + + free(s); + puts("ws_gte_sxy_test: PASS"); + return 0; +} diff --git a/runtime/tests/test_ws_hud_anchor.c b/runtime/tests/test_ws_hud_anchor.c index 5f3a7089..630d7b3e 100644 --- a/runtime/tests/test_ws_hud_anchor.c +++ b/runtime/tests/test_ws_hud_anchor.c @@ -65,6 +65,40 @@ int main(void) { assert(!ws_hud_anchor_lookup(tags, WS_HUD_ANCHOR_TABLE_SIZE, 0x80010008u, packet, 4u, 20u, &anchor)); + /* GTE-projected tags keep their edge (native-wide still translates by it) + * and carry the projected flag; plain tags do not. */ + { + int projected = 7; + ws_hud_anchor_insert_projected(tags, WS_HUD_ANCHOR_TABLE_SIZE, + 0x80010004u, 5, &guard, 20u); + anchor = 0; + assert(ws_hud_anchor_lookup_result_ex( + tags, WS_HUD_ANCHOR_TABLE_SIZE, 0x80010004u, packet, 4u, + 20u, &anchor, &projected) == WS_HUD_ANCHOR_HIT); + assert(anchor == 1 && projected == 1); + ws_hud_anchor_insert_projected(tags, WS_HUD_ANCHOR_TABLE_SIZE, + 0x80010004u, -3, &guard, 20u); + assert(ws_hud_anchor_lookup_result_ex( + tags, WS_HUD_ANCHOR_TABLE_SIZE, 0x80010004u, packet, 4u, + 20u, &anchor, &projected) == WS_HUD_ANCHOR_HIT); + assert(anchor == -1 && projected == 1); + assert(ws_hud_anchor_native_delta(1, 53, anchor) == -53); + /* A projected centre tag is not the same as no tag: it must stop + * automatic layout from squashing the piece a second time. */ + ws_hud_anchor_insert_projected(tags, WS_HUD_ANCHOR_TABLE_SIZE, + 0x80010004u, 0, &guard, 20u); + assert(ws_hud_anchor_lookup_result_ex( + tags, WS_HUD_ANCHOR_TABLE_SIZE, 0x80010004u, packet, 4u, + 20u, &anchor, &projected) == WS_HUD_ANCHOR_HIT); + assert(anchor == 0 && projected == 1); + ws_hud_anchor_insert(tags, WS_HUD_ANCHOR_TABLE_SIZE, + 0x80010004u, -7, &guard, 20u); + assert(ws_hud_anchor_lookup_result_ex( + tags, WS_HUD_ANCHOR_TABLE_SIZE, 0x80010004u, packet, 4u, + 20u, &anchor, &projected) == WS_HUD_ANCHOR_HIT); + assert(anchor == -1 && projected == 0); + } + ws_hud_anchor_clear(tags, WS_HUD_ANCHOR_TABLE_SIZE); assert(!ws_hud_anchor_lookup(tags, WS_HUD_ANCHOR_TABLE_SIZE, 0x80010004u, packet, 4u, 20u, &anchor));