#!/usr/bin/env python3 """retroDE_ps2 — Ch356: N-TEXTURE SCHEDULER (data-driven epoch descriptors, >=3 distinct texture epochs). Generalizes Ch355's hard-coded two-group flow to a DATA-DRIVEN scheduler over ordered authentic draw groups. Each epoch descriptor carries: dump-order index, texture source + LPDDR location + size + expected CRC, relocated CBP + expected palette identity, staging image + word count + expected records, expected drain transition. The scheduler operates over a PRELOADED CLUT table (the bootlet uploads all N CLUTs to distinct relocated CBPs) — runtime CLUT UPLOAD is explicitly OUT of scope (Codex); the scheduler SELECTS among preloaded palettes per epoch. Default epochs (dump order, 3 DISTINCT textures, frame f1): E0=idx11671 (tbp=10240) E1=idx19562 (tbp=11264) E2=idx89761 (tbp=9216). Preflight fail-CLOSED gates (Codex): >=3 epochs w/ >=3 distinct textures; same frame + authentic ascending order; supported format/state; resident texture + CLUT per epoch; distinct palettes (no aliasing); non-overlapping relocated CBPs within BRAM; per-triangle Q canonicalization (all-neg -> negate; mixed/zero -> FAIL CLOSED); each list fits staging (2048); deterministic union framebuffer geometry. Usage: gs_make_sh3_scheduler_fixture.py [dump.gs.zst] [--draw-list i0,i1,i2,...] [--pscale N|auto] [--xy-quant round|floor|ceil|trunc] [--subpixel-xy] [--legacy-strip-kicks] [--cbp-base N] [--ref-sample integer|center] [--ref-xy-quant|--float-ref-xy] [--emit-clamp-header] [--normalize-full-region-clamp] [--clip-fb-guardband] [--auth-scissor-clip] [--capacity-epochs PIXELS] [--emit] LOCAL/gitignored. Reuses gs_make_sh3_multidraw_fixture.load_draws / clip_rect / edge. """ import sys, os, math 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 import gs_texture_residency as R sys.path.insert(0, DATA); import bake DEFAULT_DRAWS=[11671, 19562, 89761] # dump order; textures tbp=10240, 11264, 9216 (all distinct) TEX_BYTES=512*512 CBP_BASE=0x1E000//256 # 480; each epoch relocated +4 blocks (1 KiB) -> non-overlapping STG_WORDS=2048 VRAM_BLOCKS=(128*1024)//256 # 512 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 topology_tris(fv, prim_type, honor_kick=True): """Expand one GS primitive run into independent triangle records. The hardware feeder restarts PRIM for every emitted triangle, so preserving the captured topology here is mandatory: triangle lists are disjoint triples, fans retain vertex zero, and sprites are opposing-corner pairs. """ if prim_type == 3: # TRIANGLE list if len(fv)%3: sys.exit(f"[Ch409] FAIL: TRIANGLE run has {len(fv)} vertices (not divisible by 3)") return [(fv[i],fv[i+1],fv[i+2]) for i in range(0,len(fv),3) if fv[i+2].get("kick",True)] if prim_type == 4: # TRI_STRIP return [(fv[i-2],fv[i-1],fv[i]) for i in range(2,len(fv)) if not honor_kick or fv[i].get("kick",True)] if prim_type == 5: # TRI_FAN return [(fv[0],fv[i-1],fv[i]) for i in range(2,len(fv)) if fv[i].get("kick",True)] if prim_type == 6: # SPRITE: two opposing corners -> two triangles if len(fv)%2: sys.exit(f"[Ch409] FAIL: SPRITE run has {len(fv)} vertices (not divisible by 2)") out=[] for i in range(0,len(fv),2): a,b=fv[i],fv[i+1] if not b.get("kick",True): continue # GS sprite depth/color come from the second vertex. Texture and # XY coordinates are the Cartesian product of the corner pair. def corner(x,y,s,t): v=dict(b); v.update(x=x,y=y,s=s,t=t); return v tl=corner(a["x"],a["y"],a["s"],a["t"]) tr=corner(b["x"],a["y"],b["s"],a["t"]) bl=corner(a["x"],b["y"],a["s"],b["t"]) br=corner(b["x"],b["y"],b["s"],b["t"]) out += [(tl,tr,bl),(tr,br,bl)] return out sys.exit(f"[Ch409] FAIL: unsupported textured primitive type {prim_type}") def coalesce_solid_sprite_grid(fv): """Replace an exactly tiled solid-sprite grid with one rectangle. The GS sprite primitive owns a half-open rectangle. Expanding every captured tile to two triangles independently exposes internal diagonal/ edge exclusions in the triangle walker. This opt-in transform is legal only when the captured rectangles tile their bounding box exactly and all non-coordinate interpolants match. """ if len(fv)<2 or len(fv)%2: sys.exit(f"[Ch434] cannot coalesce {len(fv)} sprite vertices") rects=[] sig=None for i in range(0,len(fv),2): a,b=fv[i],fv[i+1] x0,x1=sorted((a['x'],b['x'])); y0,y1=sorted((a['y'],b['y'])) if x0==x1 or y0==y1: sys.exit("[Ch434] degenerate solid-sprite tile") cur=(a.get('rgba'),b.get('rgba'),a.get('z'),b.get('z'),a.get('fog'),b.get('fog')) if sig is None: sig=cur elif cur!=sig: sys.exit("[Ch434] solid-sprite grid changes color/Z/fog") rects.append((x0,y0,x1,y1)) xs=sorted({x for r in rects for x in (r[0],r[2])}) ys=sorted({y for r in rects for y in (r[1],r[3])}) for ya,yb in zip(ys,ys[1:]): for xa,xb in zip(xs,xs[1:]): cx=(xa+xb)/2; cy=(ya+yb)/2 n=sum(x0<=cx=len(a) or ":" not in a[ai+1]: sys.exit("[Ch434] --override-ct32 requires DRAW_IDX:LINEAR_MEM") oi,op=a[ai+1].split(":",1) OVERRIDE_CT32[int(oi)]=op EMIT_CLAMP_HEADER = "--emit-clamp-header" in a NORMALIZE_FULL_REGION_CLAMP = "--normalize-full-region-clamp" in a AUTH_COLOR_TFX = "--auth-color-tfx" in a # Ch415 fixture-side implementation of the captured GS fog subset used by # this SH3 frame. Every selected scene draw has PRIM.FGE=1 and FOGCOL=0, # so fog is a multiply toward black. Folding each vertex RGB by F/256 # before the existing Gouraud+MODULATE path is exact at vertices and a # bounded approximation within triangles, without a blanket post-process # pass that incorrectly erases the foreground character. AUTH_FOG_BLACK_FOLD = "--auth-fog-black-fold" in a # Real per-vertex GS fog: for draws whose captured PRIM has FGE=1, set # PRIM.FGE (bit 5) in the emitted PRIM descriptor and pack the vertex commit # word as XYZF2 (0x04) carrying the 8-bit fog coefficient F=[63:56] atop the # SAME 24-bit Z. The feeder then tags the commit XYZF2 and gs_stub applies # texel*F + FOGCOL*(255-F) at emit. This is the correct per-pixel path, # distinct from --auth-fog-black-fold (an RGB fold that DECAL discards). # Default ON; --no-auth-fog reverts to XYZ2 (fog dropped). FGE=0 draws are # unaffected either way (they always emit XYZ2), preserving byte-identity. AUTH_FOG = "--no-auth-fog" not in a REF_BILINEAR = "--ref-bilinear" in a # Preserve the capture's native GS 12.4 screen coordinates in XYZ2. # The legacy/signoff path quantizes to integer pixels first; keeping this # opt-in lets the subpixel RTL land without perturbing old fixtures. SUBPIXEL_XY = "--subpixel-xy" in a # Preserve the exact strip population used by the Ch415 signoff fixture # for controlled A/B fidelity work. The newer parsed-kick interpretation # otherwise removes broad layers and confounds an XY-only experiment. LEGACY_STRIP_KICKS = "--legacy-strip-kicks" in a TRI_EPOCHS = "--tri-epochs" in a CLIP_FB_GUARDBAND = "--clip-fb-guardband" in a AUTH_SCISSOR_CLIP = "--auth-scissor-clip" in a COALESCE_SOLID_SPRITES = "--coalesce-solid-sprites" in a HALF_OPEN_SPRITES = "--half-open-sprites" in a FAST_FIT_SCALE = "--fast-fit-scale" in a SKIP_MIXED_Q = "--skip-mixed-q-tris" in a CAPACITY_EPOCH_PIXELS = int(a[a.index("--capacity-epochs")+1]) if "--capacity-epochs" in a else None if CAPACITY_EPOCH_PIXELS is not None and CAPACITY_EPOCH_PIXELS <= 0: sys.exit(f"[Ch407] FAIL: --capacity-epochs must be positive, got {CAPACITY_EPOCH_PIXELS}") if CAPACITY_EPOCH_PIXELS is not None and TRI_EPOCHS: sys.exit("[Ch407] FAIL: choose --tri-epochs or --capacity-epochs, not both") if CBP_RELOC_BASE < 0 or CBP_RELOC_BASE >= VRAM_BLOCKS: sys.exit(f"[Ch356] FAIL: --cbp-base {CBP_RELOC_BASE} outside BRAM block range 0..{VRAM_BLOCKS-1}") # Ch359 packs a fixed number of consecutive draws into an epoch. Ch364 adds an explicit group partition so # a real ordered scene can rebind textures and then retain one texture for several larger feeder lists. if "--group-size" in a and "--group-sizes" in a: sys.exit("[Ch359] FAIL: choose only one of --group-size or --group-sizes") if "--group-sizes" in a: GROUP_SIZES=[int(x) for x in a[a.index("--group-sizes")+1].split(",")] if not GROUP_SIZES or any(x < 1 for x in GROUP_SIZES): sys.exit(f"[Ch364] FAIL: --group-sizes must contain positive integers, got {GROUP_SIZES}") if sum(GROUP_SIZES) != len(idxs): sys.exit(f"[Ch364] FAIL: --group-sizes sums to {sum(GROUP_SIZES)}, need {len(idxs)} draws") MIXED_GROUPS=True else: GROUP=int(a[a.index("--group-size")+1]) if "--group-size" in a else 1 if GROUP < 1: sys.exit(f"[Ch359] FAIL: --group-size must be >=1, got {GROUP}") if len(idxs) % GROUP != 0: sys.exit(f"[Ch359] FAIL: {len(idxs)} draws not divisible by --group-size {GROUP}") GROUP_SIZES=[GROUP]*(len(idxs)//GROUP) MIXED_GROUPS=False # Explicit grouping also permits deliberately separate singleton groups; # this is needed to interleave isolated draw families chronologically. GROUPED=("--group-size" in a or "--group-sizes" in a) N_EP=len(GROUP_SIZES) REF_SAMPLE = a[a.index("--ref-sample")+1].lower() if "--ref-sample" in a else "integer" if REF_SAMPLE not in ("integer", "center"): sys.exit(f"[Ch356] FAIL: --ref-sample must be integer or center, got {REF_SAMPLE}") if "--ref-xy-quant" in a and "--float-ref-xy" in a: sys.exit("[Ch356] FAIL: choose only one of --ref-xy-quant or --float-ref-xy") # ZSCHED signoff fixtures feed integer XYZ2 screen coordinates to the RTL, so the reference must use the same # quantized geometry. --float-ref-xy is kept only as a report-only fidelity-debt diagnostic. REF_XY_QUANT = ("--ref-xy-quant" in a) or (AUTHZ and TAG in ("zsched", "zs640", "zs640c6") and "--float-ref-xy" not in a) XY_QUANT_RAW = a[a.index("--xy-quant")+1] if "--xy-quant" in a else "round" XY_QUANT_LIST = [x.strip().lower() for x in XY_QUANT_RAW.split(",")] if "," in XY_QUANT_RAW else None if XY_QUANT_LIST is not None and len(XY_QUANT_LIST) != N_EP: sys.exit(f"[Ch356] FAIL: comma --xy-quant needs {N_EP} entries (one per EPOCH), got {len(XY_QUANT_LIST)}") for q in (XY_QUANT_LIST if XY_QUANT_LIST is not None else [XY_QUANT_RAW.lower()]): if q not in ("round", "floor", "ceil", "trunc"): sys.exit(f"[Ch356] FAIL: --xy-quant must be round/floor/ceil/trunc, got {q}") PSCALE_RAW = a[a.index("--pscale")+1] if "--pscale" in a else "4096" PSCALE_LIST = [x.strip().lower() for x in PSCALE_RAW.split(",")] if "," in PSCALE_RAW else None PSCALE_AUTO = PSCALE_RAW.lower() in ("auto", "pertri", "per-tri") or (PSCALE_LIST is not None and any(x in ("auto","pertri","per-tri") for x in PSCALE_LIST)) PSCALE_ARG = None if (PSCALE_AUTO or PSCALE_LIST is not None) else int(PSCALE_RAW) if PSCALE_LIST is not None and len(PSCALE_LIST) != N_EP: sys.exit(f"[Ch356] FAIL: comma --pscale needs {N_EP} entries (one per EPOCH), got {len(PSCALE_LIST)}") if (not PSCALE_AUTO) and PSCALE_LIST is None and PSCALE_ARG <= 0: sys.exit(f"[Ch356] FAIL: --pscale must be positive, got {PSCALE_ARG}") PFX = f"sh3_{TAG}" if TAG else ("sh3_s640" if FB640 else "sh3_sched") CH = (a[a.index("--chapter")+1] if "--chapter" in a else ("Ch364" if MIXED_GROUPS else ("Ch359" if GROUPED else ("Ch357" if (FB640 or AUTHZ) else "Ch356")))) 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"[Ch356] dump={os.path.basename(dump)} epochs(dump order)={idxs}") # Parse the 15 MiB capture once. Full chronological fixtures can contain # hundreds of draws; reparsing and replaying from frame zero per draw made # generation scale quadratically. collected=R.collect(dump,0) got, vram = MD.load_draws(dump, idxs, collected=collected, allow_untextured=ALLOW_UNTEXTURED) if vram is None: sys.exit("[Ch409] FAIL: GS VRAM snapshot absent") for i in idxs: if i not in got: sys.exit(f"[Ch356] FAIL: idx{i} not found as a supported draw") eps=[got[i] for i in idxs] for e in eps: if e['first_idx'] in OVERRIDE_CT32: t0=e['state']['tex0'] if t0['tw']!=256 or t0['th']!=256: sys.exit(f"[Ch434] override idx{e['first_idx']} requires captured 256x256 TEX0, got {t0['tw']}x{t0['th']}") # The staged source is already decoded to linear CT32. Retain # the captured dimensions/function but describe the replacement # with its real 256-pixel physical row stride. t0.update(psm=0x00,tbw=4,tw=256,th=256) # ---- gate: enough draws, authentic ascending dump order, same frame ---- if not GROUPED and len(eps)<3: sys.exit(f"[Ch356] FAIL: need >=3 epochs, got {len(eps)}") # A single exact-state group is valid for a recovered local draw family; # capacity scheduling may still split it into several bounded epochs. if GROUPED and N_EP<1: sys.exit(f"[Ch359] FAIL: grouped mode needs >=1 epoch, got {N_EP}") if idxs!=sorted(idxs): sys.exit(f"[Ch356] FAIL: --draw-list not ascending dump order {idxs}") frames={e["frame"] for e in eps} if len(frames)!=1: sys.exit(f"[Ch356] FAIL: epochs span multiple frames {sorted(frames)}") # ---- gate: texture layout + supported format/state per draw ---- tbps=[e["state"]["tex0"]["tbp"] for e in eps] if not GROUPED: if len(set(tbps))<3: sys.exit(f"[Ch356] FAIL: <3 distinct textures {tbps}") for e in eps: t0=e["state"]["tex0"]; pr=e["state"]["prim"] # The LPDDR cache remains a 512x512 address window, but the authentic # sampler descriptor may address a smaller PSMT8 tile within it. indexed_shape_ok = ((t0["psm"]==0x00 and t0["tw"]==64 and t0["th"]==64 and t0["tbw"]==1) or (e['first_idx'] in OVERRIDE_CT32 and t0["psm"]==0x00 and t0["tw"]==256 and t0["th"]==256 and t0["tbw"]==4) or (t0["psm"]==0x13 and t0["tw"] in (128,256,512) and t0["th"] in (128,256,512)) or (t0["psm"]==0x14 and t0["tw"]==512 and t0["th"]==1024 and t0["tbw"]==8)) textured_ok = indexed_shape_ok and pr["tme"]==1 and pr["fst"]==0 untextured_ok = ALLOW_UNTEXTURED and pr["tme"]==0 if not (textured_ok or untextured_ok): sys.exit(f"[Ch356] FAIL: idx{e['first_idx']} unsupported format/state (psm=0x{t0['psm']:02x} {t0['tw']}x{t0['th']} tme={pr['tme']} fst={pr['fst']})") if pr["abe"] and not ALLOW_ABE: sys.exit(f"[Ch356] FAIL: idx{e['first_idx']} is ABE=1; pass --allow-abe only for the LPDDR destination-blend path") if ALLOW_ABE: fbmask=(e['state']['frame']>>32)&0xFFFFFFFF mask_bytes=[(fbmask>>(8*i))&0xFF for i in range(4)] if any(x not in (0x00,0xFF) for x in mask_bytes): sys.exit(f"[{CH}] FAIL: idx{e['first_idx']} FRAME.FBMSK 0x{fbmask:08x} is not byte-granular") # Texture addressing state is irrelevant for TME=0. Do not reject # an untextured draw because a stale TEX0/CLAMP register happens to # use a mode unsupported by the sampler it bypasses. if not pr["tme"]: continue clamp=e["state"]["clamp"] wms=clamp&3; wmt=(clamp>>2)&3 minu=(clamp>>4)&0x3FF; maxu=(clamp>>14)&0x3FF minv=(clamp>>24)&0x3FF; maxv=(clamp>>34)&0x3FF if wms>1 or wmt>1: u_full=(wms==2 and minu==0 and maxu==t0["tw"]-1) v_full=(wmt==2 and minv==0 and maxv==t0["th"]-1) if (wms>1 and not u_full) or (wmt>1 and not v_full) or wms==3 or wmt==3: sys.exit(f"[Ch356] FAIL: idx{e['first_idx']} uses unmodelled CLAMP WMS={wms} [{minu},{maxu}] WMT={wmt} [{minv},{maxv}]") if not NORMALIZE_FULL_REGION_CLAMP: sys.exit(f"[Ch356] FAIL: idx{e['first_idx']} uses full-texture region CLAMP; pass --normalize-full-region-clamp to prove and normalize it") # GS region-clamp [0,size-1] is exactly ordinary clamp for this texture. # Normalize only the mode bits; retaining the bounds makes the emitted # state auditable while keeping the software reference on modes 0/1. clamp=(clamp&~0xF) | ((1 if wms==2 else wms)<<0) | ((1 if wmt==2 else wmt)<<2) e["state"]["clamp"]=clamp print(f"[{CH}] idx{e['first_idx']} normalized full-region CLAMP WMS/WMT {wms}/{wmt} -> {clamp&3}/{(clamp>>2)&3}") if not GROUPED: print(f"[Ch356] frame f{eps[0]['frame']}; {len(eps)} epochs, textures tbp={tbps} (all distinct); PSMT8 512x512 perspective TME — supported") # ---- gate: resident texture + CLUT per epoch; collect CRC + palette; assign relocated CBP ---- descs=[]; pals=[]; next_cbp=CBP_RELOC_BASE; group_no=0; group_acc=None memseq=RC.iter_localmem_at(dump,idxs,collected=collected) for k,(e,(mem_idx,mem)) in enumerate(zip(eps,memseq)): if mem_idx != e["first_idx"]: sys.exit(f"[Ch409] internal localmem sequence mismatch {mem_idx} != {e['first_idx']}") t0=e["state"]["tex0"] is_textured=bool(e["state"]["prim"]["tme"]) override_path=OVERRIDE_CT32.get(e['first_idx']) tex=(bytes(mem.m[t0['tbp']*256:t0['tbp']*256+TEX_BYTES]) if is_textured and override_path is None else bytes(TEX_BYTES)) clut=((bytes(1024) if t0['psm']==0x00 else bytes(mem.m[t0['cbp']*256:t0['cbp']*256+1024])) if is_textured else bytes(1024)) if is_textured and override_path is None and not any(tex): sys.exit(f"[Ch356] FAIL: texture idx{e['first_idx']} @tbp={t0['tbp']} not resident") if is_textured and t0['psm']!=0x00 and not any(clut): sys.exit(f"[Ch356] FAIL: CLUT idx{e['first_idx']} @cbp={t0['cbp']} not resident") # The cache is physically 512 texels wide. Place a captured 256x256 # texture into that stride so TEX0 can retain logical TW/TH=8. Keep # the historical 128/512 reconstruction byte-identical. if override_path is not None: vals=[] with open(override_path) as of: for ln in of: s=ln.strip() if s and not s.startswith(("//","#")): vals.append(int(s,16)&0xFFFFFFFF) if len(vals)==640*480: idxw=[vals[y*640+x] for y in range(256) for x in range(256)] elif len(vals)==256*256: idxw=vals else: sys.exit(f"[Ch434] override idx{e['first_idx']} has {len(vals)} words; expected 65536 or 307200") elif not is_textured: idxw=[0]*(TEX_BYTES//4) elif t0['psm']==0x00: # Direct-color 64x64 sources are linearized into the upper-left # of the fixed 512-wide, 256-KiB cache window. TEX0 is rebound # with TBW=8 below, so each source row must use this wider stride. idx=[0]*(512*128) for yy in range(64): for xx in range(64): idx[yy*512+xx]=mem.read_ct32_word(t0['tbp'],t0['tbw'],xx,yy) idxw=idx elif t0['psm']==0x14: # The recovered character texture is 512x1024 PSMT4: exactly # 256 KiB when packed two texels per byte, matching the existing # cache window without padding or an RTL geometry change. src4=mem.read_psmt4(t0['tbp'],t0['tbw'],t0['tw'],t0['th']) idx=bytearray((src4[n]&15)|((src4[n+1]&15)<<4) for n in range(0,len(src4),2)) if len(idx)!=TEX_BYTES: sys.exit(f"[{CH}] FAIL: idx{e['first_idx']} packed PSMT4 cache is {len(idx)} bytes, expected {TEX_BYTES}") elif t0['tw']==256 and t0['th']==256: src=mem.read_psmt8(t0['tbp'], t0['tbw'], 256, 256) idx=bytearray(512*512) for yy in range(256): idx[yy*512:yy*512+256]=src[yy*256:(yy+1)*256] # Fail closed before the emitted cache image and RTL can agree on # the same bad host-side remap. The logical tile must survive # byte-exact at the start of every physical 512-texel row, and # all padding (right half plus unused lower rows) must stay zero. for yy in range(256): row=idx[yy*512:(yy+1)*512] if row[:256] != src[yy*256:(yy+1)*256] or any(row[256:]): sys.exit(f"[{CH}] FAIL: idx{e['first_idx']} 256x256 logical texture cache row {yy} remap mismatch") if any(idx[256*512:]): sys.exit(f"[{CH}] FAIL: idx{e['first_idx']} 256x256 logical texture lower cache padding is nonzero") else: idx=mem.read_psmt8(t0['tbp'], t0['tbw'], 512, 512) if t0['tw']==128 and t0['th']==128: # The emitted cache descriptor intentionally widens TBW to # eight, so verify that the logical source tile occupies the # matching upper-left portion of the 512-wide cache image. src=mem.read_psmt8(t0['tbp'],t0['tbw'],128,128) for yy in range(128): if idx[yy*512:yy*512+128] != src[yy*128:(yy+1)*128]: sys.exit(f"[{CH}] FAIL: idx{e['first_idx']} 128x128 logical texture cache row {yy} remap mismatch") if is_textured and t0['psm']!=0x00: idxw=[idx[i*4]|(idx[i*4+1]<<8)|(idx[i*4+2]<<16)|(idx[i*4+3]<<24) for i in range(512*512//4)] crc=sum(idxw)&0xFFFFFFFF pal=(tuple([0]*256) if (not is_textured or t0['psm']==0x00) else tuple(RC.read_clut32(mem,t0['cbp'],order="grid"))) one=dict(k=k, idx=e["first_idx"], tbp=t0['tbp'], cbp_orig=t0['cbp'], cbp_reloc=None, crc=crc, bbox=bbox(e), lpddr=0x200000, size=TEX_BYTES, e=e, state=e["state"], idxw=idxw, pal=list(pal), clut=clut, logical_tw=(t0['tw'] if is_textured else 512), logical_th=(t0['th'] if is_textured else 512), tex_psm=(t0['psm'] if is_textured else 0x13), tex_tbw=(4 if override_path is not None else 8)) if not GROUPED: pals.append(pal); descs.append(one) continue # Fold a declared group immediately so hundreds of full-frame draws # do not retain hundreds of 256 KiB Python texture arrays at once. if group_acc is None: group_acc=dict(one); group_acc.update(k=group_no,idx_all=[],members=[]) else: s0=group_acc['state'] for key in ("prim","tex0","test","zbuf","clamp","alpha","texa"): if one['state'].get(key)!=s0.get(key): sys.exit(f"[{CH}] FAIL: idx{one['idx']} state '{key}' differs from idx{group_acc['idx']} inside group {group_no}") if one['idxw']!=group_acc['idxw'] or one['clut']!=group_acc['clut']: sys.exit(f"[{CH}] FAIL: idx{one['idx']} texture/CLUT bytes differ from idx{group_acc['idx']} inside group {group_no}") ba=group_acc['bbox']; bb=one['bbox'] group_acc['bbox']=(min(ba[0],bb[0]),min(ba[1],bb[1]),max(ba[2],bb[2]),max(ba[3],bb[3])) group_acc['idx_all'].append(one['idx']); group_acc['members'].append(e); group_acc['e']=None if len(group_acc['members'])==GROUP_SIZES[group_no]: s0=group_acc['state']; tst=s0['test']; zbf=s0['zbuf'] zte=(tst>>16)&1; ztst=(tst>>17)&3; zpsm=(zbf>>24)&0xF; zmsk=(zbf>>32)&1 untextured_group=all(not m['state']['prim']['tme'] for m in group_acc['members']) override_group=all(m['first_idx'] in OVERRIDE_CT32 for m in group_acc['members']) # Authentic light-buffer setup uses ZTEST=ALWAYS for its opaque # gray clear and final bias-removal sprite, while the intervening # volumes use GEQUAL against the scene Z and never write Z. z_ok=(zte==1 and zpsm==0xA and (ztst==2 or ((ALLOW_UNTEXTURED and untextured_group) or override_group) and ztst==1 and zmsk==1)) if not z_ok: sys.exit(f"[{CH}] FAIL: group {group_no} idx{group_acc['idx']} lacks authentic ZTE=1/GEQUAL/PSMZ16S state") reuse=bool(descs and group_acc['idxw']==descs[-1]['idxw'] and group_acc['clut']==descs[-1]['clut']) if RUNTIME_CLUT: group_acc['cbp_reloc']=CBP_RELOC_BASE else: group_acc['cbp_reloc']=descs[-1]['cbp_reloc'] if reuse else next_cbp if not reuse: next_cbp+=4 group_acc['reuse']=int(reuse); descs.append(group_acc) group_no+=1; group_acc=None if GROUPED and (group_acc is not None or group_no!=len(GROUP_SIZES)): sys.exit(f"[{CH}] FAIL: group fold ended at {group_no}/{len(GROUP_SIZES)}") if not GROUPED: # Runtime-CLUT fixtures deliberately reuse one staging CBP: the host # commits the epoch palette before GO. Keep each draw as its own # chronological epoch while retaining an already resident identical # texture/CLUT payload. The old preloaded-CLUT mode still requires a # distinct relocated palette for every epoch. if RUNTIME_CLUT: for k,d in enumerate(descs): d['cbp_reloc']=CBP_RELOC_BASE d['reuse']=int(k>0 and d['idxw']==descs[k-1]['idxw'] and d['clut']==descs[k-1]['clut']) else: for k,d in enumerate(descs): d['cbp_reloc']=CBP_RELOC_BASE+k*4 # ---- gate: distinct palettes (real multi-texture, no aliasing) ---- if not RUNTIME_CLUT: for i in range(len(pals)): for j in range(i+1,len(pals)): if pals[i]==pals[j]: sys.exit(f"[Ch356] FAIL: epochs {idxs[i]},{idxs[j]} share a palette (aliasing)") # ---- gate: non-overlapping relocated CBPs within BRAM ---- for d in descs: if d['cbp_reloc']+4 > VRAM_BLOCKS: sys.exit(f"[Ch356] FAIL: relocated CBP {d['cbp_reloc']} (+4) exceeds VRAM {VRAM_BLOCKS} blocks") cbps=[d['cbp_reloc'] for d in descs] if not RUNTIME_CLUT: for i in range(len(cbps)): for j in range(i+1,len(cbps)): if abs(cbps[i]-cbps[j])<4: sys.exit(f"[Ch356] FAIL: relocated CBPs {cbps[i]},{cbps[j]} overlap") print(f"[Ch356] all {len(eps)} textures + CLUTs resident; palettes pairwise-distinct; relocated CBPs {cbps} distinct + in BRAM (512 blocks)") else: print(f"[{CH}] all {len(eps)} textures + CLUTs resident; runtime palette staging CBP={CBP_RELOC_BASE}; reuse epochs={sum(d['reuse'] for d in descs)}") else: print(f"[{CH}] {len(eps)} draws -> {N_EP} validated epochs; " f"groups {descs[0]['idx_all']} ... {descs[-1]['idx_all']}; reuse epochs={sum(d['reuse'] for d in descs)}") for d in descs: if d['cbp_reloc']+4 > VRAM_BLOCKS: sys.exit(f"[{CH}] FAIL: relocated CBP {d['cbp_reloc']} (+4) exceeds BRAM {VRAM_BLOCKS} blocks") # ---- framebuffer geometry ---- UX=max(d['bbox'][2] for d in descs); UY=max(d['bbox'][3] for d in descs) UMINX=int(min(d['bbox'][0] for d in descs)); UMINY=int(min(d['bbox'][1] for d in descs)) LPDDR_TEX_BASE_P = 0x00200000 if FB640: # Ch357: NATIVE 640x480 — preserve authentic screen coordinates (NO union-origin translation). OX=0; OY=0; FBPXW=640; H=480; FBW=10; STRIDE=2560; W=FBPXW # Most fixtures must be wholly in bounds. Post-process fans may carry # authentic GS guard-band vertices outside the display; accept those # only with an explicit opt-in, then use the existing x/y/s/t/q/Z/RGBA # polygon clip below before quantization and emission. if UMINX<0 or UMINY<0 or UX>FBPXW-1 or UY>H-1: if not CLIP_FB_GUARDBAND: sys.exit(f"[Ch357] FAIL: authentic coords x[{UMINX}..{UX:.0f}] y[{UMINY}..{UY:.0f}] exceed 640x480; pass --clip-fb-guardband only for intentional GS guard-band geometry") print(f"[{CH}] guard-band clip enabled: authentic coords x[{UMINX}..{UX:.0f}] y[{UMINY}..{UY:.0f}] -> 640x480") FB_END = FBPXW*H*4 if FBW!=10 or STRIDE!=2560 or FB_END!=0x12C000 or STRIDE//32!=80 or (STRIDE//32*H)!=38400: sys.exit(f"[Ch357] FAIL: geometry mismatch (FBW={FBW} stride={STRIDE} size=0x{FB_END:x} beats/row={STRIDE//32} beats/frame={STRIDE//32*H})") # gate: FB region [0,FB_END) disjoint from the texture region [0x200000, +TEX_BYTES). if FB_END > LPDDR_TEX_BASE_P: sys.exit(f"[Ch357] FAIL: FB end 0x{FB_END:x} overlaps texture base 0x{LPDDR_TEX_BASE_P:x}") print(f"[Ch357] NATIVE 640x480 (FBW={FBW}) stride={STRIDE}B size=0x{FB_END:x} beats/row={STRIDE//32} " f"scanout={STRIDE//32*H} beats/frame; authentic coords x[{UMINX}..{UX:.0f}] y[{UMINY}..{UY:.0f}] " f"(no union translation); FB[0..0x{FB_END:x}) disjoint from tex[0x{LPDDR_TEX_BASE_P:x}..0x{LPDDR_TEX_BASE_P+TEX_BYTES:x})") else: # Ch356: union-cropped geometry. OX=UMINX; OY=UMINY W=int(UX)-OX+1; H=int(UY)-OY+1; FBW=(W+63)//64; FBPXW=FBW*64; STRIDE=FBPXW*4 print(f"[Ch356] UNION origin=({OX},{OY}) content={W}x{H} -> stored FB {FBPXW}x{H} (FBW={FBW}) stride={STRIDE}B " f"size=0x{FBPXW*H*4:x} scanout={STRIDE//32*H} beats/frame") # ---- gate: per-triangle Q canonicalization (all-neg -> negate; mixed/zero -> FAIL CLOSED) + list fits staging ---- for d in descs: tris=[] # Ch359: merged epochs expand EACH member draw's strip separately (no phantom bridging triangles), dump order. for e_m in (d['members'] if d.get('members') else [d['e']]): fv=[dict(x=v["x"]-OX,y=v["y"]-OY,z=v.get("z"),fog=v.get("fog"),rgba=v.get("rgba"), s=v["s"],t=v["t"],q=v["q"],kick=v.get("kick",True)) for v in e_m["verts"]] if (COALESCE_SOLID_SPRITES and e_m["state"]["prim"]["type"]==6 and not e_m["state"]["prim"]["tme"]): fv=coalesce_solid_sprite_grid(fv) if HALF_OPEN_SPRITES and e_m["state"]["prim"]["type"]==6: fv=half_open_sprite_rects(fv) raw=topology_tris(fv,e_m["state"]["prim"]["type"], honor_kick=not LEGACY_STRIP_KICKS) clip_w=FBPXW; clip_h=H if AUTH_SCISSOR_CLIP: sc=e_m["state"].get("scissor") if sc is None: sys.exit(f"[{CH}] FAIL: idx{d['idx']} missing authentic SCISSOR state") sx0=sc&0x7FF; sx1=(sc>>16)&0x7FF; sy0=(sc>>32)&0x7FF; sy1=(sc>>48)&0x7FF if sx0!=0 or sy0!=0: sys.exit(f"[{CH}] FAIL: nonzero SCISSOR origin ({sx0},{sy0}) not yet modelled") clip_w=min(clip_w,sx1+1); clip_h=min(clip_h,sy1+1) for tri in raw: for ct in MD.clip_rect(tri, clip_w, clip_h): qs=[v["q"] for v in ct] if SKIP_MIXED_Q and not (all(q>0 for q in qs) or all(q<0 for q in qs)): d['mixed_q_skipped']=d.get('mixed_q_skipped',0)+1 continue tris.append(ct) nneg=0 for (v0,v1,v2) in tris: qs=[v0["q"],v1["q"],v2["q"]] if all(q>0 for q in qs): pass elif all(q<0 for q in qs): nneg+=1 else: sys.exit(f"[Ch356] FAIL-CLOSED: idx{d['idx']} mixed-sign/zero-Q triangle (qs={qs}) — signed-Q unsupported") d['ntris']=len(tris); d['words']=(8 if EMIT_CLAMP_HEADER else 7)+len(tris)*9; d['nneg']=nneg if d['words']>STG_WORDS and CAPACITY_EPOCH_PIXELS is None: sys.exit(f"[Ch356] FAIL: idx{d['idx']} list {d['words']} words > staging {STG_WORDS}") print(f"[Ch356] PREFLIGHT PASS. epoch descriptors (dump order):") print(f" {'idx':>7} {'tbp':>6} {'cbp_reloc':>9} {'tex_crc':>10} {'lpddr':>9} {'tris':>5} {'words':>6} {'neg-Q':>6} {'expect_records':>14}") for d in descs: print(f" {d['idx']:>7} {d['tbp']:>6} {d['cbp_reloc']:>9} 0x{d['crc']:08x} 0x{d['lpddr']:07x} {d['ntris']:>5} {d['words']:>6} {d['nneg']:>6} {d['ntris']:>14}") if "--emit" not in a: print("[Ch356] (preflight only; --emit stage pending)"); return 0 # ================= --emit: N feeder lists + N textures/CLUTs + bootlet (ALL CLUTs preloaded) + composed ref ================= NEW_TBP=0x40000//256; TEX_VRAM_BASE=NEW_TBP*256; LPDDR_TEX_BASE=0x00200000; VRAM_BYTES=0x20000 PERSP_FRAC=bake.PERSP_FRAC; PSCALE=PSCALE_ARG; TW=512; TH=512; TW_LOG=TH_LOG=9; TBW_TEX=8; S24_MAX=(1<<23)-1 # The capture contains both tiny homogeneous STQ values (opaque scene # geometry) and post-process/fan draws with normalized S/T~=1, Q=1. A # common positive scale cancels in S/Q; include exact binary fractions so # the latter fit the signed 24-bit S/T fields without sacrificing ratio. AUTO_PSCALES=(1/4096,1/3072,1/2048,1/1536,1/1024,1/768,1/512,1/384,1/256,1/192,1/128,1/96,1/64,1/48,1/32,1/24, 0.0625,0.09375,0.125,0.1875,0.25,0.375,0.5,0.75,1, 2,4,8,16,32,64,128,256,384,512,768,1024,1536,2048,3072,4096,6144) if PSCALE_LIST is not None: print(f"[{CH}] perspective fixed-point scale PSCALE per-epoch spec={PSCALE_LIST}; auto candidates={AUTO_PSCALES}") elif PSCALE_AUTO: print(f"[{CH}] perspective fixed-point scale PSCALE=auto per-triangle candidates={AUTO_PSCALES}") else: print(f"[{CH}] perspective fixed-point scale PSCALE={PSCALE} (effective frac {PERSP_FRAC}+{PSCALE.bit_length()-1})") print(f"[{CH}] XY mode={'native-12.4' if SUBPIXEL_XY else 'integer-quantized'}" f" quantization per-epoch spec={XY_QUANT_LIST if XY_QUANT_LIST is not None else [XY_QUANT_RAW.lower()]*len(idxs)}") edge=MD.edge; clip=MD.clip_rect def tex0_word(cbp, tfx=1, logical_tw=TW, logical_th=TH, tex_psm=0x13, tex_tbw=TBW_TEX): tw_log=logical_tw.bit_length()-1; th_log=logical_th.bit_length()-1 v=bake.tex0_pack(NEW_TBP,tex_tbw,psm=tex_psm,tw=tw_log,th=th_log,tfx=tfx) v|=(cbp&0x3FFF)<<37; v|=(0&0x1)<<55; v|=(0&0x1F)<<56; v|=((0 if RUNTIME_CLUT else 1)&0x7)<<61 return v def canon(v0,v1,v2): qs=[v0["q"],v1["q"],v2["q"]] if all(q>0 for q in qs): return (v0,v1,v2) neg=lambda v: dict(v, s=-v["s"],t=-v["t"],q=-v["q"]) # negate S/T/Q only; Z/color unchanged return (neg(v0),neg(v1),neg(v2)) # z-aware triangle clip (screen-linear Z, like the GS): mirrors MD.clip_rect but interpolates Z too. def clip_rect_z(tri, W, Hh): def lerp(p1,p2,al): out={k:(p1[k]+al*(p2[k]-p1[k])) for k in ("x","y","z","fog","s","t","q")} c=[] for sh in (0,8,16,24): a=(p1["rgba"]>>sh)&0xFF; b=(p2["rgba"]>>sh)&0xFF c.append(max(0,min(255,int(round(a+al*(b-a)))))) out["rgba"]=c[0]|(c[1]<<8)|(c[2]<<16)|(c[3]<<24) return out 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"],z=v.get("z",0.0),fog=v.get("fog",255.0), s=v["s"],t=v["t"],q=v["q"],rgba=v.get("rgba",0xFFFFFFFF)) 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"]))); poly=poly or [] 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"]<=Hh, lambda A,B:lerp(A,B,(Hh-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 edge_i(px, py, ax, ay, bx, by): return (px-ax)*(by-ay)-(py-ay)*(bx-ax) def top_or_left(ax, ay, bx, by): dx=bx-ax; dy=by-ay return (dy>0) or (dy==0 and dx>0) def trunc_div(num, den): if den==0: return 0 return (-1 if (num<0)^(den<0) else 1) * (abs(num)//abs(den)) def grad_num_dadx(a0,a1,a2,x0,y0,x1,y1,x2,y2): return ((a1-a0)*(y2-y0)-(a2-a0)*(y1-y0)) << 16 def grad_num_dady(a0,a1,a2,x0,y0,x1,y1,x2,y2): return ((a2-a0)*(x1-x0)-(a1-a0)*(x2-x0)) << 16 def interp_wide(base, dadx, dady, x, y, x0, y0): return (base + ((dadx*(x-x0) + dady*(y-y0)) >> 16)) & 0xFFFFFF def recip_lut(q, idx_bits=11, scale=24): out_max=(1<<(scale+1))-1 if q<=0: return out_max top=idx_bits-1; e=q.bit_length()-1 m=(q>>(e-top)) if e>=top else (q<<(top-e)) m &= (1<>e, out_max) def persp_uv(s, t, q): r=recip_lut(q); u=(s*r)>>24; v=(t*r)>>24 return (min(u,2047), min(v,2047)) def quant_xy(v, mode): if mode=="floor": return math.floor(v) if mode=="ceil": return math.ceil(v) if mode=="trunc": return math.trunc(v) return round(v) def pack_v(v, pscale, xy_mode): s_fp=round(v["s"]*TW*(1<S24_MAX or abs(t_fp)>S24_MAX: return None if q_fp<=0 or q_fp>0xFFFFFF: return None return dict(x=max(0,min(FBPXW-1,int(quant_xy(v["x"],xy_mode)))), y=max(0,min(H-1,int(quant_xy(v["y"],xy_mode)))), s=s_fp&0xFFFFFF, t=t_fp&0xFFFFFF, q=q_fp&0xFFFFFF) def prep_packed(tri, pscale, xy_mode): verts=[pack_v(v,pscale,xy_mode) for v in tri] if any(v is None for v in verts): return None x0,y0=verts[0]["x"],verts[0]["y"]; x1,y1=verts[1]["x"],verts[1]["y"]; x2,y2=verts[2]["x"],verts[2]["y"] det=(x1-x0)*(y2-y0)-(x2-x0)*(y1-y0) if det==0: return None if det<0: verts=[verts[0],verts[2],verts[1]]; det=-det v0,v1,v2=verts; x0,y0=v0["x"],v0["y"]; x1,y1=v1["x"],v1["y"]; x2,y2=v2["x"],v2["y"] bias=[0 if top_or_left(x0,y0,x1,y1) else 1, 0 if top_or_left(x1,y1,x2,y2) else 1, 0 if top_or_left(x2,y2,x0,y0) else 1] def grad(attr): a0,a1,a2=v0[attr],v1[attr],v2[attr] return (trunc_div(grad_num_dadx(a0,a1,a2,x0,y0,x1,y1,x2,y2),det), trunc_div(grad_num_dady(a0,a1,a2,x0,y0,x1,y1,x2,y2),det)) return dict(v=verts,bias=bias,ds=grad("s"),dt=grad("t"),dq=grad("q")) def tex_col(idxlin, palg, u, v, logical_tw=TW, logical_th=TH, tex_psm=0x13, tex_stride=TW): if u<0 or u>=logical_tw or v<0 or v>=logical_th: return None # The LPDDR cache has a fixed physical 512-texel row stride even when # TEX0 advertises a smaller logical texture. lin=v*tex_stride+u if tex_psm==0x00: return idxlin[lin]&0xFFFFFF if tex_psm==0x14: byte_off=lin//2 packed=(idxlin[byte_off//4]>>(8*(byte_off%4)))&0xFF ci=((packed>>4)&15) if (lin&1) else (packed&15) else: ci=(idxlin[lin//4]>>(8*(lin%4)))&0xFF return palg[ci]&0xFFFFFF def texel_coord(coord, size, mode): if mode==0: return coord % size if mode==1: return max(0,min(size-1,coord)) raise AssertionError(f"unmodelled wrap mode {mode}") def texel_uv(clamp, u, v, logical_tw=TW, logical_th=TH): return (texel_coord(u,logical_tw,clamp&3), texel_coord(v,logical_th,(clamp>>2)&3)) def ref_near_match(idxlin, palg, color, tu, tv): for rad in range(0,2): for du in range(-rad,rad+1): for dv in range(-rad,rad+1): if max(abs(du),abs(dv))!=rad: continue if tex_col(idxlin,palg,tu+du,tv+dv)==color: return True return False def scale_score(tri, pscale, idxlin, palg, xy_mode, clamp): pt=prep_packed(tri,pscale,xy_mode) if pt is None: return None v0,v1,v2=pt["v"] minx=max(0,min(v0["x"],v1["x"],v2["x"])); maxx=min(FBPXW-1,max(v0["x"],v1["x"],v2["x"])) miny=max(0,min(v0["y"],v1["y"],v2["y"])); maxy=min(H-1,max(v0["y"],v1["y"],v2["y"])) fx0,fy0=tri[0]["x"],tri[0]["y"]; fx1,fy1=tri[1]["x"],tri[1]["y"]; fx2,fy2=tri[2]["x"],tri[2]["y"] far=edge(fx0,fy0,fx1,fy1,fx2,fy2) if abs(far)<1e-12: return None inv=1.0/far; ok=0; total=0; oob=0; errsum=0 for y in range(miny,maxy+1): for x in range(minx,maxx+1): e0=edge_i(x,y,v0["x"],v0["y"],v1["x"],v1["y"])+pt["bias"][0] e1=edge_i(x,y,v1["x"],v1["y"],v2["x"],v2["y"])+pt["bias"][1] e2=edge_i(x,y,v2["x"],v2["y"],v0["x"],v0["y"])+pt["bias"][2] if e0>0 or e1>0 or e2>0: continue a0=edge(fx1,fy1,fx2,fy2,float(x),float(y))*inv a1=edge(fx2,fy2,fx0,fy0,float(x),float(y))*inv a2=1.0-a0-a1 Q=a0*tri[0]["q"]+a1*tri[1]["q"]+a2*tri[2]["q"] if abs(Q)<1e-12: continue tu,tv=texel_uv(clamp, int(((a0*tri[0]["s"]+a1*tri[1]["s"]+a2*tri[2]["s"])/Q)*TW), int(((a0*tri[0]["t"]+a1*tri[1]["t"]+a2*tri[2]["t"])/Q)*TH)) s=interp_wide(v0["s"],pt["ds"][0],pt["ds"][1],x,y,v0["x"],v0["y"]) t=interp_wide(v0["t"],pt["dt"][0],pt["dt"][1],x,y,v0["x"],v0["y"]) q=interp_wide(v0["q"],pt["dq"][0],pt["dq"][1],x,y,v0["x"],v0["y"]) u,v=persp_uv(s,t,q) total+=1 col=tex_col(idxlin,palg,u,v) if col is None: oob+=1 errsum+=2048 continue du=abs(u-tu); dv=abs(v-tv) err=max(du,dv); errsum+=err if ref_near_match(idxlin,palg,col,tu,tv): ok+=1 return (ok,total,oob,errsum) def parse_fixed_pscale(raw): v=int(raw) if v<=0: sys.exit(f"[Ch356] FAIL: --pscale must be positive, got {v}") return v def choose_pscale(tri, idxlin=None, palg=None, mode=None, xy_mode="round", clamp=0, logical_tw=TW, logical_th=TH): if mode is None: mode="auto" if PSCALE_AUTO else str(PSCALE) if mode not in ("auto","pertri","per-tri"): return parse_fixed_pscale(mode) if FAST_FIT_SCALE: # A common positive scale cancels exactly in S/Q. Choose the # largest candidate whose packed S,T,Q all fit, maximizing fixed- # point precision without an expensive per-pixel texture search. for pscale in reversed(AUTO_PSCALES): good=True for v in tri: sf=round(v["s"]*logical_tw*(1<S24_MAX or abs(tf)>S24_MAX or qf<=0 or qf>0xFFFFFF: good=False; break if good: return pscale sys.exit(f"[{CH}] FAIL: no representable common S/T/Q scale for triangle {tri}") best=None for pscale in AUTO_PSCALES: sc=scale_score(tri,pscale,idxlin,palg,xy_mode,clamp) if sc is None: continue ok,total,oob,errsum=sc # The authentic floating-point UV is known, so texel-coordinate # error is the primary fidelity metric. Color-match count cannot # lead here: large black/repeated palette regions let a badly # displaced UV score as a "match" and previously selected scales # that blacked out coherent scene geometry. key=(-oob, -errsum, ok, total, pscale) if best is None or key>best[0]: best=(key,pscale) if best is None: return 1024 return best[1] def vwords(v, pscale, xy_mode, logical_tw=TW, logical_th=TH, fge=False): s_fp=round(v["s"]*logical_tw*(1<S24_MAX or abs(t_fp)>S24_MAX: sys.exit(f"[Ch356] ST overflow pscale={pscale} xy=({v['x']},{v['y']}) stq=({v['s']},{v['t']},{v['q']}) fp=({s_fp},{t_fp},{q_fp})") if q_fp<=0 or q_fp>0xFFFFFF: sys.exit(f"[Ch356] Q overflow pscale={pscale} xy=({v['x']},{v['y']}) stq=({v['s']},{v['t']},{v['q']}) fp=({s_fp},{t_fp},{q_fp})") sx=max(0,min(FBPXW-1,int(quant_xy(v["x"],xy_mode)))); sy=max(0,min(H-1,int(quant_xy(v["y"],xy_mode)))) sx12_4=max(0,min((FBPXW<<4)-1,int(round(v["x"]*16.0)))) sy12_4=max(0,min((H<<4)-1,int(round(v["y"]*16.0)))) zval = (max(0,min(0xFFFFFF,int(round(v.get("z",0.0))))) if AUTHZ else 0x0000_5000) # authentic 24-bit XYZ2 Z, else flat rgba=v.get("rgba",0xFFFFFFFF) if AUTH_COLOR_TFX else 0 # GS per-vertex FOG — when this draw is FGE (and --auth-fog on), pack the # commit word as XYZF2: same X/Y/24-bit-Z, with F=[63:56] the clamped # per-vertex fog byte. The feeder tags it reg 0x04 (see EDIT 1). zval is # already masked to 24 bits, so [63:56] is free for F and Z is unchanged. fog_this = AUTH_FOG and fge F = max(0,min(255,int(round(v.get("fog",255.0))))) xyz = ((sx12_4 & 0xFFFF) | ((sy12_4 & 0xFFFF) << 16) | ((zval & 0xFFFFFF) << 32)) \ if SUBPIXEL_XY else bake.xyz2_dataz(sx,sy,zval) if fog_this: xyz = (xyz & 0x00FFFFFFFFFFFFFF) | ((F & 0xFF) << 56) # rgbaq_with_q() is a legacy opaque helper and always writes A=0xff. # Authentic MODULATE+TCC alpha passes depend on the captured vertex # alpha, so pack the complete RGBA word when authentic color is # enabled. Keep the legacy opaque-alpha contract for old fixtures. rgbaq = (((q_fp & 0xFFFFFFFF) << 32) | (rgba & 0xFFFFFFFF)) \ if AUTH_COLOR_TFX else bake.rgbaq_with_q(0,0,0,q_fp&0xFFFFFFFF) return [rgbaq, bake.st_data(s_fp&0xFFFFFF,t_fp&0xFFFFFF), xyz] def tris_of(d): if d.get("forced_tris") is not None: return d["forced_tris"] out=[] # Expand each captured topology into independent triangle records. The # feeder reissues PRIM for every record, so FAN records must carry the # real anchor vertex explicitly; treating a FAN as a sliding STRIP # silently changes every triangle after the first. for e_m in (d['members'] if d.get('members') else [d['e']]): def fog_fold_rgba(v): rgba=v["rgba"] if not (AUTH_FOG_BLACK_FOLD and e_m["state"]["prim"].get("fge",0)): return rgba if (e_m["state"].get("fogcol", 0) & 0xFFFFFF) != 0: sys.exit(f"[{CH}] FAIL: --auth-fog-black-fold requires FOGCOL=0, idx{e_m['first_idx']} has 0x{e_m['state']['fogcol']&0xFFFFFF:06x}") fog=max(0,min(255,int(round(v.get("fog",255.0))))) rgb=[(((rgba>>sh)&0xFF)*fog)>>8 for sh in (0,8,16)] return rgb[0] | (rgb[1]<<8) | (rgb[2]<<16) | (rgba&0xFF000000) fv=[dict(x=v["x"]-OX,y=v["y"]-OY,z=v["z"],fog=v.get("fog",255.0), s=v["s"],t=v["t"],q=v["q"],rgba=fog_fold_rgba(v),kick=v.get("kick",True)) for v in e_m["verts"]] prim_type=e_m["state"]["prim"]["type"] if COALESCE_SOLID_SPRITES and prim_type==6 and not e_m["state"]["prim"]["tme"]: fv=coalesce_solid_sprite_grid(fv) if HALF_OPEN_SPRITES and prim_type==6: fv=half_open_sprite_rects(fv) raw=topology_tris(fv,prim_type, honor_kick=not LEGACY_STRIP_KICKS) clip_w=FBPXW; clip_h=H if AUTH_SCISSOR_CLIP: sc=e_m["state"].get("scissor") if sc is None: sys.exit(f"[{CH}] FAIL: idx{d['idx']} missing authentic SCISSOR state") sx0=sc&0x7FF; sx1=(sc>>16)&0x7FF; sy0=(sc>>32)&0x7FF; sy1=(sc>>48)&0x7FF if sx0!=0 or sy0!=0: sys.exit(f"[{CH}] FAIL: nonzero SCISSOR origin ({sx0},{sy0}) not yet modelled") clip_w=min(clip_w,sx1+1); clip_h=min(clip_h,sy1+1) for t in raw: for ct in clip_rect_z(t, clip_w, clip_h): # z-aware clip carries authentic interpolants qs=[v["q"] for v in ct] if SKIP_MIXED_Q and not (all(q>0 for q in qs) or all(q<0 for q in qs)): continue out.append(ct) return out # Clip an already framebuffer-clipped triangle into a horizontal strip. # Strip boundaries are integral pixel coordinates, while coverage samples # are at pixel centers, so adjacent strips cannot double-shade a sample. # All interpolants (including authentic Z and vertex color) are carried at # the new vertices exactly as in the framebuffer guard-band clip above. def clip_y_band_z(tri, lo, hi): def lerp(p1,p2,al): out={k:(p1[k]+al*(p2[k]-p1[k])) for k in ("x","y","z","fog","s","t","q")} c=[] for sh in (0,8,16,24): av=(p1["rgba"]>>sh)&0xFF; bv=(p2["rgba"]>>sh)&0xFF c.append(max(0,min(255,int(round(av+al*(bv-av)))))) out["rgba"]=c[0]|(c[1]<<8)|(c[2]<<16)|(c[3]<<24) return out 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=list(tri) poly=clip_edge(poly, lambda p:p["y"]>=lo, lambda A,B:lerp(A,B,(lo-A["y"])/(B["y"]-A["y"]))) if not poly: return [] poly=clip_edge(poly, lambda p:p["y"]<=hi, lambda A,B:lerp(A,B,(hi-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 center_coverage(tri, xy_mode): # Match pack_v exactly at the right/bottom framebuffer edges. The # geometric clip uses the conventional exclusive W/H boundary, while # XYZ2 stores the last addressable pixel (W-1/H-1). if SUBPIXEL_XY: # Capacity scheduling must use the same native 12.4 geometry as # the enabled raster coverage path. Integer-quantizing here can # collapse thin but visible triangles and silently discard them # as zero-coverage pieces before they ever reach the RTL. pts=[(max(0.0,min(FBPXW-(1.0/16.0),round(v["x"]*16.0)/16.0)), max(0.0,min(H-(1.0/16.0),round(v["y"]*16.0)/16.0))) for v in tri] else: pts=[(max(0,min(FBPXW-1,int(quant_xy(v["x"],xy_mode)))), max(0,min(H-1,int(quant_xy(v["y"],xy_mode))))) for v in tri] (x0,y0),(x1,y1),(x2,y2)=pts; ar=edge(x0,y0,x1,y1,x2,y2) if not ar: return 0 inv=1.0/ar; count=0 for py in range(max(0,int(math.floor(min(y0,y1,y2)))),min(H-1,int(math.ceil(max(y0,y1,y2))))+1): for px in range(max(0,int(math.floor(min(x0,x1,x2)))),min(FBPXW-1,int(math.ceil(max(x0,x1,x2))))+1): cx=px+0.5; cy=py+0.5 w0=edge(x1,y1,x2,y2,cx,cy)*inv w1=edge(x2,y2,x0,y0,cx,cy)*inv if w0>=-0.001 and w1>=-0.001 and (1.0-w0-w1)>=-0.001: count+=1 return count # Ch407: large guard-band fans can cover most of the framebuffer, while # the production request FIFO holds 16K fragments. Split into 16-row bands # and pack consecutive, non-overlapping pieces into ordered epochs whose # measured center-sample coverage stays below the requested safety limit. # This is a fixture scheduling transformation only; texture/state and GS # primitive order remain unchanged. if CAPACITY_EPOCH_PIXELS is not None: split=[]; BAND_H=16; MAX_EPOCH_TRIS=(STG_WORDS-(8 if EMIT_CLAMP_HEADER else 7))//9 for d in descs: xy_mode = XY_QUANT_LIST[d['k']] if XY_QUANT_LIST is not None else XY_QUANT_RAW.lower() pieces=[] for tri in tris_of(d): for lo in range(0,H,BAND_H): band_hi=min(H,lo+BAND_H)-(1.0/16.0 if HALF_OPEN_SPRITES and SUBPIXEL_XY else 0.0) for piece in clip_y_band_z(tri,lo,band_hi): cov=center_coverage(piece,xy_mode) if cov: if cov>CAPACITY_EPOCH_PIXELS: sys.exit(f"[Ch407] FAIL: band piece coverage {cov} exceeds epoch cap {CAPACITY_EPOCH_PIXELS}") pieces.append((piece,cov)) groups=[]; cur=[]; cur_cov=0 for piece,cov in pieces: if cur and (cur_cov+cov>CAPACITY_EPOCH_PIXELS or len(cur)>=MAX_EPOCH_TRIS): groups.append((cur,cur_cov)); cur=[]; cur_cov=0 cur.append(piece); cur_cov+=cov if cur: groups.append((cur,cur_cov)) for gi,(group,cov) in enumerate(groups): nd=dict(d); nd["forced_tris"]=group; nd["k"]=len(split) nd["reuse"]=d.get("reuse",0) if gi==0 else 1 nd["ntris"]=len(group); nd["words"]=(8 if EMIT_CLAMP_HEADER else 7)+9*len(group) nd["nneg"]=sum(1 for tri in group if all(v["q"]<0 for v in tri)) nd["capacity_px"]=cov; split.append(nd) descs=split print(f"[{CH}] --capacity-epochs {CAPACITY_EPOCH_PIXELS}: 16-row band split/ordered pack -> {len(descs)} epochs; max scheduled coverage={max(d['capacity_px'] for d in descs)}") # Ch405 capacity contract: destination-alpha blending deliberately waits # for each committed write so overlapping GS fragments observe exact # destination order. Bound the unthrottled raster burst to one triangle # per scene marker; a 16K request FIFO then covers the measured worst fan # triangle (14,752 px) without hiding drops. Geometry/order are unchanged. if TRI_EPOCHS: split=[] for d in descs: for ti, tri in enumerate(tris_of(d)): nd=dict(d) nd["forced_tris"]=[tri] nd["k"]=len(split) nd["reuse"]=d.get("reuse",0) if ti==0 else 1 nd["ntris"]=1 nd["words"]=(8 if EMIT_CLAMP_HEADER else 7)+9 nd["nneg"]=1 if all(v["q"]<0 for v in tri) else 0 split.append(nd) descs=split print(f"[{CH}] --tri-epochs: split into {len(descs)} one-triangle bounded-burst epochs") # Splitting intentionally drops source groups with no covered pixels. # Rebase both the dense epoch index and the texture-residency chain from # the descriptors that will actually be emitted. Otherwise the first # surviving descriptor can inherit reuse=1 from an empty predecessor and # point at a texture payload that does not exist in the output table. prev_asset=None for k,d in enumerate(descs): d['k']=k d['reuse']=int(prev_asset is not None and prev_asset['idxw']==d['idxw'] and prev_asset['clut']==d['clut']) prev_asset=d 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") N=len(descs); FB_WORDS=FBPXW*H # Reuse epochs point at the most recent fresh texture payload; palettes # are content-deduplicated too. This keeps a 3K-epoch fixture to tens of # megabytes instead of duplicating gigabytes of identical local assets. last_asset_k=None; pal_asset={} for d in descs: if not d.get('reuse',0): last_asset_k=d['k'] if last_asset_k is None or descs[last_asset_k]['crc']!=d['crc']: sys.exit(f"[{CH}] FAIL: epoch {d['k']} reuse chain has no matching fresh texture") d['asset_k']=last_asset_k pk=tuple(d['pal']) if pk not in pal_asset: pal_asset[pk]=d['k'] d['pal_k']=pal_asset[pk] # SYNTHETIC GATE (Codex, from Ch355): a positive-Q triangle and its ALL-NEGATED twin must canonicalize to # BIT-IDENTICAL staging words (proves (-s)/(-q)=s/q exactness of the reciprocal canonicalization). def _pack(tri): out=[] ctri=canon(*tri); pscale=1024 if (PSCALE_AUTO or PSCALE_LIST is not None) else choose_pscale(ctri) for v in ctri: out+=vwords(v,pscale,"round") 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("[Ch356] FAIL: canonicalization not bit-exact (positive-Q tri vs its all-negated twin differ)") print("[Ch356] canonicalization self-test PASS: positive-Q triangle and its all-negated twin pack BIT-IDENTICALLY") # per-epoch: feeder list (canonicalized) + de-swizzled texture -> LPDDR + relocated CLUT bytes + de-gridded palette for d in descs: tris=tris_of(d) scale_hist={} scale_mode = PSCALE_LIST[d['k']] if PSCALE_LIST is not None else ("auto" if PSCALE_AUTO else str(PSCALE)) xy_mode = XY_QUANT_LIST[d['k']] if XY_QUANT_LIST is not None else XY_QUANT_RAW.lower() # Preserve authentic PRIM state bits, but not its topology. tris_of() # has already expanded every captured TRI_STRIP, TRI_FAN, and SPRITE # into independent three-vertex records, and the feeder reissues PRIM # before every such record. Advertising the captured topology here # makes gs_stub assemble a 9-word triangle record as (for example) a # two-vertex SPRITE plus a dangling vertex. The staged grammar is # unconditionally TRIANGLE; TME/ABE/FST/CTXT remain authentic. # Preserve authentic ALPHA_1, including ABE, for the LPDDR ROP. # Opaque fixtures retain their historical source-over/tri defaults. state=d['state']; p=state['prim'] # GS per-vertex FOG — carry PRIM.FGE (bit 5) so the feeder tags the # vertex commit XYZF2 and gs_stub's ras_fge applies the fog blend. Only # for captured-FGE draws with --auth-fog on; FGE=0 draws are unchanged. fog_draw = AUTH_FOG and bool(p.get('fge',0)) fge_bit = (1<<5) if fog_draw else 0 prim_word=(3 | (p['tme']<<4) | fge_bit | (p['abe']<<6) | (p['fst']<<8) | (p['ctxt']<<9)) if ALLOW_ABE else (3 | (1<<4) | fge_bit) alpha_word=state['alpha'] if ALLOW_ABE else bake.alpha_pack(0,1,0,1) # Preserve architectural TEST/ZBUF for the external LPDDR ROP. Older # fixtures could use placeholders because the de25 wrapper hardwired # ZTE=1/ZMSK=0; Ch405 carries these bits per fragment so authentic # read-only-Z overlays (ZMSK=1) can coexist with opaque Z writers. # The external framebuffer is relocated to FBP=0/FBW=10/PSMCT32, # but authentic byte-granular FRAME.FBMSK remains part of the draw. # The RTL transports its four byte enables alongside each fragment. frame_word=bake.frame_1_psmct32(FBW) if ALLOW_ABE: frame_word |= state['frame'] & 0xFFFFFFFF00000000 stg=[len(tris)|(1<<32), frame_word, alpha_word, state['test'], state['zbuf'], tex0_word(d['cbp_reloc'], state['tex0']['tfx'] if AUTH_COLOR_TFX else 1, d.get('logical_tw',TW), d.get('logical_th',TH), d.get('tex_psm',0x13), d.get('tex_tbw',8))] if EMIT_CLAMP_HEADER: stg[0] |= 1<<34 stg += [state['clamp'], prim_word] else: stg += [prim_word] for tri in tris: ctri=canon(*tri); pscale=choose_pscale(ctri,d['idxw'],d['pal'],scale_mode,xy_mode,d['state']['clamp'], d.get('logical_tw',TW),d.get('logical_th',TH)); scale_hist[pscale]=scale_hist.get(pscale,0)+1 for v in ctri: vw=vwords(v,pscale,xy_mode,d.get('logical_tw',TW),d.get('logical_th',TH),fge=fog_draw) if AUTH_COLOR_TFX and ((vw[0]>>24)&0xFF) != ((v['rgba']>>24)&0xFF): sys.exit(f"[{CH}] FAIL: epoch {d['k']} vertex alpha changed while packing RGBAQ") stg += vw if len(stg)>STG_WORDS: sys.exit(f"[Ch356] epoch {d['k']} staging {len(stg)} > {STG_WORDS}") bake.write_feeder_stg_mem(f"feeder_{PFX}{d['k']}.mem", stg, f"{CH} LOCAL epoch {d['k']} (idx{d['idx']}, TEX0 CBP={d['cbp_reloc']}) {len(tris)} tris. gitignored.", total=STG_WORDS) if d['asset_k']==d['k']: wmem(f"{PFX}{d['k']}_tex_lpddr.mem", d['idxw'], f"{CH} LOCAL epoch {d['k']} tex (idx{d['idx']}/tbp={d['tbp']}) LINEAR -> LPDDR. gitignored.") wmem(f"{PFX}{d['k']}_idx.mem", d['idxw'], f"{CH} LOCAL epoch {d['k']} idx. gitignored.") if d['pal_k']==d['k']: wmem(f"{PFX}{d['k']}_pal.mem", [p&0xFFFFFFFF for p in d['pal']], f"{CH} LOCAL epoch {d['k']} de-gridded palette. gitignored.") d['nwords']=len(stg) d['scale_hist']=scale_hist if (PSCALE_AUTO or PSCALE_LIST is not None) and (d['k']<10 or d['k']%100==0 or d['k']==N-1): print(f"[{CH}] epoch {d['k']} per-triangle PSCALE hist {dict(sorted(scale_hist.items()))}") # INDEPENDENT composed reference (dump order, paint order) + per-epoch refmaps + per-pixel OWNER (epoch idx). # refmap: [31]cov [30]int [28]multi(>=2 epochs) [26:24]owner_epoch [17:9]tu [8:0]tv print(f"[{CH}] independent reference sample={REF_SAMPLE} ref_xy_quant={int(REF_XY_QUANT)}") refmap=[0]*FB_WORDS; refpix=[(0,0,0)]*FB_WORDS; nep=[0]*FB_WORDS; lastep=[-1]*FB_WORDS emit_perep = N <= 128 perep=([[0]*FB_WORDS for _ in range(N)] if emit_perep else None) for k,d in enumerate(descs): idxlin=d['idxw']; palg=d['pal'] logical_tw=d.get('logical_tw',TW); logical_th=d.get('logical_th',TH) tex_psm=d.get('tex_psm',0x13) tex_stride=d.get('tex_tbw',8)*64 xy_mode = XY_QUANT_LIST[k] if XY_QUANT_LIST is not None else XY_QUANT_RAW.lower() for (v0,v1,v2) in tris_of(d): if REF_XY_QUANT: v0=dict(v0, x=quant_xy(v0["x"],xy_mode), y=quant_xy(v0["y"],xy_mode)) v1=dict(v1, x=quant_xy(v1["x"],xy_mode), y=quant_xy(v1["y"],xy_mode)) v2=dict(v2, x=quant_xy(v2["x"],xy_mode), y=quant_xy(v2["y"],xy_mode)) 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 sx=float(px)+0.5 if REF_SAMPLE=="center" else float(px) sy=float(py)+0.5 if REF_SAMPLE=="center" else float(py) a0=edge(x1,y1,x2,y2,sx,sy)*inv; a1=edge(x2,y2,x0,y0,sx,sy)*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 uf=(S/Q)*logical_tw; vf=(T/Q)*logical_th tu,tv=(texel_uv(d['state']['clamp'], math.floor(uf), math.floor(vf), logical_tw, logical_th) if d['state']['prim']['tme'] else (0,0)) o=py*FBPXW+px; mw=min(w0,w1,w2); interior=1 if mw>0.04 else 0 if d['state']['prim']['tme']: col=tex_col(idxlin,palg,tu,tv,logical_tw,logical_th,tex_psm,tex_stride) else: vc=[] for sh in (0,8,16): c0=(v0['rgba']>>sh)&0xFF; c1=(v1['rgba']>>sh)&0xFF; c2=(v2['rgba']>>sh)&0xFF vc.append(max(0,min(255,int(a0*c0+a1*c1+a2*c2)))) col=vc[0]|(vc[1]<<8)|(vc[2]<<16) if REF_BILINEAR and d['state']['prim']['tme']: u_next,v_next=texel_uv(d['state']['clamp'], math.floor(uf)+1, math.floor(vf)+1, logical_tw, logical_th) c00=col c10=tex_col(idxlin,palg,u_next,tv,logical_tw,logical_th,tex_psm,tex_stride) c01=tex_col(idxlin,palg,tu,v_next,logical_tw,logical_th,tex_psm,tex_stride) c11=tex_col(idxlin,palg,u_next,v_next,logical_tw,logical_th,tex_psm,tex_stride) fu=uf-math.floor(uf); fv=vf-math.floor(vf) chans=[] for sh in (0,8,16): a=((c00>>sh)&255)*(1.0-fu)+((c10>>sh)&255)*fu b=((c01>>sh)&255)*(1.0-fu)+((c11>>sh)&255)*fu chans.append(max(0,min(255,int(a*(1.0-fv)+b*fv)))) col=chans[0]|(chans[1]<<8)|(chans[2]<<16) if AUTH_COLOR_TFX and d['state']['prim']['tme'] and d['state']['tex0']['tfx']==0: vc=[] for sh in (0,8,16): c0=(v0['rgba']>>sh)&0xFF; c1=(v1['rgba']>>sh)&0xFF; c2=(v2['rgba']>>sh)&0xFF vc.append(max(0,min(255,int(a0*c0+a1*c1+a2*c2)))) tc=[col&0xFF,(col>>8)&0xFF,(col>>16)&0xFF] mc=[min(255,(tc[j]*vc[j])>>7) for j in range(3)] col=mc[0]|(mc[1]<<8)|(mc[2]<<16) if lastep[o]!=k: nep[o]+=1; lastep[o]=k # distinct epochs covering this pixel refmap[o]=(1<<31)|(interior<<30)|((k&0x7)<<24)|((tu&0x1FF)<<9)|(tv&0x1FF) # LAST epoch wins (paint order) if emit_perep: perep[k][o]=(1<<31)|(interior<<30)|((tu&0x1FF)<<9)|(tv&0x1FF) refpix[o]=(col&0xFF,(col>>8)&0xFF,(col>>16)&0xFF) for o in range(FB_WORDS): if nep[o]>=2: refmap[o]|=(1<<28) # multi-epoch (>=2 distinct) pixel multi_px=sum(1 for o in range(FB_WORDS) if refmap[o]&(1<<28)) covered=sum(1 for w in refmap if w>>31) print(f"[Ch356] independent composed reference: {covered} covered px, {multi_px} multi-epoch (>=2) px") wmem(f"{PFX}_refmap.mem", refmap, f"{CH} LOCAL composed per-pixel cov|int|multi|owner_epoch|tu|tv reference. gitignored.") if emit_perep: for k in range(N): wmem(f"{PFX}{k}_refmap.mem", perep[k], f"{CH} LOCAL epoch {k} per-pixel reference. gitignored.") else: print(f"[{CH}] skipped {N} per-epoch full-frame refmaps; composed reference remains emitted") # Bootlet preload is the legacy path. Runtime mode leaves the CLUT # table empty and emits CLD=0 lists; the HPS scheduler stages each # palette before its GO instead, removing the bootlet palette cap. RAM_QWORDS=512; pay=[]; _seen_cbp=set() for d in (() if RUNTIME_CLUT else (x for x in descs if not (x['cbp_reloc'] in _seen_cbp or _seen_cbp.add(x['cbp_reloc'])))): clutw=[int.from_bytes(d['clut'][i*4:i*4+4],'little') for i in range(256)] pay.append(bake.giftag(1,0,0,4,int('E'*4,16))) pay.append(bake.aplusd(bake.R_BITBLTBUF, bake.bitbltbuf_pack(d['cbp_reloc'],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,f"payload_{PFX}.mem"),"w") as f: f.write(f"// {CH} LOCAL {'runtime-CLUT empty' if RUNTIME_CLUT else f'{N}-CLUT preload'} payload (CBPs {[d['cbp_reloc'] for d in descs]}). 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_{PFX}.mem", bake.build_textured_demo_bootlet_disp(qwc, disp_hi, FBW), f"{CH} LOCAL {N}-CLUT preload bootlet (QWC={qwc}, DISPLAY1={FBPXW}x{H}). gitignored.") # params + epoch descriptor table (for the host + TB) with open(os.path.join(DATA,f"{PFX}_params.vh"),"w") as f: f.write(f"// {CH} LOCAL generated params for the scheduler integration TB. gitignored.\n") for kk,vv in (("FBW",FBW),("FBPXW",FBPXW),("FBH",H),("VRAM_BYTES_P",VRAM_BYTES),("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),("N_EPOCHS",N),("UNION_OX",OX),("UNION_OY",OY)): f.write(f"localparam int {kk:<14}= {vv};\n") f.write(f"localparam [29:0] LPDDR_TEX_BASE = 30'h{LPDDR_TEX_BASE:07x};\n") for d in descs: f.write(f"localparam int EP{d['k']}_NTRIS = {d['ntris']};\n") f.write(f"localparam int EP{d['k']}_CBP = {d['cbp_reloc']};\n") f.write(f"localparam [31:0] EP{d['k']}_CRC = 32'h{d['crc']:08x};\n") f.write(f"localparam bit EP{d['k']}_REUSE = 1'b{d.get('reuse',0)};\n") # The shared testbenches include this after declaring their descriptor arrays. Keeping the assignments as # generated text avoids simulator-dependent function elaboration inside a loop while retaining one source TB. with open(os.path.join(DATA, f"{PFX}_epoch_table.vh"), "w") as f: f.write(f"// {CH} LOCAL generated descriptor assignments for the shared-epoch TB. gitignored.\n") for d in descs: k = d['k'] f.write(f"EP_CRC[{k}] = EP{k}_CRC; EP_REC[{k}] = EP{k}_NTRIS; EP_REUSE[{k}] = EP{k}_REUSE;\n") with open(os.path.join(DATA,f"{PFX}_epochs.txt"),"w") as f: f.write(f"# {CH} epoch descriptor table ({N} epochs, dump order). CLUT={'runtime HPS staging' if RUNTIME_CLUT else 'bootlet preload'};\n") f.write(f"# per epoch: upload tex -> LPDDR 0x{LPDDR_TEX_BASE:x}, fill+verify CRC, stream list, GO, fresh drain.\n") f.write(f"# reuse=1 (Ch359): the texture cache is ALREADY resident from a prior epoch — the host must SKIP the\n") f.write(f"# upload and fill, and instead VERIFY the resident CRC register still equals crc (fail-closed residency).\n") f.write(f"META n_epochs {N} fbpxw {FBPXW} fbh {H} fbwords {FBPXW*H} lpddr_tex 0x{LPDDR_TEX_BASE:x} tex_words {TW*TH//4} n_beats {TW*TH//32}\n") f.write(f"# k idx tbp cbp_reloc tex_file lpddr size crc list_file words records reuse pal_file pal_sum32\n") for d in descs: pal_file = f"{PFX}{d['pal_k']}_pal.mem" if RUNTIME_CLUT else "-" pal_sum = sum(d['pal']) & 0xFFFFFFFF if RUNTIME_CLUT else 0 f.write(f"{d['k']} {d['idx']} {d['tbp']} {d['cbp_reloc']} {PFX}{d['asset_k']}_tex_lpddr.mem 0x{LPDDR_TEX_BASE:x} {d['size']} " f"0x{d['crc']:08x} feeder_{PFX}{d['k']}.mem {d['nwords']} {d['ntris']} {d.get('reuse',0)} {pal_file} 0x{pal_sum: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",f"{PFX}_ref.png")) print(f"[{CH}] wrote {PFX}_ref.png") except Exception as ex: print("(PIL skipped:", ex, ")") print(f"[{CH}] emitted {N}-epoch scheduler fixtures -> {DATA}. FB {FBPXW}x{H} stride {STRIDE}; epochs CRCs {[hex(d['crc']) for d in descs]}") return 0 if __name__=="__main__": raise SystemExit(main(sys.argv))