ec82764bef
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>
147 lines
8.6 KiB
Python
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)")
|