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