fix(gpu): retain wide composites when internal resolution changes

This commit is contained in:
Matthew Stanley
2026-10-14 14:25:24 -07:00
parent f9c287f523
commit 28e12c7372
3 changed files with 137 additions and 8 deletions
+4 -3
View File
@@ -10691,12 +10691,13 @@ static int dyn_apply(int snew) {
const uint64_t t4 = dyn_mark();
/* 5. Native-wide surfaces: presentation-only, rescaled in place over the
* rows they have presented (all rows before the first present). While
* the centre is spliced from hr at every present (wide_blit_center),
* only the margins hold anything of their own. */
* the centre is canonical it is spliced from hr at present, so only the
* margins need preserving. Explicit backgrounds and shifted views own
* their centre pixels too; preserve their full composite. */
if (g_wide_w > 0) {
int cols[2][2], nc = 0;
int native_w = g_wide_w - 2 * g_wide_off;
if (s_wide_fast && !view_enabled && native_w > 0 && g_wide_off > 0) {
if (wide_center_is_canonical() && native_w > 0 && g_wide_off > 0) {
cols[0][0] = 0; cols[0][1] = g_wide_off;
cols[1][0] = g_wide_off + native_w; cols[1][1] = g_wide_w;
nc = 2;
+25 -1
View File
@@ -40,7 +40,8 @@ final level. "chain" draws, then steps through a list: each step must leave
the native VRAM unchanged and rescale the displayed rect, the draw area and
the wide margins from their own pixels (the fixture checks every pixel); the
scene drawn again at the final level must equal the fixed-scale run there.
The window mode refuses steps.
The window mode refuses steps. A held full-composite case checks both buffer
banks through 3x -> 5x -> 3x, including their centre pixels before any redraw.
macOS/Linux/Windows (MinGW): pass the SDL3 include directory and static
library (for example from a runtime build tree's _deps/sdl3-src/include and
@@ -100,6 +101,8 @@ def main():
ap.add_argument("--fixture", type=pathlib.Path)
ap.add_argument("--scales", default="1,2,3,5,9")
ap.add_argument("--window-scales", default="2,3,5,9,18")
ap.add_argument("--steps-only", action="store_true",
help="Run only canonical and held full-composite 3x/5x scale-step checks")
args = ap.parse_args()
# ';'-separated when CMake hands over a target's include list.
sdl_includes = [str(pathlib.Path(d).resolve()) for d in args.sdl_include.split(";") if d]
@@ -188,6 +191,21 @@ def main():
print("SKIP:", r.stderr.strip()[-600:])
return SKIP_EXIT
if args.steps_only:
env = os.environ.copy()
env["PSX_GL_HIRES_WINDOW"] = "0"
ok = r.returncode == 0
for case in ([probe, 5, "steps", "3,5,3", "chain"],
[probe, 5, "composite-steps"]):
r = run(case, env=env)
parsed = parse_run(r.stdout)
print(f"{case[2]}: exit={r.returncode}", r.stdout.strip().splitlines()[-8:],
r.stderr.strip()[-1200:])
if r.returncode or not parsed or parsed[1]:
ok = False
print("PASS" if ok else "FAIL")
return 0 if ok else 1
ok = True
digests = {}
hires_full = {}
@@ -399,6 +417,12 @@ def main():
print(f"FAIL {label}: frame/wide surface differ from the fixed {final}x run:",
got, hires_full.get(final))
ok = False
r = run([probe, 5, "composite-steps"], env=env)
parsed = parse_run(r.stdout)
print(f"held full composite 3x -> 5x -> 3x: exit={r.returncode}",
r.stdout.strip().splitlines()[-6:], r.stderr.strip()[-600:])
if r.returncode or not parsed or parsed[1]:
ok = False
if not digests_agree(digests):
print("FAIL native VRAM digest differs across steps:", digests)
ok = False
+108 -4
View File
@@ -76,6 +76,9 @@
* colour); the wide margins must be the nearest rescale of the old ones.
* Then the scene is drawn again and must equal the fixed-scale run at the
* final level. In the window mode steps are refused and the scale stays.
* - mode "composite-steps": explicitly tagged backgrounds own the centre
* of native-wide too. Two held buffer composites must survive 3x -> 5x ->
* 3x as the nearest rescale of all their pixels, before any redraw.
* Original source-owned scene; no retail payload. */
#include "gpu_gl_renderer.c"
#include "gpu_hd_texture_stubs.inc"
@@ -103,7 +106,34 @@ int gpu_display_is_depth24(void){return 0;}
void gpu_get_display_info(GpuDisplayInfo *out){memset(out,0,sizeof(*out));out->width=320;out->height=240;}
int psx_ws_prim_in_backdrop(void){return 0;}
int gpu_ws_nw_flat_backdrop_enabled(void){return 0;}
int gpu_ws_background_requires_full_composite(void){return 0;}
static int test_background_full_composite;
int gpu_ws_background_requires_full_composite(void){return test_background_full_composite;}
/* Fixtures embedding this source rename main and supply their own frontend /
* HD adapters. The standalone scale fixture has no overlays or external pack. */
#ifndef main
int host_osd_needs_present(void){return 0;}
int psx_present_vsync_owns_cadence(void){return 0;}
void latency_ring_mark(LatencyStage stage){(void)stage;}
void psx_host_sleep_ms(unsigned ms){SDL_Delay(ms);}
void psx_host_sleep_micros(unsigned us){SDL_Delay((us + 999u) / 1000u);}
int present_shot_take(char *out,int n){(void)out;(void)n;return 0;}
void present_shot_done(int ok){(void)ok;}
int host_osd_image(const uint32_t **p,int *w,int *h){*p=NULL;*w=*h=0;return 0;}
int host_osd_volume_image(const uint32_t **p,int *w,int *h){return host_osd_image(p,w,h);}
void host_osd_present_done(void){}
int psx_rewind_overlay_image(const uint32_t **p,int *w,int *h){return host_osd_image(p,w,h);}
float psx_rewind_slide(void){return 0.0f;}
int psx_savestate_menu_overlay_image(const uint32_t **p,int *w,int *h){return host_osd_image(p,w,h);}
void gpu_hd_textures_track_upload(int x,int y,int w,int h,const uint16_t *words){
(void)x;(void)y;(void)w;(void)h;(void)words;
}
void gpu_hd_textures_begin_upload(int x,int y,int w,int h){(void)x;(void)y;(void)w;(void)h;}
void gpu_hd_textures_begin_copy(int sx,int sy,int dx,int dy,int w,int h){
(void)sx;(void)sy;(void)dx;(void)dy;(void)w;(void)h;
}
void gpu_hd_textures_end_copy(void){}
void gpu_hd_textures_invalidate(int x,int y,int w,int h){(void)x;(void)y;(void)w;(void)h;}
#endif
void gpu_timeline_note(uint8_t kind,uint32_t a,uint32_t b){(void)kind;(void)a;(void)b;}
int g_ws_tex_edge_pct=0;
int psx_ws_prim_is_tagged(void){return 0;}
@@ -974,10 +1004,10 @@ static uint64_t steps_hires_digest(int x, int y, int w, int h) {
return d;
}
static uint64_t steps_wide_digest(uint32_t **keep) {
static uint64_t steps_wide_digest_for(int base, uint32_t **keep) {
int ww = 426 * s_out_scale, wh = 512 * s_out_scale, gw = 0, gh = 0;
uint32_t *wb = (uint32_t *)malloc((size_t)ww * wh * 4);
int got = wb ? glb_wide_dump_full(wb, ww * wh, &gw, &gh, 0) : 0;
int got = wb ? glb_wide_dump_full(wb, ww * wh, &gw, &gh, base) : 0;
check(got == ww * wh && gw == ww && gh == wh, "wide surface dump size");
uint64_t d = got ? fnv(wb, (size_t)ww * wh * 4, 0xcbf29ce484222325ull) : 0;
if (keep) *keep = got ? wb : (free(wb), (uint32_t *)NULL);
@@ -985,6 +1015,74 @@ static uint64_t steps_wide_digest(uint32_t **keep) {
return d;
}
static uint64_t steps_wide_digest(uint32_t **keep) {
return steps_wide_digest_for(0, keep);
}
static int composite_steps_main(int ceiling, int window) {
check(!window && ceiling == 5, "full composite fixture uses full VRAM with a 5x ceiling");
if (window || ceiling != 5) return 1;
check(gl_renderer_step_internal_scale_now(3) && s_out_scale == 3,
"initial 3x level applied before drawing");
test_background_full_composite = 1;
gl_renderer_set_wide_fast(1);
glb_wide_configure(426, 53);
check(!view_enabled && !wide_center_is_canonical(),
"explicit background owns the centre with fast mirroring and no shifted view");
glb_set_draw_offset(0, 0);
glb_set_mask_bits(0, 0);
glb_set_semi_transparency(0, 0);
for (int bank = 0; bank < 2; bank++) {
int base = bank * 512;
glb_wide_disable_target();
glb_set_draw_area(base, 0, base + FRAME_W - 1, FRAME_H - 1);
glb_fill_rect(base, 0, FRAME_W, FRAME_H, 0x03e0);
/* A background composite differs from canonical VRAM in its centre.
* Keep both banks distinct and cover the held rows, including margins. */
glb_wide_clear(base, 0, 512, bank ? 0x4210 : 0x2d6b);
glb_wide_set_target(base);
glb_draw_gouraud_triangle(base - 40, 20, 0x001f,
base + 360, 50, 0x7c00,
base + 100, 220, 0x03ff);
glb_draw_flat_rect(base + 90, 80, 90, 50, bank ? 0x7c1f : 0x7fff);
}
gl_renderer_sync_cpu();
uint32_t *before[2] = {NULL, NULL};
for (int bank = 0; bank < 2; bank++) {
(void)steps_wide_digest_for(bank * 512, &before[bank]);
check(before[bank] && (before[bank][400 * 3 * 426 * 3 + 160 * 3] & 0xFFFFFFu),
"held composite has nonzero centre pixels outside the canonical frame");
}
const int levels[2] = {5, 3};
for (int i = 0; i < 2; i++) {
int sold = s_out_scale, snew = levels[i];
uint64_t native_before = steps_native_digest(0);
check(gl_renderer_step_internal_scale_now(snew) && s_out_scale == snew,
"held full composite scale step applied");
check(steps_native_digest(0) == native_before,
"held full composite step preserves guest-visible VRAM");
/* Read both banks before submitting any draw. The dump uses present's
* centre-copy rule, so copying canonical pixels cannot hide a loss. */
for (int bank = 0; bank < 2; bank++) {
uint32_t *after = NULL;
(void)steps_wide_digest_for(bank * 512, &after);
long bad = before[bank] && after
? steps_rescale_mismatch(before[bank], sold, after, snew, 426, 512) : -1;
char label[128];
snprintf(label, sizeof label, "bank %d held full composite %dx -> %dx: every pixel rescaled before redraw",
bank, sold, snew);
check(bad == 0, label);
printf("full composite bank=%d %dx -> %dx mismatch=%ld\n", bank, sold, snew, bad);
free(before[bank]);
before[bank] = after;
}
}
free(before[0]); free(before[1]);
check(glGetError() == GL_NO_ERROR, "full composite scale steps: GL error");
printf("checks=%d failures=%d\n", checks, failures);
return failures ? 1 : 0;
}
/* The hr and native-wide surfaces are allocated at the level they render at
* (not the ceiling, whose size every render pass would load and store on
* GL-on-Metal), and a step reallocates them at the new level. */
@@ -1212,7 +1310,8 @@ int main(int argc, char **argv) {
glb_init(vram);
glb_set_scale(scale);
gl_renderer_set_swap_interval(0);
if (!strcmp(mode, "steps")) gl_renderer_set_dynamic_resolution(1);
if (!strcmp(mode, "steps") || !strcmp(mode, "composite-steps"))
gl_renderer_set_dynamic_resolution(1);
if (!gl_renderer_init_context(win)) { fprintf(stderr, "FAIL context\n"); return 2; }
GlScaleInfo si;
gl_renderer_scale_info(&si);
@@ -1243,6 +1342,11 @@ int main(int argc, char **argv) {
}
check(si.effective == scale, "requested scale allocated");
if (!strcmp(mode, "composite-steps")) {
int rc = composite_steps_main(scale, si.windowed);
gl_renderer_shutdown(); SDL_DestroyWindow(win); SDL_Quit();
return rc;
}
if (!strcmp(mode, "steps")) {
int rc = steps_main(scale, si.windowed, argc > 3 ? argv[3] : "",
argc > 4 && !strcmp(argv[4], "fresh"));