From 5e413cbd841a2ceb59ebd5d269b646c6b7e82dda Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Thu, 1 Oct 2026 16:43:56 -0700 Subject: [PATCH] netplay: duplicate shared HUD counters into both unsplit views --- runtime/CMakeLists.txt | 12 ++ runtime/src/gpu.c | 90 +++++++++++- runtime/tests/test_ws_shared_split_ui_exec.c | 144 +++++++++++++++++++ 3 files changed, 240 insertions(+), 6 deletions(-) create mode 100644 runtime/tests/test_ws_shared_split_ui_exec.c diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index e0e17da8..56a208d9 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -434,6 +434,18 @@ if(BUILD_TESTING) add_test(NAME ws_auto_ui_untextured_exec_test COMMAND ws_auto_ui_untextured_exec_test) + add_executable(ws_shared_split_ui_exec_test + tests/test_ws_shared_split_ui_exec.c + src/ws_ui_group.c + src/psx_video_timing.c) + target_include_directories(ws_shared_split_ui_exec_test PRIVATE include src) + if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang") + target_compile_options(ws_shared_split_ui_exec_test PRIVATE + -ffunction-sections -fdata-sections) + target_link_options(ws_shared_split_ui_exec_test PRIVATE -Wl,--gc-sections) + endif() + add_test(NAME ws_shared_split_ui_exec_test COMMAND ws_shared_split_ui_exec_test) + add_executable(ws_view_anchor_test tests/test_ws_view_anchor.c) add_executable(ws_view_anchor_gpu_test tests/test_ws_view_anchor_gpu.c src/psx_video_timing.c) diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index 4be0e01c..6f0236ae 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -124,11 +124,16 @@ typedef struct { int32_t y; int32_t h; uint8_t op; + uint8_t full_draw_area; + uint8_t shared_split_ui; WsPrepassPacketGuard packet_guard; } WsUiPrepassItem; static WsUiPrepassItem ws_ui_prepass[WS_UI_PREPASS_MAX]; static uint32_t ws_ui_prepass_count; static uint16_t ws_ui_prepass_rank = 0xFFFFu; +/* Shared HUD packets execute once in each half. Keep the original packet + * intact for provenance guards, textures, diagnostics and guest RAM. */ +static int ws_shared_ui_copy = -1; #define WS_UI_PREPASS_NODE_MAX 8192u typedef struct { @@ -2531,6 +2536,29 @@ static int ws_auto_ui_anchor(int32_t *out_anchor) { return 0; } +static int ws_shared_ui_packet(void) { + uint32_t op = gp0_cmd_buf[0] >> 24; + if (!((op >= 0x28u && op <= 0x3Fu && (op & 8u)) || + (op >= 0x60u && op <= 0x7Fu))) return 0; + if (!ws_local_viewport_cfg || ws_mode == 2 || !ws_vertical_split_active() || + psx_ws_prim_is_tagged()) return 0; + int32_t anchor; + if (!ws_auto_ui_anchor(&anchor)) return 0; + uint32_t src = GPU_RAM_KEY(gp0_cmd_source_addr); + for (uint32_t i = 0; i < ws_ui_prepass_count; i++) + if (ws_ui_prepass[i].src_addr == src) + return ws_ui_prepass[i].shared_split_ui; + return 0; +} + +static int32_t ws_shared_ui_delta(void) { + if (ws_shared_ui_copy < 0) return 0; + /* Projection mode still renders the complete native framebuffer. Both + * peers draw both copies identically, then present their selected half. */ + int32_t W = ws_disp_w(); + return (ws_shared_ui_copy ? 3 * W / 4 : W / 4) - W / 2; +} + static uint32_t ws_auto_ui_group_key_words(const uint32_t *words, uint32_t op, int32_t y, int32_t h) { @@ -2587,7 +2615,8 @@ static int ws_auto_ui_transform_quad(int32_t vx[4], const int32_t vy[4]) { int32_t anchor; if (!ws_auto_ui_anchor(&anchor)) return 0; - for (int i = 0; i < 4; i++) vx[i] = ws_scale_about(vx[i], anchor); + for (int i = 0; i < 4; i++) + vx[i] = ws_scale_about(vx[i], anchor) + ws_shared_ui_delta(); ws_auto_ui_transform_count++; return 1; } @@ -2601,7 +2630,7 @@ static int ws_auto_ui_transform_rect(int32_t *x, int32_t y, int *w, int h) { return 0; int32_t anchor; if (!ws_auto_ui_anchor(&anchor)) return 0; - *x = ws_scale_about(*x, anchor); + *x = ws_scale_about(*x, anchor) + ws_shared_ui_delta(); *w = (int)ws_scale_len(*w); ws_auto_ui_transform_count++; return 1; @@ -5305,7 +5334,8 @@ static int gp0_command_word_count(uint8_t opcode) { } static void ws_ui_prepass_add(const uint32_t *words, uint32_t word_count, - uint32_t source_addr, uint16_t rank) { + uint32_t source_addr, uint16_t rank, + int full_draw_area) { if (rank == 0xFFFFu) return; if (ws_ui_prepass_count >= WS_UI_PREPASS_MAX) { ws_ui_reject.cap++; return; } uint32_t op = words[0] >> 24; @@ -5402,6 +5432,8 @@ static void ws_ui_prepass_add(const uint32_t *words, uint32_t word_count, item->y = min_y; item->h = height; item->op = (uint8_t)op; + item->full_draw_area = full_draw_area ? 1 : 0; + item->shared_split_ui = 0; item->packet_guard = ws_prepass_packet_guard(words, word_count); } @@ -5444,6 +5476,9 @@ void gpu_ws_prepass_linked_list(uint32_t start_addr) { uint32_t safety = 0; uint16_t rank = 0xFFFFu; const uint32_t max_nodes = 0x40000u; + int area_left = (int)draw_area_left, area_right = (int)draw_area_right; + GpuDisplayInfo di; + gpu_get_display_info(&di); for (;;) { if (safety++ > max_nodes) { @@ -5473,7 +5508,7 @@ void gpu_ws_prepass_linked_list(uint32_t start_addr) { ws_prepass_packet_guard(payload, num_words); if (num_words == 0) { rank = rank == 0xFFFFu ? 0u : (uint16_t)(rank + 1u); - } else if (rank != 0xFFFFu) { + } else { uint32_t word_addr = psx_mod_gpu_dma_resolve_address(addr + 4u); uint32_t offset = 0; @@ -5496,7 +5531,10 @@ void gpu_ws_prepass_linked_list(uint32_t start_addr) { } ws_ui_prepass_add(words, (uint32_t)count, psx_mod_gpu_dma_resolve_address( - word_addr + offset * 4u), rank); + word_addr + offset * 4u), rank, + area_right - area_left + 1 >= (int)di.width); + if (op == 0xE3u) area_left = (int)(words[0] & 0x3FFu); + if (op == 0xE4u) area_right = (int)(words[0] & 0x3FFu); offset += (uint32_t)count; } } @@ -5600,8 +5638,36 @@ void gpu_ws_prepass_linked_list(uint32_t start_addr) { 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_in_place); - for (uint32_t i = 0; i < ws_ui_prepass_count; i++) + for (uint32_t i = 0; i < ws_ui_prepass_count; i++) { ws_ui_prepass[i].group.anchor = group_origin + groups[i].anchor; + ws_ui_prepass[i].group.root = groups[i].root; + } + + /* A shared counter can comprise many glyphs and shadow packets, none of + * which individually crosses the split. Classify the complete admitted + * front-layer run, and require full-screen draw provenance. Per-player + * HUD, world geometry, divider strips and pause menus cannot qualify. */ + if (ws_local_viewport_cfg && ws_mode != 2 && ws_vertical_split_active()) { + const int32_t W = ws_disp_w(), H = ws_disp_h(), seam = W / 2; + for (uint32_t i = 0; i < ws_ui_prepass_count; i++) { + int32_t x0 = W, x1 = 0, y0 = H, y1 = 0; + int full = 1; + for (uint32_t j = 0; j < ws_ui_prepass_count; j++) { + if (groups[j].root != groups[i].root) continue; + const WsUiGroupItem *g = &groups[j]; + if (g->x < x0) x0 = g->x; + if (g->x + g->width > x1) x1 = g->x + g->width; + if (g->y < y0) y0 = g->y; + if (g->y + g->height > y1) y1 = g->y + g->height; + full &= ws_ui_prepass[j].full_draw_area; + } + if (full && x0 < seam && x1 > seam && + x1 - x0 <= W / 2 && y1 - y0 <= H / 4) { + ws_ui_prepass[i].shared_split_ui = 1; + ws_ui_prepass[i].group.anchor = group_origin + seam; + } + } + } } void gpu_ws_validate_linked_list_header(uint32_t addr, uint32_t header) { @@ -5918,6 +5984,8 @@ void gpu_ws_census_set(int on) { ws_census_on = on ? 1 : 0; } uint64_t gpu_ws_census_seq(void) { return ws_census_seq; } /* Execute a fully-collected GP0 command */ +static void gp0_dispatch_command(uint8_t opcode); + static void gp0_execute_command(void) { uint8_t opcode = (gp0_cmd_buf[0] >> 24) & 0xFF; gp0_opcode_count[opcode]++; @@ -5968,6 +6036,16 @@ static void gp0_execute_command(void) { else if (opcode >= 0x80 && opcode <= 0xDF) gp0_copy_count++; else if (opcode >= 0xE1 && opcode <= 0xE6) gp0_env_count++; + if (opcode >= 0x20 && opcode <= 0x7F && ws_shared_ui_packet()) { + for (ws_shared_ui_copy = 0; ws_shared_ui_copy < 2; ws_shared_ui_copy++) + gp0_dispatch_command(opcode); + ws_shared_ui_copy = -1; + } else { + gp0_dispatch_command(opcode); + } +} + +static void gp0_dispatch_command(uint8_t opcode) { switch (opcode) { case 0x00: case 0x01: diff --git a/runtime/tests/test_ws_shared_split_ui_exec.c b/runtime/tests/test_ws_shared_split_ui_exec.c new file mode 100644 index 00000000..a6f1fef0 --- /dev/null +++ b/runtime/tests/test_ws_shared_split_ui_exec.c @@ -0,0 +1,144 @@ +#ifdef NDEBUG +#undef NDEBUG +#endif + +/* Exercise real OT admission, complete glyph/shadow grouping and GP0 replay. + * No single glyph in this counter crosses the seam. */ +#include +#include +#include +#include +#define GPU_EXEC_REAL_UI_GROUP +#include "../src/gpu.c" +#include "gpu_exec_stubs.inc" + +#define HEAD 0x10000u +#define ENV 0x10100u +#define GLYPH 0x10200u +#define GATE 0x18000u + +static uint32_t vertex(int x, int y) { + return (uint16_t)x | ((uint32_t)(uint16_t)y << 16); +} + +static void counter(int full_area, int tall) { + memset(test_ram, 0, sizeof(test_ram)); + s_frame_count = 100; + ws_mode = 1; ws_full_2d = 1; + ws_cfg_num = 32; ws_cfg_den = 9; + ws_xnum = 3; ws_xden = 8; /* half of a 4:3 display -> 16:9 */ + ws_local_viewport_cfg = 1; ws_local_viewport_slot = 0; + split_recent_left_age = split_recent_right_age = 0; + ws_census_on = 0; + hres1 = 2; hres2 = 0; video_mode = 0; + display_depth = display_disabled = display_area_x = display_area_y = 0; + h_display_x1 = 0x200; h_display_x2 = 0xC00; + v_display_y1 = 0x10; v_display_y2 = 0x100; + draw_offset_x = draw_offset_y = 0; + draw_area_left = draw_area_top = 0; + draw_area_right = 511; draw_area_bottom = 239; + gp0_cmd_source_addr = UINT32_MAX; + gpu_ws_set_auto_ui_squash(1); + gpu_ws_set_auto_ui_in_place(0); + uint32_t running = 0; + gpu_ws_set_local_viewport_state_gate(GATE, &running, 1); + test_ram[HEAD / 4] = ENV; + test_ram[ENV / 4] = (2u << 24) | GLYPH; + test_ram[ENV / 4 + 1] = 0xE3000000u; + test_ram[ENV / 4 + 2] = 0xE4000000u | (239u << 10) | + (full_area ? 511u : 255u); + const int xs[] = {224, 246, 260, 278}; + for (unsigned i = 0; i < 8; i++) { + unsigned addr = GLYPH + 0x40u * i; + int shadow = i & 1u; + int x = xs[i / 2] + shadow; + int y = 12 + shadow; + int h = tall ? 220 : 18; + uint32_t q[] = { + shadow ? 0x2E404040u : 0x2C808080u, + vertex(x, y), shadow ? 0x01000000u : 0u, + vertex(x + 12, y), 0x0008000Cu, + vertex(x, y + h), (uint32_t)h << 8, + vertex(x + 12, y + h), ((uint32_t)h << 8) | 12u, + }; + test_ram[addr / 4] = (9u << 24) | + (i == 7 ? 0xFFFFFFu : addr + 0x40u); + memcpy(&test_ram[addr / 4 + 1], q, sizeof(q)); + } + gpu_ws_prepass_linked_list(HEAD); + assert(ws_disp_w() == 512); + assert(ws_ui_prepass_count == 8); +} + +static void load_glyph(unsigned i) { + uint32_t addr = GLYPH + 0x40u * i + 4; + memcpy(gp0_cmd_buf, &test_ram[addr / 4], 9 * sizeof(uint32_t)); + gp0_cmd_source_addr = addr; + gp0_words_needed = 9; + last_scaled_rect.calls = 0; +} + +static void expect_shared(int shared) { + for (unsigned i = 0; i < 8; i++) { + assert(ws_ui_prepass[i].shared_split_ui == shared); + load_glyph(i); + assert(ws_shared_ui_packet() == shared); + } +} + +int main(void) { + counter(1, 0); + expect_shared(1); + for (unsigned slot = 0; slot < 2; slot++) { + ws_local_viewport_slot = (int)slot; + for (unsigned i = 0; i < 8; i++) { + load_glyph(i); + uint32_t original[9]; + memcpy(original, gp0_cmd_buf, sizeof(original)); + int32_t x, y; + parse_vertex(original[1], &x, &y); + for (ws_shared_ui_copy = 0; ws_shared_ui_copy < 2; ws_shared_ui_copy++) { + gp0_exec_textured_quad(); + int base = ws_shared_ui_copy ? 256 : 0; + int expected = base + 128 + ws_scale_about(x, 256) - 256; + assert(last_scaled_rect.x == expected); + assert(last_scaled_rect.x > base && last_scaled_rect.x + + last_scaled_rect.w < base + 256); + assert(last_scaled_rect.y == y); + assert(last_scaled_rect.u0 == 0 && last_scaled_rect.u1 == 12); + } + ws_shared_ui_copy = -1; + last_scaled_rect.calls = 0; + gp0_execute_command(); + assert(last_scaled_rect.calls == 2); + assert(memcmp(original, gp0_cmd_buf, sizeof(original)) == 0); + assert(ws_shared_ui_copy == -1); + } + } + /* Wider aspects keep the complete counter inside each half. */ + ws_xnum = 2; ws_xden = 7; + load_glyph(0); + gp0_execute_command(); + assert(last_scaled_rect.calls == 2); + + /* Source reuse, pause, offline play and per-player clips fail closed. */ + load_glyph(0); + gp0_cmd_buf[1] ^= 1u; + assert(!ws_shared_ui_packet()); + counter(1, 0); + test_ram[GATE / 4] = 1; + load_glyph(0); + gp0_execute_command(); + assert(last_scaled_rect.calls == 1); + counter(1, 0); + ws_local_viewport_cfg = 0; + load_glyph(0); + gp0_execute_command(); + assert(last_scaled_rect.calls == 1); + counter(0, 0); + expect_shared(0); + counter(1, 1); + expect_shared(0); + puts("shared split HUD execution passed"); + return 0; +}