Files
retroDE_ps2/tools/gs_glyph_repack.py
T
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

147 lines
8.6 KiB
Python

#!/usr/bin/env python3
"""retroDE_ps2 — Ch345b authentic 32-glyph segment re-pack (content normalization).
The cubes-dump glyph sprites MINIFY a 256x256 PSMCT32 atlas (256KB >> the <=64KiB BRAM VRAM) and 31/32 wrap
past v=255. The dump's actual texture wrap is in the GS freeze state (absent from the packet stream), so we
DECLARE REPEAT (kept visible in the report). For each glyph we compute, per SCREEN pixel, the exact texel the
hardware samples (gs_stub `dda_uv`: coord = (u0 + ((du_dx*(x-x0))>>>16)) & 0x7FF, du_dx=((u1-u0)<<16)/(x1-x0),
then REPEAT mask & 0xFF for the 256-wide atlas) and BAKE that texel into a dense glyph-sized sub-texture.
Packing those dense glyphs + a 1:1 UV remap reproduces the EXACT rendered pixels (nearest of the original
minified sampling == 1:1 of the baked dense glyph, by construction) while fitting VRAM.
Outputs into sim/data/top_psmct32_raster_demo/ (LOCAL, dump-derived -> gitignored):
glyph_atlas.mem packed dense-glyph atlas (PSMCT32, $readmemh into VRAM at GLYPH_TBP)
glyph_sprites.mem feeder SPRITE staging (sprite_mode word0[33]): grid screen layout + 1:1 packed UV
glyph_ref.mem SOFTWARE REFERENCE FB: original-atlas + declared-REPEAT + nearest render over the BG,
for the pre-fit TB pixel-diff. No authentic claim until that diff passes.
DECLARED: REPEAT wrap, nearest sampling, white(0x80) MODULATE tint (the dump's per-vertex RGBA is a freeze-
state value; identity tint shows the raw glyph texels)."""
import sys, os, struct
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")
# Ch346: source the texture RESIDENT at the glyph-sprite draws (epoch 2, the real font),
# NOT the first upload (epoch 1 = the cube checker the demo ping-pongs into the same TBP).
# tex0_font.bin is extracted by the residency preflight; fall back to the old blob only if absent.
_FONT = os.path.join(ROOT, "captures", "gs", "cubes", "extracted", "tex0_font.bin")
ATLAS = _FONT if os.path.exists(_FONT) else os.path.join(ROOT, "captures", "gs", "cubes", "extracted", "tex0_blob.bin")
SPRITES = os.path.join(ROOT, "captures", "gs", "cubes", "extracted", "cubes.sprites")
ATLAS_W = ATLAS_H = 256
FB_W = FB_H = 128 # 128x128 fits the 32-glyph grid at native (1:1) size
FBW = 2 # FB width in 64-px pages (128-wide FB)
GLYPH_TBP = 256 # packed atlas VRAM word base = 256*64 = 16384 (right after the 128x128 FB = 16384 words)
BG = 0xFF0000C0 # opaque blue background (PSMCT32 {A,B,G,R} = FF,00,00,C0)
GAP = 2
def tdiv(a, b): # truncate-toward-zero (matches SystemVerilog signed `/`)
if b == 0: return 0
q = abs(a) // abs(b)
return q if (a < 0) == (b < 0) else -q
def htexel(p, p0, c0, e0, e1, span): # gs_stub dda_uv (one axis) -> REPEAT-masked atlas index
step = tdiv((e1 - e0) << 16, span) if span != 0 else 0
coord = (c0 + ((step * (p - p0)) >> 16)) & 0x7FF # 11-bit truncation, arithmetic >>16
return coord & 0xFF # REPEAT, 256-wide power-of-two mask
def mod8(t, c): p = (t * c) >> 7; return 0xFF if p > 0xFF else p
def srcover(cs, cd, as_): # gs_alpha_blend: ((cs-cd)*min(as,128)>>7)+cd, clamp
ae = 128 if as_ > 128 else as_
v = (((cs - cd) * ae) >> 7) + cd
return 0 if v < 0 else 255 if v > 255 else v
for p in (ATLAS, SPRITES):
if not os.path.exists(p): sys.exit(f"missing local input: {p}")
# Ch346 fail-closed gate: refuse to repack a non-glyph-plausible atlas (this is what caught Ch345b — the
# original tex0_blob.bin was the cube CHECKER, no alpha mask). The resident font epoch passes (mask-like).
from gs_texture_residency import payload_stats, font_like
_ok, _why = font_like(payload_stats(open(ATLAS, "rb").read(), PSMCT32 := 0x00))
if not _ok:
sys.exit(f"[Ch345b] REFUSING: atlas {os.path.basename(ATLAS)} is not glyph-plausible ({_why}).\n"
f" Pick the RESIDENT font epoch first: gs_texture_residency.py <dump> finds it; the wrong\n"
f" epoch (cube checker) has no alpha mask. See project_ch345b_content_finding memory.")
atlas = list(struct.unpack(f"<{ATLAS_W*ATLAS_H}I", open(ATLAS, "rb").read()))
glyphs = []
for ln in open(SPRITES):
t = ln.split()
if t and t[0] == "sprite":
x0,y0,x1,y1,u0,v0,u1,v1 = (int(round(float(v))) for v in t[1:9])
glyphs.append(dict(x0=x0,y0=y0,x1=x1,y1=y1,u0=u0,v0=v0,u1=u1,v1=v1))
# --- bake each glyph: dense screen-sized sub-texture of the EXACT sampled texels ---
for g in glyphs:
w = abs(g["x1"]-g["x0"]); h = abs(g["y1"]-g["y0"]); g["w"], g["h"] = w, h
sx0 = min(g["x0"],g["x1"]); sy0 = min(g["y0"],g["y1"])
baked = []
for ly in range(h):
for lx in range(w):
ut = htexel(sx0+lx, g["x0"], g["u0"], g["u0"], g["u1"], g["x1"]-g["x0"])
vt = htexel(sy0+ly, g["y0"], g["v0"], g["v0"], g["v1"], g["y1"]-g["y0"])
baked.append(atlas[vt*ATLAS_W + ut])
g["baked"] = baked
# --- pack dense glyphs into a grid atlas, and lay them out on a compact 64x64 screen ---
COLS = 8
cellw = max(g["w"] for g in glyphs); cellh = max(g["h"] for g in glyphs)
rows = (len(glyphs) + COLS - 1) // COLS
PACK_W = COLS * cellw; PACK_H = rows * cellh
TBW = (PACK_W + 63) // 64
pack = [0] * (PACK_W * PACK_H)
for i, g in enumerate(glyphs):
pu = (i % COLS) * cellw; pv = (i // COLS) * cellh; g["pu"], g["pv"] = pu, pv
# screen grid (1:1 so du_dx = 1<<16); must fit FB_W x FB_H
g["sx0"] = GAP + (i % COLS) * (cellw + 1); g["sy0"] = GAP + (i // COLS) * (cellh + 1)
g["sx1"] = g["sx0"] + g["w"]; g["sy1"] = g["sy0"] + g["h"]
for ly in range(g["h"]):
for lx in range(g["w"]):
pack[(pv+ly)*PACK_W + (pu+lx)] = g["baked"][ly*g["w"]+lx]
maxx = max(g["sx1"] for g in glyphs); maxy = max(g["sy1"] for g in glyphs)
if maxx > FB_W or maxy > FB_H:
sys.exit(f"screen grid {maxx}x{maxy} exceeds {FB_W}x{FB_H} — raise FB or COLS")
# --- software reference FB: render baked glyphs (identity tint) source-over the BG ---
ref = [BG] * (FB_W * FB_H)
for g in glyphs:
for ly in range(g["h"]):
for lx in range(g["w"]):
t = g["baked"][ly*g["w"]+lx]; aA = (t >> 24) & 0xFF
cs_r, cs_g, cs_b = t & 0xFF, (t>>8)&0xFF, (t>>16)&0xFF # white tint = identity MODULATE
cd = BG; cd_r, cd_g, cd_b = cd & 0xFF, (cd>>8)&0xFF, (cd>>16)&0xFF
outv = (aA<<24) | (srcover(cs_b,cd_b,aA)<<16) | (srcover(cs_g,cd_g,aA)<<8) | srcover(cs_r,cd_r,aA)
ref[(g["sy0"]+ly)*FB_W + (g["sx0"]+lx)] = outv
# --- feeder SPRITE staging (sprite_mode word0[33]): grid screen + 1:1 packed UV ---
def frame_1(fbw): return (fbw & 0x3F) << 16
def alpha_srcover(): return 0x44
def tex0_pack(tbp, tbw, tw, th, tfx): return tbp | (tbw<<14) | (0<<20) | (tw<<26) | (th<<30)
def uvd(u, v): return ((u<<4)&0x3FFF) | (((v<<4)&0x3FFF)<<14)
def xyz2(x, y): return ((x&0xFFF)<<4) | ((y&0xFFF)<<20)
TW = max(PACK_W-1, 1).bit_length(); TH = max(PACK_H-1, 1).bit_length() # log2 ceil for TEX0
stg = []
stg.append((len(glyphs) & 0xFFFF) | (1 << 33))
stg.append(frame_1(FBW)); stg.append(alpha_srcover()); stg.append(0); stg.append(0)
stg.append(tex0_pack(GLYPH_TBP, TBW, TW, TH, 0)); stg.append(6 | (1<<4) | (1<<6)) # SPRITE+TME+ABE
for g in glyphs:
tint = 0x80808080
stg += [tint, uvd(g["pu"], g["pv"]), xyz2(g["sx0"], g["sy0"]),
tint, uvd(g["pu"]+g["w"], g["pv"]+g["h"]), xyz2(g["sx1"], g["sy1"])]
if len(stg) > 256: sys.exit(f"staging {len(stg)} > 256 words")
os.makedirs(DATA, exist_ok=True)
with open(os.path.join(DATA, "glyph_atlas.mem"), "w") as f:
f.write(f"// Ch345b LOCAL packed glyph atlas {PACK_W}x{PACK_H} PSMCT32 (declared REPEAT). gitignored.\n")
for p in pack: f.write(f"{p & 0xFFFFFFFF:08x}\n")
with open(os.path.join(DATA, "glyph_sprites.mem"), "w") as f:
f.write(f"// Ch345b LOCAL feeder SPRITE staging: {len(glyphs)} re-packed authentic glyphs. gitignored.\n")
for w in stg: f.write(f"{w & 0xFFFFFFFFFFFFFFFF:016x}\n")
for _ in range(256 - len(stg)): f.write(f"{0:016x}\n")
with open(os.path.join(DATA, "glyph_ref.mem"), "w") as f:
f.write(f"// Ch345b LOCAL SOFTWARE REFERENCE FB {FB_W}x{FB_H} (orig atlas + REPEAT + nearest). gitignored.\n")
for p in ref: f.write(f"{p & 0xFFFFFFFF:08x}\n")
print(f"[Ch345b] glyphs={len(glyphs)} packed atlas={PACK_W}x{PACK_H} ({PACK_W*PACK_H*4}B, TBW={TBW} TW={TW} TH={TH})")
print(f"[Ch345b] screen grid {COLS} cols, bbox {maxx}x{maxy} in {FB_W}x{FB_H}; staging {len(stg)} words")
print(f"[Ch345b] DECLARED: REPEAT wrap + nearest + white MODULATE. atlas/sprites/ref -> {DATA} (LOCAL)")