Files
retroDE_ps2/tools/gs_make_sh3_multidraw_fixture.py
thejayman77 ba74bbd5aa Snapshot: fog implementation + fidelity tooling baseline (pre bilinear-clamp fix)
Per-vertex GS fog end-to-end (gs_stub emit incl. persp_emit5, gs_prim_list_feeder
XYZ2->XYZF2 on PRIM.FGE, gs_make_sh3_scheduler_fixture.py F/FGE packing), new fog
TBs, fidelity attribution tooling. Functional baseline before removing the dead
bilinear lerp8 clamps (Codex: 161-node comb loop -> -0.042ns setup fail).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 19:56:46 -04:00

396 lines
25 KiB
Python

#!/usr/bin/env python3
"""retroDE_ps2 — Ch354 Brick 1: MULTI-DRAW SH3 fixture.
Composite N authentic SH3 draws that SHARE one texture+CLUT into ONE LPDDR framebuffer, proving the FB path is
scene-capable (a real draw LIST, not one selected draw). ONE new variable vs Ch353: multiple authentic draws
accumulating into one FB. Explicitly NOT in Brick 1 (Codex): multi-texture residency, full 640x480, large Z-buffer.
Codex guardrails enforced (fail-CLOSED, before any integration sim):
#1 report each draw's clipped tri count + total staging words; FAIL if > FEEDER_STG_WORDS.
#2 prove ALL feeder-visible state matches: TEX0(all fields), FRAME, PRIM/FST/TME/ABE, TEST/Z, CLAMP, ALPHA, TEXA,
texture dims/format.
#3 texture/CLUT check is CONTENT-based (epoch-aware): a same-byte re-upload is fine; a changed payload FAILS.
#4 draws justified MECHANICALLY (same frame, contiguous run of the same texture key), not visually.
#5 (emit stage) the oracle is generated INDEPENDENTLY from reconstructed GS local memory, NOT from feeder records.
#6 (emit stage) preserve dump order; explicitly SCORE the overlap regions (where accumulation is actually proven).
Usage: gs_make_sh3_multidraw_fixture.py [dump.gs.zst] --draw-list 89548,89761,89974 [--emit]
LOCAL/gitignored outputs (dump-derived). This tool NEVER touches the Ch353 single-draw fixture (byte-stable).
"""
import sys, os, glob
HERE=os.path.dirname(os.path.abspath(__file__)); ROOT=os.path.normpath(os.path.join(HERE,".."))
DATA=os.path.join(ROOT,"sim","data","top_psmct32_raster_demo")
import gs_sh3_draw_census as C
import gs_sh3_recon as RC
import gs_texture_residency as R
sys.path.insert(0, DATA); import bake
# address map — identical to the Ch353 single-draw fixture (128 KiB VRAM / CBP=480 / texture in LPDDR, Codex option A)
CBP = 0x1E000//256 # 480 CLUT base (grid bytes)
NEW_TBP = 0x40000//256 # 1024 texture VRAM base (cache-intercepted)
TEX_BYTES = 512*512 # 262144 PSMT8
STG_WORDS = 2048 # FEEDER_STG_WORDS for the multi-draw PROFILE (Codex-approved; crop/Ch353 keep 768).
# Fail-CLOSED gate: a combined list beyond this must raise the param again (board-BRAM),
# never silently truncate. 16-bit tri count + 12-bit staging addr cover 2047 words.
FBW_MAX = 10 # 640 px hard ceiling for Brick 1 (no full-frame work); union wider than this = fail
def f32(bits):
import struct; return struct.unpack("<f", struct.pack("<I", bits & 0xFFFFFFFF))[0]
def edge(ax,ay,bx,by,px,py): return (px-ax)*(by-ay)-(py-ay)*(bx-ax)
def load_draws(dump, want_idxs, collected=None, allow_untextured=False):
"""Walk the GS event stream once; for each selected draw (by first primitive idx) capture the FULL feeder-visible
state latched at its first kick + its vertices. Mirrors gs_sh3_draw_census.census but captures ALL render state."""
d, h, events, uploads, runs, vram = collected if collected is not None else R.collect(dump, 0)
st = dict(PRIM=None, TEX0_1=None, TEX0_2=None, FRAME_1=None, FRAME_2=None, TEST_1=None, TEST_2=None,
ZBUF_1=None, ZBUF_2=None, CLAMP_1=None, CLAMP_2=None, SCISSOR_1=None, SCISSOR_2=None,
ALPHA_1=None, ALPHA_2=None, TEXA=None, FOGCOL=0)
prim = dict(type=7, tme=0, fge=0, fst=0, abe=0, ctxt=0)
ofx={1:0.0,2:0.0}; ofy={1:0.0,2:0.0}; cur_st=(0.0,0.0,1.0); cur_uv=(0.0,0.0); cur_rgba=0
cur=None; cur_frame=0; want=set(want_idxs); got={}
def ctx(): return 1 if prim["ctxt"]==0 else 2
def texkey():
t0=st["TEX0_1" if prim["ctxt"]==0 else "TEX0_2"]
if t0 is None: return None
return (t0["tbp"], t0["psm"], t0["tbw"], prim["type"], prim["tme"], prim["abe"])
def snapshot_state():
c=ctx()
return dict(tex0=dict(st["TEX0_1" if c==1 else "TEX0_2"]), prim=dict(prim),
frame=st["FRAME_%d"%c], test=st["TEST_%d"%c], zbuf=st["ZBUF_%d"%c],
clamp=st["CLAMP_%d"%c], scissor=st["SCISSOR_%d"%c],
alpha=st["ALPHA_%d"%c], texa=st["TEXA"], fogcol=st["FOGCOL"], ctxt=c)
def close():
nonlocal cur
if cur is not None and cur["nprim"]>=1 and cur["first_idx"] in want:
got[cur["first_idx"]]=cur
cur=None
def open_draw(idx):
nonlocal cur
cur=dict(first_idx=idx, frame=cur_frame, key=texkey(), state=snapshot_state(), nprim=0, verts=[])
for e in events:
if e.kind=="FRAME_BOUNDARY": cur_frame=e.frame+1; continue
if e.kind!="GSREG": continue
cur_frame=e.frame; r,v=e.reg,e.value
if r=="PRIM": close(); prim=dict(type=v&7, tme=(v>>4)&1, fge=(v>>5)&1,
fst=(v>>8)&1, abe=(v>>6)&1, ctxt=(v>>9)&1)
elif r in ("TEX0_1","TEX0_2"): st[r]=R.dec_tex0(v)
elif r=="XYOFFSET_1": ofx[1]=(v&0xFFFF)/16.0; ofy[1]=((v>>32)&0xFFFF)/16.0
elif r=="XYOFFSET_2": ofx[2]=(v&0xFFFF)/16.0; ofy[2]=((v>>32)&0xFFFF)/16.0
elif r in st: st[r]=v # FRAME/TEST/ZBUF/CLAMP/ALPHA/TEXA raw qwords (compared verbatim)
elif r=="RGBAQ": cur_rgba=v&0xFFFFFFFF
elif r=="ST":
s=f32(v&0xFFFFFFFF); t=f32((v>>32)&0xFFFFFFFF); q=f32(e.info.get("q_stq",0x3F800000)); cur_st=(s,t,q)
elif r=="UV": cur_uv=((v&0x3FFF)/16.0, ((v>>16)&0x3FFF)/16.0)
elif r in ("XYZF2","XYZ2","XYZF3","XYZ3"):
xf=v&0xFFFF; yf=(v>>16)&0xFFFF; c=ctx(); x=xf/16.0-ofx[c]; y=yf/16.0-ofy[c]
is_xyzf = r in ("XYZF2", "XYZF3")
z=(v>>32)&0xFFFFFF if is_xyzf else (v>>32)&0xFFFFFFFF
fog=(v>>56)&0xFF if is_xyzf else 0xFF
if (not prim["tme"] and not allow_untextured) or texkey() is None: continue
if cur is None or cur["key"]!=texkey(): close(); open_draw(e.idx)
t0=cur["state"]["tex0"]
# S/T/Q are architecturally ignored when TME=0. Canonicalize
# them so untextured fixture generation never depends on stale
# ST/UV registers or trips the perspective fixed-point gates.
stq=((0.0,0.0,1.0) if not prim["tme"] else
((cur_uv[0]/t0["tw"],cur_uv[1]/t0["th"],1.0) if prim["fst"] else cur_st))
# XYZ2/XYZF2 normally perform a drawing kick, but PACKED XYZ2/F2
# carries ADC in bit 111. gs_parse exposes that as note="adc"
# while retaining the XYZ2/F2 register name. ADC and XYZ3/F3
# both feed the primitive assembler without drawing the triangle
# completed by this vertex. Retain the vertex for strip
# continuity, but mark it as non-kicking.
cur["verts"].append(dict(x=x,y=y,z=z,fog=fog,s=stq[0],t=stq[1],q=stq[2],rgba=cur_rgba,
kick=(r in ("XYZF2","XYZ2") and e.info.get("note") != "adc")))
cur["nprim"]+=1
close()
return got, vram
def clip_rect(tri, W, H):
def lerp(p1,p2,a): return {k:(p1[k]+a*(p2[k]-p1[k])) for k in ("x","y","s","t","q")}
def clip_edge(poly, inside, isect):
out=[]; n=len(poly)
for i in range(n):
a=poly[i]; b=poly[(i+1)%n]; ina=inside(a); inb=inside(b)
if ina: out.append(a)
if ina!=inb: out.append(isect(a,b))
return out
poly=[dict(x=v["x"],y=v["y"],s=v["s"],t=v["t"],q=v["q"]) for v in tri]
poly=clip_edge(poly, lambda p:p["x"]>=0.0, lambda a,b:lerp(a,b,(0.0-a["x"])/(b["x"]-a["x"])))
if not poly: return []
poly=clip_edge(poly, lambda p:p["x"]<=W, lambda a,b:lerp(a,b,(W-a["x"])/(b["x"]-a["x"])))
if not poly: return []
poly=clip_edge(poly, lambda p:p["y"]>=0.0, lambda a,b:lerp(a,b,(0.0-a["y"])/(b["y"]-a["y"])))
if not poly: return []
poly=clip_edge(poly, lambda p:p["y"]<=H, lambda a,b:lerp(a,b,(H-a["y"])/(b["y"]-a["y"])))
if len(poly)<3: return []
return [(poly[0],poly[k],poly[k+1]) for k in range(1,len(poly)-1)]
def main(argv):
a=argv[1:]; dump=a[0] if a and not a[0].startswith("--") else None
if "--draw-list" not in a: sys.exit("need --draw-list idx,idx,...")
idxs=[int(x) for x in a[a.index("--draw-list")+1].split(",")]
tag = a[a.index("--tag")+1] if "--tag" in a else "multi" # output-file suffix (diagnostic isolation runs)
only = int(a[a.index("--only")+1]) if "--only" in a else None # emit ONLY this draw's tris (keep full-set union)
if dump is None:
c=glob.glob(os.path.join(ROOT,"captures","gs","silenthill3","*224139*.gs.zst"))
if not c: sys.exit("no SH3 dump found; pass the .gs.zst path")
dump=c[0]
print(f"[Ch354] dump={os.path.basename(dump)} draw-list={idxs}")
got, vram = load_draws(dump, idxs)
missing=[i for i in idxs if i not in got]
if missing: sys.exit(f"[Ch354] FAIL: draw idx {missing} not found as textured draws")
draws=[got[i] for i in idxs] # dump order = the order the user passed (verify below)
# ---- Guardrail #4: MECHANICAL justification — same frame, contiguous run of ONE texture key ----
frames={d["frame"] for d in draws}
keys={d["key"] for d in draws}
order_ok = idxs==sorted(idxs)
print(f"[Ch354] frames={sorted(frames)} texkeys={keys} dump-order(ascending idx)={order_ok}")
if len(frames)!=1: sys.exit(f"[Ch354] FAIL(#4): draws span multiple frames {sorted(frames)} — not one scene/run")
if len(keys)!=1: sys.exit(f"[Ch354] FAIL(#4): draws have different texture keys {keys} — not one shared texture")
if not order_ok: sys.exit(f"[Ch354] FAIL(#4): --draw-list not in ascending dump order {idxs}")
# ---- Guardrail #2: ALL feeder-visible state must match across the selected draws ----
ref=draws[0]["state"]; t0r=ref["tex0"]
for fld in ("tbp","tbw","psm","tw","th","tcc","tfx","cbp","cpsm","cld"):
vals={d["state"]["tex0"][fld] for d in draws}
if len(vals)!=1: sys.exit(f"[Ch354] FAIL(#2): TEX0.{fld} differs across draws: {vals}")
for fld in ("type","fst","tme","abe"):
vals={d["state"]["prim"][fld] for d in draws}
if len(vals)!=1: sys.exit(f"[Ch354] FAIL(#2): PRIM.{fld} differs: {vals}")
for reg in ("frame","test","zbuf","clamp","alpha","texa"):
vals={d["state"][reg] for d in draws}
if len(vals)!=1: sys.exit(f"[Ch354] FAIL(#2): {reg.upper()} state differs across draws: {vals}")
assert t0r["tw"]==512 and t0r["th"]==512 and t0r["psm"]==0x13, f"unexpected TEX0 {t0r}"
print(f"[Ch354] #2 OK: all feeder-visible state identical — TEX0 tbp={t0r['tbp']} cbp={t0r['cbp']} psm=0x{t0r['psm']:02x} "
f"{t0r['tw']}x{t0r['th']}; PRIM type={ref['prim']['type']} fst={ref['prim']['fst']} tme={ref['prim']['tme']} "
f"abe={ref['prim']['abe']}; TEST/ZBUF/CLAMP/ALPHA/TEXA match")
# ---- Guardrail #3: CONTENT-based (epoch-aware) texture + CLUT check. Reconstruct local memory to EACH draw and
# compare the texture bytes @tbp + CLUT bytes @cbp. Same-byte re-upload -> identical -> OK; changed -> FAIL. ----
tbp=t0r["tbp"]; cbp=t0r["cbp"]; texref=None; clutref=None
for d in draws:
mem, *_ = RC.build_localmem_to(dump, d["first_idx"])
if mem is None: sys.exit(f"[Ch354] FAIL(#3): VRAM snapshot absent at idx{d['first_idx']}")
tb=bytes(mem.m[tbp*256 : tbp*256+TEX_BYTES]); cb=bytes(mem.m[cbp*256 : cbp*256+1024])
if texref is None: texref=tb; clutref=cb; anchor=d["first_idx"]
else:
if tb!=texref: sys.exit(f"[Ch354] FAIL(#3): texture @tbp={tbp} CHANGED between idx{anchor} and idx{d['first_idx']} "
f"(payload differs — needs multi-texture residency, deferred past Brick 1)")
if cb!=clutref: sys.exit(f"[Ch354] FAIL(#3): CLUT @cbp={cbp} CHANGED between idx{anchor} and idx{d['first_idx']}")
print(f"[Ch354] #3 OK: texture @tbp={tbp} ({TEX_BYTES} B) + CLUT @cbp={cbp} (1024 B) byte-identical across all "
f"{len(draws)} draws (content-compared; same-byte re-uploads are fine)")
# ---- Guardrail #3: deterministic UNION bbox -> FB origin/size/stride ----
OX=int(min(min(v["x"] for v in d["verts"]) for d in draws))
OY=int(min(min(v["y"] for v in d["verts"]) for d in draws))
UXMAX=max(max(v["x"] for v in d["verts"]) for d in draws)
UYMAX=max(max(v["y"] for v in d["verts"]) for d in draws)
W=int(UXMAX)-OX+1; H=int(UYMAX)-OY+1
FBW=(W+63)//64; FBPXW=FBW*64
if FBW>FBW_MAX: sys.exit(f"[Ch354] FAIL: union width {W}px (FBW={FBW}) exceeds Brick-1 ceiling {FBW_MAX*64}px")
STRIDE=FBPXW*4
print(f"[Ch354] #3 UNION bbox: origin=({OX},{OY}) size={W}x{H}px -> FB {FBPXW}x{H} (FBW={FBW}) stride={STRIDE}B "
f"= {FBPXW*H*4} B ({FBPXW*H*4//1024} KiB)")
# ---- Guardrail #1: per-draw CLIPPED tri count + total staging words, fail-CLOSED on FEEDER_STG_WORDS ----
HEADER_WORDS=7 # ntris|flag, FRAME, ALPHA, TEST, ZBUF, TEX0, PRIM
total_tris=0; per=[]
for d in draws:
fv=[dict(x=v["x"]-OX, y=v["y"]-OY, s=v["s"], t=v["t"], q=v["q"]) for v in d["verts"]]
raw=[(fv[i-2],fv[i-1],fv[i]) for i in range(2,len(fv))] # TRI_STRIP -> triangles
clipped=[]
for tri in raw: clipped += clip_rect(tri, FBPXW, H)
per.append((d["first_idx"], len(fv), len(raw), len(clipped))); total_tris+=len(clipped)
words=HEADER_WORDS + total_tris*9
print(f"[Ch354] #1 staging capacity (FEEDER_STG_WORDS={STG_WORDS}):")
for (idx,nv,nraw,ncl) in per:
print(f" idx{idx}: {nv} verts -> {nraw} strip-tris -> {ncl} clipped-tris")
print(f" TOTAL {total_tris} clipped tris -> {words} staging words "
f"({'OK' if words<=STG_WORDS else 'OVER by %d'%(words-STG_WORDS)})")
if words>STG_WORDS:
sys.exit(f"[Ch354] FAIL-CLOSED(#1): combined list {words} words > FEEDER_STG_WORDS={STG_WORDS}. "
f"Raise the PROFILE FEEDER_STG_WORDS (board-BRAM cost) to >= {words} (e.g. {1<<(words-1).bit_length()}) "
f"OR reduce the draw set. Do NOT discover this in integration sim.")
print(f"[Ch354] all fail-closed gates PASS.")
if "--emit" not in a:
print("[Ch354] (validation only; pass --emit to generate the fixture)"); return 0
# ================= --emit: combined feeder list + INDEPENDENT oracle (guardrails #5, #6) =================
STG_WORDS_MULTI=2048 # Codex-approved profile-only staging RAM (crop/Ch353 keep 768)
PERSP_FRAC=bake.PERSP_FRAC; PSCALE=4096; TW=512; TH=512; TW_LOG=TH_LOG=9; TBW_TEX=8
NEW_TBP=0x40000//256; TEX_VRAM_BASE=NEW_TBP*256; LPDDR_TEX_BASE=0x00200000; VRAM_BYTES=0x20000
S24_MAX=(1<<23)-1
def tex0_real(tbp2,cbp2):
v=bake.tex0_pack(tbp2,TBW_TEX,psm=0x13,tw=TW_LOG,th=TH_LOG,tfx=1)
v|=(cbp2&0x3FFF)<<37; v|=(0&0xF)<<51; v|=(0&0x1)<<55; v|=(0&0x1F)<<56; v|=(1&0x7)<<61
return v
def vert_words(v):
s_fp=round(v["s"]*TW*(1<<PERSP_FRAC)*PSCALE); t_fp=round(v["t"]*TH*(1<<PERSP_FRAC)*PSCALE)
q_fp=round(v["q"]*(1<<PERSP_FRAC)*PSCALE)
if abs(s_fp)>S24_MAX or abs(t_fp)>S24_MAX: sys.exit(f"[Ch354] ST overflow {s_fp},{t_fp} (lower PSCALE)")
if abs(q_fp)>0x7FFFFFFF: sys.exit(f"[Ch354] Q overflow {q_fp}")
sx=max(0,min(FBPXW-1,int(round(v["x"])))); sy=max(0,min(H-1,int(round(v["y"]))))
return [bake.rgbaq_with_q(0,0,0,q_fp&0xFFFFFFFF), bake.st_data(s_fp&0xFFFFFF,t_fp&0xFFFFFF),
bake.xyz2_dataz(sx,sy,0x0000_5000)]
def draw_tris(d):
fv=[dict(x=v["x"]-OX,y=v["y"]-OY,s=v["s"],t=v["t"],q=v["q"]) for v in d["verts"]]
raw=[(fv[i-2],fv[i-1],fv[i]) for i in range(2,len(fv))]
out=[]
for tri in raw: out += clip_rect(tri, FBPXW, H)
return out
# shared texture/CLUT (tbp=9216) reconstructed ONCE — feeds BOTH the LPDDR upload AND the independent oracle
mem0, *_ = RC.build_localmem_to(dump, draws[0]["first_idx"])
idx = mem0.read_psmt8(tbp, t0r["tbw"], TW, TH)
pal = RC.read_clut32(mem0, cbp, order="grid")
clut_bytes = bytes(mem0.m[cbp*256 : cbp*256+1024])
idx_words = [idx[i*4]|(idx[i*4+1]<<8)|(idx[i*4+2]<<16)|(idx[i*4+3]<<24) for i in range(TW*TH//4)]
# render set: the FULL list, or a SINGLE draw (--only) for the isolation/composition diagnostic (same union frame)
render_set = [got[only]] if only is not None else draws
if only is not None and only not in got: sys.exit(f"[Ch354] --only {only} not in --draw-list")
print(f"[Ch354] tag='{tag}' render_set={[d['first_idx'] for d in render_set]} (union from full {idxs})")
# combined feeder list: ONE shared-state header + the render-set's clipped tris in DUMP ORDER (#6)
all_tris=[]
for d in render_set: all_tris += draw_tris(d)
ntris=len(all_tris)
stg=[ntris|(1<<32), bake.frame_1_psmct32(FBW), bake.alpha_pack(0,1,0,1), 0, bake.zbuf1_pack(0,zmsk=1),
tex0_real(NEW_TBP,CBP), 3|(1<<4)]
for (v0,v1,v2) in all_tris:
for v in (v0,v1,v2): stg += vert_words(v)
if len(stg)>STG_WORDS_MULTI: sys.exit(f"[Ch354] staging {len(stg)} > {STG_WORDS_MULTI}")
max_addr=len(stg)-1
print(f"[Ch354] combined feeder list: {ntris} tris -> {len(stg)} words (max staging addr {max_addr} < {STG_WORDS_MULTI}); records_emitted should == {ntris}")
# INDEPENDENT oracle (#5): rasterize the RECONSTRUCTED geometry+texture in DUMP ORDER (paint-order, DECAL/no-Z,
# matching the RTL flush order), per FB pixel — NOT parsed from feeder records. Track DISTINCT draw count per pixel
# -> OVERLAP regions (#6). refmap word: [31]covered [30]interior [28]overlap [17:9]tu [8:0]tv.
refmap=[0]*(FBPXW*H); refpix=[(0,0,0)]*(FBPXW*H); ndraw=[0]*(FBPXW*H); lastdraw=[-1]*(FBPXW*H)
for di,d in enumerate(render_set):
for (v0,v1,v2) in draw_tris(d):
x0,y0=v0["x"],v0["y"]; x1,y1=v1["x"],v1["y"]; x2,y2=v2["x"],v2["y"]
ar=edge(x0,y0,x1,y1,x2,y2)
if abs(ar)<1e-9: continue
inv=1.0/ar
minx=max(0,int(min(x0,x1,x2))); maxx=min(FBPXW-1,int(max(x0,x1,x2))+1)
miny=max(0,int(min(y0,y1,y2))); maxy=min(H-1,int(max(y0,y1,y2))+1)
for py in range(miny,maxy+1):
for px in range(minx,maxx+1):
cx,cy=px+0.5,py+0.5
w0=edge(x1,y1,x2,y2,cx,cy)*inv; w1=edge(x2,y2,x0,y0,cx,cy)*inv; w2=1.0-w0-w1
if w0<-0.001 or w1<-0.001 or w2<-0.001: continue
a0=edge(x1,y1,x2,y2,float(px),float(py))*inv; a1=edge(x2,y2,x0,y0,float(px),float(py))*inv; a2=1.0-a0-a1
S=a0*v0["s"]+a1*v1["s"]+a2*v2["s"]; T=a0*v0["t"]+a1*v1["t"]+a2*v2["t"]; Q=a0*v0["q"]+a1*v1["q"]+a2*v2["q"]
if abs(Q)<1e-12: continue
tu=int((S/Q)*TW)%TW; tv=int((T/Q)*TH)%TH
if tu<0: tu+=TW
if tv<0: tv+=TH
o=py*FBPXW+px; mw=min(w0,w1,w2); interior=1 if mw>0.04 else 0
if lastdraw[o]!=di: ndraw[o]+=1; lastdraw[o]=di # DISTINCT draws covering this pixel
refmap[o]=(1<<31)|(interior<<30)|((tu&0x1FF)<<9)|(tv&0x1FF) # later draw overwrites (paint-order)
p=pal[idx[tv*TW+tu]&0xFF]; refpix[o]=(p&0xFF,(p>>8)&0xFF,(p>>16)&0xFF)
overlap_px=0
for o in range(FBPXW*H):
if ndraw[o]>1: refmap[o]|=(1<<28); overlap_px+=1
covered=sum(1 for w in refmap if w>>31)
print(f"[Ch354] #5/#6 independent oracle: {covered} covered px, {overlap_px} OVERLAP px (>1 distinct draw) [refmap bit28]")
if only is None and overlap_px==0:
sys.exit("[Ch354] FAIL(#6): NO overlap pixels — the draws don't accumulate; multi-draw not proven")
# OWNER map (Codex diagnostic): the FULL-set winning draw index per pixel, in DUMP ORDER (last covering draw wins).
# Used to compose the isolated single-draw RTL framebuffers and compare to the combined RTL render.
owner=[255]*(FBPXW*H) # 255 = uncovered
for di,d in enumerate(draws): # ALWAYS the full set (independent of --only)
for (v0,v1,v2) in draw_tris(d):
x0,y0=v0["x"],v0["y"]; x1,y1=v1["x"],v1["y"]; x2,y2=v2["x"],v2["y"]
ar=edge(x0,y0,x1,y1,x2,y2)
if abs(ar)<1e-9: continue
inv=1.0/ar
minx=max(0,int(min(x0,x1,x2))); maxx=min(FBPXW-1,int(max(x0,x1,x2))+1)
miny=max(0,int(min(y0,y1,y2))); maxy=min(H-1,int(max(y0,y1,y2))+1)
for py in range(miny,maxy+1):
for px in range(minx,maxx+1):
cx,cy=px+0.5,py+0.5
w0=edge(x1,y1,x2,y2,cx,cy)*inv; w1=edge(x2,y2,x0,y0,cx,cy)*inv; w2=1.0-w0-w1
if w0<-0.001 or w1<-0.001 or w2<-0.001: continue
owner[py*FBPXW+px]=di # later draw overwrites -> final owner (paint order)
# ---- emit LOCAL fixtures (tag 'multi'; NEVER touch the Ch353 sh3_real_* single-draw fixture) ----
def wmem(name, words, banner):
with open(os.path.join(DATA,name),"w") as f:
f.write(f"// {banner}\n")
for x in words: f.write(f"{x & 0xFFFFFFFF:08x}\n")
wmem(f"sh3_{tag}_idx.mem", idx_words, "Ch354 LOCAL shared SH3 512x512 de-swizzled indices (4/word). gitignored.")
wmem(f"sh3_{tag}_tex_lpddr.mem", idx_words, "Ch354 LOCAL shared SH3 512x512 LINEAR indices -> LPDDR (PSMT8_SWIZZLE=0). gitignored.")
wmem(f"sh3_{tag}_clut.mem", [int.from_bytes(clut_bytes[i*4:i*4+4],'little') for i in range(256)],
"Ch354 LOCAL shared SH3 CSM1 CLUT (grid bytes @cbp) -> BRAM. gitignored.")
wmem(f"sh3_{tag}_pal.mem", [p & 0xFFFFFFFF for p in pal], "Ch354 LOCAL de-gridded palette pal[i] for the TB. gitignored.")
wmem(f"sh3_{tag}_refmap.mem", refmap, "Ch354 LOCAL per-FB-pixel covered|interior|overlap|tu|tv oracle (render_set). gitignored.")
wmem(f"sh3_{tag}_owner.mem", owner, "Ch354 LOCAL per-FB-pixel FULL-set winning draw index (dump order; 255=uncovered). gitignored.")
bake.write_feeder_stg_mem(f"feeder_sh3_{tag}.mem", stg,
f"Ch354 LOCAL multi-draw SH3 (render {[d['first_idx'] for d in render_set]} of {idxs}) feeder staging: {ntris}-tri list + TEX0(PSMT8,CSM1,DECAL). gitignored.",
total=STG_WORDS_MULTI) # padded to 2048 (Codex)
# setup bootlet: CSM1 CLUT 256x1 BITBLT -> CBP (identical pattern to Ch352; shared CLUT), DISPLAY1 = FBPXW x H
clut_words_b=[int.from_bytes(clut_bytes[i*4:i*4+4],"little") for i in range(256)]
RAM_QWORDS=512; pay=[]
pay.append(bake.giftag(1,0,0,4,int('E'*4,16)))
pay.append(bake.aplusd(bake.R_BITBLTBUF, bake.bitbltbuf_pack(CBP,1,0x00)))
pay.append(bake.aplusd(bake.R_TRXPOS, bake.trxpos_pack(0,0)))
pay.append(bake.aplusd(bake.R_TRXREG, bake.trxreg_pack(256,1)))
pay.append(bake.aplusd(bake.R_TRXDIR, bake.trxdir_pack(0)))
pay.append(bake.giftag(256//4,1,2,0,0))
for q in range(256//4):
word=0
for lane in range(4): word|=(clut_words_b[q*4+lane]&0xFFFFFFFF)<<(32*lane)
pay.append(word)
qwc=len(pay); disp_hi=((H-1)<<12)|(FBPXW-1)
with open(os.path.join(DATA,f"payload_sh3_{tag}.mem"),"w") as f:
f.write(f"// Ch354 LOCAL multi-draw setup payload (CSM1 CLUT -> CBP={CBP}). gitignored. QWC={qwc}.\n")
for _ in range(16): f.write(f"{0:032x}\n")
for x in pay: f.write(f"{x&((1<<128)-1):032x}\n")
for _ in range(RAM_QWORDS-16-qwc): f.write(f"{0:032x}\n")
bake.write_bios_mem(f"bios_sh3_{tag}.mem", bake.build_textured_demo_bootlet_disp(qwc, disp_hi, FBW),
f"Ch354 LOCAL multi-draw setup bootlet (QWC={qwc}, DISPLAY1={FBPXW}x{H}). gitignored.")
with open(os.path.join(DATA,f"sh3_{tag}_params.vh"),"w") as f:
f.write("// Ch354 LOCAL generated params for the multi-draw integration TB. gitignored.\n")
f.write(f"localparam int FBW = {FBW};\n")
f.write(f"localparam int FBPXW = {FBPXW};\n")
f.write(f"localparam int FBH = {H};\n")
f.write(f"localparam int VRAM_BYTES_P = {VRAM_BYTES};\n")
f.write(f"localparam int CLUT_CBP = {CBP};\n")
f.write(f"localparam int NEW_TBP = {NEW_TBP};\n")
f.write(f"localparam int TEX_VRAM_BASE = {TEX_VRAM_BASE};\n")
f.write(f"localparam int TEX_BYTES = {TW*TH};\n")
f.write(f"localparam [29:0] LPDDR_TEX_BASE = 30'h{LPDDR_TEX_BASE:07x};\n")
f.write(f"localparam int N_BEATS = {TW*TH//32};\n")
f.write(f"localparam int STG_WORDS = {STG_WORDS_MULTI};\n")
f.write(f"localparam int TW = {TW};\n")
f.write(f"localparam int TH = {TH};\n")
f.write(f"localparam int NDRAWS = {len(draws)};\n")
f.write(f"localparam int NTRIS = {ntris};\n")
f.write(f"localparam int UNION_OX = {OX};\n")
f.write(f"localparam int UNION_OY = {OY};\n")
try:
from PIL import Image
im=Image.new("RGB",(FBPXW,H)); im.putdata(refpix)
im.save(os.path.join(ROOT,"captures","gs","silenthill3","extracted","recon",f"sh3_{tag}_ref.png"))
print(f"[Ch354] wrote sh3_{tag}_ref.png")
except Exception as ex: print("(PIL skipped:", ex, ")")
tex_sum=sum(idx_words)&0xFFFFFFFF; tex_xor=0
for w in idx_words: tex_xor^=w
print(f"[Ch354] emitted multi-draw fixtures -> {DATA}. FB {FBPXW}x{H} stride {FBPXW*4}B; feeder {len(stg)}w (pad {STG_WORDS_MULTI}); "
f"tex sum32=0x{tex_sum:08x} xor32=0x{tex_xor:08x} @LPDDR 0x{LPDDR_TEX_BASE:07x}")
return 0
if __name__=="__main__":
raise SystemExit(main(sys.argv))