Files
James Dansereau 8f631462f3 PC port: phase 2 — D59-D68 milestone: full intro renders; blocker is D69 (BG-file BE headers)
D59/D64: the 'external GL DLL' crash was an unbounded blood-RLE write
(sentinel die_blood_image_end lost its N64 section adjacency on PC)
clobbering the gun-barrel sub-DL at ptr_logo_and_walletbond_DL+0x200.
D60: osPiStartDma DMA-target validation (VirtualQuery-based) in libultra.c.
D61/D63: TEMP env-gated ROM-read / VTX-pool / menu probes (strip list in
HANDOFF Task 3). D62: OSMesgQueue/OSScMsg PC layout verification.
D65/D65b: enum HEADS/BODIES signed sentinels under PORT (PC GCC picks
unsigned underlying types; N64 semantics were signed).
D66: romCopyAligned + ramrom replay 64-bit pointer width (s32 truncation
of .bss targets > 4 GiB). D67: struct image_entry N64 layout
reconstruction (dataoffset:24 first, all-u32 bitfields, sizeof==8).
D68: Globalimagetable BE->LE fixup (port/src/gimgfixup.c): bswap the
IMAGESEG-marked Gfx w1 words in the 17 ROM-copied DLs + sImageTableEntry.
index in the 32 tables after texReset's romCopy; marker scan moved to
bytes 6..7 under PORT; compiled globalDL shadows synced post-texLoad for
explosion.c's g_ExplosionDisplayLists.

Result: game boots, plays intro music, and renders the entire ~3.5-minute
intro (logo -> gun barrel with Brosnan -> cast screen) at ~59 fps. New
blocker D69: load_bg_file faults on first stage load - BG-file headers are
N64 big-endian segment-0x0F offsets read LE on PC. Full analysis + strategy
in PCPortResearch.md §F/D69; HANDOFF rewritten for the D69 session.
2026-08-24 03:27:29 -04:00

121 lines
3.9 KiB
Python

#!/usr/bin/env python3
"""pixcount.py — count non-clear-color pixels in PPM frame dumps (GE_PCDUMP).
The game clears to black (gfx_opengl_clear_framebuffer: glClearColor 0,0,0),
so "non-clear" == any pixel that is not exactly (0,0,0). This turns "the
scene contains actual model geometry / glyphs" into a number: a blank frame
counts ~0, a rendered legal page counts thousands.
Usage (run from anywhere; paths relative to CWD):
pixcount.py frame.ppm [more.ppm ...] per-file stats (count + bbox)
pixcount.py a.ppm b.ppm --diff pixel diff between two captures
Exit status: 0 if every listed file has at least one non-clear pixel
(--diff: 0 if the two files differ at all), 1 otherwise, 2 on usage/IO error.
"""
import re
import sys
# any non-zero byte (channels are stored R,G,B interleaved)
_NZ = re.compile(rb"[^\x00]")
def load_ppm(path):
"""Parse a binary P6 PPM -> (width, height, bytes)."""
with open(path, "rb") as f:
data = f.read()
if data[:2] != b"P6":
raise ValueError("%s: not a binary P6 PPM" % path)
pos = 2
fields = []
while len(fields) < 3:
# skip whitespace and '#' comments
while pos < len(data) and data[pos:pos + 1].isspace():
pos += 1
if data[pos:pos + 1] == b"#":
while pos < len(data) and data[pos] != 0x0A:
pos += 1
continue
start = pos
while pos < len(data) and not data[pos:pos + 1].isspace():
pos += 1
fields.append(int(data[start:pos]))
pos += 1 # single whitespace after maxval
w, h, maxval = fields
if maxval != 255:
raise ValueError("%s: unsupported maxval %d" % (path, maxval))
px = data[pos:pos + w * h * 3]
if len(px) < w * h * 3:
raise ValueError("%s: truncated pixel data (%d of %d bytes)"
% (path, len(px), w * h * 3))
return w, h, px
def _bbox(indices, w):
if not indices:
return None
minx = min(i % w for i in indices)
maxx = max(i % w for i in indices)
miny = min(i // w for i in indices)
maxy = max(i // w for i in indices)
return "(%d,%d)-(%d,%d)" % (minx, miny, maxx, maxy)
def stats(path):
w, h, px = load_ppm(path)
total = w * h
# pixel indices where any channel byte is non-zero (C-speed regex scan)
idx = set()
for off in (0, 1, 2):
ch = px[off::3]
for m in _NZ.finditer(ch):
idx.add(m.start())
pct = 100.0 * len(idx) / total
print("%s: %dx%d non-clear %d (%.2f%%) bbox %s"
% (path, w, h, len(idx), pct, _bbox(idx, w) or "-"))
return len(idx)
def diff(path_a, path_b):
wa, ha, pa = load_ppm(path_a)
wb, hb, pb = load_ppm(path_b)
if (wa, ha) != (wb, hb):
print("sizes differ: %dx%d vs %dx%d" % (wa, ha, wb, hb))
w, h = min(wa, wb), min(ha, hb)
n = w * h * 3
pa, pb = pa[:n], pb[:n]
# XOR as big ints, then scan the result for non-zero bytes (C-speed)
x = (int.from_bytes(pa, "big") ^ int.from_bytes(pb, "big")).to_bytes(n, "big")
idx = {m.start() // 3 for m in _NZ.finditer(x)}
total = w * h
print("diff %s vs %s: %d differing pixels (%.2f%% of %dx%d) bbox %s"
% (path_a, path_b, len(idx), 100.0 * len(idx) / total, w, h,
_bbox(idx, w) or "-"))
return len(idx)
def main(argv):
if len(argv) < 2:
print(__doc__)
return 2
do_diff = "--diff" in argv
files = [a for a in argv[1:] if a != "--diff"]
try:
if do_diff:
if len(files) != 2:
print("--diff needs exactly two files")
return 2
return 0 if diff(files[0], files[1]) else 1
ok = True
for f in files:
if not stats(f):
ok = False
return 0 if ok else 1
except (IOError, ValueError) as e:
print("error: %s" % e)
return 2
if __name__ == "__main__":
sys.exit(main(sys.argv))