- README, LICENSE (MIT for the port layer), NOTICE, CONTRIBUTING. - Restructure docs: internals.md, porting-notes.md, dev-process.md + dev/agentic-development.md (the research write-up), dev/ (raw record). - Replace decomp status-badge CI with a PC-port validate workflow. - Remove session/agent scaffolding and stale planning docs from the tree. - Add .gitattributes (normalize line endings to LF); add screenshots. The pre-2026-08 history is the GoldenEye 007 decompilation by Larry Ficken (KholdFuzion) and contributors, preserved unchanged. The PC-port work from 2026-08-17 on is mine.
31 KiB
HANDOFF ARCHIVE — session-by-session narrative
Reference only. Current state, next task, and environment live in
docs/HANDOFF.md; per-finding detail indocs/internals.md§F/§H (indexed at the top of §F). This file is the frozen accumulation of prior sessions' handoff briefs (through session M-11), kept so the reasoning behind a fix is recoverable. Everything below is as-written at the time and may be superseded.
Handoff brief — GoldenEye 007 PC port (Phase 2: Session M-3 —
STAGE-LOAD → RENDER crash chain CLEAR; BUNKER1 renders, next is D75 model quality)
PREFLIGHT — before touching anything (a concrete next-step below is NOT a reason to skip this)
CLAUDE.md(auto-loaded) +AGENTS.mdnon-negotiables.- This file, top-to-bottom.
docs/porting-notes.md— recurring bug classes.- Dispatching a subagent?
docs/dev-process.mdFIRST. Every brief needs FILES / BUDGET / ON-EXPIRY / CONSTRAINTS / REPORT (see CLAUDE.md "Dispatch preflight").
Paste-ready brief. Authoritative context: AGENTS.md,
docs/internals.md §F (D69, D78-D102), docs/BRIEF-D69-stage-load.md.
NEW DIRECTION (2026-08-29): docs/PLAN-linear-level-sweep.md — pivot
to breadth-first: fix the boot→intro→menu→level-select path to
functional, not crashing (cosmetics parked in GRAPHICS-BACKLOG.md),
then sweep all 21 solo levels for load+render+no-crash. Start with WS1
(auto-inject memallocstringtable args for bare -level_XX).
READ THIS FIRST — crash chain CLEAR; viewport fixed; room geometry is next
-level_09 -ml0 -me0 -mgfx100 -mvtx50 -mt700 -ma150 boots BUNKER1
and renders continuously with no fault (60 s+, 5000+ VI posts at full
framerate; attract mode also clean). The stage-load → first-frame →
in-level crash chain (blocker since D69) is resolved.
D103 (session M-4) fixed the "~46 %, lower half" symptom — it was a
single native-resolution/viewport bug (osViSetMode hard-coded height
480 → RATIO_Y half of RATIO_X), not per-model. Frame is now full
(91.7 %, correct letterbox + HUD placement). See §F "D103".
D105 (session M-4) fixed room geometry — zbufClearCurrentPlayer
(viewport.c) used the N64 "fill the Z buffer as a colour image" idiom,
which fast3d doesn't emulate, and nothing emitted G_CLEAR_DEPTH_EXT, so
the depth buffer was never cleared and all ~160 k room triangles failed
the Z test (only skyRender's background fill survived). PORT branch now
emits G_CLEAR_DEPTH_EXT. See §F "D104"/"D105". BUNKER1 now renders
recognisably — textured walls, storage racks, floor (2585 colours, sky
16 %), 70 s crash-free.
D106 (session M-4) — the big sky-void through doorways was the portal
BFS dropping the next room: a portal straddling the camera near-plane
projects z==0 clip points to ±1e20 screen coords, and on x86-64 that
garbage came back min>max on one axis, slipping past the
degenerate-box check that on N64 clamps it to full-screen. PORT guard in
sub_GAME_7F0B5864 treats non-finite / out-of-range bounds as
full-screen. Visible rooms/frame 1–3 → 2–4. See §F "D106".
D107 (session M-4) — blurry ceilings/wall-panels were fast3d sampling
GE's first mip (render tile 1), whose single-G_LOADBLOCK TMEM slot
fast3d never registers, so import_texture fabricated a 16×16 crop of
the base image and magnified it. gfx_lod_tile_offset now returns 0
(base tile) when detail textures are off. BUNKER1 rooms render crisp.
See §F "D107".
Skeletal models — FIXED (session M-5, D112). The "3D line" / missing
characters were tools_pc/d43_emit.py's put_f32 reversing the wrong 4
bytes (src[doff+4:doff:-1] → bytes doff+1..doff+4, dropping the MSB),
corrupting every f32 in converted model rodata (joint Origins, LOD
distances, BSP planes). Compiled-in front-end models were unaffected
(native LE), hence "logo fine, every guard broken". Fix: one line,
src[doff:doff+4][::-1]; regen with python tools_pc/d43_emit.py ntsc-final. BUNKER guards now render as coherent humanoids, monitor
screens draw content. See §F "D108–D112".
"props emit no geometry" was a STALE premise — props/doors emit complete leaf DLs (~15k/run, valid ptrs; fast3d transforms them). The storage-room void is NOT closed doors failing to render.
Priority (user, M-11): get BUNKER1 — then every level — playable
start-to-finish with no crashes. Fix crashes/hangs first; cosmetic
rendering (D74/D75/D76/D114/D116) is logged in docs/GRAPHICS-BACKLOG.md
and comes later. D120 (blood-stain converter) is the nearest real
converter gap; extending d43_emit.py for opcode-0x18 is a good
subagent brief. After BUNKER1: walk the objective/exit path to a level
transition, then the next stage.
Real remaining work, in order (updated session M-8):
- Input layer (Phase 3) — IN PROGRESS (M-8 agent, §F D118).
port/src/ input.cwas a stub; only a minimal keyboard path inlibultra.c(contSnapshotFromKeyboard) worked. No C-buttons/mouse-look/gamepad/ rebinding. This is the top playability blocker — you cannot aim or properly move. Reference:pd_port/port/src/input.c. - Weapon model doesn't draw (AUDIT-M6 #5) — NOT STARTED (M-8 agent
hit the account session rate-limit before any work; tree untouched).
Plan: instrument
gunfire.cweapon setup (~600-661) +field_87Fgating (~520/587-598) FIRST with aGE_D119printf — does setup even run? Then checkweaponModel.render_pos=dynAllocate'drwmtxlifetime vs thegunRenderFirstPersonGunModels(~1605) consumer (on N64 it aliased the persistenthand->mtxlist; D102 gave it own storage → arena may recycle). AlsoweaponRwPool[192]capacity vsmodelCalculateRwDataLen(mdlhdr)on PC, andflashvisptrword-index assumption (~640). Narrow#ifdef PORTlifetime/sizing fix only — if it needs a behavioural arena change, STOP + write up. Direct-level_09boot may spawn Bond holstered — may need forced weapon state or the attract path to get the gun on screen. Gate to playability. - The HUD/text X-mirror (D116) — DEPRIORITISED, parked in
docs/GRAPHICS-BACKLOG.md. M-11 confirmed the ammo digits use the SAMEtextrelated.cpath (no separate renderer — the M-8 premise was wrong) and re-hit the same contradiction: every stage verified non-mirrored, glyphs still flip. Do not re-static-trace. Next attempt needs a RenderDoc/apitrace capture or the asymmetric-1-texel experiment — nothing else. All cosmetic rendering issues (D74 dead wrap-block, D75/D76/D77) now tracked indocs/GRAPHICS-BACKLOG.md; they rank BELOW level-progression and crash work. - Rest of the
struct playeroffset pass — D115 fixed the HIGHgunfire.cTHROW* bugs; #6 (watch-preview Model pool) + the broader audit remain (docs/dev/AUDIT-M6-player-offsets.md). - Add an f32 value spot-check to
d43_emit.py's verify pass (it only checks pointers/opcodes today — the D112 bug passed "ALL CHECKS PASSED"). - GE_DETERM fixed-tick mode (§F D117) — deferred as not-narrow; would unlock exact-diff regression testing + reproducible timing bugs. Design is written up. Worth doing once the above land.
Session M-7 — see §F D116 + "D116 probe results". Ran the glyph
probe (overseer, after killing a subagent that was drifting toward a
global texrect S-swap). Corrected finding: the HUD text mirror is a
per-quad texture-U flip that is NOT path-specific — the ammo digits
"83" are mirrored too (ppm/frame_000320.ppm), refuting M-7's earlier
"proportional-font-specific" claim. Rect positions are correct; only
texture content is X-flipped. Every fast3d stage probed clean (glyph
bitmap in memory correct, texrect ul.u=0→lr.u=max, import_texture_i8
linear, GL shader UV pass-through) — so the flip is in the GL
vertex-buffer/draw layer OR a shared clip-space-X vs U desync on rect
quads. This re-opens D114's shared-mirror hypothesis (now per-quad
U/X, not screen-space) and plausibly also explains inverted guards /
mislocated door props. NEXT: shader/vertex-buffer probe — dump per-vertex
(x,u) for one glyph quad; render a 1-texel asymmetric test texture to see
which axis inverts. GE_D116-gated probes left in textrelated.c +
gfx_pc.cpp (zero-cost).
Session M-8 wrap — state for next session. Build GREEN at
587c6856, tree clean. Committed: D116 part-3 writeup + [D116/vbo]
probe (5ff012b4), CLAUDE.md/preflight (fd25628a), D117
framediff+nondeterminism (44d9ae98), D118 input layer
(4ac11fe7/587c6856), handoff reprioritisation. GE_D116 and
GE_INPUTLOG probes are in-tree, getenv-gated, zero-cost. Weapon-model
(#5) agent never ran (rate-limit) — task untouched, plan in item 2 above.
Pre-existing stash stash@{0} "d59 probes WIP" (~300 lines,
gfx_pc/gfx_opengl/crash.c) is NOT from M-8 — provenance unknown, left as
found. Next: human input playtest (tune ge007.ini [Input] MouseAimSpeed),
then weapon-model #5.
Session M-8 (continued) — D116 part 3 + input layer. Killed the
drifting D116 agent, ran the probe as overseer: buf_vbo (x,u) for
glyph quads and the GL texture upload are both runtime-verified
non-mirrored (x-left<->u=0), GL shader is gl_Position=aVtxPos +
UV-passthrough — every stage clean, yet glyphs render X-flipped
(contradiction, §F "D116 runtime probe part 3"). Deprioritised as
cosmetic. GE_D116 [D116/vbo] probe kept in gfx_pc.cpp. Then
dispatched the input-layer agent (§F D118, in progress) — top
playability blocker. framediff.py structural mode is the interim
regression gate (tools_pc/golden/ is a nondeterministic D115 capture).
Session M-8 — see §F D117. Visual-regression tooling. Root-caused the
frame-to-frame nondeterminism: pure variable-timestep frame pacing
(osGetCount() = wall clock on PC → frametiming.c waitForNextFrame
advances logic a real-time-dependent number of 60 Hz ticks per render).
PRNG seeding verified correct (random.c), not a source. A GE_DETERM=1
fixed-tick mode was assessed not narrow (redesigns VI retrace/tick
semantics, deadlock risk in load-screen loops) and deferred with a full
design in §F D117 — NOT implemented. Added tools_pc/framediff.py
(structural/tolerant: 16×12 grid mean-colour + non-clear-% + aHash,
--mask for HUD, --exact for a future deterministic build, --update
to refresh goldens). Validated against the D115 golden set. No C changes,
no probes left in tree.
Session M-10 — input playtest + guard-firefight crash chain.
- D119 — guard-attack crash FIXED (
chraction.c). Every prior-level_09run segfaulted ~frame 1200 the instant a BUNKER guard opened fire:bondwalkItemGetAutomaticFiringRate(gun.c:1334) ←chrlvInitActAttack. ~28 sites punweapons_held[]->chr(really aWeaponObjRecord*) asChrRecord*and read.act_<x>.attack_item, which on N64 aliasesWeaponObjRecord.weaponnum(act union @0x2C + 84 == 0x80). Pointer widening moves the act union to ~0x38 on PC → garbage negative item id → OOBg_ItemStats→ crash. Fix:PUN_ATTACK_ITEM()macro readsweaponnumdirectly;#elsebranch is textually identical to the original → N64 build unchanged. Class-A (D53.2 type-pun) bug. - D120 — blood-stain hang GUARDED, not fixed (
chr.c:3322). Next blocker, reachable only after D119: first guard bullet-hit →chrCreateBloodStainPointUsage[]negative-terminated chain walk cycles forever (VI thread keeps posting, logic thread spins, kernel heartbeat trips). Root cause:tools_pc/d43_emit.py's opcode-0x18ModelRoData_DisplayList_CollisionRecordconversion is incomplete — 6 pointer fields widen the struct andPointUsage/CollisionVerticessub-array endianness+stride aren't handled (onlyCollisionRelatedNodegot the D43/D45u32-vma treatment). Interim:#ifdef PORTcaps both walk loops atnumVertices+8iters + bounds-checksindex→ game survives, blood decals may be missing/wrong. Real fix (next session, own subagent brief): byte-spec the opcode-0x18 record + PointUsage + CollisionVertex sub-arrays vs a converted guard model, extendd43_emit.py. Same shape as D69/D88 converter work. - Result:
-level_09now survives the guard firefight — 45 s+ crash-free, frames past 1200 (was: hard segfault ~1200 every run).
Session M-10 — input playtested (human). Fixed in port/src/input.c:
(a) mouse-look was a draining accumulator → flicks stayed "pressed" ~8
frames after you stopped (stuck looking up/down) — now per-poll delta,
no carryover; (b) horizontal mouse was routed to C-left/right = GE
sidestep, and A/D drove analog stick-X = GE turn — swapped: mouse X
→ stick-X turn (MOUSE_TURN_GAIN 6.0), A/D → C-left/right strafe, mouse
Y → C-up/down look. Core aim/move now works.
Open input bugs (documented, deferred — fix in a later pass):
- D118a — mouse yaw slower than pitch. Horizontal turn (analog
stick-X via
MOUSE_TURN_GAIN) is visibly slower than vertical look (digital C-up/down). Different transfer curves (analog rate-limited vs digital full-press). NeedsMOUSE_TURN_GAINbump and/or a matched pitch path; ideally a single tunableMouseAimSpeed. - D118b — mouse Y inverted. Mouse up → looks down, mouse down →
looks up (X unaffected).
aimDY→C-button sign is backwards for GE's pitch convention (or SDL dy sign assumption wrong). Flip theaimDY >=/<=C-up/C-down assignment (or defaultMouseInvertY). One-line fix once confirmed against GE pitch sign. - Still TODO from D118: rebinding, gamepad hotplug,
ge007.ininot yet created (config defaults are hardcoded).
Session M-9 (Phase 3) — see §F D118. SDL input layer implemented.
port/src/input.c is now real: keyboard+mouse and SDL_GameController,
mapped to GE's N64 pad (analog stick = move, C-buttons = aim, mouse-look
bridged to digital C-buttons via a clamped accumulator, RMB/LT = aim
mode, LMB/RT = fire). libultra.c's SI section delegates to it (single
source of controller state; still driven by osContStartReadData, no
new frame hook). Config: ge007.ini [Input] MouseEnabled/MouseAimSpeed/
MouseInvertY. Build GREEN, boots -level_09 crash-free 35 s.
Owed: a human playtest — live input is untested from the headless
agent. Manual checklist: (1) WASD moves Bond, mouse turns/looks (tune
MouseAimSpeed in ge007.ini if too fast/slow), LMB fires, RMB enters
aim mode, Enter opens the pause menu; (2) plug an Xbox pad — left stick
moves, right stick aims, triggers fire/aim; (3) GE_INPUTLOG=1 prints
each nonzero OSContPad poll. Rebinding + gamepad hotplug are TODO (§F
D118).
Session M-6 — see §F D113/D114/D115, docs/PLAN-M6-playable.md,
docs/dev/AUDIT-M6-player-offsets.md, docs/dev-process.md,
docs/porting-notes.md. D113: portal BFS is correct, not the void.
D114: matrix chain + converter verified clean, residual = shared fast3d
mirror (open). D115: player raw-offset audit + gunfire.c THROW* fix
(uncommitted). Build green.
This session's fixes (all committed, master):
| # | commit | one-liner |
|---|---|---|
| D93 | 164d7f99 |
null-room (room 0) NULL-deref guards |
| D85 | 493c9838 |
bgWidenRoomGdl (8→16 Gfx + bswap32) + bgSwapRoomVtx; room DLs decode to real GBI; bg.c:2448 size-cast bug |
| D85 | 6f0208d6 |
ptr_texture_alloc_start → real struct texpool storage (pool looked exhausted → ~630 room textures now resolve; cleared Bad size for RGBA texture) |
| D94 | 63204a27 |
chrlvInitActAttack (s32)-truncated anim-table index |
| D95 | f35eba91 933ba52b |
2× g_GfxBuffers (16-byte PC Gfx) + raise PC mempool ceiling 0x702F4400→0x70700000 (reclaim ~4 MB DRAM) so it doesn't OOM MEMPOOL_STAGE |
| D96 | d86ec483 |
PROPRECORD_STAN_ROOM_LEN 4→8 (PORT) — prop room-list stack overflow (guards span ≥4 rooms → no terminator → smashed chrpropsRenderPass frame; this was the "runaway GDL append") |
| D97 | 2fbcc556 |
clamp negative damagetype (US-only OOB g_DamageTypes[] read) |
| D98 | 000ed6af |
initBONDdataforPlayer allocate real PC sizeof(struct player) (hardcoded 0x2A80 under-alloc scribbled the master DL) |
| D99 | 253caa23 |
Model.animflipfunc s32 fn-ptr truncation (flag + direct call, D92 pattern) |
| D100 | 8eaad547 |
struct player.model is an inline struct Model, not a Model* + 45 filler s32s (PC struct 0x2A8 ≫ 0xB8 hole → animInit overran); + dedicated gaitRwData[] |
| D101 | b2234f82 |
sub_GAME_7F06DB5C stashed ModelNode*/RenderPosView* through an s32 (the idiom its sibling was already fixed for) |
| D102 | 1b078f6d |
1P weapon Model + RW pool punned onto struct hand (PC Model 0xE8 ≫ N64 0xBC → modelInit aliased datas onto the pool); dedicated weaponModel/weaponRwPool |
Recurring pattern this session: the decomp pun-allocates struct Model (and struct player, PropRecord, struct texpool, …) into
N64-sized holes / hardcoded byte counts. Every one is bigger on x86-64
→ overrun → corruption. Fix = give it real inline storage or
sizeof()-based alloc under #ifdef PORT. Landmine still open
(§F D100): struct player / struct hand have raw hardcoded-offset
accessors (gunfire.c THROWMTX at +0xAD8, …) that are NOT
PORT-adjusted — already PC-wrong, a real struct player offset pass is
owed before grenade/knife-throw code works.
- Build:
export PATH="/c/msys64/mingw64/bin:$PATH" && ./build-pc.sh ntsc-final - Sidecar regen:
python tools_pc/d43_emit.py ntsc-final && python tools_pc/d69_emit.py ntsc-final && python tools_pc/d88_emit.py ntsc-final --regen.data/is gitignored; if missing,cp baserom.u.z64 data/ge007.ntsc-final.z64first (§F "data/deletion + recovery"). - Repro:
./build-pc/ge007.x86_64.exe -level_09 -ml0 -me0 -mgfx100 -mvtx50 -mt700 -ma150—-m*are BUNKER1'sboss.cper-level sizes; a bare-level_09skips them and crashes early (TODO: auto-inject).
Next: D75 — 3D model rendering quality
BUNKER1 renders but character/skeletal models are wrong (see the D75
section further down, still accurate). GE_PCDUMP + tools_pc/pixcount.py
to measure; the animated-model path (animInit + struct player raw
offsets, drawjointlist, modelApplyHeadRelations head/body splice) is
the prime suspect. struct tex headers are 24 B vs 16 B on PC — a
separate open pool-pressure item; a real PC memory-budget pass is owed.
What this session (M-2) fixed — all committed
-level_09now works.osPiReadIowas stubbed to 0, so the token string was always empty and N64 debug switches were ignored (only attract mode could reach a level). Now built fromargv[1..].- Sidecar tables patched in
obInit()instead of lazily at first model load — a direct stage boot loaded raw big-endian ROM before. - D88.5 — stan tile-name byte-swap in the converter (0/276 → 273/273 name matches during pad setup).
- D88.6 — intro CAMERA
lang1cis au16pair, not ans32(fixed thelangGetNULL-bank crash). - D89 —
init_path_table_links[-3]OOB → SIGILL fix; NULL-tile guard insub_GAME_7F0B0914. - D90 —
stanTileDistanceRelated's 80-byte zero-fill was clobbering the caller's live stan-tile local (16B struct, 80B clear). The player's spawn stan was fine all along. - D91 —
(s32)&D_800442FC[portalnum]truncation in bg portal cull. - D92 —
chrAllocates32param truncated the ailist pointer;Model.unka0s32field truncated a stored function pointer.
Next steps for the resuming session (render milestone):
- D85 — root cause confirmed (8-byte N64
Gfxvs 16-byte PCGfxin the unconverted per-room DL blob). Runtime widen+bswap fixup being implemented; verify withGE_D69BB=1(real GBI opcodes, not 00/01/02/52), thenGE_PCDUMP+tools_pc/pixcount.pyfor non-degenerate BUNKER1 geometry. See §F "D85 root cause CONFIRMED". Fallback if the widen stalls: soften the foursysFatalError("Bad size…")guards ingfx_pc.cppto skip-with-warning. - Then the front-end render bugs (D75).
D88.4 loose end (still open): PROPDEF_PC_BYTES for VEHICHLE/
AIRCRAFT/TANK/AMMO/DEPOSIT_IN_ROOM in d88_propdefs.py are
placeholder guesses (BUNKER1 doesn't use them). Probe real sizes via
d88_layoutprobe.c before loading levels that use those types.
Known rendering bugs (D75 — still open, orthogonal to the D88 crash)
Even once the crash chain is cleared, the front end has broken 3D model rendering (user-confirmed this session):
- Rareware logo: correct (fixed in D73/D74).
- Nintendo logo: renders but mispositioned.
- Gun-barrel intro: the James Bond character model is missing entirely.
- Intro credits / cast roll: the per-character 3D models do not
appear at all (names draw, models don't).
Pattern: textures/text draw; animated/skeletal character models never
appear; static 3D (logos) appears but with a bad transform. Leading
hypothesis is D75(b) — the animated-model path (
animInit+ raw offsets intostruct player, cf. D56) is broken independently of the D73 matrix sin/cos fix. Full triage plan infindings.md§F D75.
The rest of this document (below) is the last known-good, committed
status as of commit 8c9c6a2c (D86+D87 resolved) — still accurate except
D88 is now further along (D88.1–3 done/verified, D88.4 is the live crash).
Where things stand
D69 (the original "stage load faults" milestone blocker) remains
RESOLVED. load_bg_file (bg.c) doesn't fault on BUNKER1. Since then,
two more crashes further down the load chain were found and fixed this
session (D86, D87), and a third — the current blocker — was root-caused
but not fixed (D88).
D86 RESOLVED. modelInitRwData crash (model.c:6174) was a single
truncating pointer cast in the player's embedded gait/arm model:
src/game/initplayergaitobject.c:5 did
player_gait_object_header.RootNode = (int)&player_gait_hdr; — a
same-width no-op on N64 that truncates+zero-extends a real 64-bit pointer
on PC. Fixed with a narrow #ifdef PORT branch assigning the pointer
directly. Root-caused via a new node-walk trace (GE_D86=1, left in
place, gated in model.c/objecthandler_2.c).
D87 RESOLVED. Once D86 stopped blocking progress, an idle (no-input)
run eventually triggers the front-end's genuine attract-mode demo
playback (select_ramrom_to_play() picks a random compiled-in demo —
this is shipped retail behavior, not a debug feature) and crashed in
ramrom_replay_handler (ramromreplay.c). Root cause: ramromfilestructure
is a real ROM-compiled asset (big-endian, like everything else) loaded via
romCopyAligned() — a raw byte copy by design (D66) — with no
byteswap, so every multi-byte field read back scrambled (e.g. size_cmds
2 → 33554432) and drove wild pointer arithmetic. Fixed with a #ifdef PORT
ramromFixupEndian() called once after the load (same pattern as the D54
cseq fixup). Not BUNKER1-specific — attract mode picks any of 7 demo
locations at random, so don't rely on it for BUNKER1-specific testing (see
-level_09 below).
D88 — SUPERSEDED, see "READ THIS FIRST" at top. D88.1–D88.3 (header +
sub-table width/endian conversion) are now done and verified; D88.4
(propDefs byteswap) is the live blocker. The paragraph below is the
original root-cause writeup, kept for context.
D88 (original writeup) — root-caused. Launch with
-level_09 (NTSC LEVELID_BUNKER1 = 9; boss.c:199-339 decodes
-level_XX into g_StageNum, bypassing the front end/attract-mode
entirely — fast, deterministic BUNKER1 repro, crashes in well under a
minute instead of waiting ~2 min for attract mode to maybe pick Bunker).
Crash: proplvreset2 (prop.c:1306) segfaults reading
g_CurrentSetup.pathwaypoints[i1].padID. Root cause: the per-level
"Usetup<name>Z" file (prop.c:1267, struct stagesetup in
bondtypes.h:4091) is loaded as raw ROM bytes and has zero PC porting
work done on it — unlike bg/stan (D69/D80-82) and models (D43/D50).
Two compounding problems, not just one:
- The 10 top-level fields (
pathwaypoints/waypointgroups/intro/propDefs/patrolpaths/ailists/pads/boundpads/padnames/boundpadnames) are declared as real pointers in the live C struct, so on PC they're 8 bytes each (an 80-byte header) instead of the file's real 4-byte-each (40-byte) N64 layout — same class as D79 (bg_room_datapointer growth). Field 0 reads fine; everything after it is reading the wrong bytes entirely. - The 4 meaningful bytes each field does store are big-endian (the
code's own comment: "stores every internal reference as a byte offset
from the start of the file") and nothing byte-swaps them — same class
as D87.
No PORT/byteswap handling exists anywhere in
prop.c(confirmed by grep). Not fixed this session — this is D69-scale format-conversion work: a byte-accurate spec of the wholeUsetup*Zformat (top-level header + every nested sub-table:waypoint/waygroup/PropDefHeaderRecord/PathRecord/AIListRecord/PadRecord/BoundPadRecord/pname, each likely with its own internal offsets not yet audited) plus either an offline converter sidecar (preferred pattern per AGENTS.md, same shape astools_pc/d69_emit.py) or a careful runtime fixup pass that parses the raw 40-byte N64-packed header by explicit byte offset, byte-swaps each field, and writes results into the PC-widened struct.
Net effect vs. last session: the game now runs substantially further
— all the way through room-streaming setup, past the intro's model
pipeline, and into per-level "Usetup" data — before hitting D88. The
"loads without fault" acceptance bar is still not met, but the
remaining blocker is now narrowly scoped and has a fast, deterministic
repro (-level_09, no attract-mode wait, no depending on which random
demo attract-mode picks).
Recommended next steps, in order
- D88. Get a byte-level spec of
Usetup*Z(start from BUNKER1's file;strResourceis built as"U" + "sev" + "Z"-style name inprop.c:1253-1265— checksetup_text_pointers[LEVELID_BUNKER1]for the exact literal). Decide offline-converter vs. runtime-fixup (D69's bg/stan work is the template for the former; D54's cseq fixup is the template for the latter — given the struct-width mismatch on top of the byteswap, a runtime fixup that manually walks the raw 40-byte N64 header by hand (not through the livestagesetupstruct) into a freshly-populatedg_CurrentSetupis probably simpler than a full sidecar here, but verify against a raw ROM hex dump either way). - Once BUNKER1 loads past
proplvreset2, re-check for further crashes in the same vein (this session found 3 in a row — D86, D87, D88 — each only reachable after the previous one was fixed; expect more). - Once BUNKER1 reaches a rendered frame with no fault, revisit D85
(room primary/secondary GDL binaries decode to garbage via
texLoadFromGdl) — useGE_PCDUMP="<range>:10"+tools_pc/pixcount.pyto confirm non-black, non-degenerate content per the original D69 acceptance bar. (This session'sGE_PCDUMPcaptures around frame 2100-2400 during attract-mode-driven "loading" were still on a HUD/menu screen, not real 3D geometry — don't read too much into pixel counts from before D88 is fixed.) - Do NOT touch D75/D76/D77 (parked, lower priority, unrelated).
Debug tooling added this session (kept, env-gated, zero cost when unset)
GE_D86=1— node-walk trace inmodelInitRwData(model.c) + a load-identity probe inload_object_fill_header(objecthandler_2.c). Resolved the D86 crash; left in place since the same load_object_fill_header/modelInitRwData pipeline could surface new edge cases as more of the game becomes reachable.GE_D87=1— block-setup trace initerate_ramrom_entries_handle_camera_outand consumer trace inramrom_replay_handler(ramromreplay.c). Resolved the D87 crash; left in place as it's a rarely-exercised path (attract mode) worth having visibility into if it acts up again.- Pre-existing
GE_D69STAN=1,GE_D69BB=1,GE_D69=1— unchanged, still useful for the bg/stan/D85 load path (see prior session's HANDOFF entries, preserved in git history, for exactly what each logs).
New: fast, deterministic BUNKER1 repro (no attract-mode wait)
Launch with -level_09 as a program argument
(./build-pc/ge007.x86_64.exe -level_09) to skip the front end/attract
mode and load BUNKER1 directly — boss.c:199-339 decodes -level_XX
(the two digit-chars are consumed as raw ASCII bytes:
g_StageNum = tokenFindLevel[0]*10 + tokenFindLevel[1] - 0x210; NTSC
LEVELID_BUNKER1 = 9 → "09" since '0'*10 + '9' - 0x210 = 9). This is
now the preferred way to test BUNKER1-specific load/render work — it's
faster (crashes/completes in well under a minute vs. ~2+ min waiting on
attract mode) and deterministic (not dependent on which of 7 random demo
locations attract mode happens to pick).
Environment / build
export PATH="/c/msys64/mingw64/bin:$PATH" && ./build-pc.sh ntsc-final(~5 s). Build is GREEN with all D86/D87 work included.- Run from the repo root, not
build-pc/. - Regenerate sidecars if missing:
python tools_pc/d69_emit.py ntsc-final(bg/stan) and the D43/D50 model sidecar generator (pcmodels) — both gitignored, not checked in. (Both were already present in this dev environment this session.) GE_PCDUMP="<start>-<end>:<stride>"+tools_pc/pixcount.pyfor frame captures once D88 is fixed and a frame actually renders real BUNKER1 geometry.- Crash log:
ge007.crash.log(repo root); symbolicate withaddr2line -e build-pc/ge007.x86_64.exe -f -C <pc>(image base0x140000000). - gdb launch mode is too slow for timing-dependent bugs (unchanged
guidance). New this session: gdb attach mode
(
gdb -batch -x cmds.txt -p <winpid>,<winpid>= 4th column ofps -p <bashpid>) works well and is fast for "watch a global for a legitimate vs. corrupted write" questions on an already-running, not-yet-crashed process — seedocs/internals.md§F environment reminders for the exact recipe used to root-cause D87.
Non-negotiables (unchanged, see AGENTS.md)
- N64 build files untouched.
- Game logic unmodified except narrow, documented
#ifdef PORTABI/layout exceptions (D86/D87 this session, both logged infindings.md§F). D88 is explicitly not patched with a quick inline hack — it needs the same disciplined spec-then-convert/fixup treatment as D69, logged as an open finding instead per AGENTS.md's "stop and write it up" guidance for anything beyond a narrow, obviously-correct exception. - Offline sidecar conversion preferred over runtime fixup for whole ROM-asset formats (D69/D80-82 pattern) — likely the right call for D88 too, though a careful runtime fixup is also plausible; decide after the byte-level spec work.