forked from Ports/goldeneye-pc-port
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.
121 lines
3.9 KiB
Python
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))
|