ec82764bef
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>
139 lines
7.0 KiB
Python
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))
|