Files
James Dansereau 73ea03734d PC port: phase 2 — D48 review: re-plan D43 as offline pre-conversion (Plan B)
- D48 finding in PCPortResearch.md §F: d43_convert.py does not emit bytes
  (layout+remap validation only); Plan B = Python emit pass -> data/pcmodels-<region>/
  RZ sidecars served via port-layer hw_address/rom_size table patch (no C
  converter); Plan A kept as fallback with corrected staging guard (D_N64+D_PC<=avail)
- Moved all 23 d43_*.py investigation scripts from gitignored build-pc/ to tracked
  tools_pc/ (spec is now versioned); d43_cover.py path ref updated; converter
  re-run from new location: ALL CLEAN
- HANDOFF.md rewritten as two-session prompt: Session A = independent review of
  Plan B (8 claims, each with falsification criterion -> finding D49), Session B =
  execute (emit pass + round-trip validator, romdata plumbing, one-line hook,
  pixel-assert soak); verified-facts converter spec preserved
- AGENTS.md phase status updated to D48 re-plan
2026-08-22 18:43:04 -04:00

78 lines
2.8 KiB
Python

# D43 helper: decode a GE model file from ROM (N64 layout) and dump its
# node tree + GDL command streams. Usage: python d43_decode.py <name-substr>
import csv, struct, sys, zlib
ROM = r"data/ge007.ntsc-final.z64"
rom = open(ROM, "rb").read()
rows = list(csv.reader(open(r"scripts/filelist.u.csv")))
entries = []
for r in rows:
if len(r) < 3 or not r[2]:
continue
try:
addr, size = int(r[0]), int(r[1])
except ValueError:
continue
entries.append((addr, size, r[2]))
flt = sys.argv[1] if len(sys.argv) > 1 else ""
want = None
for addr, size, name in sorted(entries):
base = name.rsplit("/", 1)[-1]
if base.endswith(".bin"):
base = base[:-4]
if flt and flt.lower() in base.lower():
want = (addr, size, base)
break
if not want:
sys.exit("no file matching %r" % flt)
addr, size, base = want
data = rom[addr:addr + size]
out = zlib.decompress(data[2:], -15)
print("%s: rom=%d decomp=%d (0x%X)" % (base, size, len(out), len(out)))
# N64 header-less layout: switches[NS] then textures[NT] then RootNode.
# We don't know NS/NT from the file alone; find them via the exe record is
# not available here, so heuristic: scan for a plausible node tree start.
# Instead: dump raw words and let the eye decode. Print first 64 bytes.
print("head:", out[:64].hex())
def be16(b, o): return struct.unpack_from(">H", b, o)[0]
def be32(b, o): return struct.unpack_from(">I", b, o)[0]
# Try to find GDL streams: sequences of 8-byte words whose w0>>24 is a known cmd.
CMDS = {0xc0:"NOOP",0x05:"VTX",0x0f:"MTX",0x13:"DL",0x15:"MOVEWORD",0x19:"POPMTX",
0x02:"MATRIXLOAD",0x07:"CLIPRATIO",0x0b:"SPSETOTHERMODE?",0x0d:"ENVMODE?",
0x23:"RDPPPIPESYNC",0x25:"RDPSETSCISSOR?",0x26:"RDPSETOPTIONS?",0x27:"RDPLOADTLUT?",
0x28:"RDPSETCONVERT?",0x29:"RDPSETSCENECULL?",0x2a:"RDPFILLRECT",0x2b:"RDPSETFOGCOLOR?",
0x2c:"RDPSETPRIMCOLOR",0x2d:"RDPSETPROJECT?",0x2e:"RDPSETBADEVENTS?",0x2f:"RDPLOADUTILE?",
0x31:"RDPSETENVCOLOR",0x32:"RDPSETCOMBINE",0xba:"RDPSETOTHERMODE_H",0xbb:"RDPSETOTHERMODE_L",
0xbf:"RDPSETTEXIMAGE",0xb1:"RDPSETTILESIZE?",0xbbb:None}
def cmdname(c):
return CMDS.get(c, "0x%02x" % c)
# scan for the longest run of plausible Gfx words
best = (0, 0, 0)
i = 0
while i + 8 <= len(out):
w0 = be32(out, i)
c = w0 >> 24
if c in CMDS:
j = i
while j + 8 <= len(out) and (be32(out, j) >> 24) in CMDS:
j += 8
L = j - i
if L > best[0]:
best = (L, i, c)
i += 4
L, off, c0 = best
print("\nlongest GDL-like run: %d bytes at offset 0x%X (starts cmd %s)" % (L, off, cmdname(c0)))
# dump first 24 words of that run
for k in range(min(24, L // 8)):
w0 = be32(out, off + 8*k)
w1 = be32(out, off + 8*k + 4)
print(" [%2d] %s w0=%08X w1=%08X" % (k, cmdname(w0 >> 24), w0, w1))