netplay: duplicate shared HUD counters into both unsplit views

This commit is contained in:
Matthew Stanley
2026-10-01 16:43:56 -07:00
parent 4e373f0753
commit 5e413cbd84
3 changed files with 240 additions and 6 deletions
+12
View File
@@ -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)
+84 -6
View File
@@ -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:
@@ -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 <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#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;
}