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>
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
#!/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))
|
||||
Reference in New Issue
Block a user