#!/usr/bin/env python3 """retroDE_ps2 — Ch341 textured-triangle translator (declared affine ST/Q surrogate). Extracts the earliest contiguous single-TEX0 textured-triangle subsegment that FITS staging (<=27 tris) from a GS dump, derives faithful per-vertex texel coords u=S/Q, v=T/Q (FST=0 perspective ST), applies the declared 256->64 downscale, and emits a ps2_feeder textured scene (tex0 + tritex) — BUT ONLY if the honesty gate passes: the affine interpolation of vertex (S/Q,T/Q) must stay within MAX_ERR texels of the true perspective interpolation over the triangle. Otherwise it FAILS CLOSED (Ch342 = real ST/Q). This is NOT faithful perspective-correct GS texturing. It is authentic cube geometry + authentic extracted texels through the affine-UV feeder, with a DECLARED affine substitute for perspective ST/Q on a tiny-span segment. Sprites stay unsupported. Reports are aggregate; scene text is dump-derived. Usage: gs_translate_tex.py [--tbp 64] [--dst 64] [--maxtri 27] [--scene out.txt] [--report r.txt] """ import sys, os, struct sys.path.insert(0, os.path.dirname(__file__)) import gs_parse MAX_ERR = 0.5 # texels, on the 64x64 fixture def f32(bits): return struct.unpack(">b)&1 def extract(events, dst, maxtri): """Walk events; return (segment tris, meta). Each tri: dict(scr=[(x,y)*3], z, stq=[(S,T,Q)*3], rgb).""" st_S=st_T=0.0; q=1.0; rgb=(255,255,255); tex0=0; fst=0; ptype=7; xyoff=0; clamp=0 prim_val=0; test_val=0 uploads_to={} # TBP -> count of IMAGE uploads seen so far (to detect re-upload crossing) bitbltbuf=0 vbuf=[] # pending verts tris=[] # each: dict seg=[]; seg_tex0=None; seg_clamp=None; seg_prim=None; seg_test=None; started=False; stop_reason=None first=None; last=None for e in events: if e.kind=="IMAGE": dbp=(bitbltbuf>>32)&0x3FFF uploads_to[dbp]=uploads_to.get(dbp,0)+1 if started and seg_tex0 is not None and ((seg_tex0&0x3FFF)==dbp): stop_reason=f"re-upload to bound TBP {dbp} at event #{e.idx}"; break continue if e.kind!="GSREG": continue r=e.reg; v=e.value if r=="BITBLTBUF": bitbltbuf=v elif r=="PRIM": ptype=v&7; fst=bit(v,8); prim_val=v; vbuf=[] elif r=="TEST_1": test_val=v elif r=="PRMODE": fst=bit(v,8) elif r=="ST": st_S=f32(v&0xFFFFFFFF); st_T=f32((v>>32)&0xFFFFFFFF) if "q_stq" in e.info: q=f32(e.info["q_stq"]) # PACKED ST routes Q -> RGBAQ.Q elif r=="RGBAQ": rgb=(v&0xFF,(v>>8)&0xFF,(v>>16)&0xFF); q=f32((v>>32)&0xFFFFFFFF) elif r=="TEX0_1": tex0=v elif r in ("CLAMP_1",): clamp=v elif r in ("XYOFFSET_1",): xyoff=v elif r in ("XYZ2","XYZ3","XYZF2","XYZF3"): x=(v&0xFFFF)/16.0 - (xyoff&0xFFFF)/16.0 y=((v>>16)&0xFFFF)/16.0 - ((xyoff>>16)&0xFFFF)/16.0 kick = e.reg in ("XYZ2","XYZF2") vbuf.append(dict(x=x,y=y,z=(v>>32)&0xFFFFFFFF,S=st_S,T=st_T,Q=q,rgb=rgb,tex0=tex0,fst=fst,clamp=clamp)) if kick and ptype==3 and len(vbuf)>=3: t=vbuf[-3:]; vbuf=[] if not all(vv["fst"]==0 for vv in t): # only FST=0 textured tris in this rung if started: stop_reason="prim left FST=0 ST mode"; break continue tt = t[0]["tex0"] if not started: seg_tex0=tt; seg_clamp=t[0]["clamp"]; seg_prim=prim_val; seg_test=test_val; started=True; first=e.idx if tt!=seg_tex0: stop_reason=f"TEX0 changed at event #{e.idx}"; break if t[0]["clamp"]!=seg_clamp: stop_reason=f"CLAMP changed at event #{e.idx}"; break seg.append(t); last=e.idx if len(seg)>=maxtri: stop_reason=f"staging cap ({maxtri} tris)"; break return seg, dict(tex0=seg_tex0, clamp=seg_clamp, prim=seg_prim, test=seg_test, first=first, last=last, stop=stop_reason) def reconstruct_all_textured(events): """Every FST=0 textured triangle (3 verts) with its active tex0/clamp + texture-upload epoch (uploads-to-its-TBP seen so far). For the closeout all-window scan — NOT the mechanical earliest selection (extract() above is that). Documentation only, per Codex.""" st_S=st_T=0.0; q=1.0; rgb=(255,255,255); tex0=0; fst=0; ptype=7; xyoff=0; clamp=0; bitbltbuf=0 epoch={}; vbuf=[]; out=[] for e in events: if e.kind=="IMAGE": dbp=(bitbltbuf>>32)&0x3FFF; epoch[dbp]=epoch.get(dbp,0)+1; continue if e.kind!="GSREG": continue r=e.reg; v=e.value if r=="BITBLTBUF": bitbltbuf=v elif r=="PRIM": ptype=v&7; fst=(v>>8)&1; vbuf=[] elif r=="PRMODE": fst=(v>>8)&1 elif r=="ST": st_S=f32(v&0xFFFFFFFF); st_T=f32((v>>32)&0xFFFFFFFF) if "q_stq" in e.info: q=f32(e.info["q_stq"]) # PACKED ST routes Q -> RGBAQ.Q elif r=="RGBAQ": rgb=(v&0xFF,(v>>8)&0xFF,(v>>16)&0xFF); q=f32((v>>32)&0xFFFFFFFF) elif r=="TEX0_1": tex0=v elif r=="CLAMP_1": clamp=v elif r=="XYOFFSET_1": xyoff=v elif r in ("XYZ2","XYZ3","XYZF2","XYZF3"): kick=e.reg in ("XYZ2","XYZF2") vbuf.append(dict(S=st_S,T=st_T,Q=q)) if kick and ptype==3 and len(vbuf)>=3: t=vbuf[-3:]; vbuf=[] if fst==0: out.append(dict(v=t, tex0=tex0, clamp=clamp, epoch=epoch.get(tex0&0x3FFF,0))) return out def scan_windows(tris, dst, maxtri): """Slide a <=maxtri window from every start; a window breaks on tex0/clamp/epoch change. Returns (n_windows, n_pass<=0.5, min_window_error).""" n=len(tris); npass=0; best=1e9; total=0 for i in range(n): win=[tris[i]] for j in range(i+1, min(n, i+maxtri)): a,b=tris[i],tris[j] if a["tex0"]!=b["tex0"] or a["clamp"]!=b["clamp"] or a["epoch"]!=b["epoch"]: break win.append(tris[j]) me=max(tri_error(t["v"], dst)[0] for t in win) total+=1; best=min(best,me) if me<=MAX_ERR: npass+=1 return total, npass, best def bary_samples(): pts=[(1/3,1/3,1/3)] for a in (0.25,0.5,0.75): pts += [(a,(1-a)/2,(1-a)/2),((1-a)/2,a,(1-a)/2),((1-a)/2,(1-a)/2,a)] return pts def tri_error(t, dst): """max |affine - perspective| texel error over the triangle, for u and v (dst-sized texture).""" SQ=[(v["S"]/v["Q"] if v["Q"] else 0.0, v["T"]/v["Q"] if v["Q"] else 0.0) for v in t] uv=[(sq[0]*dst, sq[1]*dst) for sq in SQ] me=0.0 for (b0,b1,b2) in bary_samples(): Sb=b0*t[0]["S"]+b1*t[1]["S"]+b2*t[2]["S"]; Tb=b0*t[0]["T"]+b1*t[1]["T"]+b2*t[2]["T"] Qb=b0*t[0]["Q"]+b1*t[1]["Q"]+b2*t[2]["Q"] if Qb==0: continue up=(Sb/Qb)*dst; vp=(Tb/Qb)*dst ua=b0*uv[0][0]+b1*uv[1][0]+b2*uv[2][0]; va=b0*uv[0][1]+b1*uv[1][1]+b2*uv[2][1] me=max(me, abs(up-ua), abs(vp-va)) return me, uv 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 tbp=int(opt("--tbp","64")); dst=int(opt("--dst","64")); maxtri=int(opt("--maxtri","27")) h, events = gs_parse.parse_dump(path) fst0 = sum(1 for e in events if e.kind=="GSREG" and e.reg=="PRIM" and ((e.value>>8)&1)==0) seg, meta = extract(events, dst, maxtri) R=[f"# Ch341 textured-triangle translation (source {os.path.basename(path)}; aggregate facts + dump-derived scene)"] R.append(f"FST=0 (perspective ST) PRIM submissions: {fst0}") if not seg: R.append(f"NO textured-tri segment selected (stop: {meta['stop']}).") print("\n".join(R)); return 0 # Ch342 — FAITHFUL perspective ST/Q emit (no affine, no error gate; the Ch301 path is exact). # Packs the Ch301 fixed-point contract: S_fp=round(S*4096), T_fp=round(T*4096), Q_fp=round(Q*4096) # (24-bit FRAC=12). The 256->64 downscale is in the TEXTURE (TW=6); ST/Q stay normalized — the GS # computes texel=(S/Q)*2^TW=(S/Q)*64. Fails closed on Q<=0 or fixed-point overflow. if "--perspective" in argv: xs=[v["x"] for t in seg for v in t]; ys=[v["y"] for t in seg for v in t] x0,x1,y0,y1=min(xs),max(xs),min(ys),max(ys) FB=64; sc=min((FB-2)/max(1.0,x1-x0),(FB-2)/max(1.0,y1-y0)) fx=lambda x:max(0,min(FB-1,1+int((x-x0)*sc))); fy=lambda y:max(0,min(FB-1,1+int((y-y0)*sc))) L=[f"# Ch342 authentic cube subsegment ({len(seg)} tris) — FAITHFUL perspective ST/Q (Ch301 fixed-point, FRAC=12)", f"# events #{meta['first']}..#{meta['last']}; screen ({x0:.0f},{y0:.0f})..({x1:.0f},{y1:.0f}) viewport-fit -> {FB}x{FB}; texture 256->{dst}", f"tex0 {tbp} 1 6 6 0", "persp"] for t in seg: a=[] for v in t: if v["Q"]<=0: R.append(f"GATE FAILED: Q<=0 ({v['Q']}) at event #{meta['first']} — fail closed."); print("\n".join(R)); return 0 sfp=round(v["S"]*4096); tfp=round(v["T"]*4096); qfp=round(v["Q"]*4096) if not (0<=sfp<=0xFFFFFF and 0<=tfp<=0xFFFFFF and 0 fail closed."); print("\n".join(R)); return 0 a += [fx(v["x"]), fy(v["y"]), sfp, tfp, qfp] (r,g,b)=t[-1]["rgb"]; z=t[-1]["z"] L.append("persptri "+" ".join(map(str,a+[z,r,g,b]))) L.append("go") Qs=[v["Q"] for t in seg for v in t] pv=meta["prim"] or 0; tv=meta["test"] or 0 R.append(f"segment PRIM: type={pv&7}(TRI=3) IIP={(pv>>3)&1} TME={(pv>>4)&1} FGE={(pv>>5)&1} ABE={(pv>>6)&1} FST={(pv>>8)&1}" f" TEST_1: ZTE={(tv>>16)&1} ZTST={(tv>>17)&3}(GEQ=2)") if ((pv>>6)&1): R.append("WARN: segment ABE=1 -> routes to the combined-TAZ path (perspective there is a known follow-on bug), NOT the proven S1 path. Do NOT flip ABE.") R.append("S1 perspective path honors TME+FST=0 + ZTE/ZTST GEQUAL; cube segment is ABE=0 (S1 path).") R.append(f"FAITHFUL PERSPECTIVE: {len(seg)} tris, TEX0->TBP={tbp} TW=6 TH=6 TFX=0; S_fp/T_fp/Q_fp=round(*4096); Q span {min(Qs):.4f}..{max(Qs):.4f}") R.append(f"staging words: {7+9*len(seg)} (perspective format word0[32]=1, no rects)") print("\n".join(R)+"\n") sp=opt("--scene") if sp: open(sp,"w").write("\n".join(L)+"\n"); print(f"[wrote {len(seg)}-tri FAITHFUL perspective scene -> {sp}]") if opt("--report"): open(opt("--report"),"w").write("\n".join(R)+"\n") return 0 # span + Q + error Qs=[v["Q"] for t in seg for v in t]; Ss=[v["S"] for t in seg for v in t]; Ts=[v["T"] for t in seg for v in t] maxerr=0.0 for t in seg: me,_=tri_error(t,dst); maxerr=max(maxerr,me) tx=tbp; tex0=meta["tex0"] R.append(f"selected segment: {len(seg)} triangles, events #{meta['first']}..#{meta['last']}, stop after: {meta['stop']}") R.append(f"active TEX0 (orig): TBP0={tex0&0x3FFF} TW={(tex0>>26)&0xF} TH={(tex0>>30)&0xF} TFX={(tex0>>35)&3}") R.append(f"relocated TEX0 (fixture): TBP0={tbp} TBW=1 TW=6 TH=6 TFX=0 (downscale 256->{dst}, UV scale /{256//dst})") R.append(f"Q span: {min(Qs):.4f}..{max(Qs):.4f} S span {min(Ss):.4f}..{max(Ss):.4f} T span {min(Ts):.4f}..{max(Ts):.4f}") R.append(f"perspective-vs-affine max error: {maxerr:.4f} texels (threshold {MAX_ERR})") ok = maxerr <= MAX_ERR staging_words = 7 + 9*len(seg) R.append(f"staging words: {staging_words} (<=256: {'ok' if staging_words<=256 else 'OVERFLOW'})") if not ok: R.append("GATE FAILED: affine ST/Q surrogate exceeds error threshold -> FAIL CLOSED. Ch342 = real ST/Q through the feeder.") else: R.append(f"DECLARED APPROXIMATION: perspective ST/Q rendered as affine UV; max_error={maxerr:.4f} texels (NOT faithful perspective-correct texturing).") print("\n".join(R)+"\n") if opt("--report"): # write the report in BOTH branches (the failure report is the useful one) open(opt("--report"),"w").write("\n".join(R)+"\n"); print(f"[wrote report -> {opt('--report')}]") if not ok: return 0 sp=opt("--scene") if sp: # viewport-fit the segment's screen bbox into [1,dst-2] (declared, like Ch340) xs=[v["x"] for t in seg for v in t]; ys=[v["y"] for t in seg for v in t] x0,x1,y0,y1=min(xs),max(xs),min(ys),max(ys) FB=64; s=min((FB-2)/max(1.0,x1-x0),(FB-2)/max(1.0,y1-y0)) def fx(x): return max(0,min(FB-1,1+int((x-x0)*s))) def fy(y): return max(0,min(FB-1,1+int((y-y0)*s))) L=[f"# Ch341 authentic cube subsegment ({len(seg)} tris) — DECLARED affine ST/Q surrogate, max_error={maxerr:.3f} texels", f"# screen bbox ({x0:.0f},{y0:.0f})..({x1:.0f},{y1:.0f}) viewport-fit -> {FB}x{FB}; texture 256->{dst} downscale", f"tex0 {tbp} 1 6 6 0"] for t in seg: a=[] for v in t: u=max(0,min(dst-1,int(round((v["S"]/v["Q"])*dst)))) if v["Q"] else 0 w=max(0,min(dst-1,int(round((v["T"]/v["Q"])*dst)))) if v["Q"] else 0 a += [fx(v["x"]), fy(v["y"]), u, w] (r,g,b)=t[-1]["rgb"]; z=t[-1]["z"] L.append("tritex "+" ".join(map(str,a+[z,r,g,b]))) L.append("go") open(sp,"w").write("\n".join(L)+"\n"); print(f"[wrote {len(seg)}-tri textured scene -> {sp}]") return 0 if __name__ == "__main__": sys.exit(main(sys.argv))