#!/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("=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<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))