Files
goldeneye-pc-port/docs/HANDOFF.md
T
James Dansereau f98414f8c6 M-27: front-end bucket — D140/D141/D118d pause+watch menu fixes
D140 (src/game/bondview.h, bondview2.c): pause-menu crash in
bondviewRenderWatch -> bondviewTransformManyPosToViewMatrix(field_23C=NULL).
player +0x230 is a struct Model + RW pool punned into a 0x184-byte field run;
field_23C/watch_scale_destination/pause_watch_related_adjust alias
.render_pos/.scale/.animframe1. PC sizeof(struct Model) grows -> aliases
break -> NULL render_pos. Fix: real inline struct Model + u32 watchRwPool[192]
under #ifdef PORT; named reads redirect via GE_WATCH_* macros (N64 #else
verbatim).

D141 (src/game/gunfire.c): crash D140 exposed.
set_enviro_fog_for_items_in_solo_watch_menu walks bodymodel->Switches[]
(ModelNode* array) with raw byte offsets +0x48/+0x5c, j+=4 -> PC 8-byte
stride reads garbage -> AV in modelGetNodeRwData. #ifdef PORT uses
Switches[18 + (j>>2)] / Switches[23 + (j>>2)].

D118d (src/game/options.c): watch inventory list over-scrolls with keyboard
W/S (one press skips several items). GE fast-scroll is a raw per-frame level
check (joyGetStickY < -0x46); digital pads sit at max every frame. #ifdef
PORT drops the stick term -> nav via the latched single-step path. Feel
unverified headlessly.

Verified: build green (ntsc-final); -level_09 framediff 3/3, -level_20
crash-free (unregressed).
2026-08-30 14:14:41 -04:00

585 lines
34 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# HANDOFF — GoldenEye 007 PC port (Phase 2, breadth-first level sweep)
Tier 1 doc. Current state + next task + environment. Finding history is
`docs/PCPortResearch.md` §F/§H (indexed at top of §F); session narrative
is `docs/HANDOFF-ARCHIVE.md`.
## Preflight (do not skip because you have a "next step")
1. `CLAUDE.md` (auto) + `AGENTS.md` non-negotiables.
2. This file, top to bottom.
3. `docs/PORT-LEARNINGS.md` — recurring bug classes (you WILL re-derive
catalogued bugs otherwise).
4. `docs/PLAN-linear-level-sweep.md` — the plan of record.
5. Dispatching a subagent? `docs/AGENT-WORKFLOW.md` first — every brief
needs FILES / BUDGET / ON-EXPIRY / CONSTRAINTS / REPORT.
## Direction (2026-08-29)
**Breadth-first pivot.** Stop the depth-first BUNKER1 + graphics
rabbit-holes. Instead:
1. Make boot → intro → menu → level-select **functional (not crashing)** —
cosmetics parked in `docs/GRAPHICS-BACKLOG.md`.
2. Sweep all **21 solo levels** for load + render + no-crash →
`docs/LEVEL-STATUS.md`.
Full workstream breakdown (WS1–WS6) in `docs/PLAN-linear-level-sweep.md`.
## Where things stand
- **BUNKER1 (`-level_09`) is playable-ish.** Loads, renders recognisably
(textured rooms, storage racks, floor; skeletal guards render as
humanoids), and **survives a guard firefight** — 45 s+ crash-free
(D103–D120). **Silo (`-level_20`) also loads + renders clean.**
Committed through `f2beae4b` (M-12: D121 WS1 boot, D122 propDef fix).
- **Input layer (Phase 3) landed and playtested** (D118, M-9/M-10).
`port/src/input.c` is real: keyboard+mouse + SDL_GameController → N64
pad. Core aim/move works. **M-24 mouse-look rework** (`port/src/input.c`,
port-only): mode-aware map — aim mode (RMB) drives the analog stick past
±60 and emits no C-buttons; hipfire keeps digital C-up/C-down pitch.
**D118b (mouse-Y inverted) and D118c (aim + mouse-down → crouch) FIXED**;
**D118a residual** — hipfire pitch still digital vs analog yaw (minor).
`config.c` INI load/save now implemented → `ge007.ini` is written on
first run and re-read; `[Input]` `MouseEnabled` / `MouseAimSpeed` (50) /
`MouseTurnSpeed` (100) / `MouseInvertY` (0) are live-tunable. Rebinding /
gamepad hotplug still TODO. Weapon switch on kbd = A button
(`Space`/`Z`/`E`) — no dedicated key.
- **Recent fixes (M-10, `d1e93b76`):** D119 guard-attack crash
(`weapons_held[]->chr` type-pun) fixed; D120 blood-stain hang guarded
(not fixed — `d43_emit.py` opcode-0x18 converter gap).
- **Cosmetic backlog (parked, `docs/GRAPHICS-BACKLOG.md`):** D75 front-end
3D model transforms (Nintendo logo misplaced, gun-barrel Bond absent,
cast models absent), D76 disclaimer screen partial, D77 no audio,
D114/D116 HUD/text X-mirror (**DO NOT re-static-trace** — see
PORT-LEARNINGS §D2), D74 dead wrap-block.
## Done this session (M-12) — 2 commits
- **WS1 / D121 (`02c12068`)** — bare `./build-pc/ge007.x86_64.exe
-level_XX` auto-injects that stage's `memallocstringtable[]` `-m*` pool
row (`#ifdef PORT` in `boss.c bossInitMainthreadData`). No manual `-m*`
args any more. Gotchas recorded: setting `g_DebugAndUpdateStageFlag=1`
is the WRONG fix (routes boot through the title-stage intro), and
`tokenSetString` *replaces* the whole token buffer so the injected
string must carry `-level_XX` forward. See §H D121.
- **D88.4 was already resolved** (committed `ff563812`/`4ccc0d3c` weeks
ago). The "OPEN — cross-level blocker / `setupDoor` crash" note in the
old §F index / plan was **stale**. `d88_emit.py --regen` = 21/21 pass.
- **D122 (`f2beae4b`)** — the real cross-level blocker was prop/item
**model loading**. `tools_pc/d88_propdefs.py` had a per-type handler
table + a generic fallback; 6 `inherits ObjectRecord` propDef types
(47 TINTED_GLASS, 39 VEHICHLE, 40 AIRCRAFT, 45 TANK, 13 AUTOGUN, 20
AMMO/MultiAmmoCrate) had **no handler** → fell to the generic
word-granular `bswap32`, which swapped the `[s16 obj][s16 pad]` word as
one u32 → model id `obj` in the wrong half → OOB `PitemZ_entries[]`
deref → crash in `modelLoad` (`loadobjectmodel.c:393`) /
`modelInitRwData` (`model.c:6249`). Fixed in the converter
(`OBJ_TAIL_DESC` handler) + matching `sizepropdef()` `#ifdef PORT`
stride. BUNKER1/Silo were never affected (they don't emit those types).
Full write-up: §F/§H **D122**. Confidence: crash fix high; types
39/40/45 tail layout medium (structs have "locs unconfirmed" notes —
nudge if a level actually drives a tank/vehicle).
**Uncommitted docs** (tangled with the in-progress big docs restructure —
`HANDOFF.md`, `AGENTS.md`, `CLAUDE.md`): the D121 + D122 entries in
`docs/PCPortResearch.md` §F/§H and this file's edits. Commit or fold in.
## Done this session (M-17) — 4 commits, 13→18/21 PASS
- **D126** (`e851e2ab`) — objective sub-records `criteria_picture` (30),
`criteria_roomentered` (32), `criteria_deposit` (33),
`setup_objective_text` (35) each end in a `T *next` list pointer that
`set_parent_cur_obj_*` / `setup_briefing_text_entry_parent` write while
walking the setup stream. Widens 4→8B and 8-aligns at offset 16 on PC →
struct is 24B/6w (N64 16/20); the old N64-sized emit meant the runtime
8-byte `->next` store clobbered the *next* propdef record's header →
walk desync → command indices drifted ~100 → `setupDoor` `linkedDoor`
resolution landed on the wrong record. Fixed `d88_propdefs.py`
(`PROPDEF_PC_BYTES[30/32/33/35]=24` + typed handler) +
`loadobjectmodel.c` `sizepropdef` PORT returns 6. **Cleared C3r Bunker2
+ C4 Depot + C6 Surface2** in one change.
- **D127** (`1129f63d`) — `sndPlaySfx` guards a bogus `ALSound*` (the
converted libaudio bank has fewer/rearranged `soundArray` slots than
N64; audio is Phase-3 parked). **Cleared C7 Surface1.**
- **D128** (`749feb9f`) — `sub_GAME_7F0B37EC` special-portal marker used
the hardcoded N64 8-byte `bg_portal_data_entry` stride / `controlbytes1@6`;
on PC the struct is 16B with `controlbytes1@10`, so `|= 2` scribbled
into the middle of a portal's `offset_portal` pointer → `bg.c:5723`
crash on any level with a `specialportalarray` entry. `#ifdef PORT`
uses the struct accessor. **Cleared C5 Control.**
- **D129** (`<this commit>`) — `langGet` bounds-checks the slot id and
resolved bank pointer. Bare `-level_XX` boots that reach the
cast/credits text path (Cuba, `bondviewRenderCredits`, D76 area)
reference banks the menu flow never loaded. Reduces the Cuba bare-boot
end-credits crash; **Cuba itself loads + renders 300+ frames fine** and
the credits work through the real front-end flow.
## Done this session (M-18) — 1 fix, 18→20/21 PASS
- **D130** (`port/src/romdata.c`, uncommitted) — Facility `-level_34` +
Runway `-level_35` C2 crash (`import_texture_i8` AV on wild ptr
`0x72181ee8`). Root cause was NOT the model-GDL relocation the
D124-Facility addendum / `BRIEF-C2gdl-model-reloc.md` suspected —
disproved: `texLoadFromGdl` never copies a `G_SETTIMG` on these levels,
`texWriteLoadToTmem*` is never called, and `gdl` in `sub_GAME_7F0762E0`
is still a segmented `0x05xxxxxx` value so its `& 0x00ffffff` masks are
correct. Real cause: **`romdataFixupFont` corrupts BankGothic/ZurichBold
glyph indices 0/1/2** — the in-place N64 24B → PC 32B `fontchar` relayout
aliases `dst`/`src` for low glyph indices (stride grew 8B < the 24B field
read span), so a field write clobbers a later field's source mid-loop
(glyph 1 `width` = `bswap(index)`, glyph 2 `pixeldata` ≈ `0x020002e8` →
`+= font_base` → wild). Facility's title "Chemical Warfare Facility #2"
renders `#` → `gDPLoadTextureBlock(gdl, curchar->pixeldata=wild, …)` →
fast3d AV. Fix: stage all 6 N64 fields in a local `f[6]` before writing.
§F/§H **D130**; PORT-LEARNINGS §A. Verified: -level_34 0/~14, -level_35
0/4; -level_09/20/24 unregressed.
## Done this session (M-19) — D131 (Jungle), D63 scaffolding removal
- **D131** (`port/fast3d/gfx_pc.cpp`) — Jungle `-level_37` C2m crash.
`explosionRenderPropSmoke` passes a compiled `.bss` matrix symbol
(`&dword_CODE_bss_8007A100`) through `osVirtualToPhysical()` — a
`u32`-returning shim — into `gSPMatrix`, truncating the `0x1_00000000`
module high word → w1 = `0x401c68e0` → wild deref in `gfx_sp_matrix`
when the first explosion draws (~frame 300). NOT `gimgfixup` / the
model-GDL path (that premise was already dead per D130). Fix: `seg_addr`
restores the module high word for a fallthrough w1 in
`[0x40000000, 0x70000000)`. Covers ~30 latent `osVirtualToPhysical(<compiled
matrix/vtx symbol>)` sites (explosion/glass/blood/bondview2). Verified:
Jungle renders to frame 1500–2400+ crash-free; `-level_20`/`-level_24`
unregressed. §F/§H **D131**, PORT-LEARNINGS §A.
- **D63 scaffolding removed** — dead TEMP D63 probe calls stripped from
`port/src/libultra.c` (watchdog thread + activity ring + slot watchers)
and the `#ifdef PORT` probe blocks in `blood_animation.c`, `dyn.c`,
`front.c`, `rsp.c`, `title.c`. All inert (env-gated / no side effects);
build links clean. `port/fast3d/gfx_pc.cpp` D63 trail code left for a
later pass. Bug class D24-implications + `osYieldThread` watch item added
to PORT-LEARNINGS §E.
## Done this session (M-23) — WS6 Facility playthrough: D135/D137/D138/D139 FIXED, D136/D140/D118c open
First real human WS6 playtest. User speed-ran Dam OK, then hit a chain of
crashes on Facility (`-level_34`) — each one a subsystem the 21-level sweep
never exercises (no weapon fire, no stage unload, no pause menu). Fixed four,
two new open, Facility now near-complete. **Committed `ec51d9c5`** (the
older D121/D122/D126/D128/D130/D131 write-ups are still uncommitted, tangled
in the docs-restructure — fold those separately).
- **D135 (FIXED)** `src/game/propobj.c` — firefight crash. `bgTestHitOnObj`
(bullet-ray vs object triangle geometry, via `propobjFindHit` on every
shot that resolves on an object model) is an **unported N64 GBI parser**:
raw 8-byte-`Gfx` byte/word indices desync on the PC 16-byte `Gfx` stream →
walks off the DL → AV `0x709ae02a`. Ported `#ifdef PORT` to `gdl->words.w0/
.w1` shifts (mirrors PD `pd_port/src/game/bg.c:3635`). Second fault after
the parser fix: `objHit` `propobj.c:9717` `% impact_sounds->thing2_len` int
div-by-0 — the ported texnum-recovery (`*(s16*)(phys(w1)-8)`) returned a
bogus `texturenum` → OOB `g_HitTypeSounds[]`. Fixed: PORT texnum branch
always `-1` (`isnd_default`, safe; generic impact sound/decal, park w/ D77).
- **D137 (FIXED)** `src/game/gunfire.c` — right-mouse (raise crosshair) crash.
`gunDrawSight` `s32 sp54` holds a `Gfx*` the whole fn (`texSelect`/
`display_image_at_position` take `Gfx**`); 4-byte slot → `*(Gfx**)&sp54`
splices adjacent stack → wild DL write in `texSetRenderMode`. `#ifdef PORT`:
`sp54` is `Gfx *`. §A.
- **D138 (FIXED)** `src/snd.c` — the "1 room from the end" *freeze* (not a
crash — kernel-heartbeat watchdog). `sndCreatePostEvent` →
`alEvtqPostEvent` (`libultra/audio/event.c:110`) walks `evtq->allocList`;
parked audio thread never drains it, `chrobjSndCreatePostEvent` posts per
objTick per ambient-sound object, Facility is dense with machinery → the
list walk stalls the main thread. `#ifdef PORT`: `sndCreatePostEvent` is a
no-op until audio lands (D77). No gameplay effect.
- **D139 (FIXED, UNVERIFIED)** `src/game/cleanup_objects.c` — stage-unload
crash (`lvlUnloadStageTextData` → `cleanupObjects` → `objFree`, `obj->prop`
= packed-float garbage). Root cause: `cleanupObjects` walks propDefs with
`(u8)obj[0]` for the type, which works only because the header word
`[u16 extrascale][u8 state][u8 type]` is **big-endian** (type = low byte).
On LE the low byte is `extrascale` → the walk never matches `PROPDEF_END`,
runs off the blob, dispatches the switch on garbage → `objFreePermanently`
on non-objects → crash. `#ifdef PORT`: use `((PropDefHeaderRecord*)obj)->
type` (offset 3) like every other consumer (`sizepropdef`, proplvreset).
**Not confirmed** — when the user hit pause to trigger a fast teardown
test, the pause menu itself crashed first (D140). `-level_09` unregressed
(91.65%). §F **D139**, PORT-LEARNINGS §C (BE header byte on LE).
- **D140 (OPEN)** — **pause menu crashes.** `maybe_mp_interface` →
`bondviewRenderWatch` (`bondview2.c:8604`) → `bondviewTransformManyPos-
ToViewMatrix(g_CurrentPlayer->field_23C = NULL, objheader->numMatrices=9)`
→ `matrix_4x4_copy(src=0x0)` AV. The watch model's per-node view-matrix
cache (`struct player.field_23C`) is never allocated (or `field_23C` is at
the wrong PC offset — `struct player` layout, `docs/AUDIT-M6-player-
offsets.md`). Blocks the WS6 pause-menu / objective-status checks and the
watch gadgets. D75 3D-model-transform family. Files: `src/game/bondview2.c`,
`src/bondtypes.h` (`struct player`).
- **D118c (OPEN, low pri)** — in manual-aim mode, mouse-down → **crouch**.
Mouse Y emits C-down; `bondview2.c:5351` maps C-down in `insightaimmode`
to `crouchDown`/zoom as well as aim pitch. No input-layer-only fix; needs
the deferred analog-aim `#ifdef PORT` hook (§F D118). Documented, parked.
- **New harness** `tools_pc/repro_gdb.sh <XX>` — nohup-launch a level, attach
gdb, dump full `bt full` + locals on the first SIGSEGV/SIGFPE, rolling
frame dump to `ppm/`. Needed because `ge007.crash.log`'s own backtrace is
`#01 = null` (native unwinder fails). Also `build-pc/d136_cmds.txt` — a
gdb cmd file that traces every `objFree` call (`type`/`obj`/`prop`/`model`)
then catches the fault; the "last line before FAULT" localises D136-class
walk-off bugs. **Attach the winpid via `ps -W | grep ge007.x86_64 |
awk '{print $4}'`** — `repro_gdb.sh`'s own `ps -p $!` derivation is wrong.
- **Follow-up (not done):** the BG room-geometry hit-test in `bg.c`
(`~3373-3646`, walks the D85-widened `ptr_expanded_mapping_info`) is the
identical unported GBI parser to D135 — latent, fires on shooting
walls/floor. Same mechanical port. §F D135 + PORT-LEARNINGS §B.
- **Controls** (for the new session's own playtesting): kbd — WASD/arrows
move, `A`/`D` sidestep, mouse X turn (analog), mouse Y look (digital
C-button), LMB/LCtrl fire, RMB/LShift aim, `Space`/`Z`/`E` = A (action AND
weapon-switch — GE overloads it), `X`/`R`/`F` = B (reload), `Q` = L,
`Enter`/`Tab` = Start, `Esc` quit. No dedicated weapon key.
## Done this session (M-24) — mouse-look rework + real INI parser + playtest QoL
Pre-playtest QoL, port-layer only, no `src/` / game-logic change.
**Full review sheet: `docs/M-24-QOL-REVIEW.md`** (rationale, per-change
risk, verification, what to check by playing). Committed `f3ec5170`,
`65ed0315`, `33506aee`.
- **`port/src/input.c` mouse-look rework.** Read `bondviewProcessInput` /
`MoveData`: GE aim is **mode-dependent** — hipfire yaw = analog stick-X,
pitch = digital C-up/C-down (stick-Y = move, no pitch); aim mode (R held)
yaw+pitch = analog stick past ±60, and C-up/C-down there = crouch/lean/
zoom, *not* aim. New map keyed on our own RMB/LShift (hold-to-aim proxy):
aim mode pushes `stick_x/y` into the 61..80 band and emits **no**
C-buttons; hipfire keeps digital C-pitch. GE's native pitch is inverted
(C-up→look down) — hidden so mouse-down looks down; `MouseInvertY` flips.
- **D118b FIXED** (mouse-Y inversion), **D118c FIXED** (aim+down→crouch —
no `src/` hook needed, aim look no longer emits C-down).
- **D118a residual**: hipfire pitch digital vs analog yaw. Minor (precise
vertical aim is an aim-mode activity). Full fix = the deferred analog
`#ifdef PORT` `bondview.c` hook.
- **`port/src/config.c` — INI load/save implemented** (was a stub that only
logged TODO). Parses `$S/ge007.ini` (`[Section]` blocks, `Key = value`,
`#`/`;` comments), clamps ints to registered bounds, writes defaults on
first run, rewrites on clean exit. Unblocks live tuning of the mouse
knobs + all future video options. Note: constructor-registered options
present at `configLoad()` time (main.c:52) are captured; anything
registered later would miss the first save.
- **New knob** `Input.MouseTurnSpeed` (hipfire yaw %, default 100), split
from `MouseAimSpeed` (aim-mode %, default 50) since they feed different
game curves.
- Verified: `-level_09` boots crash-free to 900+ frames @ 91.65% coverage
(unregressed); `-level_20` crash-free; `ge007.ini` written then re-read
with no unknown-key warnings. Build green (`ntsc-final`).
- **`port/src/video.c` QoL:** **F12** takes a screenshot
(`ppm/shot_NNN.ppm`, dumped on the render thread via the existing
`gfx_opengl_dump_bound_fbo`); window **focus loss frees the mouse**
(`inputSetMouseGrab(0)` on `SDL_WINDOWEVENT_FOCUS_LOST`, re-grab on
gain) so alt-tab works. `inputSetMouseGrab()` also zeroes the aim delta
and suspends mouse reads while released.
- Alt-Enter fullscreen toggle already existed (`gfx_sdl2.cpp:299`).
- **Gamepad hotplug** — `SDL_CONTROLLERDEVICEADDED/REMOVED` → `inputRescanPads()`
(close all + re-open). Caveat: the game latches `inputConnectedMask()` at
`osContInit` (boot), so a pad added later merges into controller 0 for
play but won't appear as a separate channel — plug it before launch for
multi-pad.
- **Mouse-wheel = weapon cycle** — a notch queues a 2-poll A-button press;
GE's default scheme (`invButtons = A_BUTTON`, `bondview2.c:5162/5326`)
cycles the weapon forward on a bare A edge. Both wheel directions cycle
forward (no clean backward input without the A+fire combo).
- **Window title shows live FPS** (`wmAPI->set_window_title`, ~1 Hz).
- **Still TODO (not started):** key rebinding, on-screen (in-render) FPS
overlay, in-game options menu.
## Done this session (M-25) — 3 commits, port-layer QoL
Free-roam QoL pass, `port/` only, zero `src/` / game-logic change. Full
review sheet: **`docs/M-25-QOL-REVIEW.md`**. All defaults reproduce prior
behavior exactly.
- **`9ebed821` — `[Video]` ge007.ini knobs.** `Video.VSync` / `FpsCap` /
`MSAA` (1/2/4/8) / `TextureFilter` (0 nearest, 1 bilinear) / `Fullscreen`,
wired in `video.c videoInit`. Plus **config auto-migration**: `config.c`
now rewrites `ge007.ini` once when the build registers a key the file
never had (so a new section actually appears) — an up-to-date file with a
user comment is left untouched.
- **`5a7a1035` — `tools_pc/playtest.sh`** takes a level *name*
(`playtest.sh bunker2`) or number, `--list` prints the 21-level table, and
a crash now auto-runs `addr2line` on the faulting PCs.
- **`863f436b` — `[Input]` ge007.ini knobs.** `MouseYScale`,
`MouseSmoothing` (0 = off = today), `PadDeadzone` (7000), `PadTriggerPct`
(23), `PadLookInvertY`.
- Verified per commit: build green, `-level_09` crash-free to frame 620 @
91.6% (unregressed), `-level_20` crash-free.
- **Silo `-level_20` capture freeze — D117/D134 load sensitivity, NOT a
real bug and not an M-25 regression.** Full-length captures froze at
~frame 320 (silo→Bond camera descent) twice, but only on a machine still
loaded from the sweep (the pre-M-25 baseline froze the same way). On an
idle machine the user drove Silo live past ~1800 frames, VI pacemaker
healthy, no stall. See `docs/M-25-QOL-REVIEW.md`.
## Done this session (M-26) — port-layer QoL batch (A/B/C/E)
Low-risk QoL, `port/` only, zero `src/` change. All defaults reproduce
prior behaviour. Full review: **`docs/M-26-QOL-REVIEW.md`**.
- **A — save-on-exit + `[Window]` persistence.** `atexit(portAtExit)` in
`main.c` (`videoSaveWindowState()` + `configSave()`), fires on every
clean quit (`exit(0)` in `videoPumpEvents`); `abort()` paths skip it.
New `[Window]` ini section (`Width/Height/X/Y/Maximized`, sentinels =
old behaviour) in `video.c`; `Video.Fullscreen` now round-trips.
- **B — `level_sweep.sh` STALLED verdict.** Frames rendered then froze
with the process still alive → `STALLED (froze at N/M frames)` instead
of a bogus PASS (the Silo case). Retried once like NO-FRAMES.
- **C — raw mouse input.** `Input.MouseRawInput=1` (default 0) sets the
SDL relative-scale / warp hints off in `inputInit` — no OS pointer accel.
- **E — `--help` / `--version`** in `main.c` before `crashInit()`: build
id, usage, the 21-level `-level_XX` table.
- **D — `[Debug]` ini flags.** `Debug.FrameDump` (mirrors `GE_PCDUMP`),
`Debug.InputLog` (mirrors `GE_INPUTLOG`); env var wins, ini is the
fallback. `config.c` `configGetFrameDump()`/`configGetInputLog()`.
- **Verified:** build green (`ntsc-final`); `-level_09` + `-level_20`
boot crash-free 6/6 GE_PCDUMP frames (unregressed); `ge007.ini` gains a
populated `[Window]` block on exit.
## Done this session (M-27) — front-end/pause bucket: D140 + D141 FIXED
Started the parked "front-end / transitions / pause / watch" bucket.
**Pressing Start in-level no longer crashes** — the pause / watch menu
renders (weapon page verified via a temp `GE_AUTOPAUSE` input probe,
since removed). Cosmetics unchanged and still parked: mirrored watch text
(D114/D116), dark/faint weapon model (D75).
- **D140 (FIXED)** `src/game/bondview.h`, `src/game/bondview2.c` —
pause-menu crash `bondviewRenderWatch` → `bondviewTransformManyPos-
ToViewMatrix(field_23C=NULL)`. `something_with_watch_object_instance`
(N64 player +0x230) is a **`struct Model` + RW-pool punned into a
0x184-byte field-run**; `field_23C`/`watch_scale_destination`/
`pause_watch_related_adjust` are actually `.render_pos`/`.scale`/
`.animframe1`. PC `sizeof(struct Model)` grows → the aliases break →
NULL `render_pos`. Fix: real inline `struct Model` +
`u32 watchRwPool[192]` under `#ifdef PORT`; the 3 named reads redirect to
the member via `GE_WATCH_{ANIMFRAME,SCALE,RENDERPOS}` macros (N64 `#else`
= verbatim field). D56 branch of `sub_GAME_7F07E7CC` now uses the inline
pool. D100/D102 pattern. §F/§H **D140**, PORT-LEARNINGS §A.
- **D118d (FIXED, feel unconfirmed)** `src/game/options.c` — user bug report:
watch inventory list over-scrolls with keyboard W/S (one press skips several
items). GE's "slam the stick" fast-scroll is a raw per-frame level check
(`joyGetStickY < -0x46`); keyboard/digital pads sit at max every frame.
`#ifdef PORT` drops the stick term → list-nav goes through the latched
single-step path (one step per press). Build green, `-level_09` unregressed;
couldn't verify the feel headlessly (INVENTORY watch page unreachable without
page-cycling input) — **needs an in-game check**. Latent sibling in `front.c`
menu nav (same `joyGetStick*InRange` level-check pattern). §F **D118d**.
- **D141 (FIXED)** `src/game/gunfire.c` — the crash D140 exposed.
`set_enviro_fog_for_items_in_solo_watch_menu` walks `bodymodel->Switches[]`
(a `ModelNode*` array) with raw byte offsets `+0x48`/`+0x5c`, `j += 4`
(4-byte-pointer constants) → PC 8-byte stride reads garbage → bogus node
→ AV in `modelGetNodeRwData`. Fix: `#ifdef PORT` uses
`Switches[18 + (j>>2)]` / `Switches[23 + (j>>2)]`. §F **D141**,
PORT-LEARNINGS §B (cf. D128).
- **Verified:** build green (`ntsc-final`); `-level_09` framediff 3/3,
`-level_20` crash-free (both unregressed); autopause probe → watch page
renders 400+ frames, no `ge007.crash.log`.
- **D139** (stage-unload, M-23, still UNVERIFIED) not exercised — it needs
a real level-exit teardown, not a pause. Next front-end item.
- **Still in the bucket:** D139 verify; unpause / watch-page navigation
(the temp probe didn't test exiting the watch); level exit → MISSION
COMPLETE → debrief → auto-advance; main-menu / mission-select walkthrough
(WS2); watch objectives page + gadgets.
## Next task (M-24 — Opus 5)
**Scope call (user, M-23):** D139 (stage unload) + D140 (pause menu / watch)
are the same **not-yet-built front-end/transition layer** — level exit →
debrief → next briefing, pause, the watch. Bare `-level_XX` skips all of it
and the sweep never touches it. Do **not** build a throwaway exit path just
to verify D139 — its fix is unambiguous (LE reads the wrong header byte) and
committed; it'll be exercised for free once that layer is built. Both go in
a **parked "front-end / transitions / pause / watch" bucket** for one
dedicated session later (see `docs/PLAN-linear-level-sweep.md` WS2).
**M-24 = keep doing in-level playtesting.** In-level crashes (D135/D137/D138
type — pointer-width / ABI / GBI-parser bugs) are the real work and show up
during normal `-level_XX` play.
1. **Resume WS6** `docs/LEVEL-PLAYTEST.md`, bare `-level_XX`. Do the in-level
~80% (spawn, geometry, guards, doors, lifts, switches, pickups, each
difficulty-gated objective *registers* COMPLETE via `objective_status`,
alarms/reinforcements). **Defer** the exit-trigger / auto-advance / pause /
watch checks — those need the parked front-end bucket. Facility first
(it's playable start→~end now), then down the mission list.
2. Each new in-level crash: `tools_pc/repro_gdb.sh <XX>`, get the real
`bt full`, expect another `#ifdef PORT` ABI/layout fix
(D122/D126/D132/D135/D137/D139 pattern). Log as the next Dxx.
3. **Re-run `tools_pc/level_sweep.sh`** once — D135/D138 touch shared code
paths; sweep is boot-only so it only proves no *boot* regression. Refresh
`docs/LEVEL-STATUS.md`.
4. `bg.c` hit-test sibling port (§F D135 follow-up — identical unported GBI
parser to D135, `~3373-3646`) — do it before a level where wall-shooting
is heavy (i.e. soon).
**Parked bucket — front-end / transitions (STARTED M-27):**
- ~~D140 (pause menu → `bondviewRenderWatch` → NULL `field_23C`)~~ FIXED M-27
(+ D141, the watch-page crash it exposed). Pause now renders.
- D139 verify (stage unload / `cleanupObjects`) — still needs a real exit
- unpause / watch-page navigation (M-27 probe only tested opening it)
- level exit → MISSION COMPLETE → debrief → auto-advance to next briefing
- main menu / mission-select / difficulty-select playtest (WS2)
- the watch (objectives page, gadgets) — needed for the WS6 exit/objective
checks, D75 3D-model-transform family
**Verification per fix:** target level boots to ≥1 non-degenerate frame
(`tools_pc/pixcount.py`), AND `-level_09` + `-level_20` unregressed
(`tools_pc/framediff.py`). Regen sidecars after any converter change.
<details><summary>M-22 and earlier "Next task" (superseded — §H authoritative)</summary>
-1. **D134 DONE (M-22)** — the frame-2 boot hang that every session since
M-13 wrote off as "sweep flakiness / D117 / machine load" was a **real,
fixed bug**: the SP/DP task-done event was posted `OS_MESG_NOBLOCK` into
the sched `interruptQ` that the 60 Hz VI pacemaker also fills, so a slow
synchronous fast3d frame let a retrace backlog swallow the done event →
permanent stall. Fix in `port/src/libultra.c` (`portPostEventForce` +
2 reserved slots in `portPostVIEvent`). `-level_09` 6/6 boots to frame 600,
0 heartbeats (pre-fix 1/3). §F/§H **D134**, PORT-LEARNINGS §E.
0. **D132 DONE (M-21, commit `fa296b17`)** — propDef union-index slots for
types 14/19/38/44 (LINK/SWITCH/LOCK_DOOR/SAFE_ITEM) now emitted at the
right PC offsets (`d88_propdefs.py` + `loadobjectmodel.c sizepropdef`
PORT).
1. **`-level_09` (BUNKER1) boot crash — DISPROVEN as a regression** (M-20,
coordinator: 3 runs / 1800 frames crash-free on `1fc3cff6`; re-confirmed
M-20 this pass: 690+ frames at 91.7% coverage). Was sweep flakiness /
D117 nondeterminism under machine load. Do not investigate.
2. **Intro "renders mostly black" is NOT a regression — it is the known
D75/D76 parked-cosmetic steady state** (M-20 / **D133**). Verified by
building M-17 (`9ec6121e`, whose handoff claimed "the entire intro
renders") in a scratch worktree and capturing the same `GE_PCDUMP`
window: coverage is **pixel-identical** to HEAD (legal screen 6677
non-clear px = 2.17% both builds; logo-ish frames ~7%; black gaps in
between). The 2D/text layers draw; the animated character-model layers
(Nintendo-logo transform, gun-barrel Bond, cast models) never appear —
exactly D75. The M-17 "entire intro renders" handoff line was
aspirational, not a measured state. **Parked below level/crash work
(`docs/GRAPHICS-BACKLOG.md`).** Silo (#3, fly-down hang) is the same
class or D117 nondeterminism — not a fresh regression.
3. **Facility `-level_34` + Jungle `-level_37` re-verify FAILED this pass**
(M-20, machine lightly loaded). Facility: boot crash `frames=0`, PC
`0x1400c3b77` (addr2line unresolved). Jungle: renders frames 1–2 then
kernel-heartbeat hang ("no frame rendered", `frames=2`). Both were
claimed PASS in M-18/M-19 (D130/D131). **Needs a clean-machine
re-verify** before deciding regression vs. flakiness — see
`docs/LEVEL-STATUS.md`. `-level_09`/`-level_20` still PASS, so the
capture harness is sound.
After that: all 21 load+render → hand `docs/LEVEL-PLAYTEST.md` to the
user for the WS6 completion pass (real input, per-level objective
checklist).
</details>
**Regen gotcha:** `d69_emit.py` rewrites `data/pccg-<r>/pccg.bin` from
scratch with only the 52 bg/stan rows — you MUST follow it with
`d88_emit.py --regen` to re-add the 21 `Usetup*Z` rows, or every level
falls back to the raw N64 setup and crashes identically in
`proplvreset2`. Always run the full `d43 && d69 && d88 --regen` chain.
Sweep note: `level_sweep.sh` was flaky this session (spurious NO-FRAMES on
known-good levels under machine load / the 24 s watchdog + D117
nondeterminism). Verify individual levels with a 35–45 s window and
`GE_PCDUMP="60-500:40"` when a sweep row looks wrong.
Sweep runner: `tools_pc/level_sweep.sh` (bare `-level_XX`, `GE_PCDUMP="80-260:40"`,
24 s watchdog, `taskkill //F //IM ge007.x86_64.exe`; **`export PATH=".../mingw64/bin:$PATH"`
before `addr2line`** — the script's own symbolication no-op'd without it).
The M-13 matrix below is partially superseded by M-14 (see §H D125 for the current
12/21 status) — treat §H as authoritative.
| Class | Levels | Site |
|---|---|---|
| **C1** | Dam 33, Runway 35, Frigate 26, Statue 22, Streets 29, Cradle 41 | `chrIsNotDeadOrShot` chraction.c:4483 — `self` = AI-list rodata ptr |
| **C2** | Facility 34, Jungle 37 | `import_texture_i8`/`gfx_tex_normalize_source` — bad tex ptr |
| **C3** | Aztec 28, Bunker2 27 | `propobj.c` door model / `linkedDoor` walk |
| **C4** | Depot 30 | `prop.c:902` `sp4C->room` after `walkTilesBetweenPoints` |
| **C5** | Control 23 | `bg.c:5723` `portal_pts->numPoints` |
| **C6** | Surface2 43 | `loadobjectmodel.c:393` `PitemZ_entries[modelid].header` (D122 cont.) |
| **C7** | Surface1 36 | `snd.c:653` `sndSetupSound` (audio — parked subsystem) |
**Done M-13:** C1 fixed (**D123**, `tools_pc/d88_propdefs.py` — the C1
crash was D122 fallout: widened `Vehichle/AircraftRecord.ailist` slot
zeroed instead of carrying its read-before-write int id). C2 split:
Jungle's texture crash fixed (**D124**, `port/src/gimgfixup.c` — compiled
explosion-DL sync keyed on an already-erased marker); Facility+Runway
diagnosed (model-GDL relocation misaligns `dst`, `objecthandler_2.c` /
`texLoadFromGdl`, D80/D82/D83 area) — NOT fixed.
Full re-sweep after both: **12 / 21 PASS** (`docs/LEVEL-STATUS.md`).
9 crashes remain in 6 classes: **C2** Runway+Facility (model-GDL align —
biggest), **C3** Aztec+Bunker2 (`docs/BRIEF-C3-C6-prop-model.md`),
**C2m** Jungle (explosion-DL `G_MTX`), **C6** Surface2 (`PitemZ` modelid),
**C5** Control (BG portal), **C4** Depot (BG tile/room), **C7** Surface1
(`sndSetupSound`). Priority order + files in `docs/LEVEL-STATUS.md` "Next".
Uncommitted M-13 edits: `tools_pc/d88_propdefs.py`, `port/src/gimgfixup.c`,
§F/§H D123+D124, `docs/LEVEL-STATUS.md`, `docs/LEVEL-PLAYTEST.md`,
`docs/PORT-LEARNINGS.md`, `tools_pc/level_sweep.sh`, the BRIEF-C* files,
this file. Fold into the docs-restructure commit set.
Re-run `tools_pc/level_sweep.sh` after each fix; keep `-level_09` +
`-level_20` green (`framediff.py`). All-21-PASS → hand the user
`docs/LEVEL-PLAYTEST.md` for WS6.
**Verification per fix:** target level boots to ≥1 non-degenerate frame
(`tools_pc/pixcount.py` > a few %), AND `-level_09` + `-level_20`
unregressed (`tools_pc/framediff.py`). Regen sidecars after any converter
change: `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`.
**Parked (do NOT start here):** WS2 front-end menu playtest (needs a
human driving; cosmetics D75/D76/D77/D114/D116 all out of scope —
`GRAPHICS-BACKLOG.md`). Audio (Phase 3), saves (Phase 4).
## Environment / build
```sh
export PATH="/c/msys64/mingw64/bin:$PATH" && ./build-pc.sh ntsc-final # ~5 s
```
Run from the **repo root**, not `build-pc/`.
- **Repro (BUNKER1, skips attract mode):**
`./build-pc/ge007.x86_64.exe -level_09`
(per-level `-m*` pool sizes are now auto-injected — D121/WS1; pass them
by hand only to override). `boss.c:337` decodes `-level_XX` as
`d0*10 + d1 - 0x210`; LEVELID == the `-level_XX` number.
- **Sidecar regen** (after any converter change; `data/` is gitignored):
```sh
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
```
If `data/` is missing: `cp baserom.u.z64 data/ge007.ntsc-final.z64`
first (see §F "`data/` deletion + recovery").
- **Frame capture:** `GE_PCDUMP="<start>-<end>:<stride>"` → `./ppm/`
(gitignored). Analyse with `tools_pc/pixcount.py` (non-black content
check) and `tools_pc/framediff.py <ppmdir>` (structural regression vs
`tools_pc/golden/` — the port is **NOT frame-deterministic**, D117; use
`--mask` for the HUD, not an exact compare). `tools_pc/ppm2bmp.py` to
view.
- **Crashes:** `ge007.crash.log` (repo root). Symbolicate
`addr2line -e build-pc/ge007.x86_64.exe -f -C <pc>` (image base
`0x140000000`). Frames past the true chain may be stale (D56).
- **gdb:** launch mode is too slow for timing-dependent faults. **Attach**
mode is fast: `gdb -batch -x cmds.txt -p <winpid>` (`<winpid>` = 4th
column of `ps -p <bashpid>`; game must already be running, e.g. `nohup
… &`). A hardware watchpoint on a global catches a bad write in < 1 min.
- Standalone probe compiles need `-std=c11`.
## Non-negotiables (full list in AGENTS.md)
1. N64 build files (`Makefile`, `tools/`, `rsp/`, `ld/`) untouched.
2. Game logic unmodified except narrow, documented `#ifdef PORT`
ABI/layout/format exceptions — each logged as a Dxx in §F/§H with the
N64 line kept verbatim under `#else`. Anything beyond a narrow,
obviously-correct exception: **stop and write it up** with a confidence
rating, don't hack it in.
3. Prefer an **offline sidecar converter** (`tools_pc/d*_emit.py`,
D43/D69/D88 pattern) over a runtime fixup for a whole ROM-asset format.