Files
retroDE_ps2/tools/gs_make_cube_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

170 lines
8.9 KiB
Python

#!/usr/bin/env python3
"""Ch343 — build the AUTHENTIC cube board fixtures (LOCAL, dump-derived).
Inputs (local, from the cubes .gs dump pipeline):
captures/gs/cubes/extracted/cube_tex_64.bin 64x64 PSMCT32 cube texture (downscaled)
captures/gs/cubes/extracted/cube_persp.scene.txt translated perspective scene (ps2_feeder)
Outputs into sim/data/top_psmct32_raster_demo/ (gitignored — game-derived content):
payload_cube_setup.mem boot GIF payload: upload the 64x64 cube texture @ TBP=64 (32 KiB EE RAM)
bios_cube_setup.mem one-shot bootlet, QWC = payload qword count
feeder_cube_persp.mem cube perspective staging (ps2_feeder --dump-file cube_persp.scene)
Reuses bake.py's GIF packers + bootlet builder so the framing matches the proven feeder_persp path
exactly; only the texture size (64x64 vs 16x16) and the RAM/QWC budget differ. ABE=0 S1 perspective.
This GENERATOR is committable; its .mem outputs are not (provenance: authentic GS dump content)."""
import os
import sys
import struct
import subprocess
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 # noqa: E402 (reuse giftag/aplusd/*_pack/bootlet/write_bios_mem)
TEX_BIN = os.path.join(ROOT, "captures", "gs", "cubes", "extracted", "cube_tex_64.bin")
SCENE = os.path.join(ROOT, "captures", "gs", "cubes", "extracted", "cube_persp.scene.txt")
# VRAM layout (64 KiB / 16384 words) — three 64x64 surfaces must NOT alias:
# FB FBP=0 -> words 0..4095 ZBP units = 2048 words
# Z ZBP=2 -> words 4096..8191 (ps2_feeder hardcodes zbuf1_pack(2) + GEQUAL)
# TEX TBP=128-> words 8192..12287 TBP0 units = 64 words
# The scene's translator picked TBP=64 (word 4096), which ALIASES the Z buffer: the render then reads
# texel values (~0xFF000000) as Z and GEQUAL rejects the whole cube. Relocate the texture past Z.
TBP = 128 # word 8192 — clear of FB (0..4095) and Z (4096..8191)
TBW = 1 # 64-wide -> TBW=1 (64-px stride); matches the scene's tex0 stride
W = H = 64
RAM_QWORDS = 2048 # 32 KiB / 16 (EE RAM payload; unrelated to VRAM)
for p in (TEX_BIN, SCENE):
if not os.path.exists(p):
sys.exit(f"missing local input (run the cubes .gs extraction first): {p}")
# --- 64x64 PSMCT32 texels, raster order (ty*64 + tx), little-endian u32 ---
raw = open(TEX_BIN, "rb").read()
if len(raw) != W * H * 4:
sys.exit(f"{TEX_BIN}: expected {W*H*4} bytes, got {len(raw)}")
texels = list(struct.unpack(f"<{W*H}I", raw))
# --- boot GIF payload: BITBLTBUF/TRXPOS/TRXREG/TRXDIR (A+D) + IMAGE upload ---
qw = []
qw.append(bake.giftag(1, 0, 0, 4, 0x0000_0000_0000_EEEE)) # 4x A+D
qw.append(bake.aplusd(bake.R_BITBLTBUF, bake.bitbltbuf_pack(TBP, TBW, 0)))
qw.append(bake.aplusd(bake.R_TRXPOS, bake.trxpos_pack(0, 0)))
qw.append(bake.aplusd(bake.R_TRXREG, bake.trxreg_pack(W, H)))
qw.append(bake.aplusd(bake.R_TRXDIR, bake.trxdir_pack(0))) # host -> local
n_image = (W * H) // 4 # 4 texels / qword = 1024
qw.append(bake.giftag(n_image, 1, 2, 0, 0)) # IMAGE, EOP=1 (setup-only payload)
for i in range(n_image):
word = 0
for lane in range(4):
word |= (texels[i * 4 + lane] & 0xFFFFFFFF) << (32 * lane)
qw.append(word)
qwc = len(qw) # 6 + 1024 = 1030
if 16 + qwc > RAM_QWORDS:
sys.exit(f"payload {qwc} qw + 16 lead > {RAM_QWORDS} (bump RAM_SIZE_BYTES)")
payload_path = os.path.join(DATA, "payload_cube_setup.mem")
with open(payload_path, "w") as f:
f.write("// Ch343 LOCAL authentic-cube texture boot payload (64x64 PSMCT32 @ TBP=64).\n")
f.write("// game-derived (cubes .gs) -> gitignored. qw 0..15 zero; qw 16.. = GIF upload.\n")
for _ in range(16):
f.write(f"{0:032x}\n")
for x in qw:
f.write(f"{x & ((1 << 128) - 1):032x}\n")
for _ in range(RAM_QWORDS - 16 - qwc):
f.write(f"{0:032x}\n")
# --- one-shot bootlet (same shape as feeder_persp setup; QWC chosen for the cube payload) ---
bake.write_bios_mem(
"bios_cube_setup.mem",
bake.build_textured_demo_bootlet_disp(qwc, bake.CAP_DISPLAY1_HI, bake.CAP_FBW),
f"Ch343 LOCAL authentic-cube setup bootlet (QWC={qwc}). game-derived -> gitignored.")
# --- cube perspective staging via ps2_feeder --dump-file ---
# The translator bound TEX0 TBP0 to a word that aliases the Z buffer; rewrite the tex0 line so the
# render samples the relocated texture (TBP) instead. Single source of truth = the TBP above.
# The translator grabbed the EARLIEST ≤27-tri textured run, which is a WIDE-SHORT strip of ~4 tiny
# cubes (source 280x75 viewport-fit to 64x64 -> squashed to a y:[0..17] band of speckle). Isolate ONE
# cube (CUBE_TRIS contiguous tris) and re-fit it to fill the frame: a legitimate viewport zoom. The
# perspective ST/Q are interpolated linearly in SCREEN space, so a uniform 2D scale of the screen verts
# preserves per-pixel u=S/Q,v=T/Q exactly — faithful, just bigger. The first 6 tris (lines 5..10) are a
# corner-view cube: 3 faces meeting at center vertex (59,13).
CUBE_FIRST = 0 # index of the first persptri of the chosen cube
CUBE_TRIS = 6 # 3 faces x 2 tris
MARGIN = 2 # leave a 2px border in the 64x64 frame
SPAN_PX = (W - 1) - 2*MARGIN
# Two faithful coordinate conversions applied to the selected cube:
# (a) ST normalized->texel: retroDE's gs_persp_uv recovers the TEXEL index directly (expects S/Q in
# 0..TW, as bake.py persp_attrs emits S=u_texel/w). The dump's ST are NORMALIZED (0..1) so S/Q
# lands in [0,1] -> samples only texel (0,0). Scale S by TW, T by TH (what real GS does internally).
# (b) TEX0 -> DECAL (TFX=1): emit the authentic texels directly. The dump's per-vertex color came
# through as a uniform (128,0,0) (translator artifact); MODULATE with it masks G/B. DECAL matches
# the proven Ch342 checkerboard. (TODO: backport (a)+(b) into gs_translate_tex.py --perspective.)
scene_lines = open(SCENE).read().splitlines()
header = [] # comments + persp + tex0 (patched)
tris = [] # token-lists of persptri lines (len 20)
for ln in scene_lines:
tok = ln.split()
if not tok or tok[0] == "go":
continue
if tok[0] == "persptri" and len(tok) == 20:
tris.append(tok)
else:
if tok[0] == "tex0":
tok[1] = str(TBP) # tex0 <TBP> <TBW> <TW> <TH> <TFX>
tok[5] = "1" # DECAL
ln = " ".join(tok)
header.append(ln)
sel = tris[CUBE_FIRST:CUBE_FIRST + CUBE_TRIS]
VTX = ((1, 2, 3, 4), (6, 7, 8, 9), (11, 12, 13, 14)) # (X,Y,S,T) token indices per vertex
xs = [int(t[xi]) for t in sel for (xi, _, _, _) in VTX]
ys = [int(t[yi]) for t in sel for (_, yi, _, _) in VTX]
minx, maxx, miny, maxy = min(xs), max(xs), min(ys), max(ys)
scale = SPAN_PX / max(maxx - minx, maxy - miny, 1) # uniform -> preserve cube proportions
offx = MARGIN + (SPAN_PX - (maxx - minx) * scale) / 2.0
offy = MARGIN + (SPAN_PX - (maxy - miny) * scale) / 2.0
for t in sel:
for (xi, yi, si, ti) in VTX:
t[xi] = str(int(round(offx + (int(t[xi]) - minx) * scale))) # re-fit screen X
t[yi] = str(int(round(offy + (int(t[yi]) - miny) * scale))) # re-fit screen Y
t[si] = str(int(round(int(t[si]) * W))) # ST normalized -> texel
t[ti] = str(int(round(int(t[ti]) * H)))
patched = header + [" ".join(t) for t in sel] + ["go"]
N_TRI_OUT = len(sel)
scene_tmp = os.path.join(DATA, ".cube_persp.tbp.scene.txt")
with open(scene_tmp, "w") as f:
f.write("\n".join(patched) + "\n")
psf = os.path.join(HERE, "ps2_feeder")
subprocess.run(["gcc", "-O2", "-o", psf, os.path.join(HERE, "ps2_feeder.c")], check=True)
stg = subprocess.run([psf, "--dump-file", scene_tmp],
capture_output=True, text=True, check=True).stdout
os.remove(scene_tmp)
stg_words = [ln for ln in stg.splitlines() if ln.strip() and not ln.lstrip().startswith("//")]
staging_path = os.path.join(DATA, "feeder_cube_persp.mem")
with open(staging_path, "w") as f:
f.write("// Ch343 LOCAL cube perspective staging (ps2_feeder --dump-file cube_persp.scene).\n")
f.write("// game-derived (cubes .gs) -> gitignored. ABE=0 S1 perspective path.\n")
f.write(stg)
if not stg.endswith("\n"):
f.write("\n")
# --- raster-order texel hex for the smoke TB's VRAM round-trip check (LOCAL) ---
tex_ref_path = os.path.join(DATA, "feeder_cube_tex.mem")
with open(tex_ref_path, "w") as f:
f.write("// Ch343 LOCAL cube texels, raster order (vram word TBP*64+i). game-derived -> gitignored.\n")
for t in texels:
f.write(f"{t & 0xFFFFFFFF:08x}\n")
print(f"[Ch343] payload_cube_setup.mem : {qwc} qw (QWC={qwc}, 32 KiB EE RAM, TBP={TBP} TBW={TBW})")
print(f"[Ch343] bios_cube_setup.mem : bootlet QWC={qwc}")
print(f"[Ch343] feeder_cube_persp.mem : {len(stg_words)} staging words from cube_persp.scene")
print(f"[Ch343] outputs -> {DATA} (LOCAL / gitignored)")