#!/usr/bin/env python3
"""retroDE_ps2 — Ch355 Brick 1: MULTI-TEXTURE composition.

Two authentic SH3 draw groups with DIFFERENT TEX0/CLUT accumulate into ONE LPDDR framebuffer via SCENE-LEVEL texture
rebind + staged-list retriggering (Codex). Dump order (authentic): A={19562} tbp=11264/cbp=14080 THEN B={89761}
tbp=9216/cbp=13952. NOT in Brick 1: per-primitive TEX0, multi-resident cache, 640x480, cross-draw Z.

Preflight fail-CLOSED gates (this file, run first):
  - both draws same frame; overlap on screen; DIFFERENT texture keys.
  - each group's texture + CLUT independently RESIDENT at ITS draw-time epoch (per-draw local-memory reconstruction).
  - the two textures are DIFFERENT content, and the two CLUTs are DIFFERENT content (real multi-texture, not aliasing).
  - CLUTs RELOCATED to distinct, non-overlapping BRAM CBPs (CBP_A/CBP_B); reported for the TEX0-selects-CBP check.

Usage: gs_make_sh3_multitex_fixture.py [dump.gs.zst] [--emit]   (draws A/B fixed to the authentic pair)
LOCAL/gitignored. Reuses gs_make_sh3_multidraw_fixture.load_draws (same event-walk + full-state capture).
"""
import sys, os
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_recon as RC
import gs_make_sh3_multidraw_fixture as MD           # reuse load_draws + edge + clip_rect
sys.path.insert(0, DATA); import bake

DRAW_A = 19562        # dump order: A first
DRAW_B = 89761
TEX_BYTES = 512*512
# relocated CLUT bases (distinct, non-overlapping). CLUT = 256 words = 1 KiB = 4 VRAM blocks (256 B each).
CBP_A = 0x1E000//256  # 480  (bytes 0x1E000..0x1E3FF)
CBP_B = 0x1E400//256  # 484  (bytes 0x1E400..0x1E7FF) — +1 KiB, no overlap with CBP_A

def bbox(dr):
    xs=[v["x"] for v in dr["verts"]]; ys=[v["y"] for v in dr["verts"]]
    return (min(xs),min(ys),max(xs),max(ys))

def main(argv):
    a=argv[1:]; dump=a[0] if a and not a[0].startswith("--") else None
    if dump is None:
        import glob; 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"[Ch355] dump={os.path.basename(dump)}  A=idx{DRAW_A} (first) -> B=idx{DRAW_B}")

    got, vram = MD.load_draws(dump, [DRAW_A, DRAW_B])
    for i in (DRAW_A, DRAW_B):
        if i not in got: sys.exit(f"[Ch355] FAIL: draw idx{i} not found as a textured draw")
    drA, drB = got[DRAW_A], got[DRAW_B]
    tA, tB = drA["state"]["tex0"], drB["state"]["tex0"]

    # ---- gate: authentic dump order (A before B) ----
    if not (DRAW_A < DRAW_B): sys.exit(f"[Ch355] FAIL: A idx{DRAW_A} not before B idx{DRAW_B} (dump order)")
    # ---- gate: same frame ----
    if drA["frame"]!=drB["frame"]: sys.exit(f"[Ch355] FAIL: A frame {drA['frame']} != B frame {drB['frame']}")
    # ---- gate: DIFFERENT texture keys ----
    if drA["key"]==drB["key"]: sys.exit(f"[Ch355] FAIL: A and B share the same texture key {drA['key']} — not multi-texture")
    print(f"[Ch355] A: TEX0 tbp={tA['tbp']} cbp={tA['cbp']} psm=0x{tA['psm']:02x} {tA['tw']}x{tA['th']}  key={drA['key']}")
    print(f"[Ch355] B: TEX0 tbp={tB['tbp']} cbp={tB['cbp']} psm=0x{tB['psm']:02x} {tB['tw']}x{tB['th']}  key={drB['key']}")
    # ---- gate: overlap on screen ----
    ax0,ay0,ax1,ay1=bbox(drA); bx0,by0,bx1,by1=bbox(drB)
    ox0=max(ax0,bx0); oy0=max(ay0,by0); ox1=min(ax1,bx1); oy1=min(ay1,by1)
    if not (ox1>ox0 and oy1>oy0): sys.exit(f"[Ch355] FAIL: A bbox {bbox(drA)} and B bbox {bbox(drB)} do not overlap on screen")
    print(f"[Ch355] frame f{drA['frame']}; screen A[{ax0:.0f}..{ax1:.0f}]x[{ay0:.0f}..{ay1:.0f}] B[{bx0:.0f}..{bx1:.0f}]x[{by0:.0f}..{by1:.0f}] -> overlap [{ox0:.0f}..{ox1:.0f}]x[{oy0:.0f}..{oy1:.0f}]")

    # ---- gate: each group's texture + CLUT INDEPENDENTLY RESIDENT at its draw-time epoch; the two are DIFFERENT ----
    memA,*_ = RC.build_localmem_to(dump, DRAW_A)
    memB,*_ = RC.build_localmem_to(dump, DRAW_B)
    if memA is None or memB is None: sys.exit("[Ch355] FAIL: VRAM snapshot absent for a draw")
    texA=bytes(memA.m[tA['tbp']*256:tA['tbp']*256+TEX_BYTES]); clutA=bytes(memA.m[tA['cbp']*256:tA['cbp']*256+1024])
    texB=bytes(memB.m[tB['tbp']*256:tB['tbp']*256+TEX_BYTES]); clutB=bytes(memB.m[tB['cbp']*256:tB['cbp']*256+1024])
    def nz(b): return any(x for x in b)
    if not nz(texA):  sys.exit(f"[Ch355] FAIL: texture A @tbp={tA['tbp']} not resident at idx{DRAW_A}")
    if not nz(clutA): sys.exit(f"[Ch355] FAIL: CLUT A @cbp={tA['cbp']} not resident at idx{DRAW_A}")
    if not nz(texB):  sys.exit(f"[Ch355] FAIL: texture B @tbp={tB['tbp']} not resident at idx{DRAW_B}")
    if not nz(clutB): sys.exit(f"[Ch355] FAIL: CLUT B @cbp={tB['cbp']} not resident at idx{DRAW_B}")
    if texA==texB:   sys.exit("[Ch355] FAIL: texture A == texture B (aliasing, not multi-texture)")
    if clutA==clutB: sys.exit("[Ch355] FAIL: CLUT A == CLUT B (aliasing)")
    crcA=sum(int.from_bytes(texA[i*4:i*4+4],'little') for i in range(TEX_BYTES//4))&0xFFFFFFFF
    crcB=sum(int.from_bytes(texB[i*4:i*4+4],'little') for i in range(TEX_BYTES//4))&0xFFFFFFFF
    print(f"[Ch355] texture A resident (sum32=0x{crcA:08x}) + CLUT A resident; texture B resident (sum32=0x{crcB:08x}) + CLUT B resident; A!=B (real multi-texture)")

    # ---- gate: CLUT relocation to distinct, non-overlapping BRAM CBPs ----
    if abs(CBP_A-CBP_B)<4: sys.exit(f"[Ch355] FAIL: relocated CBP_A={CBP_A} CBP_B={CBP_B} overlap (<4 blocks = 1 KiB)")
    print(f"[Ch355] CLUT relocation: A -> CBP={CBP_A} (0x{CBP_A*256:x}), B -> CBP={CBP_B} (0x{CBP_B*256:x}); distinct, non-overlapping (each 1 KiB)")

    # ---- gate: relocated CLUT ranges within BRAM (VRAM 128 KiB = block 0..511; each CLUT = 4 blocks) ----
    VRAM_BLOCKS = (128*1024)//256
    for nm,cb in (("A",CBP_A),("B",CBP_B)):
        if cb+4 > VRAM_BLOCKS: sys.exit(f"[Ch355] FAIL: relocated CLUT {nm} @CBP={cb} (+4 blocks) exceeds VRAM {VRAM_BLOCKS} blocks")
    print(f"[Ch355] both relocated CLUTs within BRAM (VRAM {VRAM_BLOCKS} blocks; A@{CBP_A}+4, B@{CBP_B}+4)")

    print("[Ch355] PREFLIGHT PASS: authentic order, same frame, on-screen overlap, two independently-resident "
          "textures+CLUTs (different content), relocatable to distinct in-BRAM CBPs.")
    if "--emit" not in a:
        print("[Ch355] (preflight only; --emit stage pending)"); return 0

    # ================= --emit: two feeder lists + two textures/CLUTs + independent A->B reference =================
    NEW_TBP=0x40000//256; TEX_VRAM_BASE=NEW_TBP*256; LPDDR_TEX_BASE=0x00200000; VRAM_BYTES=0x20000
    PERSP_FRAC=bake.PERSP_FRAC; PSCALE=4096; TW=512; TH=512; TW_LOG=TH_LOG=9; TBW_TEX=8; S24_MAX=(1<<23)-1
    edge=MD.edge; clip_rect=MD.clip_rect
    def tex0_word(cbp):
        v=bake.tex0_pack(NEW_TBP,TBW_TEX,psm=0x13,tw=TW_LOG,th=TH_LOG,tfx=1)
        v|=(cbp&0x3FFF)<<37; v|=(0&0xF)<<51; v|=(0&0x1)<<55; v|=(0&0x1F)<<56; v|=(1&0x7)<<61
        return v
    # union bbox over BOTH groups (Codex: ~288x381 -> FBW=5/320x381)
    allx=[v["x"] for d in (drA,drB) for v in d["verts"]]; ally=[v["y"] for d in (drA,drB) for v in d["verts"]]
    OX=int(min(allx)); OY=int(min(ally)); W=int(max(allx))-OX+1; H=int(max(ally))-OY+1
    FBW=(W+63)//64; FBPXW=FBW*64; STRIDE=FBPXW*4
    print(f"[Ch355] UNION origin=({OX},{OY}) content={W}x{H} -> stored FB {FBPXW}x{H} (FBW={FBW}) stride={STRIDE}B "
          f"size={FBPXW*H*4} (0x{FBPXW*H*4:x}) scanout={STRIDE//32*H} beats/frame; HDMI shows all {FBPXW} cols "
          f"({FBPXW-W} right cols precleared-black beyond the {W}px content)")

    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"[Ch355] ST overflow {s_fp},{t_fp}")
        if abs(q_fp)>0x7FFFFFFF: sys.exit(f"[Ch355] 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 group_tris(dr):
        fv=[dict(x=v["x"]-OX,y=v["y"]-OY,s=v["s"],t=v["t"],q=v["q"]) for v in dr["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
    # Ch355 (Codex): the RTL perspective reciprocal is UNSIGNED — negative Q is unsupported. Canonicalize each triangle
    # to positive Q BEFORE packing: all-Q-positive unchanged; all-Q-negative -> negate every vertex's S,T,Q (EXACT:
    # (-s)/(-q)=s/q and the negation of a linear attribute interpolates identically); mixed-sign/zero Q -> FAIL CLOSED.
    def canon_tri(v0,v1,v2):
        qs=[v0["q"],v1["q"],v2["q"]]
        if all(q>0 for q in qs): return (v0,v1,v2)
        if all(q<0 for q in qs):
            neg=lambda v: dict(x=v["x"],y=v["y"],s=-v["s"],t=-v["t"],q=-v["q"])
            return (neg(v0),neg(v1),neg(v2))
        sys.exit(f"[Ch355] FAIL-CLOSED: mixed-sign or zero Q in a triangle (qs={qs}) — signed-Q not supported (later platform extension)")
    def feeder_list(dr, cbp):
        tris=group_tris(dr); stg=[len(tris)|(1<<32), bake.frame_1_psmct32(FBW), bake.alpha_pack(0,1,0,1), 0,
                                  bake.zbuf1_pack(0,zmsk=1), tex0_word(cbp), 3|(1<<4)]
        for tri in tris:
            cv0,cv1,cv2=canon_tri(*tri)
            for v in (cv0,cv1,cv2): stg += vert_words(v)
        return stg, len(tris)
    # SYNTHETIC GATE (Codex): a positive-Q triangle and its ALL-NEGATED twin must canonicalize to BIT-IDENTICAL staging.
    def _pack(tri):
        c=canon_tri(*tri); out=[]
        for v in c: out+=vert_words(v)
        return out
    _tp=[dict(x=10,y=20,s=0.00030,t=0.00040,q=0.0020), dict(x=30,y=25,s=0.00050,t=0.00010,q=0.0030), dict(x=15,y=40,s=0.00020,t=0.00060,q=0.0025)]
    _tn=[dict(x=v["x"],y=v["y"],s=-v["s"],t=-v["t"],q=-v["q"]) for v in _tp]
    if _pack(_tp)!=_pack(_tn): sys.exit("[Ch355] FAIL: canonicalization not bit-exact (positive-Q tri vs its all-negated twin differ)")
    print("[Ch355] canonicalization self-test PASS: positive-Q triangle and its all-negated twin pack BIT-IDENTICALLY (exact ratio/interp preservation)")

    STG_WORDS=2048
    stgA,ntA = feeder_list(drA, CBP_A); stgB,ntB = feeder_list(drB, CBP_B)
    for nm,stg in (("A",stgA),("B",stgB)):
        if len(stg)>STG_WORDS: sys.exit(f"[Ch355] list {nm} {len(stg)} > STG_WORDS {STG_WORDS}")
    if max(ntA,ntB)*3 >= (1<<12): sys.exit(f"[Ch355] staging addr exceeds 12-bit bridge range")
    print(f"[Ch355] list A: {ntA} tris -> {len(stgA)} words (max addr {len(stgA)-1}); list B: {ntB} tris -> {len(stgB)} words (max addr {len(stgB)-1}); both < 12-bit {1<<12}")

    # per-group de-swizzled texture (LINEAR indices -> LPDDR) + CRC ; relocated CLUT bytes + de-gridded palette
    idxA=memA.read_psmt8(tA['tbp'], tA['tbw'], TW, TH); idxB=memB.read_psmt8(tB['tbp'], tB['tbw'], TW, TH)
    palA=RC.read_clut32(memA, tA['cbp'], order="grid"); palB=RC.read_clut32(memB, tB['cbp'], order="grid")
    idxwA=[idxA[i*4]|(idxA[i*4+1]<<8)|(idxA[i*4+2]<<16)|(idxA[i*4+3]<<24) for i in range(TW*TH//4)]
    idxwB=[idxB[i*4]|(idxB[i*4+1]<<8)|(idxB[i*4+2]<<16)|(idxB[i*4+3]<<24) for i in range(TW*TH//4)]
    crcTA=sum(idxwA)&0xFFFFFFFF; crcTB=sum(idxwB)&0xFFFFFFFF
    print(f"[Ch355] LPDDR texture A sum32=0x{crcTA:08x} ; texture B sum32=0x{crcTB:08x} (the two cache-fill CRCs)")

    # independent A->B composed reference (dump order, paint-order DECAL). refmap: [31]cov [30]int [28]overlap [17:9]tu [8:0]tv
    # ALSO per-group refmaps (refmapA/refmapB) so the isolated single-group RTL renders can be scored independently.
    refmap=[0]*(FBPXW*H); refpix=[(0,0,0)]*(FBPXW*H); ndraw=[0]*(FBPXW*H)
    refmapA=[0]*(FBPXW*H); refmapB=[0]*(FBPXW*H)
    for gi,(dr,idxg,palg) in enumerate([(drA,idxA,palA),(drB,idxB,palB)]):
        for (v0,v1,v2) in group_tris(dr):
            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
                    prev_cov = refmap[o]>>31
                    rm=(1<<31)|(interior<<30)|((tu&0x1FF)<<9)|(tv&0x1FF)
                    if gi==0: refmapA[o]=rm
                    else:     refmapB[o]=rm
                    refmap[o]=rm                                             # later group (B) overwrites (paint order)
                    p=palg[idxg[tv*TW+tu]&0xFF]; refpix[o]=(p&0xFF,(p>>8)&0xFF,(p>>16)&0xFF)
                    if gi==1 and prev_cov: refmap[o]|=(1<<28); ndraw[o]=2   # B over A -> overlap
    overlap_px=sum(1 for o in range(FBPXW*H) if refmap[o]&(1<<28))
    covered=sum(1 for w in refmap if w>>31)
    print(f"[Ch355] independent A->B reference: {covered} covered px, {overlap_px} A&B overlap px [bit28]")
    if overlap_px<500: sys.exit(f"[Ch355] FAIL: only {overlap_px} overlap px — multi-texture accumulation not exercised")

    # ---- emit ----
    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("sh3_mtA_tex_lpddr.mem", idxwA, "Ch355 LOCAL texture A (idx19562/tbp=11264) LINEAR -> LPDDR. gitignored.")
    wmem("sh3_mtB_tex_lpddr.mem", idxwB, "Ch355 LOCAL texture B (idx89761/tbp=9216) LINEAR -> LPDDR. gitignored.")
    wmem("sh3_mtA_idx.mem", idxwA, "Ch355 LOCAL idx A. gitignored."); wmem("sh3_mtB_idx.mem", idxwB, "Ch355 LOCAL idx B. gitignored.")
    wmem("sh3_mtA_pal.mem", [p&0xFFFFFFFF for p in palA], "Ch355 LOCAL de-gridded palette A. gitignored.")
    wmem("sh3_mtB_pal.mem", [p&0xFFFFFFFF for p in palB], "Ch355 LOCAL de-gridded palette B. gitignored.")
    wmem("sh3_mt_refmap.mem", refmap, "Ch355 LOCAL A->B composed per-pixel covered|interior|overlap|tu|tv reference. gitignored.")
    wmem("sh3_mtA_refmap.mem", refmapA, "Ch355 LOCAL A-only (idx19562) per-pixel covered|interior|tu|tv reference. gitignored.")
    wmem("sh3_mtB_refmap.mem", refmapB, "Ch355 LOCAL B-only (idx89761) per-pixel covered|interior|tu|tv reference. gitignored.")
    bake.write_feeder_stg_mem("feeder_sh3_mtA.mem", stgA, f"Ch355 LOCAL list A (idx{DRAW_A}, TEX0 CBP={CBP_A}) {ntA} tris. gitignored.", total=STG_WORDS)
    bake.write_feeder_stg_mem("feeder_sh3_mtB.mem", stgB, f"Ch355 LOCAL list B (idx{DRAW_B}, TEX0 CBP={CBP_B}) {ntB} tris. gitignored.", total=STG_WORDS)

    # bootlet: upload BOTH relocated CLUTs (A@CBP_A, B@CBP_B) via two 256x1 BITBLTs; DISPLAY1 = FBPXW x H
    clutwA=[int.from_bytes(clutA[i*4:i*4+4],'little') for i in range(256)]
    clutwB=[int.from_bytes(clutB[i*4:i*4+4],'little') for i in range(256)]
    RAM_QWORDS=512; pay=[]
    for cbp,clutw in ((CBP_A,clutwA),(CBP_B,clutwB)):
        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|=(clutw[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,"payload_sh3_mt.mem"),"w") as f:
        f.write(f"// Ch355 LOCAL two-CLUT setup payload (A->CBP={CBP_A}, B->CBP={CBP_B}). 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("bios_sh3_mt.mem", bake.build_textured_demo_bootlet_disp(qwc, disp_hi, FBW),
        f"Ch355 LOCAL two-CLUT bootlet (QWC={qwc}, DISPLAY1={FBPXW}x{H}). gitignored.")

    with open(os.path.join(DATA,"sh3_mt_params.vh"),"w") as f:
        f.write("// Ch355 LOCAL generated params for the multi-texture integration TB. gitignored.\n")
        for k,v in (("FBW",FBW),("FBPXW",FBPXW),("FBH",H),("VRAM_BYTES_P",VRAM_BYTES),("CBP_A",CBP_A),("CBP_B",CBP_B),
                    ("NEW_TBP",NEW_TBP),("TEX_VRAM_BASE",TEX_VRAM_BASE),("TEX_BYTES",TW*TH),("N_BEATS",TW*TH//32),
                    ("STG_WORDS",STG_WORDS),("TW",TW),("TH",TH),("NTRIS_A",ntA),("NTRIS_B",ntB),
                    ("UNION_OX",OX),("UNION_OY",OY),("CONTENT_W",W)):
            f.write(f"localparam int {k:<14}= {v};\n")
        f.write(f"localparam [29:0] LPDDR_TEX_BASE = 30'h{LPDDR_TEX_BASE:07x};\n")
        f.write(f"localparam [31:0] CRC_TEX_A = 32'h{crcTA:08x};\n")
        f.write(f"localparam [31:0] CRC_TEX_B = 32'h{crcTB:08x};\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","sh3_mt_ref.png"))
        print("[Ch355] wrote sh3_mt_ref.png")
    except Exception as ex: print("(PIL skipped:", ex, ")")
    print(f"[Ch355] emitted multi-texture fixtures -> {DATA}. FB {FBPXW}x{H} stride {STRIDE}; texA crc=0x{crcTA:08x} texB crc=0x{crcTB:08x}")
    return 0

if __name__=="__main__":
    raise SystemExit(main(sys.argv))
