auto_ui_squash: group HUD primitives that are one visual element

The thirds anchor is chosen per union-find run, and runs only joined when the
group key matched within JOIN_GAP. That key folds CLUT, texpage, a 24px Y band
and the poly-vs-rect family together, so a digit and the box it is drawn on
could never land in the same run. They got independent anchors and were
squashed about different points, which pulls one piece of a readout toward a
screen edge and leaves the rest behind -- worse the wider the frame.

On WipEout 3's race HUD at 32:9 the lap readout draws its digits at
y=[219,227] x=[137,202] and the box they label at y=[228,244] x=[138,202].
The digits anchored left, the box anchored centre, and the box landed about
500 screen pixels from the digits it belongs to.

Join on being one visual element as well as on the key: share screen columns
and touch vertically within STACK_GAP. Both halves are needed. Column overlap
alone merged the top-left lap counter with the bottom-left lap timer 200
scanlines below, and since union is transitive that chained all 71 HUD
primitives into one run spanning the full width, which anchors centre and
drags the corners inward. A strict row intersection missed the stacked case
above, whose seam is one pixel.

WsUiGroupItem carries y/height for this; the anchor stays horizontal. After:
9 runs, bottom-left readout whole and anchored left, timer and energy bar
right, speed centre -- matching the 4:3 layout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 72196d3dc9)
This commit is contained in:
tetrisgm
2026-08-17 02:31:31 -07:00
committed by Matthew Stanley
parent 6c37cbb495
commit df41ea6b02
4 changed files with 98 additions and 16 deletions
+12
View File
@@ -10,10 +10,22 @@ extern "C" {
#define WS_UI_GROUP_JOIN_GAP 24
/* Vertical slack, in scanlines, still counted as "stacked on" rather than
* "somewhere else on screen". A readout drawn as a label directly above its
* box leaves a one-pixel seam; the nearest thing that must NOT join is the
* other end of the screen, ~200 scanlines away. */
#define WS_UI_GROUP_STACK_GAP 4
typedef struct {
uint32_t key;
int32_t x;
int32_t width;
/* Vertical extent, needed to tell "drawn on top of each other" (one visual
* element) from "happens to share screen columns" (the top-left lap counter
* and the bottom-left lap timer). Grouping only ever produces a HORIZONTAL
* anchor, but deciding the grouping is a 2-D question. */
int32_t y;
int32_t height;
int32_t anchor;
/* Diagnostic only: index of the union-find representative this item
* merged into, so an observer can tell "these two share an anchor because
+2
View File
@@ -4563,6 +4563,8 @@ static void ws_ui_prepass_add(const uint32_t *words, uint32_t source_addr,
ws_auto_ui_group_key_words(words, op, min_y, height);
item->group.x = min_x - X;
item->group.width = width;
item->group.y = min_y;
item->group.height = height;
item->group.anchor = 0;
item->group.root = ws_ui_prepass_count - 1u;
item->src_addr = source_addr & 0x1FFFFCu;
+57 -8
View File
@@ -10,13 +10,45 @@ int32_t ws_ui_anchor_for_bounds(int32_t x, int32_t width,
return display_width / 2;
}
static int32_t axis_gap(int32_t a0, int32_t a1, int32_t b0, int32_t b1) {
if (a1 < b0) return b0 - a1;
if (b1 < a0) return a0 - b1;
return 0;
}
static int32_t interval_gap(const WsUiGroupItem *a,
const WsUiGroupItem *b) {
int32_t a_right = a->x + a->width;
int32_t b_right = b->x + b->width;
if (a_right < b->x) return b->x - a_right;
if (b_right < a->x) return a->x - b_right;
return 0;
return axis_gap(a->x, a->x + a->width, b->x, b->x + b->width);
}
/* Are the two primitives one visual element -- drawn over or directly against
* each other -- as opposed to merely close? interval_gap() cannot tell those
* apart, but they are very different signals: horizontal adjacency is a weak
* hint that two runs might belong together, whereas sharing screen columns
* while touching vertically means a glyph on its background box, a bar in its
* frame, or a label sitting on the readout it annotates. Those must never be
* anchored independently.
*
* Both halves of the test are load-bearing, and each was learned from a real
* failure on WipEout 3's race HUD at 32:9:
*
* - Drop the column test and everything joins by key alone, so a digit and
* the box under it (different CLUT, different poly-vs-rect family) get
* independent thirds anchors and are dragged to opposite screen edges.
* - Drop the row test and X alone merges the top-left lap counter with the
* bottom-left lap timer 200 scanlines below. Union is transitive, so that
* chained all 71 HUD primitives into one run spanning [24,486], which
* anchors centre and pulls the corners inward.
* - Demand a strict row INTERSECTION and stacked rows are missed: the lap
* readout draws its digits at y=[219,227] and the box they label at
* y=[228,244] -- a one-pixel seam. The box then anchored centre while its
* own digits anchored left, landing 500 screen pixels apart. Hence the
* STACK_GAP slack rather than a bare intersection. */
static int same_element(const WsUiGroupItem *a, const WsUiGroupItem *b) {
if (a->x >= b->x + b->width || b->x >= a->x + a->width)
return 0; /* no shared columns */
return axis_gap(a->y, a->y + a->height, b->y, b->y + b->height) <=
WS_UI_GROUP_STACK_GAP;
}
static size_t root_of(size_t *parent, size_t index) {
@@ -50,11 +82,28 @@ void ws_ui_group_assign(WsUiGroupItem *items, size_t count,
for (size_t i = 0; i < count; i++) parent[i] = i;
/* Transitive union is important for text: each adjacent pair is close,
* even when the complete string spans more than one thirds region. */
* even when the complete string spans more than one thirds region.
*
* Two ways to join, and the second is not a relaxation of the first:
*
* - SAME KEY and within JOIN_GAP — a run of glyphs from one font/CLUT.
* - SAME ELEMENT, whatever the key — see same_element(). Requiring a
* matching key here was a defect: the key folds in CLUT, texpage, a 24px
* Y band and the poly-vs-rect family, so a digit and the box it sits on,
* or a bar and its frame, could never merge. They then got INDEPENDENT
* thirds anchors and were dragged toward opposite screen edges as the
* frame widened.
*
* Observed on WipEout 3's race HUD at 32:9: the lap readout's digits at
* [137,202] anchored left while the box beneath them at [138,202] anchored
* centre, putting the box ~500 screen pixels from the digits it belongs to.
* That is the HUD pulling itself apart, and it gets worse the wider the
* aspect. */
for (size_t i = 0; i < count; i++) {
for (size_t j = i + 1; j < count; j++) {
if (items[i].key != items[j].key ||
interval_gap(&items[i], &items[j]) > WS_UI_GROUP_JOIN_GAP)
if (!same_element(&items[i], &items[j]) &&
(items[i].key != items[j].key ||
interval_gap(&items[i], &items[j]) > WS_UI_GROUP_JOIN_GAP))
continue;
size_t ri = root_of(parent, i);
size_t rj = root_of(parent, j);
+27 -8
View File
@@ -15,10 +15,11 @@ static int32_t scale_about(int32_t x, int32_t anchor,
int main(void) {
const int display_width = 384;
/* {key, x, width, y, height} — anchor and root are outputs. */
WsUiGroupItem text[] = {
{1, 120, 16, 0}, {1, 136, 16, 0}, {1, 152, 16, 0},
{1, 168, 16, 0}, {1, 184, 16, 0}, {1, 200, 16, 0},
{1, 216, 16, 0}, {1, 232, 16, 0}, {1, 248, 16, 0},
{1, 120, 16, 100, 16}, {1, 136, 16, 100, 16}, {1, 152, 16, 100, 16},
{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);
for (int i = 0; i < 9; i++) assert(text[i].anchor == 192);
@@ -26,8 +27,8 @@ int main(void) {
scale_about(184, text[4].anchor, 4, 7));
WsUiGroupItem monkeys[] = {
{2, 300, 16, 0}, {2, 320, 16, 0},
{2, 340, 16, 0}, {2, 360, 16, 0},
{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);
for (int i = 0; i < 4; i++) assert(monkeys[i].anchor == 384);
@@ -39,17 +40,35 @@ int main(void) {
}
WsUiGroupItem edge_runs[] = {
{3, 20, 16, 0}, {3, 36, 16, 0},
{3, 330, 16, 0}, {3, 346, 16, 0},
{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);
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, 0}, {5, 340, 16, 0}};
WsUiGroupItem dense[] = {{4, 8, 16, 100, 16}, {5, 340, 16, 100, 16}};
ws_ui_group_assign(dense, 2, display_width, 1);
assert(dense[0].anchor == 192 && dense[1].anchor == 192);
/* A digit drawn on top of its background box is ONE element even though
* the group key (CLUT / texpage / Y band / poly-vs-rect family) differs.
* A primitive elsewhere on screen that merely shares the same columns is
* NOT — overlap has to be judged in 2-D or union-find chains the whole HUD
* into a single run and the corners collapse toward the centre. */
WsUiGroupItem stacked[] = {
{ 9, 200, 90, 200, 20, 0, 0}, /* background box */
{10, 210, 16, 204, 12, 0, 0}, /* digit sitting on the box */
{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);
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. */
assert(stacked[2].root == stacked[0].root);
assert(stacked[3].root != stacked[0].root);
assert(ws_ui_anchor_for_bounds(8, 32, display_width) == 0);
assert(ws_ui_anchor_for_bounds(344, 32, display_width) == 384);
return 0;