diff --git a/runtime/include/gpu.h b/runtime/include/gpu.h index 8bcac11f..2adbae7c 100644 --- a/runtime/include/gpu.h +++ b/runtime/include/gpu.h @@ -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(). diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index 557c7726..f4affc18 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -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 }, diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index 731a7c8b..9f2dc6fc 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -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]; diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index a876f71a..12ff547f 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -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. */ diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 6412dee7..3f5e7b4d 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -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"); diff --git a/runtime/src/text_xlate.cpp b/runtime/src/text_xlate.cpp index bf7c3799..80b47d4c 100644 --- a/runtime/src/text_xlate.cpp +++ b/runtime/src/text_xlate.cpp @@ -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(e, "src_hex", ""); auto bytes = hex_to_bytes(hex);