WS4 level sweep driven to completion: 12/21 solo levels PASS
(load + render + no-crash). Full matrix in docs/LEVEL-STATUS.md,
reusable runner tools_pc/level_sweep.sh, WS6 checklist docs/LEVEL-PLAYTEST.md.
Fixes:
- D123 (tools_pc/d88_propdefs.py): crash class C1 on 6 levels
(Dam/Runway/Frigate/Statue/Streets/Cradle). D122 fallout - the widened
Vehichle/AircraftRecord.ailist slot was zeroed instead of carrying its
read-before-write integer AI-list id, so prop.c -> ailistFindById(0) ->
GAILIST_AIM_AT_BOND -> ai() ran a CHR aim list with a NULL chr ->
chrIsNotDeadOrShot(NULL). New OBJ_ID_WORDS arm. 4 levels -> PASS,
Runway falls through to C2. BUNKER1/Silo unregressed.
- D124 (port/src/gimgfixup.c): crash class C2 on Jungle. Compiled
explosion-DL pointer-sync keyed on an IMAGESEG marker that texLoad()
had already erased -> compiled globalDL_0xNNN arrays kept link-time
IMAGESEG words -> 0xABCDxxxx into fast3d on first explosion draw.
Latent on every level. Detect the slot from the compiled array instead.
Diagnosed, NOT fixed (write-ups in PCPortResearch.md SF):
- C2-GDL (D124-Facility addendum): Facility/Runway - model texture-pool
path yields a garbage tex->data; every model file loads at an odd
bank base (mempAllocBytesInBank does no alignment). Suspect
d43_emit.py/pcmodels sidecar texture-blob offsets (N64 8B vs PC 16B Gfx).
- C3/C6 (D125): Aztec/Bunker2/Surface2 - converted propDefs blob in RAM
doesn't match d88_propdefs.py offline output (garbage after record 0);
runtime sizepropdef() walk drifts ~100 records -> wrong door/modelid.
d88_propdefs.py convert_stream + sizepropdef() PORT strides verified
correct; suspect a dest-offset/pd_end bug in d88_emit.py propdefs emit.
Remaining: 9 crashes / 6 classes (C2-GDL, C3/C6, C2m Jungle G_MTX,
C5 Control BG portal, C4 Depot BG tile/room, C7 Surface1 sndSetupSound).
Priority order in docs/LEVEL-STATUS.md.
Also folds in the in-progress docs restructure (HANDOFF/AGENTS/CLAUDE,
PCPortResearch D121/D122 SF/SH entries, PLAN-linear-level-sweep,
HANDOFF-ARCHIVE, BACKLOG).
tools_pc/d88_propdefs.py had a per-type handler table plus a generic
fallback. Six `inherits ObjectRecord` propDef types (47 TINTED_GLASS,
39 VEHICHLE, 40 AIRCRAFT, 45 TANK, then 13 AUTOGUN, 20 AMMO/MultiAmmoCrate)
had no handler and fell through to the generic arm, which bswap32's N64
word 1. For an ObjectRecord that word is [s16 obj][s16 pad] and needs a
half-swap, not a word-swap - so the model id `obj` landed in the wrong
16 bits, giving a garbage/OOB index into PitemZ_entries[] and a crash in
modelLoad (loadobjectmodel.c:393) / modelInitRwData (model.c:6249).
BUNKER1/Silo were unaffected because neither emits any of the six types.
Fix (converter + matching walk stride, no game logic):
- d88_propdefs.py: OBJ_TAIL_DESC handler - real 144B PC ObjectRecord
prefix + typed tail (pointers widened 4->8B/8-aligned, [u16|u16] words
half-swapped). PROPDEF_PC_BYTES 39/40 176/180->208, 45 224->248.
- loadobjectmodel.c sizepropdef() #ifdef PORT arm: VEHICHLE/AIRCRAFT
44/45->52, TANK 56->62 words (N64 values in the #else sizeof switch).
Verified: -level_33/34/35 clear the model-load chain (they now crash
further in, in unrelated chr/fast3d code - separate blockers);
-level_09 BUNKER1 and -level_20 Silo unregressed. Sidecars regenerated
(d88_emit.py --regen, 21/21). Full write-up: PCPortResearch.md D122
(uncommitted, rides with the in-progress docs restructure).
Root cause (HIGH confidence): pure variable-timestep frame pacing, not
PRNG/uninit state. osGetCount() is wall-clock on PC (libultra.c:88), and
frametiming.c waitForNextFrame advances game logic by however many 60Hz
ticks of real time elapsed per render -> two runs diverge 32-69% of
pixels by frame 200-440. random.c seeding verified deterministic and
correct; the PRNG stream only diverges as a victim of the pacing jitter.
GE_DETERM fixed-tick mode: assessed NOT narrow (frame-coupled virtual
clock deadlocks the boss.c retrace gate / pre-first-frame waits;
robust version redesigns VI retrace + pacemaker semantics). Deferred
with a full design in PCPortResearch.md F/D117 per AGENTS.md.
tools_pc/framediff.py: structural/tolerant frame compare (16x12 grid
mean-colour delta + non-clear-% swing + 16x16 aHash), --mask for the
animated HUD, --exact for a future deterministic build, --update to
refresh tools_pc/golden/. Reads .ppm and .png (built-in PNG decoder).
Validated: two nondeterministic re-runs pass structural, --exact
reports the real 32-69%, a wrong-frame pair fails.
No game-logic or port-layer C changes; no probes left in tree.
put_f32 reversed bytes doff+1..doff+4 instead of doff..doff+3, dropping
each float's MSB and pulling in a byte of the adjacent field. Corrupted
every f32 in converted model rodata (joint Origins, LOD distances, BSP
planes, bounding radii) for all sidecar-loaded models. Compiled-in
front-end models were unaffected, hence 'logo fine, every guard a line'.
render_pos[] joint matrices now coherent; BUNKER guards render as
humanoids and monitor screens draw content. Regen: python
tools_pc/d43_emit.py ntsc-final.
SetupIntroCamera.lang1c is union { u16 lang_index[2]; u32 lang_ptr; }
and bondview_r.c reads lang1c.lang_index[1]. The intro converter
bswap32'd the whole word, which swaps element 0 and element 1 -- the
consumer then read the wrong language slot id, indexed g_LangBanks out
of range and crashed in langGet on a NULL bank. Byte-swap each u16 of
lang1c in place instead; lang20 stays a genuine s32 bswap32. Requires
d88_emit.py --regen.
stanMatchTileName reads a tile's packed name id through a StandTilePoint
alias: (u16)tile->x == stanIdHi and *((u8*)&tile->y) == stanIdLo. D78
left the id/room word as a verbatim byte array ("id is provably dead"),
which missed this aliased scalar read -- on little-endian PC the u16
load of the big-endian id-hi bytes came back byte-swapped, so every
name lookup during pad setup missed (0/276). Swap bytes 0-1 of the
id/room word in d69_emit's stan converter; idLo (byte 2) and room
(byte 3) stay put. Verified: 273/273 matches, pads now get real stan
tiles. Requires sidecar regen (d69_emit.py + d88_emit.py --regen).
d88_emit.py runs d88_propdefs.convert_stream over the propDefs region and
feeds its growth into the cumulative delta so intro + every later table
shifts correctly.
The propDefs stream is a flat s32[]; each record's serialized N64 word count
is fixed across all 21 levels (verified: the getools C sources tile every
propDefs region byte-for-byte against the ROM). Every pointer member is 0 in
the file, so records containing pointers grow 4->8B per pointer on PC.
d88_propdefs.py rewrites each record to its native PC struct layout: header
word (u16+u8+u8) and _mkword half-pairs swapped independently, scalars
bswap32'd, pointer slots widened to 8 zero bytes, runtime areas zero-filled.
d88_layoutprobe.c (expanded) is the compiler-verified sizeof/offsetof source
for PROPDEF_PC_BYTES. Converts + walk-verifies all 21 levels clean.
--regen drops all Usetup*Z rows (manifest + bytes) and rebuilds the sidecar
from the surviving d69 rows before re-converting, for clean converter
iteration. d88_layoutprobe.c prints sizeof/offsetof for every propDef record
struct against the real port include chain.
Read-only tool to enumerate PROPDEF_* record types per Usetup*Z file.
Walk currently drifts on the sizeof()-derived record types (DOOR/GUARD/
PROP/etc) because their exact N64 word sizes aren't yet confirmed -- that
confirmation is the core remaining D88.4 spec task.
Converts the 21 per-level Usetup*Z stage-setup files to PC layout,
appending rows to the same pccg.bin/manifest sidecar as d69_emit.py.
Delta-relocates the 8 pointer-widened sub-tables (header 40->80B,
waypoint/waygroup/PathRecord/AIListRecord/PadRecord/BoundPadRecord/pname)
and byte-swaps the s32 ID/offset fields. propDefs left as an opaque
relocated passthrough for now (D88.4).
Implements the D80-D81 format spec: size-preserving bswap of bg .seg
header/room/envdata tables, growth+relocation of the portal_data_entry
table (8B N64 -> 16B PC, offset_portal stays a real dereferenced pointer)
and its point-data blob (relocated by portal_delta, content size
unchanged), and the Tbg_*_stanZ prefix room-offset array (4B N64 -> 8B PC
slots, matching StanPrefixRecord's PC-compiled pointer alignment) plus
per-tile bswap of mid/tail halves and point coordinates. Round-trip
validated against BUNKER1 (bg/bg_sev_all_p.seg, Tbg_sev_all_p_stanZ):
header offsets, MaxNumRooms walk, portal targets/connectedRoom bytes, and
all 1066 stan tiles match the N64 source exactly.
Output: data/pccg-<region>/pccg.bin + manifest.csv (gitignored, same shape
as pcmodels.bin/D43). Regenerate: python tools_pc/d69_emit.py ntsc-final.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C3xib6jjMV8o2iE4JfixAo
- Move the two N64 reference screenshots out of the repo root into
docs/reference/ (n64-footage-2026-08-24.jpg, n64-footage-gun-barrel.png);
HANDOFF.md now points there.
- Gitignore screenshots/ (session captures) and tools_pc/mupen64/ (local
Mupen64 Plus checkout used for N64 reference footage; 8 MB of binaries).
- Commit tools_pc/mtxtest.c, the standalone fast3d matrix-convention
harness that HANDOFF/D72.3 docs reference as kept-for-reference.
- Refresh PORTING.md: status was still 'scaffolding / building once
implementation lands'; now reflects Phase 2 in progress and points at
docs/HANDOFF.md for current session state.
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.
- 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
No-Intro 'GoldenEye 007 (U) [!].z64' placed in data/ and verified
byte-identical (MD5 70c52588...) to the dev ROM copies
(data/ge007.ntsc-final.z64, baserom.u.z64). Header magic/title/country
check out; embedded CRCs DCBC50D1/09FD1AA3 re-computed and matched via
tools/n64cksum.c's CIC-6102 routine.
Adds tools_pc/romverify.c: one-shot integrity check (size, GE header
magic at 0x00, title, country byte, CRC1/CRC2) so any future ROM swap
is detectable in seconds. Fingerprint + provenance recorded in
docs/HANDOFF.md; gitignore the built romverify.exe.
structured fields + x86-64 loop-stride bug; fix designed, handoff updated
- Finding D33 in docs/PCPortResearch.md: the .z64 stores multi-byte
structured fields in big-endian byte order (bit-packed streams raw);
per-field load-time transform rule (u32 bswap32 / u16 bswap16 / u8
identity) validated over all 173 animation entries; full blob layout
mapped; exact reproduction of the gdb-observed garbage values.
- Second root cause: expand_ani_table_entries iterates s32** (+8 bytes/iter
on x86-64 vs +4 on N64), rebasing only even-indexed entries (verified in
compiled binary). Fix design: romdataFixupAnimationData() in port/, one
call between romCopy and expand, s32* loop stride fix.
- docs/HANDOFF.md rewritten with the implementation task; AGENTS.md phase
status updated. tools_pc/disasm.py (BE ROM research tool) added.