mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
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:
@@ -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 |
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user