mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
widescreen(8C): always-on draw-census ring + live cull-margin override
Observability for the 16:9 backdrop-cull investigation (runtime-only,
4:3-identity-safe, no regen):
- gpu.c: always-on per-prim draw-census ring (ws_census). Every drawn GP0
primitive (0x20-0x7F) records {frame, src_addr, camX/camY, first-vertex
screen x/y, opcode} so object spawn/despawn and edge culls are observable
in data instead of screenshots. gpu_ws_census_dump() writes a frame range
to CSV (large dumps must not ride TCP).
- gpu.c: gpu_ws_set_margin_override() forces psx_ws_x_margin() to any value
for a live cull sweep at a fixed camera; gpu_ws_get_debug() snapshots live
widescreen state.
- debug_server.c: gpu_state now reports ws.{configured,active,game_mode,
present_native_43,x_margin,squash,cur_frame,last_tag_frame}; new ws_margin
and ws_census TCP commands.
- gpu.h: GpuWsDebug + census/override API.
- debug_client.py: ws_margin and ws_census subcommands.
Used to prove the shipped [widescreen.cull] sites are a no-op for 8C (their
4:3 margins already cover the 16:9 view) and that the real pop is an
overlay-resident backdrop layer. See TombaRecomp ISSUES.md #8C.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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++;
|
||||
|
||||
@@ -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 <value|-1>"
|
||||
return {"cmd": "ws_margin", "value": int(args[1])}, pretty_json
|
||||
elif cmd == "ws_census":
|
||||
# ws_census on|off OR ws_census <start> <end> [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 <start> <end> [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":
|
||||
|
||||
Reference in New Issue
Block a user