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>
96 lines
4.6 KiB
Python
96 lines
4.6 KiB
Python
#!/usr/bin/env python3
|
|
"""retroDE_ps2 — Ch341 Brick 2: faithful texture downscale + boot-fixture generation.
|
|
|
|
Reads the LOCAL extracted cube texture (256x256 PSMCT32, from gs_texture.py --extract), box-downsamples
|
|
it to 64x64 PSMCT32 (a declared, reported linear transform — NOT a GS-feature approximation; the same
|
|
class as the Ch340 viewport fit), and emits the 64x64 texels as a $readmemh .mem the boot setup payload
|
|
uploads to a VRAM-resident TBP. Also runs Codex's gate: hash the dump's N texture uploads and report
|
|
whether they are byte-identical (so one preload suffices). Only aggregate facts/hashes are committable;
|
|
the texel .mem stays LOCAL (derived from the dump).
|
|
|
|
Declared transform: source 256x256 -> 64x64 (factor 4) => translator must scale UVs by /4.
|
|
|
|
Usage:
|
|
gs_bake_texture.py <dump.gs[.xz|.zst]> [--blob extracted/tex0_blob.bin] [--out outdir] [--report r.txt]
|
|
"""
|
|
import sys, os, struct, hashlib
|
|
sys.path.insert(0, os.path.dirname(__file__))
|
|
import gs_parse, gs_texture
|
|
|
|
SRC=256; DST=64; FACTOR=SRC//DST # 4
|
|
|
|
def upload_identity_gate(path):
|
|
"""Hash every IMAGE upload payload; report if all byte-identical."""
|
|
d = gs_parse.read_dump_bytes(path)
|
|
h, events = gs_parse.parse_dump(path)
|
|
hashes=[]
|
|
for e in events:
|
|
if e.kind=="IMAGE":
|
|
blob = d[e.byte_off:e.byte_off+e.info.get("bytes",0)]
|
|
hashes.append(hashlib.sha256(blob).hexdigest())
|
|
return hashes
|
|
|
|
def downsample_psmct32(blob):
|
|
"""256x256 ABGR words -> 64x64 ABGR words, box filter (average each 4x4 source block per channel)."""
|
|
assert len(blob) == SRC*SRC*4, f"expected {SRC*SRC*4} bytes, got {len(blob)}"
|
|
src = struct.unpack(f"<{SRC*SRC}I", blob)
|
|
out = []
|
|
for oy in range(DST):
|
|
for ox in range(DST):
|
|
ar=ag=ab=aa=0
|
|
for dy in range(FACTOR):
|
|
for dx in range(FACTOR):
|
|
w = src[(oy*FACTOR+dy)*SRC + (ox*FACTOR+dx)]
|
|
ar += w & 0xFF; ag += (w>>8)&0xFF; ab += (w>>16)&0xFF; aa += (w>>24)&0xFF
|
|
n = FACTOR*FACTOR
|
|
out.append((ar//n) | ((ag//n)<<8) | ((ab//n)<<16) | ((aa//n)<<24))
|
|
return out # 64*64 ABGR words
|
|
|
|
def main(argv):
|
|
if len(argv)<2: print(__doc__); return 2
|
|
path=argv[1]
|
|
def opt(n,d=None): return argv[argv.index(n)+1] if n in argv else d
|
|
blobp = opt("--blob", os.path.join(os.path.dirname(path),"extracted","tex0_blob.bin"))
|
|
outdir = opt("--out", os.path.join(os.path.dirname(path),"extracted"))
|
|
|
|
R=[f"# Ch341 Brick 2 texture bake (source {os.path.basename(path)}; aggregate facts/hashes only)"]
|
|
# --- gate: are the uploads byte-identical? (auditable: list every distinct hash + its count) ---
|
|
hh = upload_identity_gate(path)
|
|
from collections import Counter
|
|
cnt = Counter(hh)
|
|
R.append(f"texture uploads: {len(hh)} distinct payloads: {len(cnt)} "
|
|
+ ("ALL BYTE-IDENTICAL -> one preload suffices" if len(cnt)==1 else "DIFFER -> translator must not let a subsegment cross a re-upload to its TBP"))
|
|
for i,(hsh,c) in enumerate(sorted(cnt.items(), key=lambda x:-x[1])):
|
|
R.append(f" upload payload #{i}: sha256[0..15]={hsh[:16]} count={c}")
|
|
|
|
if not os.path.exists(blobp):
|
|
R.append(f"!! no extracted blob at {blobp} (run gs_texture.py --extract first)")
|
|
print("\n".join(R)); return 1
|
|
blob = open(blobp,"rb").read()
|
|
src_hash = hashlib.sha256(blob).hexdigest()
|
|
out = downsample_psmct32(blob)
|
|
out_bytes = struct.pack(f"<{len(out)}I", *out)
|
|
out_hash = hashlib.sha256(out_bytes).hexdigest()
|
|
R.append("")
|
|
R.append(f"downsample: {SRC}x{SRC} PSMCT32 -> {DST}x{DST} PSMCT32 (box filter /{FACTOR}; UV scale /{FACTOR})")
|
|
R.append(f" src sha256[0..15]={src_hash[:16]} dst sha256[0..15]={out_hash[:16]} dst_bytes={len(out_bytes)} (= {DST}*{DST}*4)")
|
|
|
|
os.makedirs(outdir, exist_ok=True)
|
|
# boot IMAGE upload packs 4 ABGR words / 128-bit qword -> $readmemh as 64-bit? bake.py packs the
|
|
# IMAGE payload as 64-bit words (lane pairs). Emit one 32-bit ABGR texel per line (the bake helper
|
|
# will pack); also emit a raw .bin for reuse. LOCAL only.
|
|
memp = os.path.join(outdir, "cube_tex_64.mem")
|
|
with open(memp,"w") as f:
|
|
for w in out: f.write(f"{w & 0xFFFFFFFF:08x}\n")
|
|
binp = os.path.join(outdir, "cube_tex_64.bin")
|
|
open(binp,"wb").write(out_bytes)
|
|
R.append(f" wrote LOCAL fixture: {os.path.basename(memp)} ({len(out)} texels) + {os.path.basename(binp)}")
|
|
|
|
report="\n".join(R)+"\n"
|
|
print(report)
|
|
if opt("--report"): open(opt("--report"),"w").write(report); print(f"[wrote report -> {opt('--report')}]")
|
|
return 0
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main(sys.argv))
|