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

173 lines
8.2 KiB
Python

#!/usr/bin/env python3
"""Emit the authentic SH3 4x4 PSMT4 post-process tile pass as scheduler epochs.
The dump expresses this pass as sixteen 128x128 FST SPRITEs. The runtime
feeder is triangle-record based, so each sprite is expanded to two textured
triangles and split into ordered 16-row bands. Consecutive bands are packed
under the proven 14,500-fragment request-FIFO safety bound. The PSMT4 asset
is de-swizzled to the texture cache's linear packed-nibble layout and padded
to its fixed 256 KiB residency window.
"""
import glob
import os
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")
sys.path.insert(0, HERE)
sys.path.insert(0, DATA)
import gs_make_sh3_multidraw_fixture as MD
import gs_sh3_recon as RC
import bake
DRAW_IDS = [198945,198954,198963,198972,198981,198990,198999,199008,
199017,199026,199035,199044,199053,199062,199071,199080]
FBW, W, H = 10, 640, 480
NEW_TBP, CBP = 1024, 480
TEX_CACHE_BYTES = 512*512
STG_WORDS = 2048
CAPACITY = 14500
PERSP_FRAC = bake.PERSP_FRAC
PSCALE = 1/64
def wmem(path, words, banner, width=8):
with open(os.path.join(DATA, path), "w") as f:
f.write(f"// {banner}\n")
for word in words:
f.write(f"{word & ((1 << (4*width))-1):0{width}x}\n")
def vwords(v):
s_fp = round(v["s"] * 128 * (1 << PERSP_FRAC) * PSCALE)
t_fp = round(v["t"] * 128 * (1 << PERSP_FRAC) * PSCALE)
q_fp = round((1 << PERSP_FRAC) * PSCALE)
x = max(0, min(W-1, round(v["x"])))
y = max(0, min(H-1, round(v["y"])))
return [bake.rgbaq_with_q(0x80, 0x80, 0x80, q_fp),
bake.st_data(s_fp, t_fp), bake.xyz2_dataz(x, y, 16)]
def main(argv):
tag = argv[argv.index("--tag")+1] if "--tag" in argv else "zsrt139e1"
dump = next((x for x in argv[1:] if x.endswith(".gs.zst")), None)
if dump is None:
found = glob.glob(os.path.join(ROOT, "captures", "gs", "silenthill3", "*224139*.gs.zst"))
if not found:
sys.exit("no dump 224139 found")
dump = found[0]
got, _ = MD.load_draws(dump, DRAW_IDS)
if sorted(got) != DRAW_IDS:
sys.exit(f"missing overlay draws: {sorted(set(DRAW_IDS)-set(got))}")
states = [got[i]["state"] for i in DRAW_IDS]
ref = states[0]
for i, state in zip(DRAW_IDS, states):
t0, pr = state["tex0"], state["prim"]
if (t0["tbp"],t0["tbw"],t0["psm"],t0["tw"],t0["th"],t0["cbp"]) != (13376,2,0x14,128,128,14276):
sys.exit(f"idx{i}: unexpected TEX0 {t0}")
if (pr["type"],pr["tme"],pr["fst"],pr["abe"]) != (6,1,1,1):
sys.exit(f"idx{i}: unexpected PRIM {pr}")
for key in ("test","zbuf","clamp","scissor","alpha","texa"):
if state[key] != ref[key]:
sys.exit(f"idx{i}: {key} differs")
if ref["scissor"] != 0x01ff000001ff0000:
sys.exit(f"unexpected SCISSOR 0x{ref['scissor']:016x}")
mem, *_ = RC.build_localmem_to(dump, DRAW_IDS[0])
idx = mem.read_psmt4(13376, 2, 128, 128)
packed = bytearray(128*128//2)
for n in range(0, len(idx), 2):
packed[n//2] = (idx[n]&15) | ((idx[n+1]&15)<<4)
packed.extend(bytes(TEX_CACHE_BYTES-len(packed)))
tex_words = [int.from_bytes(packed[n:n+4], "little") for n in range(0,TEX_CACHE_BYTES,4)]
crc = sum(tex_words) & 0xFFFFFFFF
pal = RC.read_clut32(mem, 14276, order="grid")
# Convert each authentic sprite to 16-row triangle bands. The sampled V
# coordinate is intentionally constant zero in the dump; U spans 0..128.
bands = []
for draw_id in DRAW_IDS:
vv = got[draw_id]["verts"]
if len(vv) != 2:
sys.exit(f"idx{draw_id}: expected two sprite vertices, got {len(vv)}")
a,b = vv
x0,x1 = a["x"],b["x"]
y0,y1 = a["y"],min(b["y"],float(H))
if y1 <= 0 or y0 >= H:
continue
for lo in range(max(0,int(round(y0))), int(round(y1)), 16):
hi = min(int(round(y1)), lo+16)
tl=dict(x=x0,y=lo,s=a["s"],t=a["t"])
tr=dict(x=x1,y=lo,s=b["s"],t=a["t"])
bl=dict(x=x0,y=hi,s=a["s"],t=b["t"])
br=dict(x=x1,y=hi,s=b["s"],t=b["t"])
cov=max(0,round(x1)-round(x0))*max(0,hi-lo)
bands.append((draw_id,[(tl,tr,bl),(tr,br,bl)],cov))
epochs=[]; cur=[]; cov=0
for draw_id,tris,npx in bands:
if cur and cov+npx > CAPACITY:
epochs.append((cur,cov)); cur=[]; cov=0
cur.extend(tris); cov += npx
if cur:
epochs.append((cur,cov))
if max(x[1] for x in epochs) > CAPACITY:
sys.exit("capacity pack failure")
tex0 = bake.tex0_pack(NEW_TBP,2,psm=0x14,tw=7,th=7,tfx=0)
tex0 |= (CBP&0x3FFF)<<37 # runtime CLUT: CLD=0
prim = 3 | (1<<4) | (1<<6) # TRIANGLE, TME, ABE; FST converted to ST/Q
for k,(tris,npx) in enumerate(epochs):
stg=[len(tris)|(1<<32)|(1<<34), bake.frame_1_psmct32(FBW), ref["alpha"],
ref["test"], ref["zbuf"], tex0, ref["clamp"], prim]
for tri in tris:
for v in tri:
stg.extend(vwords(v))
if len(stg)>STG_WORDS:
sys.exit(f"epoch{k}: staging overflow {len(stg)}")
bake.write_feeder_stg_mem(f"feeder_sh3_{tag}{k}.mem",stg,
f"Ch408 authentic PSMT4 overlay epoch{k}, {len(tris)} tris, capacity {npx}. gitignored.",total=STG_WORDS)
wmem(f"sh3_{tag}{k}_tex_lpddr.mem",tex_words,
f"Ch408 PSMT4 packed-linear texture padded to fixed 256 KiB cache; crc=0x{crc:08x}. gitignored.")
wmem(f"sh3_{tag}{k}_pal.mem",pal,
f"Ch408 authentic PSMT4 CSM1 palette, runtime staged. gitignored.")
n=len(epochs)
with open(os.path.join(DATA,f"sh3_{tag}_epochs.txt"),"w") as f:
f.write("# Ch408 authentic PSMT4 post-process overlay scheduler fixture\n")
f.write("# k idx tbp cbp_reloc tex_file lpddr size crc list_file words records reuse pal_file pal_sum32\n")
f.write(f"META n_epochs {n} fbpxw {W} fbh {H} fbwords {W*H} lpddr_tex 0x200000 tex_words {TEX_CACHE_BYTES//4} n_beats {TEX_CACHE_BYTES//32}\n")
for k,(tris,npx) in enumerate(epochs):
words=8+9*len(tris)
f.write(f"{k} {DRAW_IDS[0]} 13376 {CBP} sh3_{tag}{k}_tex_lpddr.mem 0x200000 {TEX_CACHE_BYTES} 0x{crc:08x} "
f"feeder_sh3_{tag}{k}.mem {words} {len(tris)} {0 if k==0 else 1} sh3_{tag}{k}_pal.mem 0x{sum(pal)&0xFFFFFFFF:08x}\n")
with open(os.path.join(DATA,f"sh3_{tag}_params.vh"),"w") as f:
f.write("// Ch408 generated params. gitignored.\n")
for key,val in (("FBW",FBW),("FBPXW",W),("FBH",H),("VRAM_BYTES_P",128*1024),("NEW_TBP",NEW_TBP),
("TEX_VRAM_BASE",NEW_TBP*256),("TEX_BYTES",TEX_CACHE_BYTES),("N_BEATS",TEX_CACHE_BYTES//32),
("STG_WORDS",STG_WORDS),("TW",128),("TH",128),("N_EPOCHS",n),("UNION_OX",0),("UNION_OY",0)):
f.write(f"localparam int {key:<14}= {val};\n")
f.write("localparam [29:0] LPDDR_TEX_BASE = 30'h0200000;\n")
for k,(tris,npx) in enumerate(epochs):
f.write(f"localparam int EP{k}_NTRIS={len(tris)}; localparam int EP{k}_CBP={CBP}; "
f"localparam [31:0] EP{k}_CRC=32'h{crc:08x}; localparam bit EP{k}_REUSE=1'b{0 if k==0 else 1};\n")
with open(os.path.join(DATA,f"sh3_{tag}_epoch_table.vh"),"w") as f:
for k in range(n):
f.write(f"EP_CRC[{k}]=EP{k}_CRC; EP_REC[{k}]=EP{k}_NTRIS; EP_REUSE[{k}]=EP{k}_REUSE;\n")
# Runtime CLUT mode uses an empty boot payload.
qwc=0; disp=((H-1)<<12)|(W-1)
with open(os.path.join(DATA,f"payload_sh3_{tag}.mem"),"w") as f:
f.write(f"// Ch408 runtime-CLUT empty payload (CBPs [{CBP}]). gitignored. QWC={qwc}.\n")
for _ in range(512): f.write("00000000000000000000000000000000\n")
bake.write_bios_mem(f"bios_sh3_{tag}.mem",bake.build_textured_demo_bootlet_disp(qwc,disp,FBW),
f"Ch408 runtime PSMT4 overlay bootlet DISPLAY1={W}x{H}. gitignored.")
print(f"[Ch408] PASS: {len(DRAW_IDS)} sprites -> {len(bands)} bands -> {n} epochs; max coverage={max(x[1] for x in epochs)}")
print(f"[Ch408] PSMT4 indices={sorted(set(idx))}; texture cache crc=0x{crc:08x}; palette sum=0x{sum(pal)&0xFFFFFFFF:08x}")
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv))