widescreen: auto_ui_anchor = "in_place" squashes each HUD run about its own centre

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matthew Stanley
2026-10-01 11:13:30 -07:00
parent 3505f2a014
commit 85c88d269c
11 changed files with 94 additions and 18 deletions
+13
View File
@@ -95,6 +95,10 @@ auto_ui_squash = true # pre-scan the current GPU linked list,
# select its highest populated UI rank,
# and share one anchor across each
# complete glyph/icon run.
auto_ui_anchor = "edges" # "edges" (default): each run pins to
# its left/centre/right third.
# "in_place": each run squashes about
# its own centre.
clear_reveal = true # clear synthetic native-wide side margins
# at opted-in scene/map boundaries (default false).
nw_left_hud_packet_lo = "0x000E3400" # optional targeted left-HUD packet range
@@ -141,6 +145,15 @@ layer are grouped by texture and screen row before any command is transformed.
Depth-sorted world packets, full-frame backdrops, and true 4:3 frames remain
untouched.
By default each run is pinned to the nearest screen edge or centre third, so
corner HUD moves to the wide-frame corners. Some HUD widgets mix those flat
quads with GTE-projected parts the correction cannot move: Spider-Man's compass
is two ring quads around a 3D arrow. Edge-anchoring the ring pulled it ~70 px
off its arrow at 32:9. `auto_ui_anchor = "in_place"` squashes each run about
its own centre instead, which corrects its proportions and leaves it where the
stretched 4:3 layout places it. `gpu_state` reports the mode as
`ws.auto_ui.in_place`.
**Changing `sprite_tag_funcs` requires a game regen** (the tag callback is
emitted into the generated C). `widescreen.cull.keep` is consumed by both the
native recompiler and the dirty-RAM interpreter: regenerate main/overlay native
+11
View File
@@ -1494,6 +1494,7 @@ GameConfig load_game_config(const fs::path& config_path_in) {
uint32_t ws_sprite_anchor_addr = 0;
bool ws_hud_sprt_squash = false;
bool ws_auto_ui_squash = false;
bool ws_auto_ui_in_place = false;
bool ws_full_2d = false;
bool ws_gte_game_mode = false;
bool ws_precise_nclip = false;
@@ -1640,6 +1641,15 @@ GameConfig load_game_config(const fs::path& config_path_in) {
ws_hud_sprt_squash = toml::find<bool>(ws, "hud_sprt_squash");
if (ws.contains("auto_ui_squash"))
ws_auto_ui_squash = toml::find<bool>(ws, "auto_ui_squash");
if (ws.contains("auto_ui_anchor")) {
const auto anchor = toml::find<std::string>(ws, "auto_ui_anchor");
if (anchor == "in_place")
ws_auto_ui_in_place = true;
else if (anchor != "edges")
throw std::runtime_error(fmt::format(
"{}: [widescreen] auto_ui_anchor must be \"edges\" or "
"\"in_place\", got \"{}\"", config_path.string(), anchor));
}
if (ws.contains("full_2d"))
ws_full_2d = toml::find<bool>(ws, "full_2d");
if (ws.contains("gte_game_mode"))
@@ -2223,6 +2233,7 @@ GameConfig load_game_config(const fs::path& config_path_in) {
/*ws_sprite_anchor_addr*/ ws_sprite_anchor_addr,
/*ws_hud_sprt_squash*/ ws_hud_sprt_squash,
/*ws_auto_ui_squash*/ ws_auto_ui_squash,
/*ws_auto_ui_in_place*/ ws_auto_ui_in_place,
/*data_shard_funcs*/ data_shard_funcs,
/*mod_function_entry_funcs*/ mod_function_entry_funcs,
/*hot_funcs*/ hot_funcs,
+5
View File
@@ -864,6 +864,11 @@ struct GameConfig {
// the final ordering-table layer. Repeated glyph/icon rows share an anchor
// so centred text and edge counters cannot split at thirds boundaries.
bool ws_auto_ui_squash = false;
// auto_ui_anchor = "in_place": squash each grouped UI run about its own
// centre instead of pinning it to the nearest screen edge/centre third.
// For HUD widgets that combine flat quads with GTE-projected parts the
// correction cannot move (Spider-Man's compass ring and 3D arrow).
bool ws_auto_ui_in_place = false;
// [data_shards] funcs: functions that get the memoized pure-function
// replay entry/return hooks (psx_datashard_enter/psx_datashard_ret).
+4
View File
@@ -204,6 +204,9 @@ void gpu_ws_bg2d_set_host_arena(uint32_t base, uint32_t size);
struct WsViewAnchor;
int gpu_ws_bg2d_get_view(unsigned layer, struct WsViewAnchor *view);
void gpu_ws_set_auto_ui_squash(int on);
/* [widescreen] auto_ui_anchor = "in_place": each UI run squashes about its own
* centre rather than an edge/centre third (default "edges"). */
void gpu_ws_set_auto_ui_in_place(int on);
/* [widescreen.bg2d] Capcom 2D background tile-loop widen — hooked at the renderer's
* column-count / start-tile-col / start-screen-x instructions. Identity at 4:3
* and in the engine's 512 hi-res mode. */
@@ -535,6 +538,7 @@ typedef struct {
uint32_t last_ovh_frame; /* newest SUSTAINED polygon-overhang frame (the
2D-only-scene classifier's world signal) */
int auto_ui_squash; /* final-OT grouped UI correction configured */
int auto_ui_in_place; /* runs anchor at their own centre, not thirds */
int auto_ui_dense; /* current list classified as a dense menu */
uint32_t auto_ui_ot_rank; /* highest populated UI rank in current list */
uint64_t auto_ui_candidates;
+8 -3
View File
@@ -37,12 +37,17 @@ typedef struct {
uint32_t root;
} WsUiGroupItem;
/* Assign one thirds anchor to each complete spatial run. Items with the same
/* Assign one anchor to each complete spatial run. Items with the same
* texture/row key join when their horizontal intervals touch or are separated
* by at most WS_UI_GROUP_JOIN_GAP pixels. This is intentionally a whole-frame
* operation: animated text is coherent on its first frame, not one frame late. */
* operation: animated text is coherent on its first frame, not one frame late.
*
* The anchor is the run's thirds anchor (left edge, centre, right edge), or
* with in_place the run's own centre, so it keeps the position the stretched
* 4:3 layout gives it. in_place is for HUD widgets whose GTE-projected parts
* (never corrected) must stay registered with their flat quads. */
void ws_ui_group_assign(WsUiGroupItem *items, size_t count,
int32_t display_width, int dense_menu);
int32_t display_width, int dense_menu, int in_place);
int32_t ws_ui_anchor_for_bounds(int32_t x, int32_t width,
int32_t display_width);
+3 -2
View File
@@ -5385,7 +5385,7 @@ static void handle_gpu_state(int id, const char *json)
"\"cur_frame\":%llu,\"last_tag_frame\":%u,\"last_3d_frame\":%u,"
"\"gte_verts\":%u,\"last_world3d_frame\":%u,"
"\"ovh_prims\":%u,\"last_ovh_frame\":%u,"
"\"auto_ui\":{\"configured\":%d,\"dense\":%d,\"ot_rank\":%u,"
"\"auto_ui\":{\"configured\":%d,\"in_place\":%d,\"dense\":%d,\"ot_rank\":%u,"
"\"candidates\":%llu,\"transforms\":%llu},"
"\"aspect_cone\":{\"calls\":%llu,\"identity_43\":%llu,"
"\"vanilla_keep\":%llu,\"visible_keep\":%llu,"
@@ -5418,7 +5418,8 @@ static void handle_gpu_state(int id, const char *json)
(unsigned long long)ws.cur_frame, ws.last_tag_frame,
ws.last_3d_frame, ws.gte_verts, ws.last_world3d_frame,
ws.ovh_prims, ws.last_ovh_frame,
ws.auto_ui_squash, ws.auto_ui_dense, ws.auto_ui_ot_rank,
ws.auto_ui_squash, ws.auto_ui_in_place, ws.auto_ui_dense,
ws.auto_ui_ot_rank,
(unsigned long long)ws.auto_ui_candidates,
(unsigned long long)ws.auto_ui_transforms,
(unsigned long long)ws.aspect_cone_calls,
+7 -1
View File
@@ -106,6 +106,7 @@ static int32_t ws_xnum = 1, ws_xden = 1; /* X squash factor; 1/1 = off */
static uint32_t ws_anchor_addr = 0; /* scratchpad addr of anchor SXY */
static int ws_hud_sprt = 0; /* edge-anchor untagged HUD SPRTs */
static int ws_auto_ui_squash;
static int ws_auto_ui_in_place;
static int ws_auto_ui_dense;
static int ws_active(void);
static int gp0_command_word_count(uint8_t opcode);
@@ -215,6 +216,10 @@ static void ws_clear_all_reveal_margins(void);
* engage (gpu_ws_set_full_2d); PSX_WS_FORCE_2D=1 forces it on for testing. */
static int ws_full_2d = 0;
void gpu_ws_set_full_2d(int on) { ws_full_2d = on ? 1 : 0; }
void gpu_ws_set_auto_ui_in_place(int on) {
ws_auto_ui_in_place = on ? 1 : 0;
}
void gpu_ws_set_auto_ui_squash(int on) {
ws_auto_ui_squash = on ? 1 : 0;
ws_ui_prepass_count = 0;
@@ -2061,6 +2066,7 @@ void gpu_ws_get_debug(GpuWsDebug* out) {
out->ovh_prims = ws_ovh_prev;
out->last_ovh_frame = ws_sust_ovh_stamp;
out->auto_ui_squash = ws_auto_ui_squash;
out->auto_ui_in_place = ws_auto_ui_in_place;
out->auto_ui_dense = ws_auto_ui_dense;
out->auto_ui_ot_rank =
ws_ui_prepass_rank != 0xFFFFu ? ws_ui_prepass_rank : UINT32_MAX;
@@ -5471,7 +5477,7 @@ void gpu_ws_prepass_linked_list(uint32_t start_addr) {
for (uint32_t i = 0; i < ws_ui_prepass_count; i++)
groups[i] = ws_ui_prepass[i].group;
ws_ui_group_assign(groups, ws_ui_prepass_count, ws_disp_w(),
ws_auto_ui_dense);
ws_auto_ui_dense, ws_auto_ui_in_place);
for (uint32_t i = 0; i < ws_ui_prepass_count; i++)
ws_ui_prepass[i].group.anchor = group_origin + groups[i].anchor;
}
+1
View File
@@ -13416,6 +13416,7 @@ int main(int argc, char** argv) {
g_ws_anchor_addr = gc.ws_sprite_anchor_addr;
g_ws_hud_sprt = gc.ws_hud_sprt_squash;
gpu_ws_set_auto_ui_squash(gc.ws_auto_ui_squash ? 1 : 0);
gpu_ws_set_auto_ui_in_place(gc.ws_auto_ui_in_place ? 1 : 0);
/* [widescreen] full_2d — opt a pure-2D sprite game (MMX6) into the
* widescreen present path. Applied to the GPU layer up front so the
* ws engage at game entry classifies every frame as gameplay. */
+10 -4
View File
@@ -103,8 +103,14 @@ static size_t root_of(size_t *parent, size_t index) {
return index;
}
static int32_t run_anchor(int32_t x, int32_t width, int32_t display_width,
int in_place) {
if (in_place) return x + width / 2;
return ws_ui_anchor_for_bounds(x, width, display_width);
}
void ws_ui_group_assign(WsUiGroupItem *items, size_t count,
int32_t display_width, int dense_menu) {
int32_t display_width, int dense_menu, int in_place) {
if (!items || count == 0 || display_width <= 0) return;
if (dense_menu) {
for (size_t i = 0; i < count; i++) {
@@ -117,8 +123,8 @@ void ws_ui_group_assign(WsUiGroupItem *items, size_t count,
size_t *parent = (size_t *)malloc(count * sizeof(*parent));
if (!parent) {
for (size_t i = 0; i < count; i++) {
items[i].anchor = ws_ui_anchor_for_bounds(
items[i].x, items[i].width, display_width);
items[i].anchor = run_anchor(
items[i].x, items[i].width, display_width, in_place);
items[i].root = (uint32_t)i; /* diag: each item stands alone */
}
return;
@@ -164,7 +170,7 @@ void ws_ui_group_assign(WsUiGroupItem *items, size_t count,
max_x = items[j].x + items[j].width;
}
items[i].anchor =
ws_ui_anchor_for_bounds(min_x, max_x - min_x, display_width);
run_anchor(min_x, max_x - min_x, display_width, in_place);
items[i].root = (uint32_t)root;
}
free(parent);
@@ -208,8 +208,9 @@ int psx_ws_aspect_cone_contains(int32_t x, int32_t z, int32_t y,
return 0;
}
void ws_ui_group_assign(WsUiGroupItem *items, size_t count,
int32_t display_width, int dense_menu) {
int32_t display_width, int dense_menu, int in_place) {
(void)items; (void)count; (void)display_width; (void)dense_menu;
(void)in_place;
}
int32_t ws_ui_anchor_for_bounds(int32_t x, int32_t width,
int32_t display_width) {
+30 -7
View File
@@ -21,7 +21,7 @@ int main(void) {
{1, 168, 16, 100, 16}, {1, 184, 16, 100, 16}, {1, 200, 16, 100, 16},
{1, 216, 16, 100, 16}, {1, 232, 16, 100, 16}, {1, 248, 16, 100, 16},
};
ws_ui_group_assign(text, 9, display_width, 0);
ws_ui_group_assign(text, 9, display_width, 0, 0);
for (int i = 0; i < 9; i++) assert(text[i].anchor == 192);
assert(scale_about(184, text[3].anchor, 4, 7) ==
scale_about(184, text[4].anchor, 4, 7));
@@ -30,7 +30,7 @@ int main(void) {
{2, 300, 16, 100, 16}, {2, 320, 16, 100, 16},
{2, 340, 16, 100, 16}, {2, 360, 16, 100, 16},
};
ws_ui_group_assign(monkeys, 4, display_width, 0);
ws_ui_group_assign(monkeys, 4, display_width, 0, 0);
for (int i = 0; i < 4; i++) assert(monkeys[i].anchor == 384);
for (int i = 0; i < 3; i++) {
int32_t a = scale_about(monkeys[i].x, monkeys[i].anchor, 3, 4);
@@ -43,12 +43,12 @@ int main(void) {
{3, 20, 16, 100, 16}, {3, 36, 16, 100, 16},
{3, 330, 16, 100, 16}, {3, 346, 16, 100, 16},
};
ws_ui_group_assign(edge_runs, 4, display_width, 0);
ws_ui_group_assign(edge_runs, 4, display_width, 0, 0);
assert(edge_runs[0].anchor == 0 && edge_runs[1].anchor == 0);
assert(edge_runs[2].anchor == 384 && edge_runs[3].anchor == 384);
WsUiGroupItem dense[] = {{4, 8, 16, 100, 16}, {5, 340, 16, 100, 16}};
ws_ui_group_assign(dense, 2, display_width, 1);
ws_ui_group_assign(dense, 2, display_width, 1, 0);
assert(dense[0].anchor == 192 && dense[1].anchor == 192);
/* A digit drawn on top of its background box is ONE element even though
@@ -62,7 +62,7 @@ int main(void) {
{12, 204, 60, 191, 8, 0, 0}, /* label stacked directly above it */
{11, 205, 24, 10, 16, 0, 0}, /* top row, same columns */
};
ws_ui_group_assign(stacked, 4, display_width, 0);
ws_ui_group_assign(stacked, 4, display_width, 0, 0);
assert(stacked[0].root == stacked[1].root);
/* One scanline of seam between the label and the box is still one
* element — this is the WipEout 3 lap readout that flew apart. */
@@ -82,7 +82,7 @@ int main(void) {
{0xb938, 306,112, 236, 6, 0, 0}, /* meter bar, gap 18, rows meet */
{0xf3e1, 418, 16, 236, 8, 0, 0}, /* bar end cap */
};
ws_ui_group_assign(submitted, 4, 508, 0);
ws_ui_group_assign(submitted, 4, 508, 0, 0);
assert(submitted[1].root == submitted[2].root); /* readout joins meters */
assert(submitted[2].root == submitted[3].root);
assert(submitted[0].root != submitted[1].root); /* left stays separate */
@@ -96,9 +96,32 @@ int main(void) {
{0x1111, 10, 4, 10, 4, 0, 0}, /* unrelated, breaks adjacency */
{0xb938, 306,112, 236, 6, 0, 0},
};
ws_ui_group_assign(apart, 3, 508, 0);
ws_ui_group_assign(apart, 3, 508, 0, 0);
assert(apart[0].root != apart[2].root);
/* in_place: each run squashes about its own centre. Spider-Man's compass
* is two ring quads at [381,482] around a GTE-projected arrow the UI
* correction never moves; an edge anchor (512) pulled the ring ~70 px
* off the arrow at 32:9. The run keeps one shared anchor, so its two
* halves still meet. */
WsUiGroupItem compass[] = {
{0x5d9e901c, 381, 51, 184, 40, 0, 0},
{0x5d9e901c, 431, 51, 184, 40, 0, 0},
{0x2f754f62, 23, 26, 63, 12, 0, 0}, /* top-left health readout */
};
ws_ui_group_assign(compass, 3, 512, 0, 0);
assert(compass[0].anchor == 512 && compass[1].anchor == 512);
assert(compass[2].anchor == 0);
ws_ui_group_assign(compass, 3, 512, 0, 1);
assert(compass[0].anchor == 431 && compass[1].anchor == 431);
assert(compass[0].root == compass[1].root);
assert(compass[2].anchor == 23 + 26 / 2);
assert(scale_about(381 + 51, compass[0].anchor, 9, 32) ==
scale_about(431, compass[1].anchor, 9, 32));
/* A dense 2D menu still centres every item, whatever the anchor mode. */
ws_ui_group_assign(dense, 2, display_width, 1, 1);
assert(dense[0].anchor == 192 && dense[1].anchor == 192);
assert(ws_ui_anchor_for_bounds(8, 32, display_width) == 0);
assert(ws_ui_anchor_for_bounds(344, 32, display_width) == 384);
return 0;