mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
93 lines
4.3 KiB
Python
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())
|