Anchor GTE-projected HUD pieces by translation; expose recent-GTE-projection query

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Matthew Stanley
2026-10-24 14:12:26 -07:00
parent a6b9c2cac0
commit bcda320f9c
11 changed files with 292 additions and 27 deletions
+5
View File
@@ -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
+12
View File
@@ -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
+11
View File
@@ -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.
+59
View File
@@ -0,0 +1,59 @@
#ifndef PSXRECOMP_WS_GTE_SXY_H
#define PSXRECOMP_WS_GTE_SXY_H
#include <stdint.h>
#include <string.h>
#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
+51 -11
View File
@@ -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,
+66 -16
View File
@@ -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())
+3
View File
@@ -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. */
+6
View File
@@ -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);
}
@@ -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; }
+44
View File
@@ -0,0 +1,44 @@
#include "ws_gte_sxy.h"
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
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;
}
+34
View File
@@ -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));