Files
psxrecomp/tools/qualify_render_passes.py
2026-10-06 18:36:52 -07:00

93 lines
4.3 KiB
Python

#!/usr/bin/env python3
"""Record evidence of native scene passes during a caller-selected gameplay case.
This observes an already running debug build. It never boots, drives, pauses or
changes settings. A passing sample is one gameplay window, not title acceptance.
Use correctness with PSX_RENDER_PASS_VERIFY=1 to check guest-state restoration.
For performance, relaunch without verification and select --purpose performance;
synchronous verification readbacks and hashing are not normal-play costs.
"""
import argparse
import hashlib
import json
from pathlib import Path
import time
from debug_client import connect, send_cmd
COUNTERS = ("passes", "pass_presents", "verify_checks", "verify_mismatch",
"vram_leaks", "watchdog", "nesting_repairs", "span_failures",
"aborted", "blended_presents")
def assess(before, after, purpose="correctness"):
reasons, delta = [], {}
for key in COUNTERS:
a, b = before.get(key), after.get(key)
if type(a) is not int or type(b) is not int or b < a:
reasons.append("missing, invalid or reset counter: " + key)
else:
delta[key] = b - a
required = ("passes", "pass_presents", "verify_checks") if purpose == "correctness" else ("passes", "pass_presents")
for key in required:
if key in delta and not delta[key]:
reasons.append("no evidence during this window: " + key)
for key in ("verify_mismatch", "vram_leaks", "watchdog", "nesting_repairs",
"span_failures", "aborted", "blended_presents"):
if delta.get(key, 0):
reasons.append("unexpected replay result: " + key)
if after.get("disabled") != 0:
reasons.append("replay disabled or status missing")
return delta, reasons
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--port", type=int, required=True)
parser.add_argument("--case", required=True, help="actual gameplay path being observed")
parser.add_argument("--seconds", type=float, default=10)
parser.add_argument("--purpose", choices=("correctness", "performance"), default="correctness")
parser.add_argument("--executable", type=Path, required=True)
parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args()
if not 0 < args.seconds <= 60:
parser.error("--seconds must be greater than zero and at most 60")
executable = args.executable.resolve(strict=True)
with executable.open("rb") as stream:
binary_sha = hashlib.file_digest(stream, "sha256").hexdigest()
manifest_path = executable.with_suffix(".execution.json")
manifest = json.loads(manifest_path.read_text()) if manifest_path.exists() else None
receipt = dict(schema=1, case=args.case, executable=str(executable),
executable_sha256=binary_sha, execution=manifest,
started_utc=time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
port=args.port, purpose=args.purpose, sample_passed=False)
def query():
with connect(port=args.port) as sock:
response = send_cmd(sock, {"cmd": "render_pass_stats"})
if not isinstance(response, dict) or response.get("ok") is not True:
raise RuntimeError("render_pass_stats refused: " + repr(response))
return response
try:
receipt["before"] = query()
start = time.monotonic()
time.sleep(args.seconds)
receipt["after"] = query()
receipt["elapsed_seconds"] = time.monotonic() - start
receipt["delta"], receipt["reasons"] = assess(receipt["before"], receipt["after"], args.purpose)
if args.purpose == "performance" and receipt["delta"].get("verify_checks", 0):
receipt["reasons"].append("performance sample includes verification readbacks and hashing")
receipt["sample_passed"] = not receipt["reasons"]
except (OSError, ValueError, RuntimeError) as error:
receipt["reasons"] = [str(error)]
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(json.dumps(receipt, indent=2) + "\n", encoding="utf-8")
print(json.dumps({key: receipt.get(key) for key in ("case", "sample_passed", "delta", "reasons")}))
return 0 if receipt["sample_passed"] else 1
if __name__ == "__main__":
raise SystemExit(main())