diagnostics: per-prim PGXP census, texcorr-miss census, live overclock; xlate fixes-only files

- pgxp_census / pgxp_texcensus (debug server): per-primitive resolution
  classes and UV-correction miss reasons with screen bboxes for the last
  complete frame, double-buffered by frame parity. These attributed the
  swimming-wall and flickering-ship classes down to individual packets.
- overclock command: live CPU overclock set/get for anchored same-scene
  A/Bs (the 100%-vs-900% pacing measurements).
- text_xlate: load translation files that carry only vram_patch/glyph
  entries (previously gated on a non-empty "entry" array), enabling
  data-fix-only files like the WipEout 3 palette-gap patch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
tetrisgm
2026-08-18 13:54:01 -07:00
parent 18272616a1
commit e27eac7366
6 changed files with 263 additions and 24 deletions
+21
View File
@@ -147,6 +147,27 @@ uint64_t gpu_gp0_ring_total(void);
uint32_t gpu_gp0_ring_capacity(void);
uint32_t gpu_gp0_ring_max_words(void);
int gpu_gp0_ring_dump_frame(uint32_t frame, GpuGp0RingEntry *out, int max_out);
/* PGXP per-prim resolution census: one entry per prepared precise triangle
* whose vertices did NOT all resolve through the dataflow shadow. cls[] is
* per-vertex: 0 = shadow miss (addr known, no valid entry), 1 = geometry-cache
* fallback, 2 = dataflow, 3 = no packet address (CPU-built submission path).
* Draw-space bbox, so it overlays the display half directly. */
typedef struct {
int16_t x0, y0, x1, y1;
uint8_t cls[3];
uint8_t _pad;
uint32_t src;
uint32_t frame;
} PgxpCensusEnt;
/* Copies the census of the most recent COMPLETE frame; returns entry count.
* tris/clean report that frame's totals (all triangles vs all-dataflow). */
int gpu_pgxp_census_dump(uint32_t *frame_out, uint32_t *tris, uint32_t *clean,
PgxpCensusEnt *out, int max_out);
/* Same shape for texture-UV correction misses: cls[0] carries the reason
* (0 = no_source, 1 = no_depth), cls[1] the offending vertex index. */
int gpu_pgxp_texcensus_dump(uint32_t *frame_out, PgxpCensusEnt *out, int max_out);
void gpu_gp0_ring_frame_span(uint32_t *out_oldest, uint32_t *out_newest);
/* Vblank presentation callback — called from gpu_vblank_tick().
+64
View File
@@ -8213,6 +8213,67 @@ static void handle_savestate(int id, const char *json)
send_fmt("{\"id\":%d,\"ok\":true,\"op\":\"%s\",\"slot\":%d}", id, op, slot);
}
/* Live CPU overclock for anchored A/B: animation-rate artifacts cannot be
* attributed to the overclock without flipping it on the exact same scene. */
/* Census of imperfect PGXP triangles for the last complete frame: which
* screen prims are NOT fully dataflow-corrected, and why, per vertex. */
static void handle_pgxp_census(int id, const char *json)
{
(void)json;
static PgxpCensusEnt ents[8192];
uint32_t frame = 0, tris = 0, clean = 0;
int n = gpu_pgxp_census_dump(&frame, &tris, &clean, ents, 8192);
size_t buf_sz = 256 + (size_t)n * 96u;
char *buf = (char *)malloc(buf_sz);
if (!buf) { send_err(id, "alloc failed"); return; }
size_t pos = (size_t)snprintf(buf, buf_sz,
"{\"id\":%d,\"ok\":true,\"frame\":%u,\"tris\":%u,\"clean\":%u,"
"\"imperfect\":%d,\"entries\":[", id, frame, tris, clean, n);
for (int i = 0; i < n && pos < buf_sz - 128; i++) {
PgxpCensusEnt *e = &ents[i];
pos += (size_t)snprintf(buf + pos, buf_sz - pos,
"%s{\"bb\":[%d,%d,%d,%d],\"cls\":[%u,%u,%u],\"src\":\"0x%08X\"}",
i ? "," : "", e->x0, e->y0, e->x1, e->y1,
e->cls[0], e->cls[1], e->cls[2], e->src);
}
snprintf(buf + pos, buf_sz - pos, "]}");
debug_server_send_line(buf);
free(buf);
}
static void handle_pgxp_texcensus(int id, const char *json)
{
(void)json;
static PgxpCensusEnt ents[8192];
uint32_t frame = 0;
int n = gpu_pgxp_texcensus_dump(&frame, ents, 8192);
size_t buf_sz = 256 + (size_t)n * 96u;
char *buf = (char *)malloc(buf_sz);
if (!buf) { send_err(id, "alloc failed"); return; }
size_t pos = (size_t)snprintf(buf, buf_sz,
"{\"id\":%d,\"ok\":true,\"frame\":%u,\"misses\":%d,\"entries\":[",
id, frame, n);
for (int i = 0; i < n && pos < buf_sz - 128; i++) {
PgxpCensusEnt *e = &ents[i];
pos += (size_t)snprintf(buf + pos, buf_sz - pos,
"%s{\"bb\":[%d,%d,%d,%d],\"reason\":%u,\"vtx\":%u,\"why\":%u,\"src\":\"0x%08X\"}",
i ? "," : "", e->x0, e->y0, e->x1, e->y1,
e->cls[0], e->cls[1], e->cls[2], e->src);
}
snprintf(buf + pos, buf_sz - pos, "]}");
debug_server_send_line(buf);
free(buf);
}
static void handle_overclock(int id, const char *json)
{
int pct = json_get_int(json, "percent", -1);
if (pct > 0)
psx_set_cpu_overclock((uint32_t)pct);
send_fmt("{\"id\":%d,\"ok\":true,\"percent\":%u}",
id, psx_get_cpu_overclock());
}
static void handle_turbo(int id, const char *json)
{
int enabled = json_get_int(json, "enabled", -1);
@@ -13645,6 +13706,9 @@ static const CmdEntry s_commands[] = {
{ "input_route_stop", handle_input_route_stop },
{ "input_route_status",handle_input_route_status },
{ "savestate", handle_savestate },
{ "pgxp_census", handle_pgxp_census },
{ "pgxp_texcensus", handle_pgxp_texcensus },
{ "overclock", handle_overclock },
{ "turbo", handle_turbo },
{ "turbo_state", handle_turbo_state },
{ "pause", handle_pause },
+145 -7
View File
@@ -3411,6 +3411,31 @@ uint32_t gpu_texture_correction_hits(void) {
* it, so shared edges between neighbouring triangles cannot disagree by more
* than the sub-pixel fraction. The integer delta folds in draw offsets and
* any widescreen adjustment already applied by the caller. */
/* PGXP resolution census (G1 coverage gap). Double-buffered by frame parity
* so the debug thread can read the completed frame while this one builds.
* Only imperfect triangles get entries; totals give the denominator. */
#define PGXP_CENSUS_CAP 8192
static PgxpCensusEnt s_pgxp_census[2][PGXP_CENSUS_CAP];
static uint32_t s_pgxp_census_n[2], s_pgxp_census_frame[2];
static uint32_t s_pgxp_census_tris[2], s_pgxp_census_clean[2];
int gpu_pgxp_census_dump(uint32_t *frame_out, uint32_t *tris, uint32_t *clean,
PgxpCensusEnt *out, int max_out) {
/* newest COMPLETE frame: never the one still building, else the fresher */
uint32_t cur = (uint32_t)s_frame_count;
int slot;
if (s_pgxp_census_frame[0] == cur) slot = 1;
else if (s_pgxp_census_frame[1] == cur) slot = 0;
else slot = (s_pgxp_census_frame[0] > s_pgxp_census_frame[1]) ? 0 : 1;
if (frame_out) *frame_out = s_pgxp_census_frame[slot];
if (tris) *tris = s_pgxp_census_tris[slot];
if (clean) *clean = s_pgxp_census_clean[slot];
int n = (int)s_pgxp_census_n[slot];
if (n > max_out) n = max_out;
for (int i = 0; i < n; i++) out[i] = s_pgxp_census[slot][i];
return n;
}
static void prepare_precise_triangle(int i0, int i1, int i2,
const int32_t vx[3], const int32_t vy[3]) {
gr_set_perspective_triangle(0, 0.0f, 0.0f, 0.0f);
@@ -3421,6 +3446,7 @@ static void prepare_precise_triangle(int i0, int i1, int i2,
const int idx[3] = { i0, i1, i2 };
int32_t fx[3], fy[3];
int any_precise = 0;
uint8_t cls[3];
for (int i = 0; i < 3; i++) {
uint32_t word = gp0_cmd_buf[idx[i]];
int32_t raw_x, raw_y;
@@ -3430,12 +3456,44 @@ static void prepare_precise_triangle(int i0, int i1, int i2,
: gp0_cmd_source_addr + (uint32_t)idx[i] * 4u;
int32_t px, py;
uint16_t sz;
if (pgxp_get_precise_vertex(addr, word, raw_x, raw_y,
&px, &py, &sz) != PGXP_SRC_NATIVE)
int src = pgxp_get_precise_vertex(addr, word, raw_x, raw_y,
&px, &py, &sz);
if (src != PGXP_SRC_NATIVE)
any_precise = 1;
cls[i] = (src == PGXP_SRC_NATIVE && addr == 0xFFFFFFFFu) ? 3u
: (uint8_t)src;
fx[i] = (int32_t)((int64_t)px + (int64_t)(vx[i] - raw_x) * 65536);
fy[i] = (int32_t)((int64_t)py + (int64_t)(vy[i] - raw_y) * 65536);
}
{
uint32_t f = (uint32_t)s_frame_count;
int slot = (int)(f & 1);
if (s_pgxp_census_frame[slot] != f) {
s_pgxp_census_frame[slot] = f;
s_pgxp_census_n[slot] = 0;
s_pgxp_census_tris[slot] = 0;
s_pgxp_census_clean[slot] = 0;
}
s_pgxp_census_tris[slot]++;
if (cls[0] == 2 && cls[1] == 2 && cls[2] == 2) {
s_pgxp_census_clean[slot]++;
} else if (s_pgxp_census_n[slot] < PGXP_CENSUS_CAP) {
PgxpCensusEnt *e = &s_pgxp_census[slot][s_pgxp_census_n[slot]++];
int32_t x0 = vx[0], x1 = vx[0], y0 = vy[0], y1 = vy[0];
for (int i = 1; i < 3; i++) {
if (vx[i] < x0) x0 = vx[i];
if (vx[i] > x1) x1 = vx[i];
if (vy[i] < y0) y0 = vy[i];
if (vy[i] > y1) y1 = vy[i];
}
e->x0 = (int16_t)x0; e->y0 = (int16_t)y0;
e->x1 = (int16_t)x1; e->y1 = (int16_t)y1;
e->cls[0] = cls[0]; e->cls[1] = cls[1]; e->cls[2] = cls[2];
e->_pad = 0;
e->src = gp0_cmd_source_addr;
e->frame = f;
}
}
if (!any_precise) {
gr_set_precise_triangle(0, 0,0, 0,0, 0,0);
return;
@@ -3458,6 +3516,7 @@ static struct {
uint64_t no_correction; /* texture correction off */
uint64_t no_source; /* CPU-built primitive, no packet address */
uint64_t no_depth; /* a vertex had no recorded Z, or Z == 0 */
uint64_t z_interp; /* armed with mean-filled Z for clipped vertices */
} s_texcorr;
void gpu_texture_correction_stats(uint64_t *attempts, uint64_t *armed,
@@ -3473,22 +3532,101 @@ void gpu_texture_correction_stats(uint64_t *attempts, uint64_t *armed,
/* Enable perspective UVs only when every position word came from an exact
* SWC2 projection store at that same DMA packet address. This preserves the
* association through ordering-table reordering and rejects CPU-built UI. */
static PgxpCensusEnt s_pgxp_texcensus[2][PGXP_CENSUS_CAP];
static uint32_t s_pgxp_texcensus_n[2], s_pgxp_texcensus_frame[2];
/* Record a texture-UV correction miss with the prim's rough screen bbox
* (raw packet coords + draw offset; close enough to localize the surface). */
static void pgxp_texcensus_note(const int *indices, uint8_t reason, uint8_t vtx,
uint8_t why) {
uint32_t f = (uint32_t)s_frame_count;
int slot = (int)(f & 1);
if (s_pgxp_texcensus_frame[slot] != f) {
s_pgxp_texcensus_frame[slot] = f;
s_pgxp_texcensus_n[slot] = 0;
}
if (s_pgxp_texcensus_n[slot] >= PGXP_CENSUS_CAP) return;
PgxpCensusEnt *e = &s_pgxp_texcensus[slot][s_pgxp_texcensus_n[slot]++];
int32_t x0 = 0, y0 = 0, x1 = 0, y1 = 0;
for (int i = 0; i < 3; i++) {
int32_t rx, ry;
parse_vertex(gp0_cmd_buf[indices[i]], &rx, &ry);
rx += draw_offset_x; ry += draw_offset_y;
if (i == 0) { x0 = x1 = rx; y0 = y1 = ry; }
else {
if (rx < x0) x0 = rx;
if (rx > x1) x1 = rx;
if (ry < y0) y0 = ry;
if (ry > y1) y1 = ry;
}
}
e->x0 = (int16_t)x0; e->y0 = (int16_t)y0;
e->x1 = (int16_t)x1; e->y1 = (int16_t)y1;
e->cls[0] = reason; e->cls[1] = vtx; e->cls[2] = why; e->_pad = 0;
e->src = gp0_cmd_source_addr;
e->frame = f;
}
int gpu_pgxp_texcensus_dump(uint32_t *frame_out, PgxpCensusEnt *out, int max_out) {
uint32_t cur = (uint32_t)s_frame_count;
int slot; /* prefer the newest COMPLETE frame, not merely opposite parity */
if (s_pgxp_texcensus_frame[0] == cur) slot = 1;
else if (s_pgxp_texcensus_frame[1] == cur) slot = 0;
else slot = (s_pgxp_texcensus_frame[0] > s_pgxp_texcensus_frame[1]) ? 0 : 1;
if (frame_out) *frame_out = s_pgxp_texcensus_frame[slot];
int n = (int)s_pgxp_texcensus_n[slot];
if (n > max_out) n = max_out;
for (int i = 0; i < n; i++) out[i] = s_pgxp_texcensus[slot][i];
return n;
}
static void prepare_texture_triangle(int i0, int i1, int i2) {
gr_set_perspective_triangle(0, 0.0f, 0.0f, 0.0f);
gr_set_depth_triangle(0, 0.0f, 0.0f, 0.0f);
s_texcorr.attempts++;
if (!s_texture_correction_enabled) { s_texcorr.no_correction++; return; }
if (gp0_cmd_source_addr == 0xFFFFFFFFu) { s_texcorr.no_source++; return; }
int indices[3] = { i0, i1, i2 };
if (gp0_cmd_source_addr == 0xFFFFFFFFu) {
s_texcorr.no_source++;
pgxp_texcensus_note(indices, 0u, 0u, 0u);
return;
}
uint16_t z[3];
int have_z[3];
int n_have = 0;
int noted = 0;
for (int i = 0; i < 3; i++) {
uint32_t addr = psx_ram_map_read((gp0_cmd_source_addr + (uint32_t)indices[i] * 4u) & 0x1FFFFFFFu) & ~3u;
if (!gte_precision_load_word(addr, gp0_cmd_buf[indices[i]], NULL, NULL, &z[i]) ||
z[i] == 0) {
s_texcorr.no_depth++;
return;
have_z[i] = gte_precision_load_word(addr, gp0_cmd_buf[indices[i]],
NULL, NULL, &z[i]) && z[i] != 0;
if (have_z[i]) n_have++;
else if (!noted) {
noted = 1;
pgxp_texcensus_note(indices, 1u, (uint8_t)indices[i],
(uint8_t)pgxp_debug_shadow_class(addr, gp0_cmd_buf[indices[i]]));
}
}
if (n_have == 0) {
/* No depth anywhere: affine is the only honest option. */
s_texcorr.no_depth++;
return;
}
if (n_have < 3) {
/* Clip-generated vertices: the game's near/edge clipper computes new
* screen coords with mult/div interpolation, which no exact-provenance
* shadow can follow, so their depth is unknowable here. Their TRUE
* view depth lies between the polygon's surviving projected vertices,
* so the mean of the known depths is a bounded approximation — and
* perspective UVs with an approximate q beat the affine fallback,
* whose error is what reads as texture swimming on every large
* clipped wall (the class lives on near-field walls/pillars). */
uint32_t sum = 0;
for (int i = 0; i < 3; i++) if (have_z[i]) sum += z[i];
uint16_t fill = (uint16_t)(sum / (uint32_t)n_have);
if (fill == 0) fill = 1;
for (int i = 0; i < 3; i++) if (!have_z[i]) z[i] = fill;
s_texcorr.z_interp++;
}
float q[3] = { 1.0f / (float)z[0], 1.0f / (float)z[1], 1.0f / (float)z[2] };
float qmax = q[0];
if (q[1] > qmax) qmax = q[1];
+20 -13
View File
@@ -1262,19 +1262,26 @@ static const char *REPL_FS =
"uniform int u_debug;\n"
"void main(){\n"
" vec2 uv = (v_persp != 0) ? v_uv_p : v_uv;\n"
" uv = clamp(uv, vec2(v_limits.xy), vec2(v_limits.zw));\n"
/* +0.5 is texel CENTRE, and it is load-bearing here in a way it never was
* on the PS1 path. An integer u addresses texel u, whose span in the
* replacement is [u, u+1); sampling at u alone lands on its left EDGE. With
* NEAREST that still resolves to the right texel, so the omission was
* invisible — but this path is LINEAR (a pack image carries detail the
* texel grid does not), and there a half-texel shift blends in whatever
* sits one texel over. On a font atlas that neighbour is the adjacent
* glyph's cell, so every glyph quad drew a hairline of its neighbour along
* its own edge: background-coloured slivers cutting through letters and
* ink-coloured ones in the gaps. Clamp to v_limits.zw (not +0.999) so the
* far edge lands on the last texel's centre rather than past it. */
" vec4 t = texture(u_repl, (uv - u_origin + vec2(0.5)) / u_src);\n"
/* Continuous source-space sampling with a half-IMAGE-pixel interior clamp.
*
* The interpolated uv is already a CONTINUOUS texel-space position (a
* fragment in the middle of texel u carries uv ~ u+0.5), so adding a
* blanket +0.5 texel here double-shifted every sample by half a source
* texel. Invisible on big art; fatal on the segment-atlas strips, whose
* source rows are ONE texel tall: half a texel is half the image, so the
* strip drew with its top half cut off and the wrapped top row appended
* at the bottom (the press-start/further-information offset). At glyph
* edges the same shift pushed LINEAR samples into the neighbouring atlas
* cell (the stray marks below G / right of O and E on loading text).
*
* The sliver bug the +0.5 originally fixed is handled the right way: the
* clamp keeps samples half an IMAGE pixel inside the prim UV cell
* ([lo, hi] inclusive texels => continuous cell edge at hi+1), so LINEAR
* can never blend across the cell boundary no matter the pack scale. */
" vec2 halfimg = 0.5 * u_src / vec2(textureSize(u_repl, 0));\n"
" uv = clamp(uv, vec2(v_limits.xy) + halfimg,\n"
" vec2(v_limits.zw) + vec2(1.0) - halfimg);\n"
" vec4 t = texture(u_repl, (uv - u_origin) / u_src);\n"
/* Alpha carries the texel's meaning as the dumper wrote it: 0 = colour
* index 0, the cutout hole; 127 = opaque with STP set; 255 = opaque with
* STP clear. It is not coverage. */
+8
View File
@@ -1142,6 +1142,7 @@ static int g_video_perspective_texturing = 0;
/* [video] pgxp_depth — depth-test using PGXP's recovered W instead of relying
* solely on the ordering table. See gl_renderer_set_pgxp_depth. */
static int g_video_pgxp_depth = 0;
extern "C" void gl_renderer_set_depth_always(int on);
static int g_video_pgxp_cpu_mode = 0;
static float g_video_pgxp_tolerance = 0.5f;
static int g_video_renderer = PSXRecompV4::DEFAULT_VIDEO_RENDERER;
@@ -13273,6 +13274,13 @@ session_reboot:
* stretch), squash mode stretches the 4:3 frame into it. */
gl_renderer_set_display_aspect(g_video_aspect_num, g_video_aspect_den);
gl_renderer_set_pgxp_depth(g_video_pgxp_depth);
if (g_video_pgxp_depth) {
/* Unarmed prims (no recovered depth: ship model, HUD, CPU-built
* effects) must ALWAYS PASS, or the depth test culls them against
* world Z they never took part in — measured live: the player ship
* and trackside monitors vanish without this. */
gl_renderer_set_depth_always(1);
}
if (g_video_pgxp_depth)
std::fprintf(stdout, "psxrecomp: PGXP depth buffer on (ordering-table "
"sort augmented by recovered per-vertex W)\n");
+5 -4
View File
@@ -509,10 +509,11 @@ void load_tables_locked() {
try { data = toml::parse(de.path().string()); }
catch (...) { continue; }
++files;
if (!data.contains("entry")) continue;
const auto& arr = toml::find(data, "entry");
if (!arr.is_array()) continue;
for (const auto& e : arr.as_array()) {
/* A fixes-only file (vram_patch / glyph-label / msg blocks, no string
* table) is valid: gate each block on its own key, never the file. */
const auto arr = data.contains("entry") ? toml::find(data, "entry")
: toml::value(toml::array{});
if (arr.is_array()) for (const auto& e : arr.as_array()) {
if (!e.contains("src_hex")) continue;
std::string hex = toml::find_or<std::string>(e, "src_hex", "");
auto bytes = hex_to_bytes(hex);