ba74bbd5aa
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>
155 lines
6.4 KiB
Python
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))
|