diff --git a/runtime/include/gpu.h b/runtime/include/gpu.h index 6aaa8111..36e63dd0 100644 --- a/runtime/include/gpu.h +++ b/runtime/include/gpu.h @@ -403,6 +403,12 @@ void psx_ws_backdrop_ring_note(uint32_t pc, int kind, int wcols, uint32_t orig, uint32_t base, uint32_t dl); int psx_ws_backdrop_ring_json(char *buf, int cap); +/* auto_ui_squash partition dump (`ws_ui_groups`). Reports, for the last UI + * prepass, each primitive's raw key inputs (CLUT/texpage band/family via op, + * y, h) alongside its union-find root and final anchor — enough to tell whether + * two HUD primitives shared a run, and which key component split them if not. */ +int psx_ws_ui_groups_json(char *buf, int cap); + /* Live-tunable backdrop widen amount (ws_backdrop_margin command): <0 whole-row, * 0 off, >0 N-column widen. g_ws_bd_from_interp is the interp's one-shot * "I will record the rich entry myself" flag (see gpu.c / dirty_ram_interp.c). */ diff --git a/runtime/include/ws_ui_group.h b/runtime/include/ws_ui_group.h index 07921723..cf29aabb 100644 --- a/runtime/include/ws_ui_group.h +++ b/runtime/include/ws_ui_group.h @@ -15,6 +15,14 @@ typedef struct { int32_t x; int32_t width; 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 + * they are one run" from "these two happen to land on the same third". + * Anchor equality cannot distinguish those — there are only three anchor + * values — and that distinction is the whole question when a HUD element + * splits apart under the squash. Written by ws_ui_group_assign; never read + * by the transform path. */ + uint32_t root; } WsUiGroupItem; /* Assign one thirds anchor to each complete spatial run. Items with the same diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index 958505a2..0c2f69bc 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -7842,6 +7842,31 @@ static void handle_ws_backdrop_ring(int id, const char *json) free(buf); } +/* ws_ui_groups: dump the auto_ui_squash partition for the last UI prepass — + * per primitive its op / key / raw key inputs (y, h, derived band and family) / + * union-find root / final anchor. + * + * auto_ui_squash squashes each spatial run about its own anchor, so a HUD + * element split across two runs gets two anchors and comes apart as the frame + * widens (elements drifting to opposite edges, glyphs sliding off their + * background box). Diagnosing that needed to know which run each primitive + * landed in, and nothing exposed it: `key` is a hash, so unequal keys do not + * say WHICH of CLUT/texpage/band/family differed, and `anchor` takes only three + * values, so equal anchors do not prove two prims actually co-grouped. + * Read-only; sized for the 2048-entry prepass cap. */ +static void handle_ws_ui_groups(int id, const char *json) +{ + (void)json; + size_t cap = 1u << 19; /* 512 KB: 2048 items * ~180 chars */ + char *buf = (char *)malloc(cap); + if (!buf) { send_err(id, "alloc failed"); return; } + int hdr = snprintf(buf, cap, "{\"id\":%d,\"ok\":true,", id); + int body = psx_ws_ui_groups_json(buf + hdr, (int)cap - hdr - 4); + snprintf(buf + hdr + body, cap - (size_t)(hdr + body), "}"); + debug_server_send_line(buf); + free(buf); +} + /* ws_backdrop_margin [m=]: live-tune the far-backdrop widen strategy without * a rebuild. m<0 = whole-row preload, m=0 = off, m>0 = widen N columns each side. * No m= just reports the current value. */ @@ -13236,6 +13261,7 @@ static const CmdEntry s_commands[] = { { "ws_aspect", handle_ws_aspect }, { "ws_nw", handle_ws_nw }, { "ws_backdrop_ring", handle_ws_backdrop_ring }, + { "ws_ui_groups", handle_ws_ui_groups }, { "ws_backdrop_margin", handle_ws_backdrop_margin }, { "ws_backdrop_stretch", handle_ws_backdrop_stretch }, { "ws_dbg_stretch", handle_ws_dbg_stretch }, diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index 208ad37d..3e3b3be4 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -95,6 +95,13 @@ typedef struct { WsUiGroupItem group; uint32_t src_addr; uint16_t ot_rank; + /* Diagnostic only (ws_ui_groups): the key is a hash, so a dump of keys + * alone says two prims differ without saying WHICH component differed. + * Keep the raw inputs so an observer can attribute a split to the CLUT, + * the texpage, the 24px Y band, or the poly-vs-rect family. */ + int32_t y; + int32_t h; + uint8_t op; } WsUiPrepassItem; static WsUiPrepassItem ws_ui_prepass[WS_UI_PREPASS_MAX]; static uint32_t ws_ui_prepass_count; @@ -1582,6 +1589,43 @@ int psx_ws_backdrop_ring_json(char *buf, int cap) { return off; } +/* ws_ui_groups — dump the auto_ui_squash partition for the LAST prepass. + * + * auto_ui_squash squashes each spatial run about its own anchor, so a HUD + * element that lands in two runs gets two anchors and comes apart as the frame + * widens. Nothing exposed which run a primitive ended up in, which made that + * failure mode guesswork: the key is a hash of CLUT/texpage/Y-band/family, so + * comparing keys tells you two prims differ without telling you why, and the + * anchor takes only three values so anchor equality cannot prove co-grouping. + * + * This reports both the raw key inputs and the union-find root, which together + * answer "did these two merge, and if not, which component split them". */ +int psx_ws_ui_groups_json(char *buf, int cap) { + int off = snprintf(buf, (size_t)cap, + "\"active\":%d,\"squash\":%d,\"dense\":%d,\"rank\":%d," + "\"disp_x\":%d,\"disp_w\":%d,\"join_gap\":%d,\"n\":%u,\"items\":[", + ws_active(), ws_auto_ui_squash, ws_auto_ui_dense, + ws_ui_prepass_rank != 0xFFFFu ? (int)ws_ui_prepass_rank : -1, + ws_disp_x(), ws_disp_w(), WS_UI_GROUP_JOIN_GAP, ws_ui_prepass_count); + for (uint32_t i = 0; i < ws_ui_prepass_count && off < cap - 220; i++) { + const WsUiPrepassItem *it = &ws_ui_prepass[i]; + /* Recover the key components so a split is attributable. Mirrors + * ws_auto_ui_group_key_words; kept in step with it by construction. */ + int32_t centre_y = it->y + it->h / 2; + unsigned band = (unsigned)((centre_y < 0 ? 0 : centre_y / 24) & 0x1F); + unsigned family = it->op < 0x60u ? 1u : 2u; + off += snprintf(buf + off, (size_t)(cap - off), + "%s{\"i\":%u,\"op\":\"%02x\",\"key\":\"%08x\",\"root\":%u," + "\"x\":%d,\"w\":%d,\"y\":%d,\"h\":%d," + "\"band\":%u,\"family\":%u,\"anchor\":%d,\"src\":\"%08x\"}", + i ? "," : "", i, it->op, it->group.key, it->group.root, + it->group.x, it->group.width, it->y, it->h, + band, family, it->group.anchor, it->src_addr); + } + off += snprintf(buf + off, (size_t)(cap - off), "]"); + return off; +} + /* Backdrop store-site registry. The [widescreen.backdrop] x_sites are emitted * into native cache-DLL code by the recompiler, but overlay code very often * runs INTERPRETED (no DLL loaded), where the emit can't reach. So the runtime @@ -4546,8 +4590,12 @@ static void ws_ui_prepass_add(const uint32_t *words, uint32_t source_addr, item->group.x = min_x - X; item->group.width = width; item->group.anchor = 0; + item->group.root = ws_ui_prepass_count - 1u; item->src_addr = psx_ram_map_read(source_addr & 0x1FFFFFFFu) & ~3u; item->ot_rank = rank; + item->y = min_y; + item->h = height; + item->op = (uint8_t)op; } void gpu_ws_prepass_linked_list(uint32_t start_addr) { diff --git a/runtime/src/ws_ui_group.c b/runtime/src/ws_ui_group.c index 33f30480..58bdce23 100644 --- a/runtime/src/ws_ui_group.c +++ b/runtime/src/ws_ui_group.c @@ -31,8 +31,10 @@ void ws_ui_group_assign(WsUiGroupItem *items, size_t count, int32_t display_width, int dense_menu) { if (!items || count == 0 || display_width <= 0) return; if (dense_menu) { - for (size_t i = 0; i < count; i++) + for (size_t i = 0; i < count; i++) { items[i].anchor = display_width / 2; + items[i].root = (uint32_t)i; /* diag: no runs formed */ + } return; } @@ -41,6 +43,7 @@ void ws_ui_group_assign(WsUiGroupItem *items, size_t count, 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].root = (uint32_t)i; /* diag: each item stands alone */ } return; } @@ -71,6 +74,7 @@ void ws_ui_group_assign(WsUiGroupItem *items, size_t count, } items[i].anchor = ws_ui_anchor_for_bounds(min_x, max_x - min_x, display_width); + items[i].root = (uint32_t)root; } free(parent); }