Files
retroDE_ps2/tools/gs_census_sprites.py
T
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

95 lines
4.7 KiB
Python

#!/usr/bin/env python3
"""retroDE_ps2 — SPRITE census (Ch344 sprite-ingestion scoping).
Consumes gs_parse.parse_dump()'s event stream and characterises every SPRITE primitive in a .gs dump:
prim-type split, per-sprite TEX0 PSM, TME/ABE/FST, the ALPHA blend equation, TEX1 mag/min filter, and an
XYOFFSET-corrected size histogram. Output is AGGREGATE (counts/histograms) only — no copyrighted pixel
content — so the report is committable per captures/gs/.gitignore policy. This is census/scoping ONLY;
it renders nothing and asserts nothing.
Usage: gs_census_sprites.py <dump.gs[.xz|.zst]> [--report out.txt]
"""
import sys, os
from collections import Counter
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from gs_parse import parse_dump
PSM = {0x00:"PSMCT32",0x01:"PSMCT24",0x02:"PSMCT16",0x0A:"PSMCT16S",0x13:"PSMT8",0x14:"PSMT4",
0x1B:"PSMT8H",0x24:"PSMT4HL",0x2C:"PSMT4HH"}
PRIMT = {0:"POINT",1:"LINE",2:"LINE_STRIP",3:"TRI",4:"TRISTRIP",5:"TRIFAN",6:"SPRITE",7:"INVALID"}
ABCD = {0:"Cs",1:"Cd",2:"0"}; C_SEL = {0:"As",1:"Ad",2:"FIX"}
FILT = {0:"NEAREST",1:"LINEAR",2:"N_MIPN",3:"L_MIPN",4:"N_MIPL",5:"L_MIPL"}
def dec_tex0(v):
return (v>>20)&0x3F, 1<<((v>>26)&0xF), 1<<((v>>30)&0xF), v&0x3FFF, (v>>35)&3, (v>>34)&1
def dec_alpha(v):
a=v&3; b=(v>>2)&3; c=(v>>4)&3; d=(v>>6)&3; fix=(v>>32)&0xFF
return f"({ABCD[a]}-{ABCD[b]})*{C_SEL[c]}>>7+{ABCD[d]}" + (f" FIX={fix}" if c==2 else "")
def census(path):
h, events = parse_dump(path)
cur = {"type":None,"tme":0,"abe":0,"fst":0}
tex0 = {1:None, 2:None}; xyoff = {1:(0,0), 2:(0,0)}; tex1 = {1:None, 2:None}
alpha = {1:None, 2:None}; ctxt = 0
kicks = {}; xyz = []
spr = {"n":0, "psm":Counter(), "abe":Counter(), "fst":Counter(), "tme":Counter(),
"alpha":Counter(), "magfilt":Counter(), "sizes":Counter()}
for e in events:
if e.kind != "GSREG": continue
a, v = e.addr, e.value
if a == 0x00: # PRIM
cur = {"type":v&7,"tme":(v>>4)&1,"abe":(v>>6)&1,"fst":(v>>8)&1}; ctxt=(v>>9)&1; xyz=[]
elif a in (0x06,0x07): tex0[1 if a==0x06 else 2] = dec_tex0(v)
elif a in (0x14,0x15): tex1[1 if a==0x14 else 2] = (v>>5)&7 # MMAG bit5 -> mag filter (1bit here: 0/1)
elif a in (0x18,0x19): xyoff[1 if a==0x18 else 2] = (v&0xFFFF, (v>>32)&0xFFFF)
elif a in (0x42,0x43): alpha[1 if a==0x42 else 2] = v
elif a == 0x05: # XYZ2 vertex kick
t = cur["type"]
if t is None: continue
kicks[t] = kicks.get(t,0)+1
if t == 6:
xyz.append(v)
if len(xyz) == 2:
cx = 1 if ctxt==0 else 2
ox, oy = xyoff[cx]
x0=(xyz[0]&0xFFFF)-ox; y0=((xyz[0]>>16)&0xFFFF)-oy
x1=(xyz[1]&0xFFFF)-ox; y1=((xyz[1]>>16)&0xFFFF)-oy
w=abs(x1-x0)//16; ht=abs(y1-y0)//16 # 12.4 fixed -> pixels
spr["n"]+=1; spr["sizes"][(w,ht)]+=1
spr["abe"][cur["abe"]]+=1; spr["fst"][cur["fst"]]+=1; spr["tme"][cur["tme"]]+=1
tx = tex0[cx]
spr["psm"][("(untextured)" if not cur["tme"] else
"(no TEX0)" if tx is None else PSM.get(tx[0],f"0x{tx[0]:02x}"))] += 1
if cur["abe"]:
al = alpha[cx]
spr["alpha"][dec_alpha(al) if al is not None else "(no ALPHA set)"] += 1
if cur["tme"]:
f1 = tex1[cx]
spr["magfilt"][("NEAREST" if f1==0 else "LINEAR" if f1==1 else "(unset)") ] += 1
xyz=[]
return h, kicks, spr
def fmt(h, kicks, spr):
L = []
L.append(f"SPRITE CENSUS — serial={h.serial!r} crc=0x{h.crc:08x}")
L.append(f"prim-kicks by type: {dict((PRIMT[k],v) for k,v in sorted(kicks.items(),key=lambda x:-x[1]))}")
L.append(f"SPRITES: {spr['n']} rectangles")
L.append(f" TEX0 PSM : {dict(spr['psm'].most_common())}")
L.append(f" TME(textured) : {dict(spr['tme'])} FST(0=STQ,1=UV): {dict(spr['fst'])}")
L.append(f" ABE(alpha) : {dict(spr['abe'])}")
L.append(f" ALPHA eqn : {dict(spr['alpha'].most_common())}")
L.append(f" mag filter : {dict(spr['magfilt'].most_common())}")
L.append(f" top sizes WxH : {[(f'{w}x{ht}',n) for (w,ht),n in spr['sizes'].most_common(8)]}")
return "\n".join(L)
if __name__ == "__main__":
if len(sys.argv) < 2:
print(__doc__); sys.exit(2)
h, kicks, spr = census(sys.argv[1])
out = fmt(h, kicks, spr)
print(out)
if "--report" in sys.argv:
p = sys.argv[sys.argv.index("--report")+1]
with open(p, "w") as f: f.write(out + "\n")
print(f"\nwrote report -> {p}")