Files
retroDE_ps2/tools/gs_extract_sh3_clut.py
thejayman77 ec82764bef Initial commit: retroDE_ps2 — first-of-its-kind PS2 GS FPGA core (DE25-Nano / Agilex 5)
RTL (GS rasterizer, EE core stub, platform bridge, LPDDR4B path), sim regression
(272 TBs), docs, and tooling. Copyrighted PS2 content (BIOS, game code, GS dumps,
and all dump-derived textures/traces) is excluded via .gitignore and stays local.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 20:10:50 -04:00

110 lines
6.6 KiB
Python

#!/usr/bin/env python3
"""retroDE_ps2 — Ch347 authentic asset extractor: real Silent Hill 3 PSMT8 texture + real CLUT.
Codex target A: "authentic SH3 palettized texture + real palette rendered through chosen sprite geometry"
— NOT a faithful SH3 draw. We pick a CLEAN candidate whose texture is a NATIVE PSMT8 upload (linear payload,
no GS-memory model needed) and whose CLUT is a clean 256-entry PSMCT32 upload, both validated by rendering to
a coherent SH3 surface (see captures/gs/silenthill3/extracted/*.png).
Default candidate (dump 224139): PSMT8 tex tbp=13824 (128x128, native upload, 99 distinct indices) + CLUT
cbp=14282 (16x16=256 PSMCT32, read LINEARLY — csm=0 but linear order renders coherent; CSM1 bit-swap scrambles).
Primary render = DECAL (opaque): the authentic CLUT RGB is kept BYTE-FOR-BYTE; alpha (SH3's real ~0x04) is
IGNORED by the render mode, never rewritten (Codex guardrail). Emits the index texture, the CLUT, and a
software DECAL reference FB for the pre-fit pixel-diff.
Outputs (LOCAL, dump-derived -> gitignored) into sim/data/top_psmct32_raster_demo/:
sh3_tex_idx.mem 128x128 PSMT8 indices, 4 per 32-bit word (low->high byte), $readmemh
sh3_clut.mem 256 PSMCT32 ABGR CLUT entries (one per line) — authentic RGB, authentic alpha
sh3_ref.mem software DECAL reference FB 128x128 PSMCT32 (CLUT[idx] RGB, opaque) for pixel-diff
sh3_authentic.png visual confirmation (DECAL)
"""
import sys, os, glob, struct
HERE=os.path.dirname(os.path.abspath(__file__)); ROOT=os.path.normpath(os.path.join(HERE,".."))
sys.path.insert(0, os.path.join(ROOT,"tools")); import gs_texture_residency as R
DATA=os.path.join(ROOT,"sim","data","top_psmct32_raster_demo")
EXTR=os.path.join(ROOT,"captures","gs","silenthill3","extracted")
def main(argv):
dump = argv[1] if len(argv)>1 else None
if not dump:
cands = glob.glob(os.path.join(ROOT,"captures","gs","silenthill3","*224139*.gs.zst"))
if not cands: sys.exit("no SH3 dump found; pass the .gs.zst path")
dump = cands[0]
TBP = int(argv[2]) if len(argv)>2 else 13824 # native PSMT8 texture base
CBP = int(argv[3]) if len(argv)>3 else 14282 # CLUT base
W = H = 128
d,h,events,uploads,runs,vram = R.collect(dump,0)
# texture: latest NATIVE PSMT8 upload to TBP (linear row-major payload)
tu = [u for u in uploads if u["dbp"]==TBP and u["dpsm"]==0x13]
if not tu: sys.exit(f"no native PSMT8 (dpsm=0x13) upload to tbp={TBP} — not a clean candidate")
tup = max(tu, key=lambda u:u["idx"]); tex = d[tup["blob_range"][0]:tup["blob_range"][1]]
if len(tex) < W*H: sys.exit(f"texture payload {len(tex)}B < {W*H}")
idx = [tex[y*W+x] for y in range(H) for x in range(W)]
# CLUT: latest PSMCT32 256-entry upload to CBP, read LINEARLY
cu = [u for u in uploads if u["dbp"]==CBP and u["dpsm"]==0x00]
if not cu: sys.exit(f"no PSMCT32 CLUT upload to cbp={CBP}")
cup = max(cu, key=lambda u:u["idx"]); cb = d[cup["blob_range"][0]:cup["blob_range"][1]]
pal = [int.from_bytes(cb[i*4:i*4+4],"little") for i in range(256)]
# software DECAL reference = EXACTLY what the hardware DECAL emit stores: the texel = CLUT[idx], byte-for-
# byte authentic (RGB AND the real ~0x04 alpha — nothing rewritten). The pre-fit TB pixel-diffs RGB (the
# authentic-art claim); the A byte is preserved here but not the focus. The PNG forces opaque for display.
ref = [pal[i] for i in idx]
os.makedirs(DATA, exist_ok=True); os.makedirs(EXTR, exist_ok=True)
# PSMT8 indices packed 4/word (byte0=lowest x), $readmemh as 32-bit
with open(os.path.join(DATA,"sh3_tex_idx.mem"),"w") as f:
f.write(f"// Ch347 LOCAL authentic SH3 PSMT8 indices {W}x{H} (native upload tbp={TBP}). gitignored.\n")
for w in range(0, W*H, 4):
word = idx[w] | (idx[w+1]<<8) | (idx[w+2]<<16) | (idx[w+3]<<24)
f.write(f"{word:08x}\n")
with open(os.path.join(DATA,"sh3_clut.mem"),"w") as f:
f.write(f"// Ch347 LOCAL authentic SH3 CLUT 256xPSMCT32 (cbp={CBP}, linear). RGB+alpha authentic. gitignored.\n")
for p in pal: f.write(f"{p & 0xFFFFFFFF:08x}\n")
with open(os.path.join(DATA,"sh3_ref.mem"),"w") as f:
f.write(f"// Ch347 LOCAL SW DECAL reference FB {W}x{H} (CLUT[idx] RGB, opaque display). gitignored.\n")
for p in ref: f.write(f"{p & 0xFFFFFFFF:08x}\n")
try:
from PIL import Image
img=Image.new('RGBA',(W,H)); img.putdata([(p&0xFF,(p>>8)&0xFF,(p>>16)&0xFF,0xFF) for p in ref])
img.save(os.path.join(EXTR,"sh3_authentic.png"))
except Exception as e:
print("(PIL unavailable, skipped PNG:", e, ")")
# --- Ch347 (ii): a DETERMINISTIC 64x64 authentic CROP (Codex first-silicon target). ---
# The crop is a DIRECT SUBSET of the extracted indices (no resample/transform); the CLUT is unchanged.
# Deterministic rule: the 64x64 window (stride 8) with the MOST distinct indices, tie-break smallest (cy,cx)
# — guarantees real content + a reproducible, reported origin.
CW = CH = 64
best = (-1, 0, 0)
for cy in range(0, H-CH+1, 8):
for cx in range(0, W-CW+1, 8):
s = set(idx[(cy+ly)*W + (cx+lx)] for ly in range(CH) for lx in range(CW))
if len(s) > best[0]: best = (len(s), cx, cy)
_, CX, CY = best
cidx = [idx[(CY+ly)*W + (CX+lx)] for ly in range(CH) for lx in range(CW)]
cref = [pal[i] for i in cidx]
with open(os.path.join(DATA,"sh3_tex_idx64.mem"),"w") as f:
f.write(f"// Ch347 LOCAL authentic SH3 PSMT8 64x64 CROP @({CX},{CY}) of tbp={TBP} 128x128 (direct subset). gitignored.\n")
for w in range(0, CW*CH, 4):
f.write(f"{cidx[w] | (cidx[w+1]<<8) | (cidx[w+2]<<16) | (cidx[w+3]<<24):08x}\n")
with open(os.path.join(DATA,"sh3_ref64.mem"),"w") as f:
f.write(f"// Ch347 LOCAL SW DECAL reference 64x64 CROP @({CX},{CY}) (CLUT[idx] RGB). gitignored.\n")
for p in cref: f.write(f"{p & 0xFFFFFFFF:08x}\n")
try:
from PIL import Image
ci=Image.new('RGBA',(CW,CH)); ci.putdata([(p&0xFF,(p>>8)&0xFF,(p>>16)&0xFF,0xFF) for p in cref])
ci.save(os.path.join(EXTR,"sh3_authentic64.png"))
except Exception: pass
print(f"[Ch347] SH3 authentic asset: tex tbp={TBP} {W}x{H} ({len(set(idx))} distinct indices), CLUT cbp={CBP} "
f"({len(set(pal))} colors). DECAL reference emitted.")
print(f"[Ch347] 64x64 CROP @({CX},{CY}) [deterministic max-distinct window]: {len(set(cidx))} distinct indices, "
f"{len(set(cref))} colors -> sh3_tex_idx64.mem, sh3_ref64.mem")
print(f"[Ch347] -> {DATA}/sh3_tex_idx*.mem, sh3_clut.mem, sh3_ref*.mem (+ {EXTR}/sh3_authentic*.png) — all LOCAL")
if __name__ == "__main__":
main(sys.argv)