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>
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
#!/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)")
|
||||
Reference in New Issue
Block a user