#!/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 [--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< 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))