Files
goldeneye-pc-port/tools_pc/ppm2bmp.py
T
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

67 lines
2.0 KiB
Python

#!/usr/bin/env python3
"""ppm2bmp.py — convert binary P6 PPM to 24-bit BMP (no dependencies).
Usage: ppm2bmp.py in.ppm out.bmp [scale] (scale = integer, default 1)
Useful for eyeballing GE_PCDUMP frames without an image library.
"""
import struct
import sys
def load_ppm(path):
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:
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]))
w, h, maxval = fields
if maxval != 255:
raise ValueError("unsupported maxval %d" % maxval)
px = data[pos + 1:pos + 1 + w * h * 3]
if len(px) < w * h * 3:
raise ValueError("truncated pixel data")
return w, h, px
def main(argv):
if len(argv) < 3:
print(__doc__)
return 2
scale = int(argv[3]) if len(argv) > 3 else 1
w, h, px = load_ppm(argv[1])
w *= scale
h *= scale
row_size = (w * 3 + 3) & ~3
out = bytearray()
out += struct.pack("<2sIHHI", b"BM", 54 + row_size * h, 0, 0, 54)
out += struct.pack("<IiiHHIIiiII", 40, w, h, 1, 24, 0, w * h * 3, 2835, 2835, 0, 0)
for y in range(h - 1, -1, -1): # BMP is bottom-up
src_y = (y // scale) * (w // scale) * 3
row = bytearray(row_size)
for x in range(w):
i = src_y + (x // scale) * 3
row[x * 3] = px[i + 2] # B
row[x * 3 + 1] = px[i + 1] # G
row[x * 3 + 2] = px[i] # R
out += row
with open(argv[2], "wb") as f:
f.write(out)
print("%s -> %s (%dx%d)" % (argv[1], argv[2], w, h))
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))