Files
goldeneye-pc-port/docs/HANDOFF.md
T
James Dansereau 7bdb19e96f D115: fix gunfire THROW* raw struct player offsets; M-6 D113/D114 findings
D115: THROWMTX/THROWPOS/THROWPREV in gunfire.c were raw byte offsets
((u8*)g_CurrentPlayer + handnum*sizeof(struct hand) + 0xAD8/0xB08/0xB48).
Both the stride (PC sizeof(struct hand) ~0x968 vs N64 0x3B8, D102) and the
base offset are N64-sized, so on x86-64 the address lands inside the
inline gait Model / bondhead-matrix region of struct player and
matrix_4x4_copy(THROWMTX,..) scribbles 64 bytes of live render state on
every casing-ejecting shot. The offsets are exactly
hands[handnum].throw_item_pos_related[_prev] and its translation row -
macros now use those field accessors under #ifdef PORT; N64 build
unchanged. Full survey in docs/AUDIT-M6-player-offsets.md (10 sites,
3 HIGH fixed here, 2 MED open incl. the likely 'weapon model doesn't
draw' cause).

D113 (investigation, no code change): the 'portal-BFS under-reach / void'
premise is stale - the BFS output is correct for BUNKER1's topology
(rooms 26/27 are behind PORTALFLAG_DISABLED closed-door portals, culled
identically on N64). The void is the closed-door models obj 153/154 not
rendering in their doorway -> D114 track.

D114 (investigation, no code change): joint-matrix chain + d43_emit.py
float emission verified clean post-D112; basemtx is a proper
right-handed +Y-up lookat, joint render_pos coherent and upright. All
plan hypotheses disproven. Residual = a horizontal mirror in the shared
port/fast3d viewport / MP-matrix path vs GE's left-handed screen
convention (also explains the open D75 mirrored-text / logo items).
Still open.

Plan + agent reports: docs/PLAN-M6-playable.md. §F: D113/D114/D115.
2026-08-29 09:53:00 -04:00

21 KiB
Raw Blame History

Handoff brief — GoldenEye 007 PC port (Phase 2: Session M-3 —

STAGE-LOAD → RENDER crash chain CLEAR; BUNKER1 renders, next is D75 model quality)

Paste-ready brief. Authoritative context: AGENTS.md, docs/PCPortResearch.md §F (D69, D78-D102), docs/BRIEF-D69-stage-load.md.

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.

Real remaining work, in order (updated session M-6):

  1. Shared fast3d screen mirror (D114 → D75 class) — the single most likely cause of: inverted BUNKER guard, mispositioned Nintendo logo, mirrored front-end text, AND the storage-room "void" (D113 proved the portal BFS is correct — the void is the closed-door models obj 153/154 rendering outside their doorway). The joint-matrix chain and the model converter were verified clean in M-6 (see §F D114); the residual is an X (± Y) flip in port/fast3d's viewport / MP-matrix path vs GE's left-handed screen convention (gmain.s / bondview.c:726). Suspects: gfx_calc_and_set_viewport / gfx_adjust_viewport_or_scissor (gfx_pc.cpp:1739/1771), MP_matrix (gfx_pc.cpp:1122). Rebuild with D112+D115 and eyeball a guard first — D115 stops a per-shot 64-byte scribble that could itself present as "inverted".
  2. Weapon model doesn't draw (D115 #5) — D102 pointed weaponModel. render_pos at a transient dynAllocate arena; on N64 it aliased the persistent hand->mtxlist. Gate to playability. May fold into #1.
  3. Rest of the struct player offset pass — D115 fixed the HIGH gunfire.c THROW* bugs; #6 (watch-preview Model pool) + the broader audit remain (docs/AUDIT-M6-player-offsets.md).
  4. 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").

Session M-6 (this session) — see §F D113/D114/D115, docs/PLAN-M6-playable.md, docs/AUDIT-M6-player-offsets.md, docs/AGENT-WORKFLOW.md, docs/PORT-LEARNINGS.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.z64 first (§F "data/ deletion + recovery").
  • Repro: ./build-pc/ge007.x86_64.exe -level_09 -ml0 -me0 -mgfx100 -mvtx50 -mt700 -ma150 — -m* are BUNKER1's boss.c per-level sizes; a bare -level_09 skips 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

  1. -level_09 now works. osPiReadIo was 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 from argv[1..].
  2. Sidecar tables patched in obInit() instead of lazily at first model load — a direct stage boot loaded raw big-endian ROM before.
  3. D88.5 — stan tile-name byte-swap in the converter (0/276 → 273/273 name matches during pad setup).
  4. D88.6 — intro CAMERA lang1c is a u16 pair, not an s32 (fixed the langGet NULL-bank crash).
  5. D89 — init_path_table_links [-3] OOB → SIGILL fix; NULL-tile guard in sub_GAME_7F0B0914.
  6. 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.
  7. D91 — (s32)&D_800442FC[portalnum] truncation in bg portal cull.
  8. D92 — chrAllocate s32 param truncated the ailist pointer; Model.unka0 s32 field truncated a stored function pointer.

Next steps for the resuming session (render milestone):

  1. D85 — root cause confirmed (8-byte N64 Gfx vs 16-byte PC Gfx in the unconverted per-room DL blob). Runtime widen+bswap fixup being implemented; verify with GE_D69BB=1 (real GBI opcodes, not 00/01/02/52), then GE_PCDUMP + tools_pc/pixcount.py for non-degenerate BUNKER1 geometry. See §F "D85 root cause CONFIRMED". Fallback if the widen stalls: soften the four sysFatalError("Bad size…") guards in gfx_pc.cpp to skip-with-warning.
  2. 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 into struct player, cf. D56) is broken independently of the D73 matrix sin/cos fix. Full triage plan in PCPortResearch.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:

  1. 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_data pointer growth). Field 0 reads fine; everything after it is reading the wrong bytes entirely.
  2. 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 whole Usetup*Z format (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 as tools_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).

  1. D88. Get a byte-level spec of Usetup*Z (start from BUNKER1's file; strResource is built as "U" + "sev" + "Z"-style name in prop.c:1253-1265 — check setup_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 live stagesetup struct) into a freshly-populated g_CurrentSetup is probably simpler than a full sidecar here, but verify against a raw ROM hex dump either way).
  2. 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).
  3. Once BUNKER1 reaches a rendered frame with no fault, revisit D85 (room primary/secondary GDL binaries decode to garbage via texLoadFromGdl) — use GE_PCDUMP="<range>:10" + tools_pc/pixcount.py to confirm non-black, non-degenerate content per the original D69 acceptance bar. (This session's GE_PCDUMP captures 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.)
  4. 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 in modelInitRwData (model.c) + a load-identity probe in load_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 in iterate_ramrom_entries_handle_camera_out and consumer trace in ramrom_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.py for frame captures once D88 is fixed and a frame actually renders real BUNKER1 geometry.
  • Crash log: ge007.crash.log (repo root); symbolicate with addr2line -e build-pc/ge007.x86_64.exe -f -C <pc> (image base 0x140000000).
  • 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 of ps -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 — see docs/PCPortResearch.md §F environment reminders for the exact recipe used to root-cause D87.

Non-negotiables (unchanged, see AGENTS.md)

  1. N64 build files untouched.
  2. Game logic unmodified except narrow, documented #ifdef PORT ABI/layout exceptions (D86/D87 this session, both logged in PCPortResearch.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.
  3. 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.