Files
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

139 lines
7.0 KiB
Python

#!/usr/bin/env python3
"""retroDE_ps2 — Ch341 Brick 1: host-side texture state analysis + extraction from a GS dump.
Decodes the texture UPLOADS (BITBLTBUF / TRXREG / TRXPOS / TRXDIR + IMAGE) and TEX0 BINDS in a dump,
matches the dominant textured-TRIANGLE primitives to the texture they sample, and finds the EARLIEST
contiguous textured-triangle segment that uses a SINGLE TEX0 bind (the no-RTL Ch341 v1 target: one
scene-level TEX0 + per-vertex real UV). Reports aggregate facts (committable). With --extract it
writes the matched texture blob + a generated-fixture descriptor LOCALLY (gitignored, per provenance).
Usage:
gs_texture.py <dump.gs[.xz|.zst]> [--report out.txt] [--extract outdir]
"""
import sys, os, json
sys.path.insert(0, os.path.dirname(__file__))
import gs_parse, gs_translate
PSM = {0x00:"PSMCT32",0x01:"PSMCT24",0x02:"PSMCT16",0x0A:"PSMCT16S",0x13:"PSMT8",0x14:"PSMT4",
0x1B:"PSMT8H",0x24:"PSMT4HL",0x2C:"PSMT4HH",0x30:"PSMZ32",0x31:"PSMZ24",0x32:"PSMZ16",0x3A:"PSMZ16S"}
BPP = {0x00:32,0x01:24,0x02:16,0x0A:16,0x13:8,0x14:4,0x1B:8,0x24:4,0x2C:4}
def dec_bitbltbuf(v): return dict(SBP=v&0x3FFF,SBW=(v>>16)&0x3F,SPSM=(v>>24)&0x3F,DBP=(v>>32)&0x3FFF,DBW=(v>>48)&0x3F,DPSM=(v>>56)&0x3F)
def dec_trxreg(v): return dict(RRW=v&0xFFF, RRH=(v>>32)&0xFFF)
def dec_trxpos(v): return dict(DSAX=v&0x7FF, DSAY=(v>>16)&0x7FF)
def dec_tex0(v): return dict(TBP0=v&0x3FFF,TBW=(v>>14)&0x3F,PSM=(v>>20)&0x3F,TW=(v>>26)&0xF,TH=(v>>30)&0xF,TCC=(v>>34)&1,TFX=(v>>35)&3)
def analyze(path):
d = gs_parse.read_dump_bytes(path)
h, events = gs_parse.parse_dump(path)
# --- pass 1: texture uploads (transfer state machine) ---
st = dict(bitbltbuf=0, trxreg=0, trxpos=0)
uploads = [] # each: dict(dbp,dbw,dpsm,w,h,bytes,data_off,event_idx)
for e in events:
if e.kind=="GSREG":
if e.reg=="BITBLTBUF": st["bitbltbuf"]=e.value
elif e.reg=="TRXREG": st["trxreg"]=e.value
elif e.reg=="TRXPOS": st["trxpos"]=e.value
elif e.kind=="IMAGE":
bb=dec_bitbltbuf(st["bitbltbuf"]); rr=dec_trxreg(st["trxreg"]); tp=dec_trxpos(st["trxpos"])
uploads.append(dict(dbp=bb["DBP"],dbw=bb["DBW"],dpsm=bb["DPSM"],w=rr["RRW"],h=rr["RRH"],
dsax=tp["DSAX"],dsay=tp["DSAY"],bytes=e.info.get("bytes",0),
data_off=e.byte_off,event_idx=e.idx))
# --- pass 2: triangles + their active TEX0 ---
prims,_ = gs_translate.reconstruct_prims(events)
# track TEX0 active at each triangle by re-walking (reconstruct doesn't keep tex0)
tex0_at = {}
cur_tex0 = 0
pi = 0
# rebuild active TEX0 per primitive index, matching reconstruct's order
# (simple: re-run the kick model tracking tex0)
cur_tex0=0; vcount=0; ptype=7; idxmap=[]
for e in events:
if e.kind=="GSREG":
if e.reg=="TEX0_1": cur_tex0=e.value
elif e.reg=="PRIM": ptype=e.value&7; vcount=0
elif e.reg in ("XYZ2","XYZ3","XYZF2","XYZF3"):
need={0:1,1:2,2:2,3:3,4:3,5:3,6:2}.get(ptype,99)
kick = e.reg in ("XYZ2","XYZF2")
vcount+=1
if kick and vcount>=need:
idxmap.append(cur_tex0)
if ptype in (3,6): vcount=0
tris = [(p, idxmap[i] if i < len(idxmap) else 0) for i,p in enumerate(prims) if p.type==3]
# --- earliest contiguous textured-triangle segment with a SINGLE TEX0 ---
seg=[]; seg_tex0=None
for (p,t) in tris:
c,_=gs_translate.classify(p) # textured tri -> unsupported in v0 envelope, but here we WANT textured
if not p.tme:
if seg: break
else: continue
if seg_tex0 is None: seg_tex0=t; seg=[p]
elif t==seg_tex0: seg.append(p)
else: break # crossed a TEX0 bind -> stop (single-TEX0 segment)
return h, uploads, tris, seg, seg_tex0
def match_upload(uploads, tex0):
tx=dec_tex0(tex0)
for u in uploads:
if u["dbp"]==tx["TBP0"] and u["dpsm"]==tx["PSM"]:
return u
# fall back: TBP match only
for u in uploads:
if u["dbp"]==tx["TBP0"]:
return u
return None
def main(argv):
if len(argv)<2: print(__doc__); return 2
path=argv[1]
def opt(n,d=None): return argv[argv.index(n)+1] if n in argv else d
h, uploads, tris, seg, seg_tex0 = analyze(path)
R=[]
R.append(f"# Ch341 Brick 1 texture analysis (source {os.path.basename(path)} serial={h.serial!r}; aggregate facts only)")
R.append(f"texture uploads: {len(uploads)}")
seen=set()
for u in uploads:
k=(u["dbp"],u["dpsm"],u["w"],u["h"])
if k in seen: continue
seen.add(k)
R.append(f" TBP={u['dbp']} DBW={u['dbw']} PSM={PSM.get(u['dpsm'],hex(u['dpsm']))} "
f"{u['w']}x{u['h']} bytes={u['bytes']} (expect {u['w']*u['h']*BPP.get(u['dpsm'],0)//8})")
R.append(f"textured triangles: {sum(1 for p,_ in tris if p.tme)} / {len(tris)} total triangles")
if seg:
tx=dec_tex0(seg_tex0)
R.append("")
R.append(f"EARLIEST single-TEX0 textured-tri segment: {len(seg)} triangles")
R.append(f" TEX0: TBP0={tx['TBP0']} TBW={tx['TBW']} PSM={PSM.get(tx['PSM'],hex(tx['PSM']))} "
f"TW={tx['TW']}({1<<tx['TW']}px) TH={tx['TH']}({1<<tx['TH']}px) TFX={tx['TFX']}")
u=match_upload(uploads, seg_tex0)
if u:
R.append(f" -> matched upload: TBP={u['dbp']} {u['w']}x{u['h']} {PSM.get(u['dpsm'],hex(u['dpsm']))} "
f"bytes={u['bytes']} data@0x{u['data_off']:x}")
R.append(f" VERDICT: single scene-level TEX0 + per-vertex UV — NO RTL feeder change needed for v1.")
else:
R.append(f" !! no matching upload found for TBP0={tx['TBP0']} — texture may be CLUT/region or uploaded elsewhere.")
else:
R.append("NO single-TEX0 textured-triangle segment found (every textured run crosses TEX0 binds).")
report="\n".join(R)+"\n"
print(report)
if opt("--report"):
open(opt("--report"),"w").write(report); print(f"[wrote report -> {opt('--report')}]")
outdir=opt("--extract")
if outdir and seg:
u=match_upload(uploads, seg_tex0)
if u:
os.makedirs(outdir, exist_ok=True)
d = gs_parse.read_dump_bytes(path)
blob = d[u["data_off"]:u["data_off"]+u["bytes"]]
bp=os.path.join(outdir,"tex0_blob.bin"); open(bp,"wb").write(blob)
desc=dict(schema=1, tbp0=dec_tex0(seg_tex0)["TBP0"], tbw=dec_tex0(seg_tex0)["TBW"],
psm=u["dpsm"], psm_name=PSM.get(u["dpsm"]), w=u["w"], h=u["h"], bytes=u["bytes"],
tw=dec_tex0(seg_tex0)["TW"], th=dec_tex0(seg_tex0)["TH"], tfx=dec_tex0(seg_tex0)["TFX"],
provenance="cubes_demo (MIT, glampert/ps2-homebrew) — LOCAL only")
open(os.path.join(outdir,"tex0_desc.json"),"w").write(json.dumps(desc,indent=2))
print(f"[extracted {len(blob)}-byte texture blob + descriptor -> {outdir}/ (LOCAL, gitignored)]")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))