diff --git a/runtime/include/gpu.h b/runtime/include/gpu.h index f1cebbc5..65e991b3 100644 --- a/runtime/include/gpu.h +++ b/runtime/include/gpu.h @@ -103,6 +103,31 @@ int gpu_ws_present_native_43(void); * cull immediates by the recompiler ([widescreen.cull]); 0 unless stretching. */ int psx_ws_x_margin(void); +/* Live widescreen state for diagnostics (TCP gpu_state). All pointers + * optional. last_tag_frame/cur_frame let the caller see game_mode freshness. */ +typedef struct { + int configured; /* ws_xnum != ws_xden */ + int active; /* squash currently applied this frame */ + int game_mode; /* tagged char/billboard prim within 2 frames */ + int present_native_43; /* frame presents pillarboxed 4:3 (FMV/full-2D) */ + int x_margin; /* psx_ws_x_margin() right now */ + int xnum, xden; /* squash factor */ + uint64_t cur_frame; + uint32_t last_tag_frame; /* frame of newest tagged prim */ +} GpuWsDebug; +void gpu_ws_get_debug(GpuWsDebug* out); + +/* Diagnostic: force psx_ws_x_margin() to return v (>=0) regardless of state, + * or -1 to restore the normal computed margin. For live cull-margin sweeps. */ +void gpu_ws_set_margin_override(int v); + +/* Always-on draw-census ring: every drawn primitive records frame / source + * addr / camera / first-vertex screen pos, so object spawn/despawn and edge + * culls are observable in data. Dump frames [f0,f1] to a CSV file. */ +int gpu_ws_census_dump(uint32_t f0, uint32_t f1, const char *path); +void gpu_ws_census_set(int on); +uint64_t gpu_ws_census_seq(void); + #ifdef __cplusplus } #endif diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index 1d8f1bb3..29de08a8 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -3563,6 +3563,8 @@ static void handle_gpu_state(int id, const char *json) gpu_get_draw_area(&da); uint64_t nop, fill, draw, env, copy; gpu_get_gp0_stats(&nop, &fill, &draw, &env, ©); + GpuWsDebug ws; + gpu_ws_get_debug(&ws); send_fmt("{\"id\":%d,\"ok\":true," "\"display_x\":%d,\"display_y\":%d," "\"width\":%d,\"height\":%d," @@ -3572,7 +3574,10 @@ static void handle_gpu_state(int id, const char *json) "\"gp0_writes\":%llu," "\"gp0_nop\":%llu,\"gp0_fill\":%llu,\"gp0_draw\":%llu,\"gp0_env\":%llu,\"gp0_copy\":%llu," "\"draw_area\":[%u,%u,%u,%u]," - "\"draw_offset\":[%d,%d]}", + "\"draw_offset\":[%d,%d]," + "\"ws\":{\"configured\":%d,\"active\":%d,\"game_mode\":%d," + "\"present_native_43\":%d,\"x_margin\":%d,\"squash\":[%d,%d]," + "\"cur_frame\":%llu,\"last_tag_frame\":%u}}", id, di.display_x, di.display_y, di.width, di.height, di.depth24 ? 24 : 15, di.depth24, @@ -3583,7 +3588,10 @@ static void handle_gpu_state(int id, const char *json) (unsigned long long)draw, (unsigned long long)env, (unsigned long long)copy, da.left, da.top, da.right, da.bottom, - da.offset_x, da.offset_y); + da.offset_x, da.offset_y, + ws.configured, ws.active, ws.game_mode, + ws.present_native_43, ws.x_margin, ws.xnum, ws.xden, + (unsigned long long)ws.cur_frame, ws.last_tag_frame); } static void handle_mem_words(int id, const char *json) @@ -5187,6 +5195,36 @@ static void handle_clear_input(int id, const char *json) send_ok(id); } +static void handle_ws_margin(int id, const char *json) +{ + int v = json_get_int(json, "value", -2); + if (v < -1) { send_err(id, "missing value (>=0 to force, -1 to clear)"); return; } + gpu_ws_set_margin_override(v); + GpuWsDebug ws; + gpu_ws_get_debug(&ws); + send_fmt("{\"id\":%d,\"ok\":true,\"override\":%d,\"x_margin\":%d,\"active\":%d}", + id, v, ws.x_margin, ws.active); +} + +static void handle_ws_census(int id, const char *json) +{ + char act[16] = {0}; + json_get_str(json, "action", act, sizeof(act)); + if (strcmp(act, "on") == 0) { gpu_ws_census_set(1); send_fmt("{\"id\":%d,\"ok\":true,\"on\":1,\"seq\":%llu}", id, (unsigned long long)gpu_ws_census_seq()); return; } + if (strcmp(act, "off") == 0) { gpu_ws_census_set(0); send_fmt("{\"id\":%d,\"ok\":true,\"on\":0,\"seq\":%llu}", id, (unsigned long long)gpu_ws_census_seq()); return; } + /* default action = dump */ + int f0 = json_get_int(json, "start", -1); + int f1 = json_get_int(json, "end", -1); + if (f0 < 0 || f1 < 0) { send_err(id, "missing start/end (or action on|off)"); return; } + char path[256]; + if (!json_get_str(json, "out", path, sizeof(path))) + snprintf(path, sizeof(path), "psx_census.csv"); + int n = gpu_ws_census_dump((uint32_t)f0, (uint32_t)f1, path); + if (n < 0) { send_err(id, "census dump: cannot open file"); return; } + send_fmt("{\"id\":%d,\"ok\":true,\"rows\":%d,\"path\":\"%s\",\"seq\":%llu}", + id, n, path, (unsigned long long)gpu_ws_census_seq()); +} + static void handle_turbo(int id, const char *json) { int enabled = json_get_int(json, "enabled", -1); @@ -8434,6 +8472,8 @@ static const CmdEntry s_commands[] = { { "dump_ram", handle_read_ram }, /* alias: one request, one response */ { "write_ram", handle_write_ram }, { "gpu_state", handle_gpu_state }, + { "ws_margin", handle_ws_margin }, + { "ws_census", handle_ws_census }, { "mem_words", handle_mem_words }, { "vram_peek", handle_vram_peek }, { "gl_coh_ring", handle_gl_coh_ring }, diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index 24418775..4a39b68f 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -122,11 +122,35 @@ static int ws_active(void) { return ws_configured() && !gpu_ws_present_native_43 * the visible edge moves out by 160*(1/s - 1) = 160*(xden-xnum)/xnum. Widening * the cull window by this restores the original off-screen margin at the new * edge. 0 whenever squash is inactive (4:3/boot/menu/FMV) → original cull. */ +/* Diagnostic override (8C): when >= 0, psx_ws_x_margin() returns this value + * unconditionally so a probe can sweep the cull margin live (0 = force 4:3 + * cull while still stretching; large = over-draw) at a fixed camera position. + * -1 = normal computed margin. */ +static int ws_margin_override = -1; +void gpu_ws_set_margin_override(int v) { ws_margin_override = v; } + int psx_ws_x_margin(void) { + if (ws_margin_override >= 0) return ws_margin_override; if (!ws_active()) return 0; return (160 * (ws_xden - ws_xnum) + ws_xnum / 2) / ws_xnum; } +/* Snapshot live widescreen state for the TCP gpu_state diagnostic. Mirrors + * the predicates above so a probe can see, mid-gameplay, whether the squash + * and the cull-margin are actually engaged (8C). */ +void gpu_ws_get_debug(GpuWsDebug* out) { + if (!out) return; + out->configured = ws_configured(); + out->active = ws_active(); + out->game_mode = ws_game_mode(); + out->present_native_43 = gpu_ws_present_native_43(); + out->x_margin = psx_ws_x_margin(); + out->xnum = ws_xnum; + out->xden = ws_xden; + out->cur_frame = s_frame_count; + out->last_tag_frame = ws_last_tag_stamp; +} + void gpu_ws_configure(int aspect_num, int aspect_den, uint32_t sprite_anchor_addr, int hud_sprt_squash) { /* X squash = (4*den)/(3*num) — the same factor the GTE applies. */ @@ -1635,12 +1659,84 @@ void gpu_gp0_ring_frame_span(uint32_t *out_oldest, uint32_t *out_newest) { if (out_newest) *out_newest = gp0_ring[newest_idx].frame; } +/* ---- Draw census ring (ALWAYS-ON) ----------------------------------------- + * Every drawn primitive (GP0 opcode 0x20-0x7F) records: frame, the prim's DMA + * source address, the live camera (scratchpad camX/camY), and the prim's first + * vertex in DRAWING space (pre draw_offset). Purpose: see object spawn/despawn + * and edge-cull in DATA, not screenshots. When a background object despawns, + * its prim simply stops appearing in the census at a specific camX while its + * last recorded screen-x shows how far on-screen it still was — i.e. the + * effective (4:3-sized) despawn margin, which the 16:9 view exceeds. A prim + * present in the census but absent on screen would instead indict the renderer + * gate. Query via TCP `ws_census` → CSV file (large dumps mustn't ride TCP). */ +#define WS_CENSUS_CAP (1u << 21) /* 2,097,152 entries * 16B = 32 MiB */ +typedef struct { + uint32_t frame; + uint32_t src_addr; + int16_t cam_x, cam_y; + int16_t x, y; + uint8_t opcode; + uint8_t pad[3]; +} WsCensusEntry; +static WsCensusEntry *ws_census = NULL; +static uint64_t ws_census_seq = 0; +static int ws_census_on = 1; /* always-on; toggle via ws_census */ +extern uint16_t psx_read_half(uint32_t addr); + +static void ws_census_record(uint8_t opcode, int32_t x, int32_t y) { + if (!ws_census_on) return; + if (!ws_census) { + ws_census = (WsCensusEntry *)calloc(WS_CENSUS_CAP, sizeof(WsCensusEntry)); + if (!ws_census) { ws_census_on = 0; return; } + } + WsCensusEntry *e = &ws_census[ws_census_seq & (WS_CENSUS_CAP - 1)]; + e->frame = (uint32_t)s_frame_count; + e->src_addr = gp0_cmd_source_addr; + e->cam_x = (int16_t)psx_read_half(0x1F800176); + e->cam_y = (int16_t)psx_read_half(0x1F800186); + e->x = (int16_t)x; + e->y = (int16_t)y; + e->opcode = opcode; + ws_census_seq++; +} + +/* Dump census rows for frames [f0,f1] to a CSV file. Returns row count. */ +int gpu_ws_census_dump(uint32_t f0, uint32_t f1, const char *path) { + if (!ws_census) return 0; + FILE *fp = fopen(path, "w"); + if (!fp) return -1; + fprintf(fp, "frame,src_addr,cam_x,cam_y,x,y,opcode\n"); + uint64_t total = ws_census_seq; + uint64_t avail = total < WS_CENSUS_CAP ? total : WS_CENSUS_CAP; + uint64_t start = total - avail; + int n = 0; + for (uint64_t s = start; s < total; s++) { + WsCensusEntry *e = &ws_census[s & (WS_CENSUS_CAP - 1)]; + if (e->frame < f0 || e->frame > f1) continue; + fprintf(fp, "%u,0x%08X,%d,%d,%d,%d,0x%02X\n", + e->frame, e->src_addr, e->cam_x, e->cam_y, e->x, e->y, e->opcode); + n++; + } + fclose(fp); + return n; +} + +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_execute_command(void) { uint8_t opcode = (gp0_cmd_buf[0] >> 24) & 0xFF; gp0_opcode_count[opcode]++; gp0_ring_record(gp0_cmd_buf, gp0_words_needed); + /* Draw-census: capture every drawing primitive's first vertex + camera. */ + if (opcode >= 0x20 && opcode <= 0x7F) { + int32_t cvx, cvy; + parse_vertex(gp0_cmd_buf[1], &cvx, &cvy); + ws_census_record(opcode, cvx, cvy); + } + /* Categorize for diagnostics */ if (opcode <= 0x01) gp0_nop_count++; else if (opcode == 0x02) gp0_fill_count++; diff --git a/tools/debug_client.py b/tools/debug_client.py index ad10493e..8db30fc1 100644 --- a/tools/debug_client.py +++ b/tools/debug_client.py @@ -265,6 +265,20 @@ def build_cmd(args): return {"cmd": "set_input", "buttons": args[1]}, pretty_json elif cmd == "clear_input": return {"cmd": "clear_input"}, pretty_json + elif cmd == "ws_margin": + if len(args) < 2: + return None, lambda _: "Usage: ws_margin " + return {"cmd": "ws_margin", "value": int(args[1])}, pretty_json + elif cmd == "ws_census": + # ws_census on|off OR ws_census [outfile] + if len(args) >= 2 and args[1] in ("on", "off"): + return {"cmd": "ws_census", "action": args[1]}, pretty_json + if len(args) < 3: + return None, lambda _: "Usage: ws_census on|off | ws_census [outfile]" + d = {"cmd": "ws_census", "start": int(args[1]), "end": int(args[2])} + if len(args) > 3: + d["out"] = args[3] + return d, pretty_json elif cmd == "quit": return {"cmd": "quit"}, pretty_json elif cmd == "dispatch_check":