Files
retroDE_ps2/tools/gs_make_sh3_fixture.py
thejayman77 ec82764bef Initial commit: retroDE_ps2 — first-of-its-kind PS2 GS FPGA core (DE25-Nano / Agilex 5)
RTL (GS rasterizer, EE core stub, platform bridge, LPDDR4B path), sim regression
(272 TBs), docs, and tooling. Copyrighted PS2 content (BIOS, game code, GS dumps,
and all dump-derived textures/traces) is excluded via .gitignore and stays local.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 20:10:50 -04:00

114 lines
6.9 KiB
Python

#!/usr/bin/env python3
"""retroDE_ps2 — Ch347 authentic SH3 board fixture: bootlet GIF payload that uploads the real SH3 PSMT8
texture + real CLUT and draws it as a DECAL sprite. Built directly on the Ch296 PSMT8+CLUT board template
(bake.build_clut8_demo_payload), scaled to 128x128 + a 256-entry CLUT.
Mirrors gs_make_cube_fixture.py: imports bake.py helpers, reads the dump-derived assets emitted by
gs_extract_sh3_clut.py (sh3_tex_idx.mem + sh3_clut.mem), and writes the LOCAL/gitignored board fixtures:
payload_sh3_clut.mem GIF payload: BITBLT CLUT (256) + BITBLT PSMT8 tex (128x128) + TEX0(PSMT8,CLD=1,CSM2
linear) + one 128x128 DECAL SPRITE
bios_sh3_clut.mem one-shot bootlet, QWC = payload qword count, DISPLAY1 = 128x128
DECAL/opaque (TFX=1) so the authentic art is visible; the real CLUT alpha (~0x04) is kept BYTE-FOR-BYTE in
the palette, just ignored by the render mode (Codex guardrail). CLUT declared CSM2/linear (the order
validated against the authentic render in tools/gs_extract_sh3_clut.py).
"""
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")
sys.path.insert(0, DATA); import bake # reuse giftag/aplusd/*_pack/bootlet helpers
# Ch347 (ii): the 64x64 authentic CROP — fits the PROVEN 64x64 bram CLUT config (no new 128 KiB/scanout
# integration). Full 128x128 is the follow-up rung (i). Swap sh3_tex_idx64->sh3_tex_idx + W/H/FBW for (i).
W = H = 64
FBW = 1 # 64 px / 64
TBP0 = 64 # texture base byte = 64*256 = 0x4000 (just past the 64x64 PSMCT32 FB = 16 KiB)
TBW = 1 # 64 texels/row stride
CBP = 80 # CLUT base byte = 80*256 = 0x5000 (past the 4 KiB texture)
RAM_QWORDS = 512 # 8 KiB EE RAM payload; SH3 64x64 payload ~340 qw fits
def read_mem(name, n):
vals=[]
for ln in open(os.path.join(DATA,name)):
ln=ln.strip()
if ln and not ln.startswith("//"): vals.append(int(ln,16))
if len(vals) < n: sys.exit(f"{name}: {len(vals)} < {n} (run tools/gs_extract_sh3_clut.py first)")
return vals
tex_words = read_mem("sh3_tex_idx64.mem", W*H//4) # 1024 packed words (4 indices each) — the 64x64 crop
clut = read_mem("sh3_clut.mem", 256) # 256 PSMCT32 entries
# TEX0_1: PSMT8 128x128 (TW=TH=7), TFX=DECAL, + CLUT side (CBP, CPSM=PSMCT32, CSM2 linear, CLD=1 -> load)
def tex0_sh3():
v = bake.tex0_pack(TBP0, TBW, psm=0x13, tw=6, th=6, tfx=1) # 64x64 (TW=TH=6)
v |= (CBP & 0x3FFF) << 37
v |= (0 & 0xF) << 51 # CPSM = PSMCT32
v |= (1 & 0x1) << 55 # CSM = CSM2 (linear)
v |= (0 & 0x1F) << 56 # CSA = 0
v |= (1 & 0x7) << 61 # CLD = 1 (fire VRAM->CLUT load on commit)
return v
def build_payload():
qw=[]
# U1: BITBLT the CLUT (256 PSMCT32 entries, 16x16) -> VRAM[CBP*256]
qw.append(bake.giftag(1,0,0,4,0x0000_0000_0000_EEEE))
qw.append(bake.aplusd(bake.R_BITBLTBUF, bake.bitbltbuf_pack(CBP, 1, 0))) # DPSM=PSMCT32
qw.append(bake.aplusd(bake.R_TRXPOS, bake.trxpos_pack(0,0)))
qw.append(bake.aplusd(bake.R_TRXREG, bake.trxreg_pack(256,1))) # 256 entries as ONE CONTIGUOUS
# row — PSMCT32 row_stride = DBW*256, so a 16x16 upload would scatter entries (16/256-B row) while
# clut_loader reads them contiguously; 256x1 keeps them packed (the Ch296 8-entry demo used 8x1).
qw.append(bake.aplusd(bake.R_TRXDIR, bake.trxdir_pack(0)))
qw.append(bake.giftag(256//4,0,2,0,0)) # IMAGE: 4 entries/qword
for q in range(256//4):
word=0
for lane in range(4): word |= (clut[q*4+lane] & 0xFFFFFFFF) << (32*lane)
qw.append(word)
# U2: upload the PSMT8 index texture as a PSMCT32 transfer (full 32-bit WORD writes — NOT PSMT8 byte
# writes, which RMW the 32-bit VRAM word and corrupt one word on 4 consecutive same-word byte-writes).
# The bytes land identically (TBP*256 + w*4); the TEX0 still READS them as PSMT8. This is exactly the
# "upload as PSMCT32, bind as PSMT8" trick SH3 itself uses. Upload as (W*H/4)x1 = 1024x1 PSMCT32.
qw.append(bake.giftag(1,0,0,4,0x0000_0000_0000_EEEE))
qw.append(bake.aplusd(bake.R_BITBLTBUF, bake.bitbltbuf_pack(TBP0, 16, 0x00))) # DPSM=PSMCT32, DBW=16 (1 row)
qw.append(bake.aplusd(bake.R_TRXPOS, bake.trxpos_pack(0,0)))
qw.append(bake.aplusd(bake.R_TRXREG, bake.trxreg_pack(W*H//4, 1))) # 1024 PSMCT32 words x 1 row
qw.append(bake.aplusd(bake.R_TRXDIR, bake.trxdir_pack(0)))
qw.append(bake.giftag(W*H//16,0,2,0,0)) # 256 qwords, 4 PSMCT32 words each (= the packed indices)
for q in range(W*H//16):
word=0
for k in range(4): word |= (tex_words[q*4+k] & 0xFFFFFFFF) << (32*k)
qw.append(word)
# U3: PRIM(SPRITE+TME) + FRAME + TEX0(PSMT8+CLUT load) + RGBAQ + UV0/XYZ2_0
frame_val = bake.frame_1_psmct32(FBW)
qw.append(bake.giftag(1,0,0,6,0x0000_0000_00EE_EEEE))
qw.append(bake.aplusd(bake.R_PRIM, bake.prim_sprite_tme()))
qw.append(bake.aplusd(bake.R_FRAME_1, frame_val))
qw.append(bake.aplusd(bake.R_TEX0_1, tex0_sh3()))
qw.append(bake.aplusd(bake.R_RGBAQ, bake.rgbaq_data(0x80,0x80,0x80)))
qw.append(bake.aplusd(bake.R_UV, bake.uv_data(0,0)))
qw.append(bake.aplusd(bake.R_XYZ2, bake.xyz2_data(0,0)))
# U4: UV1/XYZ2_1 closing the 128x128 textured sprite. EOP.
qw.append(bake.giftag(1,1,0,2,0x0000_0000_0000_00EE))
qw.append(bake.aplusd(bake.R_UV, bake.uv_data(W-1,H-1)))
qw.append(bake.aplusd(bake.R_XYZ2, bake.xyz2_data(W-1,H-1)))
return qw
payload = build_payload()
qwc = len(payload)
if 16 + qwc > RAM_QWORDS: sys.exit(f"payload {qwc} qw + 16 > {RAM_QWORDS} (bump RAM_QWORDS / RAM_SIZE_BYTES)")
# DISPLAY1 hi = (DH<<12)|DW for 64x64 (magh=magv=1): DW=DH=63
disp_hi = (63 << 12) | 63
with open(os.path.join(DATA,"payload_sh3_clut.mem"),"w") as f:
f.write(f"// Ch347 LOCAL authentic SH3 64x64-CROP PSMT8+CLUT board payload (tex @ TBP={TBP0}, CLUT @ CBP={CBP}).\n")
f.write(f"// SH3-derived -> gitignored. qw 0..15 zero; qw 16.. = GIF (BITBLT CLUT + tex + DECAL sprite). QWC={qwc}.\n")
for _ in range(16): f.write(f"{0:032x}\n")
for w in payload: f.write(f"{w & ((1<<128)-1):032x}\n")
for _ in range(RAM_QWORDS-16-qwc): f.write(f"{0:032x}\n")
bake.write_bios_mem(
"bios_sh3_clut.mem",
bake.build_textured_demo_bootlet_disp(qwc, disp_hi, FBW),
f"Ch347 LOCAL authentic SH3 64x64-crop PSMT8+CLUT setup bootlet (QWC={qwc}, DISPLAY1={W}x{H}). SH3-derived -> gitignored.")
print(f"[Ch347] payload_sh3_clut.mem: {qwc} qw (BITBLT 256-CLUT + {W}x{H} PSMT8 tex + DECAL sprite, 8 KiB RAM)")
print(f"[Ch347] bios_sh3_clut.mem : bootlet QWC={qwc}, DISPLAY1={W}x{H} (disp_hi=0x{disp_hi:x}), FBW={FBW}")
print(f"[Ch347] VRAM layout: FB {W}x{H} PSMCT32 @0 ({W*H*4//1024}KiB) + PSMT8 tex @TBP={TBP0} ({W*H//1024}KiB) + CLUT @CBP={CBP} (1KiB) -> fits the proven 64x64 bram config")