Files
retroDE_ps2/tools/gs_make_sh3_ct32_darken_fixture.py
T
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

155 lines
6.4 KiB
Python

#!/usr/bin/env python3
"""Emit SH3's two authentic PSMCT32 full-frame darkening sprites.
The source frame builds a 256x256 direct-color intermediate at TBP 11264,
then draws it over the 512-wide display as two ABE sprites (indices 196167
and 196177). The production texture cache is exactly 256 KiB, so the
intermediate can be reconstructed from GS local memory, linearized, and
replayed without first implementing the two off-screen render targets.
"""
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 = (196167, 196177)
W, H, FBW = 640, 480, 10
TW = TH = 256
NEW_TBP = 1024
TEX_BYTES = TW * TH * 4
N_BEATS = TEX_BYTES // 32
STG_WORDS = 2048
CAPACITY = 14500
PERSP_FRAC = bake.PERSP_FRAC
PSCALE = 4
def wmem(name, words, banner):
with open(os.path.join(DATA, name), "w") as f:
f.write(f"// {banner}\n")
for word in words:
f.write(f"{word & 0xffffffff:08x}\n")
def vertex_words(v):
s_fp = round(v["s"] * TW * (1 << PERSP_FRAC) * PSCALE)
t_fp = round(v["t"] * TH * (1 << PERSP_FRAC) * PSCALE)
q_fp = round(v["q"] * (1 << PERSP_FRAC) * PSCALE)
if not (-(1 << 23) <= s_fp < (1 << 23) and -(1 << 23) <= t_fp < (1 << 23)):
raise ValueError(f"S/T overflow: {s_fp},{t_fp}")
x = max(0, min(W - 1, round(v["x"])))
y = max(0, min(H - 1, round(v["y"])))
rgba = v["rgba"]
# Preserve the authentic RGBAQ alpha. bake.rgbaq_with_q() is a legacy
# opaque helper (A=0xff), but these MODULATE+TCC passes depend on the
# captured 0x50 vertex alpha: As = At*Av/128.
return [((q_fp & 0xffffffff) << 32) | (rgba & 0xffffffff),
bake.st_data(s_fp & 0xffffff, t_fp & 0xffffff),
bake.xyz2_dataz(x, y, v["z"])]
def sprite_bands(draw):
a, b = draw["verts"]
if not (a.get("kick", True) and b.get("kick", True)):
return []
y0 = max(0.0, a["y"])
y1 = min(float(H), b["y"])
if y1 <= y0:
return []
out = []
for lo in range(int(y0), int(y1), 16):
hi = min(int(y1), lo + 16)
fa = (lo - a["y"]) / (b["y"] - a["y"])
fb = (hi - a["y"]) / (b["y"] - a["y"])
top = dict(a, y=float(lo), t=a["t"] + fa * (b["t"] - a["t"]),
q=a["q"] + fa * (b["q"] - a["q"]))
bot = dict(b, y=float(hi), t=a["t"] + fb * (b["t"] - a["t"]),
q=a["q"] + fb * (b["q"] - a["q"]))
tl = dict(top, x=a["x"], s=a["s"])
tr = dict(top, x=b["x"], s=b["s"])
bl = dict(bot, x=a["x"], s=a["s"])
br = dict(bot, x=b["x"], s=b["s"])
tris = ((tl, tr, bl), (tr, br, bl))
coverage = max(0, round(b["x"]) - round(a["x"])) * (hi - lo)
out.append((tris, coverage))
return out
def main(argv):
tag = argv[argv.index("--tag") + 1] if "--tag" in argv else "zsrt139f11d"
dump = next((x for x in argv[1:] if x.endswith(".gs.zst")), None)
if dump is None:
dump = next(iter(glob.glob(os.path.join(ROOT, "captures", "gs", "silenthill3", "*224139*.gs.zst"))), None)
if dump is None:
raise SystemExit("no 224139 dump found")
got, _ = MD.load_draws(dump, DRAW_IDS)
if sorted(got) != list(DRAW_IDS):
raise SystemExit(f"missing draws: {sorted(set(DRAW_IDS) - set(got))}")
ref = got[DRAW_IDS[0]]["state"]
for idx in DRAW_IDS:
st = got[idx]["state"]
t0, pr = st["tex0"], st["prim"]
if (t0["tbp"], t0["tbw"], t0["psm"], t0["tw"], t0["th"], t0["tfx"]) != (11264, 4, 0, 256, 256, 0):
raise SystemExit(f"idx{idx}: unexpected TEX0 {t0}")
if (pr["type"], pr["tme"], pr["fst"], pr["abe"]) != (6, 1, 0, 1):
raise SystemExit(f"idx{idx}: unexpected PRIM {pr}")
for key in ("frame", "test", "zbuf", "alpha", "clamp"):
if st[key] != ref[key]:
raise SystemExit(f"idx{idx}: {key} differs")
mem, *_ = RC.build_localmem_to(dump, DRAW_IDS[0])
tex = [mem.read_ct32_word(11264, 4, x, y) for y in range(TH) for x in range(TW)]
crc = sum(tex) & 0xffffffff
wmem(f"sh3_{tag}0_tex_lpddr.mem", tex,
f"Ch414 authentic 256x256 PSMCT32 intermediate, linearized; crc=0x{crc:08x}")
pieces = []
for idx in DRAW_IDS:
pieces.extend(sprite_bands(got[idx]))
epochs, cur, cov = [], [], 0
for tris, npx in pieces:
if cur and cov + npx > CAPACITY:
epochs.append((cur, cov)); cur, cov = [], 0
cur.extend(tris); cov += npx
if cur:
epochs.append((cur, cov))
tex0 = bake.tex0_pack(NEW_TBP, 4, psm=0, tw=8, th=8, tfx=0)
prim = 3 | (1 << 4) | (1 << 6) # independent TRIANGLE records, TME, ABE
# Preserve authentic RGB-only FBMSK while changing only FBP/FBW.
frame = bake.frame_1_psmct32(FBW) | (ref["frame"] & 0xffffffff00000000)
for k, (tris, npx) in enumerate(epochs):
stg = [len(tris) | (1 << 32) | (1 << 34), frame, ref["alpha"],
ref["test"], ref["zbuf"], tex0, ref["clamp"], prim]
for tri in tris:
for v in tri:
stg.extend(vertex_words(v))
if len(stg) > STG_WORDS:
raise SystemExit(f"epoch{k}: staging overflow {len(stg)}")
bake.write_feeder_stg_mem(f"feeder_sh3_{tag}{k}.mem", stg,
f"Ch414 authentic CT32 darken epoch{k}, {len(tris)} tris, coverage {npx}", total=STG_WORDS)
with open(os.path.join(DATA, f"sh3_{tag}_epochs.txt"), "w") as f:
f.write("# Ch414 authentic PSMCT32 intermediate darkening sprites\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 {len(epochs)} fbpxw {W} fbh {H} fbwords {W*H} lpddr_tex 0x200000 tex_words {len(tex)} n_beats {N_BEATS}\n")
for k, (tris, _npx) in enumerate(epochs):
f.write(f"{k} {DRAW_IDS[0]} 11264 0 sh3_{tag}0_tex_lpddr.mem 0x200000 {TEX_BYTES} 0x{crc:08x} "
f"feeder_sh3_{tag}{k}.mem {8 + 9*len(tris)} {len(tris)} {0 if k == 0 else 1} - 0x00000000\n")
print(f"[Ch414] PASS: {len(DRAW_IDS)} CT32 sprites -> {len(pieces)} bands -> {len(epochs)} epochs; "
f"max coverage={max(x[1] for x in epochs)} crc=0x{crc:08x}")
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv))