mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-28 10:59:16 -04:00
14b1163b32
Asked whether the bug can be fixed now. Not yet — there is a symptom and no cause, and guessing at this point would join the six hypotheses already dead. But the captured lists narrow it further without touching an emulator. For PolyG3+semi|B+F: strides psx-runtime [28, 2332] oracle [28, 1052, 2332] range 0x10D05C..0x10DCC0 0x10D0AC..0x10FD48 colours (30,0,0) (24,0,30) dark (47,23,0) (127,55,0) bright orange 28 is one packet, so those are contiguous runs. The oracle has a THIRD group the runtime does not — the 1052 stride — and builds it in a byte range the runtime never touches. The colours differ in character too: dark red/blue against bright red/green, which is what the screenshots show as missing orbs. So the census now records the address span each class is built in, and when a class is systematically absent it names the region the oracle uses and the runtime does not. That converts "short by 258 primitives" into an address range to point range_writers at: if nothing on psx-runtime writes there, that is where they are lost, and the code that should have written it is the fix site. Still a single capture, so still to be confirmed by the census itself — phase can hide a group in one frame, not in forty. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
236 lines
9.6 KiB
Python
236 lines
9.6 KiB
Python
#!/usr/bin/env python3
|
|
"""class_census.py -- what each emulator EVER draws, over many frames.
|
|
|
|
python3 class_census.py --samples 40
|
|
|
|
Why maxima rather than a snapshot
|
|
---------------------------------
|
|
A single display-list capture from each side is confounded by animation phase:
|
|
the two are never at the same moment, so any class-count difference could be
|
|
"this emulator does not draw that" or "it was not drawing it just then". That
|
|
ambiguity has cost this investigation several wrong turns.
|
|
|
|
Maxima are not symmetric in that way. If psx-runtime never once reaches the
|
|
count the oracle routinely shows, across many samples spanning the whole
|
|
effect, "it was not drawing it just then" stops being available as an
|
|
explanation. Phase can hide a primitive in one frame; it cannot hide it in
|
|
forty.
|
|
|
|
This is what the images pointed at: the oracle renders two glowing orbs and a
|
|
green artifact that psx-runtime does not, and a single-capture comparison put
|
|
psx-runtime at 32 additive gouraud triangles against the oracle's 290.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
import threading
|
|
import time
|
|
from collections import defaultdict
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
from psx_gpu_frame import ( # noqa: E402
|
|
DEFAULT_DUCKSTATION_PORT, DEFAULT_NATIVE_PORT, RAM_SIZE, STP_MODES,
|
|
DebugConn, DebugError, decode_entries, find_display_lists, oracle_resume,
|
|
read_ram_range, snapshot_ram, walk_ordering_table,
|
|
)
|
|
|
|
KIND = "psx-class-census"
|
|
|
|
|
|
def prim_class(p):
|
|
mode = STP_MODES.get(p.get("stp", 0), "?") if p.get("semi") else "opaque"
|
|
return f"{p['op_name']}|{mode}"
|
|
|
|
|
|
def census(conn, samples, gap, out=sys.stderr):
|
|
"""Per-class counts over `samples` captures. Returns {class: [counts]}."""
|
|
oracle_resume(conn)
|
|
seen = defaultdict(list)
|
|
spans = {}
|
|
root = span = None
|
|
misses = 0
|
|
for _ in range(samples):
|
|
try:
|
|
if root is None:
|
|
ram = snapshot_ram(conn)
|
|
cands = find_display_lists(ram)
|
|
if not cands:
|
|
misses += 1
|
|
time.sleep(gap)
|
|
continue
|
|
root = cands[0]["root"]
|
|
span = (cands[0]["lo"], cands[0]["hi"])
|
|
else:
|
|
# Re-read only the list's span: a full 2 MB snapshot per sample
|
|
# would make forty samples take minutes, and against a parked
|
|
# oracle it would not finish at all.
|
|
lo = max(0, (span[0] - 0x800) & ~3)
|
|
hi = min(RAM_SIZE, span[1] + 0x800)
|
|
blob = read_ram_range(conn, 0x80000000 + lo, hi - lo)
|
|
buf = bytearray(RAM_SIZE)
|
|
buf[lo:lo + len(blob)] = blob
|
|
ram = bytes(buf)
|
|
|
|
prims = decode_entries(walk_ordering_table(ram, root))
|
|
if not prims:
|
|
root = span = None
|
|
misses += 1
|
|
time.sleep(gap)
|
|
continue
|
|
counts = defaultdict(int)
|
|
lo_hi = {}
|
|
for p in prims:
|
|
if p["kind"] in ("poly", "rect", "line", "fill"):
|
|
k = prim_class(p)
|
|
counts[k] += 1
|
|
if p.get("src"):
|
|
a = int(p["src"], 16) & 0x1FFFFFFF
|
|
cur = lo_hi.get(k)
|
|
lo_hi[k] = (min(cur[0], a), max(cur[1], a)) if cur else (a, a)
|
|
for k in set(seen) | set(counts):
|
|
seen[k].append(counts.get(k, 0))
|
|
# Track WHERE each class is built, not just how many.
|
|
#
|
|
# A class that is short by 258 primitives is one thing; knowing the
|
|
# oracle builds them in a byte range psx-runtime never touches is
|
|
# what turns that into somewhere to look. Measured on a single
|
|
# capture: the oracle used 0x10D0AC..0x10FD48 for this class and
|
|
# psx-runtime only 0x10D05C..0x10DCC0.
|
|
for k, (a, b) in lo_hi.items():
|
|
cur = spans.get(k)
|
|
spans[k] = (min(cur[0], a), max(cur[1], b)) if cur else (a, b)
|
|
except DebugError:
|
|
root = span = None
|
|
misses += 1
|
|
time.sleep(gap)
|
|
if misses:
|
|
print(f" ({misses} of {samples} captures could not be walked)", file=out)
|
|
return dict(seen), spans
|
|
|
|
|
|
def summarise(nat, orc, out=sys.stdout, ratio=3.0, floor=20,
|
|
nat_spans=None, orc_spans=None):
|
|
keys = sorted(set(nat) | set(orc))
|
|
rows = []
|
|
for k in keys:
|
|
a, b = nat.get(k, []), orc.get(k, [])
|
|
rows.append({"key": k,
|
|
"native_max": max(a) if a else 0,
|
|
"oracle_max": max(b) if b else 0,
|
|
"native_med": sorted(a)[len(a) // 2] if a else 0,
|
|
"oracle_med": sorted(b)[len(b) // 2] if b else 0,
|
|
"native_span": (nat_spans or {}).get(k),
|
|
"oracle_span": (orc_spans or {}).get(k)})
|
|
rows.sort(key=lambda r: -(r["oracle_max"] - r["native_max"]))
|
|
|
|
print(f"\n {'class':<26}{'runtime max':>12}{'oracle max':>12}{'gap':>8}",
|
|
file=out)
|
|
for r in rows:
|
|
gap = r["oracle_max"] - r["native_max"]
|
|
mark = ""
|
|
if r["oracle_max"] >= floor and r["native_max"] * ratio < r["oracle_max"]:
|
|
mark = " <- psx-runtime never draws these"
|
|
elif r["native_max"] >= floor and r["oracle_max"] * ratio < r["native_max"]:
|
|
mark = " <- the ORACLE never draws these"
|
|
print(f" {r['key']:<24}{r['native_max']:>12}{r['oracle_max']:>12}"
|
|
f"{gap:>+8}{mark}", file=out)
|
|
return rows
|
|
|
|
|
|
def main(argv=None):
|
|
ap = argparse.ArgumentParser(description=__doc__,
|
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
ap.add_argument("--host", default="127.0.0.1")
|
|
ap.add_argument("--native-port", type=int, default=DEFAULT_NATIVE_PORT)
|
|
ap.add_argument("--ds-port", type=int, default=DEFAULT_DUCKSTATION_PORT)
|
|
ap.add_argument("--samples", type=int, default=40)
|
|
ap.add_argument("--gap", type=float, default=0.15)
|
|
ap.add_argument("--timeout", type=float, default=60.0)
|
|
ap.add_argument("--json", default=None)
|
|
args = ap.parse_args(argv)
|
|
|
|
n = DebugConn(args.host, args.native_port, args.timeout)
|
|
o = DebugConn(args.host, args.ds_port, args.timeout)
|
|
res = {}
|
|
|
|
def go(key, conn):
|
|
try:
|
|
res[key], res[key + "_spans"] = census(conn, args.samples, args.gap)
|
|
except DebugError as e:
|
|
res[key] = {}
|
|
res[key + "_err"] = str(e)
|
|
|
|
print(f"censusing {args.samples} captures on each side — "
|
|
f"run the effect through on BOTH")
|
|
ts = [threading.Thread(target=go, args=("nat", n)),
|
|
threading.Thread(target=go, args=("orc", o))]
|
|
for t in ts:
|
|
t.start()
|
|
for t in ts:
|
|
t.join()
|
|
|
|
nat, orc = res.get("nat", {}), res.get("orc", {})
|
|
doc = {"kind": KIND, "version": 1, "samples": args.samples}
|
|
if not nat or not orc:
|
|
missing = "psx-runtime" if not nat else "the oracle"
|
|
doc["error"] = f"{missing} produced no captures"
|
|
print(f"error: {doc['error']}", file=sys.stderr)
|
|
return _finish(doc, args, 1)
|
|
|
|
rows = summarise(nat, orc,
|
|
nat_spans=res.get("nat_spans"),
|
|
orc_spans=res.get("orc_spans"))
|
|
doc["classes"] = rows
|
|
absent = [r for r in rows
|
|
if r["oracle_max"] >= 20 and r["native_max"] * 3 < r["oracle_max"]]
|
|
doc["absent_on_native"] = [r["key"] for r in absent]
|
|
if absent:
|
|
total = sum(r["oracle_max"] - r["native_max"] for r in absent)
|
|
for r in absent:
|
|
ns, os_ = r.get("native_span"), r.get("oracle_span")
|
|
if ns and os_:
|
|
r["unwritten_region"] = [f"0x{max(ns[1], os_[0]):08X}",
|
|
f"0x{os_[1]:08X}"]
|
|
print(f"\n {r['key']}: the oracle builds these across "
|
|
f"0x{os_[0]:08X}..0x{os_[1]:08X}; psx-runtime only uses "
|
|
f"0x{ns[0]:08X}..0x{ns[1]:08X}. Trace the writers of "
|
|
f"{r['unwritten_region'][0]}..{r['unwritten_region'][1]} "
|
|
f"on psx-runtime — if nothing writes there, that is where "
|
|
f"the primitives are lost.", file=sys.stdout)
|
|
elif os_:
|
|
r["unwritten_region"] = [f"0x{os_[0]:08X}", f"0x{os_[1]:08X}"]
|
|
print(f"\n {r['key']}: the oracle builds these across "
|
|
f"0x{os_[0]:08X}..0x{os_[1]:08X}; psx-runtime builds none "
|
|
f"at all.", file=sys.stdout)
|
|
print(f"\nFINDING: psx-runtime never draws {total} primitive(s) the "
|
|
f"oracle does, across {args.samples} captures spanning the "
|
|
f"effect: {', '.join(r['key'] for r in absent)}.\n"
|
|
f"Phase can hide a primitive in one frame; it cannot hide it in "
|
|
f"{args.samples}. These are not being SUBMITTED, which puts the "
|
|
f"fault in the guest code that builds them — not in the "
|
|
f"renderer.")
|
|
doc["verdict"] = "classes-absent-on-native"
|
|
else:
|
|
print("\nNo class is systematically absent: every one psx-runtime draws "
|
|
"fewer of at some moment, it also reaches comparable counts at "
|
|
"another. The single-capture differences were phase.")
|
|
doc["verdict"] = "no-systematic-absence"
|
|
return _finish(doc, args, 0)
|
|
|
|
|
|
def _finish(doc, args, rc):
|
|
if args.json:
|
|
with open(args.json, "w", encoding="utf-8") as f:
|
|
json.dump(doc, f, indent=1)
|
|
print(f"wrote {args.json}")
|
|
return rc
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|