Files
retroDE_ps2/tools/diagnose_zsched_persp.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

523 lines
18 KiB
Python

#!/usr/bin/env python3
"""Render the SH3 ZSCHED fixture with the RTL's fixed-point perspective path.
The existing dump-derived reference is intentionally float-ish: it uses clipped
float vertices and computes S/Q at a reference sample point. This tool answers a
different question: if we mirror the RTL's rounded XYZ2 vertices, Q16.16
gradient setup, top-left edge rule, gs_persp_uv reciprocal, and GEQUAL Z RMW,
does the framebuffer line up with the board/zint dump?
"""
import os
import sys
import glob
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")
sys.path.insert(0, HERE)
import gs_make_sh3_multidraw_fixture as MD
FBW = 256
FBH = 210
TW = 512
TH = 512
PERSP_FRAC = 12
PSCALE = 4096
S24_MAX = (1 << 23) - 1
def load_mem(path):
out = []
with open(path) as f:
for ln in f:
s = ln.strip()
if s and not s.startswith("//"):
out.append(int(s, 16) & 0xFFFFFFFF)
return out
def load_mem64(path):
out = []
with open(path) as f:
for ln in f:
s = ln.strip()
if s and not s.startswith("//"):
out.append(int(s, 16) & 0xFFFFFFFFFFFFFFFF)
return out
def load_epochs(tag="zsched"):
out = []
with open(os.path.join(DATA, f"sh3_{tag}_epochs.txt")) as f:
for ln in f:
s = ln.strip()
if s and not s.startswith("#") and not s.startswith("META"):
out.append(int(s.split()[1]))
return out
def find_dump():
c = glob.glob(os.path.join(ROOT, "captures", "gs", "silenthill3", "*224139*.gs.zst"))
if not c:
raise SystemExit("[persp] no SH3 224139 dump found")
return c[0]
def clip_rect_z(tri, w, h):
def lerp(a, b, t):
return {k: (a[k] + t * (b[k] - a[k])) for k in ("x", "y", "z", "s", "t", "q")}
def clip(poly, inside, cross):
out = []
for i, a in enumerate(poly):
b = poly[(i + 1) % len(poly)]
ina, inb = inside(a), inside(b)
if ina:
out.append(a)
if ina != inb:
out.append(cross(a, b))
return out
poly = [dict(x=v["x"], y=v["y"], z=v["z"], s=v["s"], t=v["t"], q=v["q"]) for v in tri]
poly = clip(poly, lambda p: p["x"] >= 0.0, lambda a, b: lerp(a, b, (0.0 - a["x"]) / (b["x"] - a["x"])))
if not poly:
return []
poly = clip(poly, lambda p: p["x"] <= w, lambda a, b: lerp(a, b, (w - a["x"]) / (b["x"] - a["x"])))
if not poly:
return []
poly = clip(poly, lambda p: p["y"] >= 0.0, lambda a, b: lerp(a, b, (0.0 - a["y"]) / (b["y"] - a["y"])))
if not poly:
return []
poly = clip(poly, lambda p: p["y"] <= h, lambda a, b: lerp(a, b, (h - a["y"]) / (b["y"] - a["y"])))
if len(poly) < 3:
return []
return [(poly[0], poly[i], poly[i + 1]) for i in range(1, len(poly) - 1)]
def pack_vertex(v):
s = round(v["s"] * TW * (1 << PERSP_FRAC) * PSCALE)
t = round(v["t"] * TH * (1 << PERSP_FRAC) * PSCALE)
q = round(v["q"] * (1 << PERSP_FRAC) * PSCALE)
if abs(s) > S24_MAX or abs(t) > S24_MAX:
raise SystemExit(f"[persp] ST overflow s={s} t={t}")
if q < 0 or q > 0xFFFFFF:
raise SystemExit(f"[persp] Q out of RTL 24-bit range q={q}")
return {
"x": max(0, min(FBW - 1, int(round(v["x"])))),
"y": max(0, min(FBH - 1, int(round(v["y"])))),
"z": max(0, min(0xFFFFFF, int(round(v["z"])))),
"s": s & 0xFFFFFF,
"t": t & 0xFFFFFF,
"q": q & 0xFFFFFF,
}
def edge(px, py, ax, ay, bx, by):
return (px - ax) * (by - ay) - (py - ay) * (bx - ax)
def top_or_left(ax, ay, bx, by):
dx = bx - ax
dy = by - ay
return (dy > 0) or (dy == 0 and dx > 0)
def trunc_div(num, den):
if den == 0:
return 0
sign = -1 if (num < 0) ^ (den < 0) else 1
return sign * (abs(num) // abs(den))
def grad_num_dadx(a0, a1, a2, x0, y0, x1, y1, x2, y2):
return ((a1 - a0) * (y2 - y0) - (a2 - a0) * (y1 - y0)) << 16
def grad_num_dady(a0, a1, a2, x0, y0, x1, y1, x2, y2):
return ((a2 - a0) * (x1 - x0) - (a1 - a0) * (x2 - x0)) << 16
def interp_wide(base, dadx, dady, x, y, x0, y0):
step = dadx * (x - x0) + dady * (y - y0)
return (base + (step >> 16)) & 0xFFFFFF
def interp_z(base, dadx, dady, x, y, x0, y0):
step = dadx * (x - x0) + dady * (y - y0)
return max(0, min(0xFFFFFFFF, base + (step >> 16)))
def recip_lut(q, idx_bits=11, scale=24):
out_max = (1 << (scale + 1)) - 1
if q <= 0:
return out_max
top = idx_bits - 1
e = q.bit_length() - 1
m = (q >> (e - top)) if e >= top else (q << (top - e))
m &= (1 << idx_bits) - 1
if m == 0:
return out_max
r = ((1 << (scale + top)) // m) >> e
return min(r, out_max)
def persp_uv(s, t, q):
r = recip_lut(q)
u = (s * r) >> 24
v = (t * r) >> 24
if u > 2047:
u = 2047
if v > 2047:
v = 2047
return u, v
def texel(idx_words, pal, u, v):
lin = v * TW + u
if lin < 0 or lin >= len(idx_words) * 4:
return 0
ci = (idx_words[lin // 4] >> (8 * (lin % 4))) & 0xFF
return pal[ci] & 0xFFFFFF
def prep_tri(tri):
verts = [pack_vertex(v) for v in tri]
x0, y0 = verts[0]["x"], verts[0]["y"]
x1, y1 = verts[1]["x"], verts[1]["y"]
x2, y2 = verts[2]["x"], verts[2]["y"]
det = (x1 - x0) * (y2 - y0) - (x2 - x0) * (y1 - y0)
if det == 0:
return None
if det < 0:
verts = [verts[0], verts[2], verts[1]]
det = -det
v0, v1, v2 = verts
x0, y0 = v0["x"], v0["y"]
x1, y1 = v1["x"], v1["y"]
x2, y2 = v2["x"], v2["y"]
bias = [
0 if top_or_left(x0, y0, x1, y1) else 1,
0 if top_or_left(x1, y1, x2, y2) else 1,
0 if top_or_left(x2, y2, x0, y0) else 1,
]
def grad(attr):
a0, a1, a2 = v0[attr], v1[attr], v2[attr]
return (
trunc_div(grad_num_dadx(a0, a1, a2, x0, y0, x1, y1, x2, y2), det),
trunc_div(grad_num_dady(a0, a1, a2, x0, y0, x1, y1, x2, y2), det),
)
return {
"v": verts,
"det": det,
"bias": bias,
"ds": grad("s"),
"dt": grad("t"),
"dq": grad("q"),
"dz": grad("z"),
}
def prep_tri_packed(verts):
verts = [dict(v) for v in verts]
x0, y0 = verts[0]["x"], verts[0]["y"]
x1, y1 = verts[1]["x"], verts[1]["y"]
x2, y2 = verts[2]["x"], verts[2]["y"]
det = (x1 - x0) * (y2 - y0) - (x2 - x0) * (y1 - y0)
if det == 0:
return None
if det < 0:
verts = [verts[0], verts[2], verts[1]]
det = -det
v0, v1, v2 = verts
x0, y0 = v0["x"], v0["y"]
x1, y1 = v1["x"], v1["y"]
x2, y2 = v2["x"], v2["y"]
bias = [
0 if top_or_left(x0, y0, x1, y1) else 1,
0 if top_or_left(x1, y1, x2, y2) else 1,
0 if top_or_left(x2, y2, x0, y0) else 1,
]
def grad(attr):
a0, a1, a2 = v0[attr], v1[attr], v2[attr]
return (
trunc_div(grad_num_dadx(a0, a1, a2, x0, y0, x1, y1, x2, y2), det),
trunc_div(grad_num_dady(a0, a1, a2, x0, y0, x1, y1, x2, y2), det),
)
return {
"v": verts,
"det": det,
"bias": bias,
"ds": grad("s"),
"dt": grad("t"),
"dq": grad("q"),
"dz": grad("z"),
}
def build_tris(draw_idxs):
got, _ = MD.load_draws(find_dump(), draw_idxs)
eps = [got[i] for i in draw_idxs]
ox = int(min(min(v["x"] for v in e["verts"]) for e in eps))
oy = int(min(min(v["y"] for v in e["verts"]) for e in eps))
out = []
for e in eps:
rawv = [dict(x=v["x"] - ox, y=v["y"] - oy, z=v["z"], s=v["s"], t=v["t"], q=v["q"]) for v in e["verts"]]
raw = [(rawv[i - 2], rawv[i - 1], rawv[i]) for i in range(2, len(rawv))]
tris = []
for tri in raw:
for ct in clip_rect_z(tri, FBW, FBH):
qs = [v["q"] for v in ct]
if all(q < 0 for q in qs):
ct = tuple(dict(x=v["x"], y=v["y"], z=v["z"], s=-v["s"], t=-v["t"], q=-v["q"]) for v in ct)
elif not all(q > 0 for q in qs):
raise SystemExit(f"[persp] mixed/zero Q in idx{e['first_idx']}: {qs}")
pt = prep_tri(ct)
if pt is not None:
tris.append(pt)
out.append(tris)
return out
def build_tris_from_feeder(tag="zsched"):
out = []
ep = 0
while True:
path = os.path.join(DATA, f"feeder_sh3_{tag}{ep}.mem")
if not os.path.exists(path):
break
words = load_mem64(path)
ntris = words[0] & 0xFFFF
tris = []
off = 7
for _ in range(ntris):
verts = []
for _v in range(3):
rgbaq = words[off]
st = words[off + 1]
xyz = words[off + 2]
verts.append({
"x": (xyz >> 4) & 0xFFF,
"y": (xyz >> 20) & 0xFFF,
"z": (xyz >> 32) & 0xFFFFFFFF,
"s": st & 0xFFFFFF,
"t": (st >> 32) & 0xFFFFFF,
"q": (rgbaq >> 32) & 0xFFFFFF,
})
off += 3
pt = prep_tri_packed(verts)
if pt is not None:
tris.append(pt)
out.append(tris)
ep += 1
return out
def render(tag="zsched"):
draw_idxs = load_epochs(tag)
idx = [load_mem(os.path.join(DATA, f"sh3_{tag}{e}_idx.mem")) for e in range(len(draw_idxs))]
pal = [load_mem(os.path.join(DATA, f"sh3_{tag}{e}_pal.mem")) for e in range(len(draw_idxs))]
tris_by_ep = build_tris_from_feeder(tag)
if len(tris_by_ep) != len(draw_idxs):
tris_by_ep = build_tris(draw_idxs)
fb = [0] * (FBW * FBH)
zbuf = [-1] * (FBW * FBH)
owner = [-1] * (FBW * FBH)
emitted = [0] * len(draw_idxs)
accepted = [0] * len(draw_idxs)
frags = []
for ep, tris in enumerate(tris_by_ep):
for tri in tris:
v0, v1, v2 = tri["v"]
minx = max(0, min(v0["x"], v1["x"], v2["x"]))
maxx = min(FBW - 1, max(v0["x"], v1["x"], v2["x"]))
miny = max(0, min(v0["y"], v1["y"], v2["y"]))
maxy = min(FBH - 1, max(v0["y"], v1["y"], v2["y"]))
for y in range(miny, maxy + 1):
for x in range(minx, maxx + 1):
e0 = edge(x, y, v0["x"], v0["y"], v1["x"], v1["y"]) + tri["bias"][0]
e1 = edge(x, y, v1["x"], v1["y"], v2["x"], v2["y"]) + tri["bias"][1]
e2 = edge(x, y, v2["x"], v2["y"], v0["x"], v0["y"]) + tri["bias"][2]
if e0 > 0 or e1 > 0 or e2 > 0:
continue
s = interp_wide(v0["s"], tri["ds"][0], tri["ds"][1], x, y, v0["x"], v0["y"])
t = interp_wide(v0["t"], tri["dt"][0], tri["dt"][1], x, y, v0["x"], v0["y"])
q = interp_wide(v0["q"], tri["dq"][0], tri["dq"][1], x, y, v0["x"], v0["y"])
z = interp_z(v0["z"], tri["dz"][0], tri["dz"][1], x, y, v0["x"], v0["y"])
u, v = persp_uv(s, t, q)
col = texel(idx[ep], pal[ep], u, v)
o = y * FBW + x
emitted[ep] += 1
frags.append((ep, x, y, z, col, u, v, s, t, q))
zq = min(z, 0xFFFF)
if zq >= zbuf[o]:
zbuf[o] = zq
fb[o] = col
owner[o] = ep
accepted[ep] += 1
return draw_idxs, tris_by_ep, fb, owner, emitted, accepted, frags
def nearest_color(idx_words, pal, u, v, color, max_radius=64):
for rad in range(max_radius + 1):
for dv in range(-rad, rad + 1):
for du in range(-rad, rad + 1):
if max(abs(du), abs(dv)) != rad:
continue
uu = u + du
vv = v + dv
if 0 <= uu < TW and 0 <= vv < TH and texel(idx_words, pal, uu, vv) == color:
return rad, du, dv
return None
def compare_trace(frags, path, idx, pal):
if not os.path.exists(path):
return
got = []
with open(path) as f:
for ln in f:
p = ln.split()
if len(p) >= 5:
got.append((int(p[0]), int(p[1]), int(p[2]), int(p[3]), int(p[4], 16) & 0xFFFFFF))
n = min(len(frags), len(got))
coord_miss = color_miss = z_miss256 = 0
near_bins = {"r1": 0, "r8": 0, "r32": 0, "r64": 0, "miss": 0}
by_ep = {}
examples = []
for i in range(n):
me = frags[i]
hw = got[i]
ep = hw[0]
st = by_ep.setdefault(ep, {"n": 0, "coord": 0, "color": 0, "z256": 0})
st["n"] += 1
if me[:3] != hw[:3]:
coord_miss += 1
st["coord"] += 1
if (me[4] & 0xFFFFFF) != hw[4]:
color_miss += 1
st["color"] += 1
near = nearest_color(idx[ep], pal[ep], me[5], me[6], hw[4])
if near is None:
near_bins["miss"] += 1
else:
rad, _du, _dv = near
if rad <= 1:
near_bins["r1"] += 1
if rad <= 8:
near_bins["r8"] += 1
if rad <= 32:
near_bins["r32"] += 1
if rad <= 64:
near_bins["r64"] += 1
if abs(me[3] - hw[3]) > 256:
z_miss256 += 1
st["z256"] += 1
if len(examples) < 12 and (me[:3] != hw[:3] or (me[4] & 0xFFFFFF) != hw[4] or abs(me[3] - hw[3]) > 256):
near = nearest_color(idx[ep], pal[ep], me[5], me[6], hw[4]) if (me[4] & 0xFFFFFF) != hw[4] else None
examples.append((i, me, hw, near))
print(f"[persp] trace={path} model_frags={len(frags)} trace_frags={len(got)} compared={n}")
print(f"[persp] trace_miss coord={coord_miss} color={color_miss} z_gt256={z_miss256}")
print(f"[persp] trace color-near model_uv: <=1 {near_bins['r1']} <=8 {near_bins['r8']} <=32 {near_bins['r32']} <=64 {near_bins['r64']} >64/notfound {near_bins['miss']}")
for ep in sorted(by_ep):
st = by_ep[ep]
print(f"[persp] trace e{ep}: n={st['n']} coord={st['coord']} color={st['color']} z_gt256={st['z256']}")
for i, me, hw, near in examples:
ns = "near=none" if near is None else f"near r={near[0]} du={near[1]} dv={near[2]}"
print(f"[persp] trace miss#{i}: model ep{me[0]} ({me[1]},{me[2]}) z={me[3]} uv=({me[5]},{me[6]}) col={me[4]&0xFFFFFF:06x} "
f"trace ep{hw[0]} ({hw[1]},{hw[2]}) z={hw[3]} col={hw[4]:06x} {ns}")
def compare_issue(frags, path):
if not os.path.exists(path):
return
got = []
with open(path) as f:
for ln in f:
p = ln.split()
if len(p) >= 7:
got.append((int(p[0]), int(p[1]), int(p[2]), int(p[3]), int(p[4]), int(p[5]), int(p[6])))
n = min(len(frags), len(got))
by_ep = {}
s_miss = t_miss = u_miss = v_miss = recip_miss = 0
examples = []
for i in range(n):
me = frags[i]
hw = got[i]
ep = hw[0]
st = by_ep.setdefault(ep, {"n": 0, "s": 0, "t": 0, "u": 0, "v": 0, "recip": 0})
st["n"] += 1
mr = recip_lut(me[9])
if me[7] != hw[1]:
s_miss += 1
st["s"] += 1
if me[8] != hw[2]:
t_miss += 1
st["t"] += 1
if mr != hw[3]:
recip_miss += 1
st["recip"] += 1
if me[5] != hw[4]:
u_miss += 1
st["u"] += 1
if me[6] != hw[5]:
v_miss += 1
st["v"] += 1
if len(examples) < 12 and (me[7] != hw[1] or me[8] != hw[2] or mr != hw[3] or me[5] != hw[4] or me[6] != hw[5]):
examples.append((i, me, hw, mr))
print(f"[persp] issue={path} model_frags={len(frags)} issue_rows={len(got)} compared={n}")
print(f"[persp] issue_miss s={s_miss} t={t_miss} recip={recip_miss} u={u_miss} v={v_miss}")
for ep in sorted(by_ep):
st = by_ep[ep]
print(f"[persp] issue e{ep}: n={st['n']} s={st['s']} t={st['t']} recip={st['recip']} u={st['u']} v={st['v']}")
for i, me, hw, mr in examples:
print(f"[persp] issue miss#{i}: model ep{me[0]} ({me[1]},{me[2]}) s={me[7]} t={me[8]} q={me[9]} recip={mr} uv=({me[5]},{me[6]}) "
f"rtl ep{hw[0]} s={hw[1]} t={hw[2]} recip={hw[3]} uv=({hw[4]},{hw[5]}) valid={hw[6]}")
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")
board = load_mem(fb_path)
trace_path = args[args.index("--trace") + 1] if "--trace" in args else os.path.join(ROOT, "sim", "traces", "rtl", "zsched_frags.txt")
draw_idxs, tris_by_ep, fb, owner, emitted, accepted, frags = render()
if len(board) != len(fb):
raise SystemExit(f"[persp] {fb_path}: {len(board)} words != expected {len(fb)}")
mism = []
cov = sum(1 for x in owner if x >= 0)
for i, (a, b) in enumerate(zip(board, fb)):
if (a & 0xFFFFFF) != (b & 0xFFFFFF):
mism.append(i)
by_owner = [0] * len(draw_idxs)
by_miss = [0] * len(draw_idxs)
for o in range(len(fb)):
ep = owner[o]
if ep >= 0:
by_owner[ep] += 1
miss_set = set(mism)
for o in miss_set:
ep = owner[o]
if ep >= 0:
by_miss[ep] += 1
print(f"[persp] fb={fb_path}")
print(f"[persp] draw_idxs={draw_idxs} FB={FBW}x{FBH} covered={cov} mismatches={len(mism)}/{len(fb)}")
for ep, idx in enumerate(draw_idxs):
print(f"[persp] e{ep} idx{idx}: tris={len(tris_by_ep[ep])} emitted={emitted[ep]} accepted={accepted[ep]} final_owner={by_owner[ep]} miss={by_miss[ep]}")
for o in mism[:12]:
print(f"[persp] miss x={o % FBW} y={o // FBW} owner=e{owner[o]} model={fb[o] & 0xFFFFFF:06x} board={board[o] & 0xFFFFFF:06x}")
trace_idxs = [load_mem(os.path.join(DATA, f"sh3_zsched{e}_idx.mem")) for e in range(len(draw_idxs))]
trace_pals = [load_mem(os.path.join(DATA, f"sh3_zsched{e}_pal.mem")) for e in range(len(draw_idxs))]
compare_trace(frags, trace_path, trace_idxs, trace_pals)
compare_issue(frags, os.path.join(ROOT, "sim", "traces", "rtl", "zsched_issue.txt"))
return 0 if not mism else 1
if __name__ == "__main__":
raise SystemExit(main(sys.argv))