Files
retroDE_ps2/tools/analyze_zsched_fb.py
thejayman77 ba74bbd5aa Snapshot: fog implementation + fidelity tooling baseline (pre bilinear-clamp fix)
Per-vertex GS fog end-to-end (gs_stub emit incl. persp_emit5, gs_prim_list_feeder
XYZ2->XYZF2 on PRIM.FGE, gs_make_sh3_scheduler_fixture.py F/FGE packing), new fog
TBs, fidelity attribution tooling. Functional baseline before removing the dead
bilinear lerp8 clamps (Codex: 161-node comb loop -> -0.042ns setup fail).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 19:56:46 -04:00

396 lines
15 KiB
Python

#!/usr/bin/env python3
"""Score a Ch357 ZSCHED framebuffer dump.
The z/paint/replay owner modes compare against the dump-derived texture oracle.
The replay-color mode is the hard board/RTL content check: it replays the emitted
fragment trace colors through the same clamp16 GEQUAL persistent-Z rule and
compares the framebuffer colors directly.
Usage:
tools/analyze_zsched_fb.py sim/data/top_psmct32_raster_demo/sh3_zsched_board_fb.mem
tools/analyze_zsched_fb.py <fb.mem> --owner paint
tools/analyze_zsched_fb.py <fb.mem> --owner replay --frags sim/traces/rtl/zsched_frags.txt
tools/analyze_zsched_fb.py <fb.mem> --owner replay-color --frags sim/traces/rtl/zsched_frags.txt
tools/analyze_zsched_fb.py <fb.mem> --owner replay --radius 8
tools/analyze_zsched_fb.py <fb.mem> --maps
"""
import os
import re
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.normpath(os.path.join(HERE, ".."))
DATA = os.path.join(ROOT, "sim", "data", "top_psmct32_raster_demo")
def load_mem(path):
vals = []
with open(path) as f:
for ln in f:
s = ln.strip()
if not s or s.startswith("//"):
continue
vals.append(int(s, 16) & 0xFFFFFFFF)
return vals
def parse_params(tag):
path = os.path.join(DATA, f"sh3_{tag}_params.vh")
txt = open(path).read()
out = {}
for key in ("FBPXW", "FBH", "TW", "TH", "N_EPOCHS"):
m = re.search(rf"localparam\s+int\s+{key}\s*=\s*([0-9]+)\s*;", txt)
if not m:
raise SystemExit(f"[analyze] missing {key} in {path}")
out[key] = int(m.group(1))
return out
def cell(idx, pal, tw, u, v):
lin = v * tw + u
w = idx[lin // 4]
ix = (w >> (8 * (lin % 4))) & 0xFF
return pal[ix] & 0xFFFFFF
def match_radius(fbc, idx, pal, tw, th, tu, tv, max_radius):
for rad in range(max_radius + 1):
for du in range(-rad, rad + 1):
for dv in range(-rad, rad + 1):
if max(abs(du), abs(dv)) != rad:
continue
uu = tu + du
vv = tv + dv
if 0 <= uu < tw and 0 <= vv < th and fbc == cell(idx, pal, tw, uu, vv):
return rad
return None
def write_maps(prefix, w, h, fb, status):
try:
from PIL import Image
except Exception:
print("[analyze] PIL not available; skipping maps")
return
colors = {
0: (0, 0, 0), # uncovered
1: (0, 180, 0), # owner exact
2: (170, 170, 0), # owner <=1 texel
3: (0, 80, 220), # wrong owner/other covering epoch
4: (220, 0, 0), # no covering epoch match
5: (255, 0, 255), # no palette
}
img = Image.new("RGB", (w, h))
for o, st in enumerate(status):
img.putpixel((o % w, o // w), colors.get(st, (255, 255, 255)))
out = f"{prefix}_class.png"
img.resize((w * 3, h * 3), Image.NEAREST).save(out)
print(f"[analyze] wrote {out}")
diff = Image.new("RGB", (w, h))
for o, px in enumerate(fb[:w * h]):
if status[o] in (4, 5):
diff.putpixel((o % w, o // w), (255, 0, 0))
else:
diff.putpixel((o % w, o // w), (px & 0xFF, (px >> 8) & 0xFF, (px >> 16) & 0xFF))
out = f"{prefix}_bad_overlay.png"
diff.resize((w * 3, h * 3), Image.NEAREST).save(out)
print(f"[analyze] wrote {out}")
def replay_owner(frags, npx, w):
zbuf = [-1] * npx
z_owner = [-1] * npx
paint_owner = [-1] * npx
cov = [0] * npx
with open(frags) as f:
for ln in f:
p = ln.split()
if len(p) < 5:
continue
ep = int(p[0])
x = int(p[1])
y = int(p[2])
z = int(p[3])
o = y * w + x
if o < 0 or o >= npx:
continue
zq = 0xFFFF if z > 0xFFFF else (0 if z < 0 else z)
cov[o] = 1
paint_owner[o] = ep
if zq >= zbuf[o]:
zbuf[o] = zq
z_owner[o] = ep
return cov, z_owner, paint_owner, zbuf
def replay_color(frags, npx, w):
zbuf = [-1] * npx
cov = [0] * npx
col = [0] * npx
frag_count = 0
pass_count = 0
with open(frags) as f:
for ln in f:
p = ln.split()
if len(p) < 5:
continue
x = int(p[1])
y = int(p[2])
z = int(p[3])
c = int(p[4], 16) & 0xFFFFFF
o = y * w + x
if o < 0 or o >= npx:
continue
frag_count += 1
zq = 0xFFFF if z > 0xFFFF else (0 if z < 0 else z)
if zq >= zbuf[o]:
zbuf[o] = zq
cov[o] = 1
col[o] = c
pass_count += 1
return cov, col, frag_count, pass_count
def replay_color_check(fb_path, fb, frags, w, h):
npx = w * h
cov, exp, frag_count, pass_count = replay_color(frags, npx, w)
covered = sum(cov)
mismatches = []
stray = 0
for o in range(npx):
got = fb[o] & 0xFFFFFF
want = exp[o] & 0xFFFFFF
if cov[o]:
if got != want:
if len(mismatches) < 12:
mismatches.append((o, got, want, "covered"))
elif got != 0:
stray += 1
if len(mismatches) < 12:
mismatches.append((o, got, want, "uncovered"))
mismatch_count = 0
for o in range(npx):
got = fb[o] & 0xFFFFFF
want = exp[o] & 0xFFFFFF
if (cov[o] and got != want) or ((not cov[o]) and got != 0):
mismatch_count += 1
print(f"[analyze] fb={fb_path}")
print(f"[analyze] owner=replay-color FB={w}x{h}")
print(f"[analyze] replay_frags={frags}")
print(f"[analyze] replay fragments={frag_count} zpass_updates={pass_count} covered={covered} stray_uncovered={stray}")
print(f"[analyze] replay-color exact {npx - mismatch_count}/{npx} ({100.0 * (npx - mismatch_count) / npx:.2f}%) mismatches={mismatch_count}")
for o, got, want, kind in mismatches:
print(f"[analyze] bad {kind}: px={o} x={o % w} y={o // w} got={got:06x} exp={want:06x}")
return 0 if mismatch_count == 0 else 1
def main(argv):
args = argv[1:]
fb_path = args[0] if args and not args[0].startswith("--") else os.path.join(DATA, "sh3_zsched_board_fb.mem")
tag = args[args.index("--tag") + 1] if "--tag" in args else "zsched"
owner_mode = args[args.index("--owner") + 1] if "--owner" in args else "z"
frags = args[args.index("--frags") + 1] if "--frags" in args else os.path.join(ROOT, "sim", "traces", "rtl", "zsched_frags.txt")
max_radius = int(args[args.index("--radius") + 1]) if "--radius" in args else 1
emit_maps = "--maps" in args
if owner_mode not in ("z", "paint", "replay", "replay-color"):
raise SystemExit("[analyze] --owner must be z, paint, replay, or replay-color")
p = parse_params(tag)
w, h, tw, th, ne = p["FBPXW"], p["FBH"], p["TW"], p["TH"], p["N_EPOCHS"]
npx = w * h
fb = load_mem(fb_path)
if len(fb) != npx:
raise SystemExit(f"[analyze] {fb_path}: {len(fb)} words != expected {npx}")
if owner_mode == "replay-color":
return replay_color_check(fb_path, fb, frags, w, h)
idx = [load_mem(os.path.join(DATA, f"sh3_{tag}{e}_idx.mem")) for e in range(ne)]
pal = [load_mem(os.path.join(DATA, f"sh3_{tag}{e}_pal.mem")) for e in range(ne)]
ref = load_mem(os.path.join(DATA, f"sh3_{tag}_refmap.mem"))
rep = [load_mem(os.path.join(DATA, f"sh3_{tag}{e}_refmap.mem")) for e in range(ne)]
zowner_path = os.path.join(DATA, f"sh3_{tag}_zowner.mem")
zowner = load_mem(zowner_path) if os.path.exists(zowner_path) else None
if owner_mode == "z" and zowner is None:
raise SystemExit(f"[analyze] missing {zowner_path}; use --owner paint or regenerate z oracle")
replay = replay_owner(frags, npx, w) if owner_mode == "replay" else None
palette_set = set()
for e in range(ne):
palette_set.update(x & 0xFFFFFF for x in pal[e])
totals = {
"covered": 0, "interior": 0, "multi": 0, "reject": 0,
"owner_r0": 0, "owner_r1": 0, "any_r1": 0,
"interior_owner_r1": 0, "multi_owner_r1": 0, "reject_owner_r1": 0,
"palette_bad": 0, "owner_missing_ref": 0,
}
status = [0] * npx
owner_counts = [0] * ne
by_owner = [{"cov": 0, "r0": 0, "r1": 0, "multi": 0, "multi_r1": 0, "reject": 0, "reject_r1": 0} for _ in range(ne)]
radius_hist = [0] * (max_radius + 1)
bad_examples = []
for o in range(npx):
if owner_mode == "z":
zw = zowner[o]
cov = (zw >> 31) & 1
if not cov:
continue
owner = ((zw >> 24) & 0xF) - 1
paint_owner = (zw & 0xF) - 1
reject = owner != paint_owner
multi = (ref[o] >> 28) & 1
interior = (rep[owner][o] >> 30) & 1 if 0 <= owner < ne else 0
elif owner_mode == "replay":
covs, owners, paint_owners, _zq = replay
if not covs[o]:
continue
owner = owners[o]
paint_owner = paint_owners[o]
reject = owner != paint_owner
multi = (ref[o] >> 28) & 1
interior = (rep[owner][o] >> 30) & 1 if 0 <= owner < ne else 0
else:
rw = ref[o]
cov = (rw >> 31) & 1
if not cov:
continue
owner = (rw >> 24) & 0x7
reject = False
multi = (rw >> 28) & 1
interior = (rw >> 30) & 1
totals["covered"] += 1
totals["interior"] += int(interior)
totals["multi"] += int(multi)
totals["reject"] += int(reject)
if 0 <= owner < ne:
owner_counts[owner] += 1
by_owner[owner]["cov"] += 1
by_owner[owner]["multi"] += int(multi)
by_owner[owner]["reject"] += int(reject)
fbc = fb[o] & 0xFFFFFF
if fbc not in palette_set:
totals["palette_bad"] += 1
status[o] = 5
if len(bad_examples) < 12:
bad_examples.append((o, owner, fbc, "palette"))
continue
owner_rad = None
if 0 <= owner < ne and (rep[owner][o] >> 31):
rm = rep[owner][o]
tu = (rm >> 9) & 0x1FF
tv = rm & 0x1FF
owner_rad = match_radius(fbc, idx[owner], pal[owner], tw, th, tu, tv, max_radius)
else:
totals["owner_missing_ref"] += 1
if owner_rad == 0:
radius_hist[0] += 1
totals["owner_r0"] += 1
totals["owner_r1"] += 1
totals["interior_owner_r1"] += int(interior)
totals["multi_owner_r1"] += int(multi)
totals["reject_owner_r1"] += int(reject)
if 0 <= owner < ne:
by_owner[owner]["r0"] += 1
by_owner[owner]["r1"] += 1
by_owner[owner]["multi_r1"] += int(multi)
by_owner[owner]["reject_r1"] += int(reject)
status[o] = 1
continue
if owner_rad == 1:
radius_hist[1] += 1
totals["owner_r1"] += 1
totals["interior_owner_r1"] += int(interior)
totals["multi_owner_r1"] += int(multi)
totals["reject_owner_r1"] += int(reject)
if 0 <= owner < ne:
by_owner[owner]["r1"] += 1
by_owner[owner]["multi_r1"] += int(multi)
by_owner[owner]["reject_r1"] += int(reject)
status[o] = 2
continue
if owner_rad is not None:
radius_hist[owner_rad] += 1
totals["owner_r1"] += 1
totals["interior_owner_r1"] += int(interior)
totals["multi_owner_r1"] += int(multi)
totals["reject_owner_r1"] += int(reject)
if 0 <= owner < ne:
by_owner[owner]["r1"] += 1
by_owner[owner]["multi_r1"] += int(multi)
by_owner[owner]["reject_r1"] += int(reject)
status[o] = 2
continue
any_ok = False
for e in range(ne):
if not (rep[e][o] >> 31):
continue
rm = rep[e][o]
tu = (rm >> 9) & 0x1FF
tv = rm & 0x1FF
if match_radius(fbc, idx[e], pal[e], tw, th, tu, tv, max_radius) is not None:
any_ok = True
break
if any_ok:
totals["any_r1"] += 1
status[o] = 3
else:
status[o] = 4
if len(bad_examples) < 12:
bad_examples.append((o, owner, fbc, "nomatch"))
def pct(num, den):
return 100.0 * num / den if den else 0.0
print(f"[analyze] fb={fb_path}")
print(f"[analyze] tag={tag} owner={owner_mode} FB={w}x{h} epochs={ne} radius={max_radius}")
if owner_mode == "replay":
print(f"[analyze] replay_frags={frags}")
print(f"[analyze] oracle covered={totals['covered']} interior={totals['interior']} multi={totals['multi']} reject={totals['reject']}")
print("[analyze] owner counts: " + " ".join(f"e{e}={owner_counts[e]}" for e in range(ne)))
for e in range(ne):
b = by_owner[e]
rlbl = "<=1" if max_radius == 1 else f"<=R{max_radius}"
print(f"[analyze] e{e}: {rlbl} {b['r1']}/{b['cov']} ({pct(b['r1'], b['cov']):.2f}%) "
f"multi{rlbl} {b['multi_r1']}/{b['multi']} ({pct(b['multi_r1'], b['multi']):.2f}%) "
f"reject{rlbl} {b['reject_r1']}/{b['reject']} ({pct(b['reject_r1'], b['reject']):.2f}%)")
print(f"[analyze] owner exact {totals['owner_r0']}/{totals['covered']} ({pct(totals['owner_r0'], totals['covered']):.2f}%)")
if max_radius == 1:
print(f"[analyze] owner <=1 texel {totals['owner_r1']}/{totals['covered']} ({pct(totals['owner_r1'], totals['covered']):.2f}%)")
print(f"[analyze] interior <=1 {totals['interior_owner_r1']}/{totals['interior']} ({pct(totals['interior_owner_r1'], totals['interior']):.2f}%)")
print(f"[analyze] multi <=1 {totals['multi_owner_r1']}/{totals['multi']} ({pct(totals['multi_owner_r1'], totals['multi']):.2f}%)")
else:
print(f"[analyze] owner <=R texel {totals['owner_r1']}/{totals['covered']} ({pct(totals['owner_r1'], totals['covered']):.2f}%)")
print(f"[analyze] interior <=R {totals['interior_owner_r1']}/{totals['interior']} ({pct(totals['interior_owner_r1'], totals['interior']):.2f}%)")
print(f"[analyze] multi <=R {totals['multi_owner_r1']}/{totals['multi']} ({pct(totals['multi_owner_r1'], totals['multi']):.2f}%)")
print("[analyze] owner radius hist: " + " ".join(f"r{r}={radius_hist[r]}" for r in range(max_radius + 1)))
if owner_mode == "z":
print(f"[analyze] reject <=1 {totals['reject_owner_r1']}/{totals['reject']} ({pct(totals['reject_owner_r1'], totals['reject']):.2f}%)")
print(f"[analyze] wrong-owner <=1 {totals['any_r1']}/{totals['covered']} ({pct(totals['any_r1'], totals['covered']):.2f}%)")
print(f"[analyze] palette_bad={totals['palette_bad']} owner_missing_ref={totals['owner_missing_ref']}")
if bad_examples:
for o, owner, fbc, kind in bad_examples:
print(f"[analyze] bad {kind}: px={o} x={o % w} y={o // w} owner=e{owner} fb={fbc:06x}")
if emit_maps:
prefix = os.path.splitext(fb_path)[0] + f"_{owner_mode}oracle"
write_maps(prefix, w, h, fb, status)
return 0 if totals["palette_bad"] == 0 else 1
if __name__ == "__main__":
raise SystemExit(main(sys.argv))