mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
445 lines
22 KiB
Python
445 lines
22 KiB
Python
"""Build and run test_gl_scale_invariance.c on a hidden real OpenGL context.
|
|
|
|
The fixture runs once per internal scale; the guest-visible (native) VRAM
|
|
digest must be identical at every scale, and each run checks line thickness
|
|
at internal resolution. Window runs (PSX_GL_HIRES_WINDOW=1) keep VRAM at 1x
|
|
and only the frame at S: their native digest must match too, and their frame
|
|
and native-wide (16:9) surface at internal resolution must equal the
|
|
full-VRAM run's at the same scale.
|
|
An 18x window run is 8K past a 16384 texture limit. Two clamp runs check that
|
|
an over-limit request (32x, and a tiny memory budget) stays on GL.
|
|
Side-by-side runs (mode sbs) flip two 512-wide buffers at x=0 and x=512: the
|
|
window must hold both at S and match the full-VRAM surface, as one surface
|
|
while their union fits (5x) and as two tiles when it does not (9x under a
|
|
simulated 8192 limit, PSX_GL_MAX_DIM, and 18x on a 16384 GPU); copies 1000 px
|
|
wide are staged in chunks the limit allows.
|
|
Line runs (mode lines) batch lines with triangles over native-wide: the window
|
|
runs' native frame must equal the 1x run's (the 1x authoritative surface draws
|
|
its lines as GL_LINES, in painter order), and their frame and wide surface at S
|
|
the full-VRAM run's at the same scale.
|
|
Capture runs (mode capture) check the frame-blend history and hold-last
|
|
captures: at the source scale outside the window mode, at the presented
|
|
(letterbox) size in it.
|
|
Mask runs (mode mask) change the GP0(E6) mask-check bit after a line, a flat
|
|
triangle or an opaque textured rect and before its batch is drawn: the draw
|
|
must keep the check bit it was submitted under, at 1x, above it and in the
|
|
window mode.
|
|
Texture-window runs (mode twin) draw prims that alternate GP0(E2h) windows
|
|
with [video] texture_window_batching off and on: the native VRAM, the frame at
|
|
S and the wide surface must be the same both ways, at 1x, above it and in the
|
|
window mode, and on must draw in fewer batches. Each of those runs also checks
|
|
raw rects drawn through every window against the PS1 window rule computed in C
|
|
from the VRAM, so a decode error both ways share still fails.
|
|
Pass runs (mode passes, where the renderer has the frame-rate stack's render
|
|
passes) check that the window mode refuses them and the full-VRAM surface
|
|
offers them.
|
|
Step runs (mode steps, dynamic resolution) keep the surfaces at a ceiling and
|
|
change the scale at run time. "fresh" steps before drawing: the native VRAM,
|
|
the frame at S and the wide surface must equal the fixed-scale run at the
|
|
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. 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
|
|
_deps/sdl3-build/libSDL3.a) and a C compiler. Evidence (commands, output) is
|
|
written to receipt.json under --output.
|
|
When the host cannot create a hidden window with a GL 3.3 core context (a
|
|
headless or non-interactive session, such as Windows over SSH) the fixture
|
|
exits SKIP_EXIT and so does this script: CTest reports a skip, not a pass.
|
|
"""
|
|
import argparse
|
|
import json
|
|
import os
|
|
import pathlib
|
|
import platform
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
MAC_FRAMEWORKS = ["Cocoa", "OpenGL", "IOKit", "CoreVideo", "CoreAudio", "AudioToolbox",
|
|
"Carbon", "ForceFeedback", "GameController", "Metal", "QuartzCore",
|
|
"CoreMedia", "AVFoundation", "Foundation", "CoreHaptics",
|
|
"UniformTypeIdentifiers"]
|
|
# The static SDL3's Win32 dependencies (as run_gl_texture_filter.py links them).
|
|
WIN_LIBS = ["opengl32", "kernel32", "user32", "gdi32", "winmm", "imm32", "ole32",
|
|
"oleaut32", "version", "uuid", "advapi32", "setupapi", "shell32", "dinput8"]
|
|
SKIP_EXIT = 77 # CTest SKIP_RETURN_CODE, as wtrace_dump_test uses it
|
|
WINDOWS = os.name == "nt" or platform.system().startswith(("MINGW", "MSYS", "CYGWIN"))
|
|
|
|
|
|
def parse_run(stdout):
|
|
"""Return (checks, failures, digest or None) from a fixture run."""
|
|
summary = re.search(r"^checks=(\d+) failures=(\d+)$", stdout, re.M)
|
|
digest = re.search(r"^digest=([0-9a-f]{16})$", stdout, re.M)
|
|
if not summary:
|
|
return None
|
|
return int(summary[1]), int(summary[2]), digest[1] if digest else None
|
|
|
|
|
|
def parse_hires(stdout, key="hires"):
|
|
m = re.search(r"^" + key + r"=([0-9a-f]{16})$", stdout, re.M)
|
|
return m[1] if m else None
|
|
|
|
|
|
def digests_agree(results):
|
|
"""results: {scale: digest}. All present and equal."""
|
|
values = list(results.values())
|
|
return bool(values) and all(v is not None for v in values) and len(set(values)) == 1
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description=__doc__)
|
|
ap.add_argument("--cc", default="cc")
|
|
ap.add_argument("--sdl-include", required=True)
|
|
ap.add_argument("--sdl-library", required=True)
|
|
ap.add_argument("--output", required=True)
|
|
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]
|
|
args.sdl_library = str(pathlib.Path(args.sdl_library).resolve())
|
|
framework = pathlib.Path(__file__).resolve().parents[2]
|
|
fixture = args.fixture or framework / "runtime/tests/test_gl_scale_invariance.c"
|
|
out_root = pathlib.Path(args.output).resolve()
|
|
out_root.mkdir(parents=True, exist_ok=True)
|
|
dest = pathlib.Path(tempfile.mkdtemp(prefix="scale-", dir=out_root))
|
|
print("Evidence directory:", dest)
|
|
receipt = []
|
|
|
|
def run(command, env=None):
|
|
command = [str(c) for c in command]
|
|
r = subprocess.run(command, cwd=dest, capture_output=True, text=True,
|
|
encoding="utf-8", errors="replace", env=env)
|
|
receipt.append({"cmd": command, "exit": r.returncode,
|
|
"stdout": r.stdout, "stderr": r.stderr[-4000:]})
|
|
(dest / "receipt.json").write_text(json.dumps(receipt, indent=2), encoding="utf-8")
|
|
return r
|
|
|
|
includes = ["-I", framework / "runtime/include", "-I", framework / "runtime/src"]
|
|
for d in sdl_includes:
|
|
includes += ["-I", d]
|
|
# The fixture #includes gpu_gl_renderer.c; these are the other runtime
|
|
# sources the renderer calls into. render_pass_plan.c exists once the
|
|
# frame-rate render passes have landed, and the renderer calls it from then
|
|
# on, so it is linked whenever it is there.
|
|
sources = [("probe", fixture), ("sw", framework / "runtime/src/gpu_sw_renderer.c"),
|
|
("fi", framework / "runtime/src/frame_interpolation.c")]
|
|
if (framework / "runtime/src/render_pass_plan.c").exists():
|
|
sources.append(("rp", framework / "runtime/src/render_pass_plan.c"))
|
|
if (framework / "runtime/src/psx_openxr.c").exists():
|
|
sources.append(("xr", framework / "runtime/src/psx_openxr.c"))
|
|
if (framework / "runtime/src/render_thread.c").exists():
|
|
sources.append(("rth", framework / "runtime/src/render_thread.c"))
|
|
if (framework / "runtime/src/present_thread.c").exists():
|
|
sources.append(("pth", framework / "runtime/src/present_thread.c"))
|
|
if (framework / "runtime/src/frame_gen.c").exists():
|
|
sources.append(("fg", framework / "runtime/src/frame_gen.c"))
|
|
# Unused renderer functions reference the rest of the runtime; the linker
|
|
# drops them (-dead_strip, or per-function sections with --gc-sections).
|
|
# MinGW's PE linker reports undefined references from sections it later
|
|
# collects, so there LTO drops them first (as run_gl_readback_region.py).
|
|
# Anything still unresolved is a link error, not a NULL call at run time.
|
|
sections = [] if platform.system() == "Darwin" else ["-ffunction-sections", "-fdata-sections"]
|
|
if WINDOWS:
|
|
sections.append("-flto")
|
|
# Render passes (the frame-rate stack): build the fixture's passes mode.
|
|
renderer = (framework / "runtime/src/gpu_gl_renderer.c").read_text(encoding="utf-8")
|
|
passes = "uint32_t gl_renderer_pass_unavailable(void)" in renderer
|
|
if passes:
|
|
sections.append("-DPSX_TEST_RENDER_PASSES=1")
|
|
objs = []
|
|
# The renderer calls the in-game overlay hook (host_overlay.c) at present.
|
|
if (framework / "runtime/src" / "host_overlay.c").exists():
|
|
sources.append(("hov", framework / "runtime/src" / "host_overlay.c"))
|
|
for name, src in sources:
|
|
o = dest / (name + ".o")
|
|
r = run([args.cc, "-std=gnu11", "-O1", "-DPSX_SDL3=1", "-DPSX_NO_DEBUG_TOOLS=1",
|
|
"-DGL_SILENCE_DEPRECATION=1", "-w", *sections, *includes, "-c", src, "-o", o])
|
|
if r.returncode:
|
|
print(r.stderr[-3000:])
|
|
return 2
|
|
objs.append(o)
|
|
probe = dest / ("probe.exe" if WINDOWS else "probe")
|
|
link = [args.cc, *objs, args.sdl_library, "-o", probe]
|
|
if platform.system() == "Darwin":
|
|
for f in MAC_FRAMEWORKS:
|
|
link += ["-framework", f]
|
|
link += ["-liconv", "-lm", "-Wl,-dead_strip"]
|
|
elif WINDOWS:
|
|
# -static: no libwinpthread/libgcc DLLs needed next to probe.exe.
|
|
link += ["-flto", "-static", *["-l" + x for x in WIN_LIBS], "-lm", "-Wl,--gc-sections"]
|
|
else:
|
|
link += ["-lGL", "-lm", "-ldl", "-lpthread", "-Wl,--gc-sections"]
|
|
r = run(link)
|
|
if r.returncode:
|
|
print(r.stderr[-3000:])
|
|
return 2
|
|
|
|
# One preflight run: no window/GL context on this host means skip, before
|
|
# any check is counted. After this, a skip exit is a failure like any other.
|
|
r = run([probe, 1])
|
|
if r.returncode == SKIP_EXIT:
|
|
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 = {}
|
|
for s in [int(v) for v in args.scales.split(",") if v]:
|
|
r = run([probe, s])
|
|
parsed = parse_run(r.stdout)
|
|
print(f"scale {s}: exit={r.returncode}", r.stdout.strip().splitlines()[-3:],
|
|
r.stderr.strip()[-600:])
|
|
if r.returncode or not parsed or parsed[1]:
|
|
ok = False
|
|
digests[("full", s)] = parsed[2] if parsed else None
|
|
hires_full[s] = (parse_hires(r.stdout), parse_hires(r.stdout, "wide"))
|
|
env = os.environ.copy()
|
|
wenv = dict(env)
|
|
wenv["PSX_GL_HIRES_WINDOW"] = "1"
|
|
for s in [int(v) for v in args.window_scales.split(",") if v]:
|
|
r = run([probe, s, "window"], env=wenv)
|
|
parsed = parse_run(r.stdout)
|
|
print(f"window {s}: exit={r.returncode}", r.stdout.strip().splitlines()[-3:],
|
|
r.stderr.strip()[-600:])
|
|
if r.returncode or not parsed or parsed[1]:
|
|
ok = False
|
|
digests[("window", s)] = parsed[2] if parsed else None
|
|
h = (parse_hires(r.stdout), parse_hires(r.stdout, "wide"))
|
|
if s in hires_full and hires_full[s] != h:
|
|
print(f"FAIL window {s}x frame/wide surface differ from the full-VRAM "
|
|
f"surface at {s}x:", h, hires_full[s])
|
|
ok = False
|
|
if not digests_agree(digests):
|
|
print("FAIL native VRAM digest differs across scales/modes:", digests)
|
|
ok = False
|
|
# Side-by-side flips: (label, scale, env, reference run or None, tiles).
|
|
sbs = {}
|
|
sbs_runs = (
|
|
("full", 1, {}, None, None),
|
|
("full", 5, {}, None, None),
|
|
("full", 9, {}, None, None),
|
|
("window", 5, {"PSX_GL_HIRES_WINDOW": "1"}, ("full", 5), 1),
|
|
("window-8192", 9, {"PSX_GL_MAX_DIM": "8192"}, ("full", 9), 2),
|
|
("window", 18, {"PSX_GL_HIRES_WINDOW": "1"}, None, None),
|
|
)
|
|
for label, s, extra, ref, tiles in sbs_runs:
|
|
e = dict(env)
|
|
e.update(extra)
|
|
r = run([probe, s, "sbs"], env=e)
|
|
parsed = parse_run(r.stdout)
|
|
got_tiles = re.search(r"^tiles=(\d+)$", r.stdout, re.M)
|
|
print(f"sbs {label} {s}: exit={r.returncode}", r.stdout.strip().splitlines()[-5:],
|
|
r.stderr.strip()[-600:])
|
|
if r.returncode or not parsed or parsed[1]:
|
|
ok = False
|
|
sbs[(label, s)] = (parsed[2] if parsed else None,
|
|
parse_hires(r.stdout), parse_hires(r.stdout, "wide"))
|
|
if tiles is not None and (not got_tiles or int(got_tiles[1]) != tiles):
|
|
print(f"FAIL sbs {label} {s}x: tiles", got_tiles and got_tiles[1], "want", tiles)
|
|
ok = False
|
|
if ref is not None and sbs.get(ref, (None,))[1:] != sbs[(label, s)][1:]:
|
|
print(f"FAIL sbs {label} {s}x buffers differ from {ref}:",
|
|
sbs[(label, s)][1:], sbs.get(ref))
|
|
ok = False
|
|
if not digests_agree({k: v[0] for k, v in sbs.items()}):
|
|
print("FAIL sbs native VRAM digest differs across scales/modes:", sbs)
|
|
ok = False
|
|
# Lines batched with triangles: (label, scale, env).
|
|
lines = {}
|
|
for label, s, extra in (("full", 1, {}), ("full", 9, {}),
|
|
("window", 9, {"PSX_GL_HIRES_WINDOW": "1"}),
|
|
("window", 18, {"PSX_GL_HIRES_WINDOW": "1"})):
|
|
e = dict(env)
|
|
e.update(extra)
|
|
r = run([probe, s, "lines"], env=e)
|
|
parsed = parse_run(r.stdout)
|
|
print(f"lines {label} {s}: exit={r.returncode}", r.stdout.strip().splitlines()[-4:],
|
|
r.stderr.strip()[-600:])
|
|
if r.returncode or not parsed or parsed[1]:
|
|
ok = False
|
|
lines[(label, s)] = (parse_hires(r.stdout, "lband"), parse_hires(r.stdout),
|
|
parse_hires(r.stdout, "wide"))
|
|
for key in (("window", 9), ("window", 18)):
|
|
if lines[key][0] is None or lines[key][0] != lines[("full", 1)][0]:
|
|
print(f"FAIL lines {key}: native frame differs from the 1x run:",
|
|
lines[key][0], lines[("full", 1)][0])
|
|
ok = False
|
|
if None in lines[("window", 9)][1:] or lines[("window", 9)][1:] != lines[("full", 9)][1:]:
|
|
print("FAIL lines window 9x frame/wide surface differ from the full-VRAM run:",
|
|
lines[("window", 9)][1:], lines[("full", 9)][1:])
|
|
ok = False
|
|
# Frame-blend and hold-last captures: (label, scale, env, windowed).
|
|
for label, s, extra, windowed in (("full", 9, {}, False),
|
|
("window", 9, {"PSX_GL_HIRES_WINDOW": "1"}, True),
|
|
("window", 18, {"PSX_GL_HIRES_WINDOW": "1"}, True)):
|
|
e = dict(env)
|
|
e.update(extra)
|
|
r = run([probe, s, "capture"], env=e)
|
|
parsed = parse_run(r.stdout)
|
|
print(f"capture {label} {s}: exit={r.returncode}", r.stdout.strip().splitlines()[-2:],
|
|
r.stderr.strip()[-600:])
|
|
if r.returncode or not parsed or parsed[1]:
|
|
ok = False
|
|
m = re.search(r"^capture=(\d+)x(\d+) source=(\d+)x(\d+)$", r.stdout, re.M)
|
|
if not m or ((m[1], m[2]) == (m[3], m[4])) == windowed:
|
|
print(f"FAIL capture {label} {s}x: size", m and m.group(0))
|
|
ok = False
|
|
# Mask-check changes between a draw and its batch: (label, scale, env).
|
|
for label, s, extra in (("full", 1, {}), ("full", 4, {}), ("full", 9, {}),
|
|
("window", 9, {"PSX_GL_HIRES_WINDOW": "1"}),
|
|
("window", 18, {"PSX_GL_HIRES_WINDOW": "1"})):
|
|
e = dict(env)
|
|
e.update(extra)
|
|
r = run([probe, s, "mask"], env=e)
|
|
parsed = parse_run(r.stdout)
|
|
print(f"mask {label} {s}: exit={r.returncode}", r.stdout.strip().splitlines()[-1:],
|
|
r.stderr.strip()[-600:])
|
|
if r.returncode or not parsed or parsed[1]:
|
|
ok = False
|
|
# Texture-window batching off vs on: (label, scale, env).
|
|
for label, s, extra in (("full", 1, {}), ("full", 4, {}), ("full", 9, {}),
|
|
("window", 9, {"PSX_GL_HIRES_WINDOW": "1"}),
|
|
("window", 18, {"PSX_GL_HIRES_WINDOW": "1"})):
|
|
e = dict(env)
|
|
e.update(extra)
|
|
got = {}
|
|
for on in (0, 1):
|
|
r = run([probe, s, "twin", on], env=e)
|
|
parsed = parse_run(r.stdout)
|
|
m = re.search(r"^twin_flushes=(\d+) batches=(\d+)$", r.stdout, re.M)
|
|
print(f"twin {label} {s} batching={on}: exit={r.returncode}",
|
|
r.stdout.strip().splitlines()[-5:], r.stderr.strip()[-600:])
|
|
if r.returncode or not parsed or parsed[1] or not m:
|
|
ok = False
|
|
got[on] = (parsed[2] if parsed else None, parse_hires(r.stdout),
|
|
parse_hires(r.stdout, "wide"), int(m[2]) if m else None)
|
|
if None in got[0][:3] or got[0][:3] != got[1][:3]:
|
|
print(f"FAIL twin {label} {s}x: batching changes the image:", got)
|
|
ok = False
|
|
if got[0][3] is None or got[1][3] is None or got[1][3] >= got[0][3]:
|
|
print(f"FAIL twin {label} {s}x: batching on did not draw fewer batches:", got)
|
|
ok = False
|
|
# Native-wide mirror queue and stale-rect wide stencil rebuild: the same
|
|
# native VRAM, frame at S and wide surface with each off (the previous
|
|
# immediate mirrors and whole-surface rebuilds) and on.
|
|
ab_envs = (("default", {}), ("queue-off", {"PSX_GL_WIDE_QUEUE": "0"}),
|
|
("stencil-full", {"PSX_GL_WIDE_STENCIL_FULL": "1"}),
|
|
("both-off", {"PSX_GL_WIDE_QUEUE": "0", "PSX_GL_WIDE_STENCIL_FULL": "1"}))
|
|
ab_runs = [("wmask", s, fast, {}) for s in (1, 3, 9) for fast in ("0", "1")]
|
|
ab_runs += [("wmask", 9, fast, {"PSX_GL_HIRES_WINDOW": "1"}) for fast in ("0", "1")]
|
|
ab_runs += [("lines", 9, None, {}), ("twin", 9, "1", {}), ("scene", 3, None, {}),
|
|
("scene", 9, None, {})]
|
|
for mode, s, arg, extra in ab_runs:
|
|
got = {}
|
|
for name, ab in ab_envs:
|
|
e = dict(env)
|
|
e.update(extra)
|
|
e.update(ab)
|
|
cmd = [dest / "probe", s, mode] + ([arg] if arg is not None else [])
|
|
r = run(cmd, env=e)
|
|
parsed = parse_run(r.stdout)
|
|
if r.returncode or not parsed or parsed[1]:
|
|
print(f"wide a/b {mode} {s} {arg} {name}: exit={r.returncode}",
|
|
r.stdout.strip().splitlines()[-3:], r.stderr.strip()[-600:])
|
|
ok = False
|
|
got[name] = (parsed[2] if parsed else None, parse_hires(r.stdout),
|
|
parse_hires(r.stdout, "wide"))
|
|
same = len(set(got.values())) == 1 and None not in got["default"][1:]
|
|
print(f"wide a/b {mode} {s}x {arg or ''}{' window' if extra else ''}:",
|
|
"same" if same else got)
|
|
if not same:
|
|
print(f"FAIL wide a/b {mode} {s}x: queue/stencil change the image:", got)
|
|
ok = False
|
|
# Render passes: offered on the full-VRAM surface, refused in the window mode.
|
|
if not passes:
|
|
print("passes: skipped (this renderer has no render passes)")
|
|
for label, s, extra in ((("full", 9, {}), ("window", 9, {"PSX_GL_HIRES_WINDOW": "1"}),
|
|
("window", 18, {"PSX_GL_HIRES_WINDOW": "1"})) if passes else ()):
|
|
e = dict(env)
|
|
e.update(extra)
|
|
r = run([probe, s, "passes"], env=e)
|
|
parsed = parse_run(r.stdout)
|
|
print(f"passes {label} {s}: exit={r.returncode}", r.stdout.strip().splitlines()[-2:],
|
|
r.stderr.strip()[-600:])
|
|
if r.returncode or not parsed or parsed[1]:
|
|
ok = False
|
|
# Dynamic resolution steps: (ceiling, levels, kind, env). The final level
|
|
# must be one of --scales (its fixed-scale run is the reference).
|
|
step_runs = (
|
|
(9, "5", "fresh", {}), (9, "3,9", "fresh", {}), (9, "1", "fresh", {}),
|
|
(5, "2", "fresh", {}),
|
|
(9, "8,5,9,3,1,2,9", "chain", {}), (5, "3,5,1,2", "chain", {}),
|
|
(9, "5", "chain", {"PSX_GL_HIRES_WINDOW": "1"}),
|
|
)
|
|
for ceiling, levels, kind, extra in step_runs:
|
|
e = dict(env)
|
|
e.update(extra)
|
|
r = run([dest / "probe", ceiling, "steps", levels, kind], env=e)
|
|
parsed = parse_run(r.stdout)
|
|
label = f"steps {kind} {ceiling}x [{levels}]" + (" window" if extra else "")
|
|
print(f"{label}: exit={r.returncode}",
|
|
[ln for ln in r.stdout.strip().splitlines() if ln.startswith("step ")][-8:],
|
|
r.stdout.strip().splitlines()[-1:], r.stderr.strip()[-600:])
|
|
if r.returncode or not parsed or parsed[1]:
|
|
ok = False
|
|
if extra:
|
|
continue # the window mode only has to refuse
|
|
final = int(levels.split(",")[-1])
|
|
if parsed:
|
|
digests[("steps", ceiling, levels, kind)] = parsed[2]
|
|
got = (parse_hires(r.stdout), parse_hires(r.stdout, "wide"))
|
|
if final not in hires_full or None in got or got != hires_full[final]:
|
|
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
|
|
for label, s, budget in (("over-limit", 32, None), ("budget", 12, "40")):
|
|
e = dict(env)
|
|
if budget is not None:
|
|
e["PSX_GL_VRAM_BUDGET_MB"] = budget
|
|
r = run([probe, s, "clamp"], env=e)
|
|
parsed = parse_run(r.stdout)
|
|
print(f"clamp {label}: exit={r.returncode}", r.stdout.strip().splitlines()[-2:],
|
|
r.stderr.strip()[-600:])
|
|
if r.returncode or not parsed or parsed[1]:
|
|
ok = False
|
|
print("PASS" if ok else "FAIL")
|
|
return 0 if ok else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|