feat(debug): screenshot_hires — capture the surface the player actually sees

screenshot/screenshot_file read native 15-bit VRAM via gpu_display_pixel_rgb.
That is correct for faithfulness work but BLIND to any enhancement that only
exists in the supersampled mirror: at supersampling >= 2 the geometry-correction
sub-pixel vertices, SSAA edges and perspective UVs are resolved in the hi-res
surface and gone once pixels are packed back to native.

This bit hard. Native captures of Ape Escape looked clean while the user was
looking at a visibly cracked frame, so a real regression got reported as fixed
and the argument moved to counters. An instrument that cannot show the defect
is worse than no instrument.

screenshot_hires routes gr_render_display_hires — the same present path the
window uses — and falls back to the native resolve (reporting scale 1) when
there is no hi-res surface, so it always answers truthfully about real output.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matthew Stanley
2026-08-05 15:36:27 -07:00
parent 3fbb9effa0
commit af6dff5132
3 changed files with 83 additions and 0 deletions
+1
View File
@@ -50,6 +50,7 @@ Columns: **N** = native, **D** = DuckStation oracle.
| `read_scratch` | | ✓ | `addr`, `len` | Read PS1 scratchpad (0x1F800000 region) |
| `read_vram` / `vram_peek` | ✓¹ | ✓ | `x`, `y`, `w`, `h` | Read 16-bit VRAM pixels (max 128×128) |
| `gpu_state` | ✓ | ✓ | — | Display area, display depth, draw offset, GPUSTAT, clip rect, xfer state |
| `screenshot_hires` | | ✓ | `path` | PNG of the **supersampled** surface (the present path the window uses), at `display × gr_scale()`. ⚠ `screenshot`/`screenshot_file` capture native 15-bit VRAM and are **blind to anything that only exists in the hi-res mirror** — geometry correction, SSAA edges, perspective UVs — so they show a clean frame while the player sees a broken one. Use this one to verify those. Falls back to the native resolve (and reports `scale: 1`) when no hi-res surface exists |
| `geom_correction` | | ✓ | — | `[video] geometry_correction` / `perspective_texturing` engagement: enable flag plus free-running `geometry_vertex_hits` and `perspective_triangles` totals. Both enhancements silently fall back to the faithful path on anything they cannot prove is projected geometry, so a zero counter with the flag on means the title never qualifies — sample twice and diff for a rate |
| `sio_state` | ✓ | ✓ | — | SIO registers + (native only) pad/memcard protocol + TX/RX history |
| `irq_state` | ✓ | ✓ | — | `I_STAT`, `I_MASK` (both), plus chain state on native |
+73
View File
@@ -24,6 +24,7 @@
#include "cpu_state.h"
#include "dma.h"
#include "gpu.h"
#include "gpu_render.h" /* gr_scale + gr_render_display_hires (screenshot_hires) */
#include "present_ring.h"
#include "load_transition_ring.h"
#include "cdrom.h"
@@ -8202,6 +8203,77 @@ static void handle_screenshot_file(int id, const char *json)
id, path, w, h);
}
/* screenshot_hires — capture the SUPERSAMPLED surface, not native VRAM.
*
* screenshot_file above reads gpu_display_pixel_rgb, i.e. the native 15-bit
* VRAM the PS1 would have produced. That is the right capture for faithfulness
* work, but it is BLIND to any enhancement that lives only in the high-
* resolution mirror: at [video] supersampling >= 2 the geometry-correction
* sub-pixel vertices, the SSAA edges and the perspective UVs are all resolved
* in the hi-res surface and are gone by the time pixels are packed back to
* native. Verifying those against a native screenshot silently "confirms" a
* clean frame while the player is looking at a broken one — which is exactly
* how the geometry-correction cracking got mis-reported as fixed.
*
* This routes the same present path the window uses (gr_render_display_hires),
* so what lands in the PNG is what the player sees. Falls back to the native
* resolve when no hi-res surface exists (supersampling 1 / software), so the
* command always answers with something truthful about the actual output. */
static void handle_screenshot_hires(int id, const char *json)
{
GpuDisplayInfo di;
gpu_get_display_info(&di);
if (di.disabled || di.width == 0 || di.height == 0) {
send_err(id, "display disabled"); return;
}
if (di.depth24) { send_err(id, "24bpp scanout (unsupported)"); return; }
int scale = gr_scale();
if (scale < 1) scale = 1;
uint32_t w = di.width, h = di.height;
if (w > 640) w = 640;
if (h > 512) h = 512;
uint32_t ow = w * (uint32_t)scale, oh = h * (uint32_t)scale;
char path[512];
if (!json_get_str(json, "path", path, sizeof(path)))
strncpy(path, "psx_screenshot_hires.png", sizeof(path) - 1);
path[sizeof(path) - 1] = '\0';
uint32_t *argb = (uint32_t *)malloc((size_t)ow * oh * sizeof(uint32_t));
if (!argb) { send_err(id, "alloc failed"); return; }
int got = gr_render_display_hires(argb, (int)ow, (int)di.display_x,
(int)di.display_y, (int)w, (int)h);
if (!got) {
/* No hi-res surface (scale 1, or a backend without one): resolve the
* native display instead and say so, rather than emitting a blank. */
scale = 1; ow = w; oh = h;
got = gr_render_display(argb, (int)ow, (int)di.display_x,
(int)di.display_y, (int)w, (int)h);
if (!got) { free(argb); send_err(id, "no display surface"); return; }
}
uint8_t *rgb = (uint8_t *)malloc((size_t)ow * oh * 3);
if (!rgb) { free(argb); send_err(id, "alloc failed"); return; }
for (size_t i = 0; i < (size_t)ow * oh; i++) {
uint32_t p = argb[i];
rgb[i * 3 + 0] = (uint8_t)((p >> 16) & 0xFF);
rgb[i * 3 + 1] = (uint8_t)((p >> 8) & 0xFF);
rgb[i * 3 + 2] = (uint8_t)(p & 0xFF);
}
free(argb);
FILE *f = fopen(path, "wb");
if (!f) { free(rgb); send_err(id, "cannot open file"); return; }
int ok = png_write_rgb(f, rgb, ow, oh);
free(rgb);
fclose(f);
if (!ok) { send_err(id, "png encode failed"); return; }
send_fmt("{\"id\":%d,\"ok\":true,\"path\":\"%s\",\"width\":%u,"
"\"height\":%u,\"scale\":%d}", id, path, ow, oh, scale);
}
/* dump_buffer: dump a raw 512x240 VRAM region starting at display Y = `y` to a
* PNG, regardless of what the game currently displays. Used to inspect BOTH
* double-buffer halves (y=0 and y=256) coherently in one call to see whether a
@@ -12828,6 +12900,7 @@ static const CmdEntry s_commands[] = {
{ "get_snapshots", handle_get_snapshots },
{ "screenshot", handle_present_screenshot },
{ "screenshot_file", handle_screenshot_file },
{ "screenshot_hires", handle_screenshot_hires },
{ "display_ring_get", handle_display_ring_get },
{ "display_ring_aux", handle_display_ring_aux },
{ "display_ring_stats", handle_display_ring_stats },
+9
View File
@@ -328,6 +328,15 @@ def build_cmd(args):
if len(args) > 1:
d["path"] = args[1]
return d, pretty_json
elif cmd in ("screenshot_hires", "shot_hires"):
# Captures the SUPERSAMPLED surface the player actually sees. Use this
# (not `screenshot`) to verify anything that only exists in the hi-res
# mirror -- geometry correction, SSAA edges, perspective UVs -- because
# the native capture resolves those away and looks clean regardless.
d = {"cmd": "screenshot_hires"}
if len(args) > 1:
d["path"] = args[1]
return d, pretty_json
elif cmd == "bios_trace":
d = {"cmd": "bios_trace"}
if len(args) > 1: