Files
goldeneye-pc-port/docs/HANDOFF.md
T

938 lines
57 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 continued) — FULL front-end loop works end to end
**User playtested Dam → exit → post-mission report → Facility briefing →
Start, exactly like the retail game.** No crashes, no hangs. 10 commits
`bb6ac627`..`0fdb14a3` (all committed).
- **D142** (`src/bondconstants.h`) — GCC/mingw compiles an all-non-negative
`enum` as UNSIGNED, so `for (s = SP_LEVEL_EGYPT; s >= SP_LEVEL_DAM; s--)`
in `fileGetHighestStageDifficultyCompletedForFolder` never terminated →
SELECT FILE screen froze the game. Fix: `#ifdef PORT` negative sentinel
in `LEVEL_SOLO_SEQUENCE`. PORT-LEARNINGS §D3.
- **D143** (`src/game/textrelated.c`) — `langGet()` returns NULL for a
string slot the PC menu flow hasn't loaded; `textRender/textMeasure/
textWrap` faulted on it. NULL guards → blank text instead of crash.
(A string bank still isn't loading — briefing/objective text is blank.
Cosmetic, chase later.)
- **D144 / D146** (`port/fast3d/gfx_pc.cpp`) — a front-end 3D model
(MISSION COMPLETE dossier / mode-select wallets, D75 family) emits a
malformed compiled sub-DL (`seg5+0x9ee4`): unresolved matrix pointer,
then garbage opcodes. D144 = bad matrix ptr → identity; D146 = unknown
opcode → end the DL (was `sysFatalError`→`abort()`, no crash log) +
`fast3d_ptr_ok()` guards on `gfx_sp_vertex/movemem/set_vertex_colors`.
Model renders wrong/absent (GRAPHICS-BACKLOG D149) but the game survives.
- **D145** (`port/src/video.c`, `gfx_sdl2.cpp`, `input.c`) — bare **ESC was
`exit(0)`** in two event handlers; on the debrief screens ESC is the
natural "back" key so paging with it quit the game (clean exit, no log,
looked like a crash). ESC now = N64 B (back/cancel); quit = window-X /
Alt+F4.
- **D147** (`port/src/libultra.c`) — **the end-of-Dam hang.** `sndPlaySfx`
→ `alEvtqPostEvent` on the main thread races the `amMain` audio thread
in `sndRemoveEvents` on the same `ALEventQueue`; both "lock" with
`osSetIntMask(OS_IM_NONE)` which was a **no-op** → list corruption →
infinite spin. `osSetIntMask` is now a process-wide **recursive mutex**
(`OS_IM_NONE` acquires, `OS_IM_ALL` token releases). Thread dump
confirmed. PORT-LEARNINGS §D4. Verified `-level_09` 1200 frames clean.
**New test tooling (committed):**
- `tools_pc/debug.ps1` — `.\tools_pc\debug.ps1 [-level_09] [-Menu 13]
[-NoBuild]`. Builds, runs under gdb, prints FATAL line + backtrace +
crash log on exit → `gdb.txt`. Use this for every playtest so a
crash/hang always leaves a trace.
- `GE_INPUTSCRIPT="<frame>:<tok>,…;…"` (`port/src/input.c`) — headless
scripted controller-0 input (buttons pulse, `SUP/SDOWN/SLEFT/SRIGHT/
SNONE` sustain). Sole input source when set.
- `GE_STARTMENU=<id>` (+ `_PAGE`, `_DIFF`) (`src/game/lv.c`) — boot
straight into a front-end menu (13=MISSION_COMPLETE, 10=BRIEFING,
7=MISSION_SELECT, 12=MISSION_FAILED, 6=MODE_SELECT). Skips a save/unlock
so it lands on Dam regardless; good for crash-testing a screen, not for
reproducing real objective state.
### Still open (cosmetic / not blocking, parked in GRAPHICS-BACKLOG)
- **D148** — Dam level-end cutscene (Bond rappelling down the dam) doesn't
play; cuts straight to the report. Scripted-cutscene / cinematic-camera
+ rappel anim; D75 family.
- **D149** — front-end MISSION COMPLETE / mode-select 3D models garbled
or absent (the D144/D146 corrupt DL). D75 family.
- **D143 side effect** — briefing / objective text renders blank (a lang
string bank not loaded in the PC menu flow). Find which bank.
### Next
- Continue the mission list: Facility → Runway → … playtest with
`tools_pc/debug.ps1`, same loop. In-level ABI/GBI crashes are the
expected work (D122/D135/D147 pattern).
- The D75 front-end / cutscene 3D-model family (D148/D149) is now the
biggest *visible* gap but is cosmetic — below crashes.
## Done this session (M-30) — user batch defect list; commits + docs
### BLOCKER (M-30) — campaign progression not persisting — **FIXED (D157), CONFIRMED by user playtest**
User replayed Dam on Agent post-fix: `gdb.txt` shows `obj 0 objdiff=1` /
`obj 1,2 objdiff=2` (skipped on Agent) / `obj 3 objdiff=0 status=1` →
`objectiveIsAllComplete=1` → `end_of_mission_briefing` → `fileUnlockStage...
stage=0 diff=0` → `fileWriteSave slot=0 bitflags=10`. `data/ge007.eep` slot 0
now carries the Dam completion time; the Agent checkmark shows in mission-
select **and persists across a kill + restart**. Campaign progression works.
(`GE_SAVELOG` diagnostics — `#ifdef PORT`, env-gated — left in
`objective_status.c` / `boss.c` / `file.c` / `file2.c` for now; strip once a
few more level boundaries are playtested. `dump_objectives.py` per-criterion
`MinDif=` is only valid on the `type=23` lines — garbage on 2-word sub-records,
harmless.)
<details><summary>D157 root cause</summary>
`D157` (`src/bondtypes.h`, `#ifdef PORT`): the type-23 objective record's
`MinDificulty` is a BE `s32` at word 3; `d88_propdefs.py` bswap32's it, so on
LE the value moved from byte 0xF to 0xC, but `struct objective_entry.difficulty`
still read `s8`@0xF → **0 for every objective**. `get_difficulty_for_objective()`
returned Agent(0) for all → `objectiveIsAllComplete()` on Agent evaluated
objectives that should be difficulty-gated out (Dam "Neutralize all alarms" =
Secret Agent, "Install covert modem" = 00 Agent) → always incomplete →
`end_of_mission_briefing()` (the unlock write) never ran. **User was right —
you don't shoot an alarm on Agent Dam.** Fixed the struct tail order under
`#ifdef PORT`; also fixed `dump_objectives.py` (same byte-offset bug — it
showed "Agent" for everything). **Next: user replays Dam on Agent (just reach
the exit) under `GE_SAVELOG=1`; expect `MinDif=1/2/2/0` on the crit lines,
`objectiveIsAllComplete=1`, a `fileWriteSave slot=N ... TIMES` line, and
Facility unlocked in mission-select.** If obj 3 (Agent, flag 0x1000) still
shows incomplete, the level isn't setting that stage flag on exit — separate
issue. `GE_UNLOCK_ALL=1` remains the playtest workaround meanwhile.
</details>
<details><summary>Original M-30 blocker writeup (superseded by D157)</summary>
User completed Dam, hit "previous" from the debrief to mission select,
**Facility still locked**. `data/ge007.eep` has zero completion times → the
unlock save isn't landing (or is wiped on reload). Chain:
`bossReturnTitleStage` (`objectiveIsAllComplete()` gate) →
`end_of_mission_briefing` (`briefingpage`/`selected_difficulty` guard) →
`fileUnlockStageInFolderAtDifficulty` → `fileOverwriteSaveSlotWithNewSave`
(needs a slot with `SAVEFLAG_DORESET`) → `fileWriteSave` (`fileGamePakProbe`)
→ EEPROM; then `fileValidateSaves` CRC-checks every slot on menu re-entry and
`fileResetSave`s any mismatch.
**M-30 GE_SAVELOG run 1 (user's gdb.txt) — narrowed:**
`bossReturnTitleStage stage=33` (Dam) fires with **`objectiveIsAllComplete=0`**
→ `end_of_mission_briefing` never called → no unlock write. `fileValidateSaves`
/ CRC / DORESET-slot all fine (first-boot fresh-save init, expected). So the
blocker is **objective-completion detection**: `objectiveIsAllComplete()` bails
at obj 0 (`get_status_of_objective(0) != COMPLETE`). Dam obj 0 = 4×
DESTROY_OBJECT (ALARM tags). This is D126/D151 propDef-decode family — either
`objective->ObjRefID` at the wrong byte offset on LE (criteria records
`_bswap32`'d, `MissionObjectiveRecord.ObjRefID` s32@4 vs the criterion's
`u16` tag), or the `sizepropdef` walk stride is off for a criterion type.
Note `sizepropdef(PROPDEF_OBJECTIVE_START)` PC-branch returns **4** and
`d88_propdefs.py` emits **16 B** for type 23 — but `MissionObjectiveRecord`
(bondtypes.h) is header+ObjRefID+TextID+MinDificulty+`*nextentry` = 20 B N64
/ 24 B PC. If `nextentry` is real+serialized, both the stride and the
converter are 1-2 words short → whole-stream desync. (`struct objective_entry`,
the other view of type 23, is only 16 B and has no nextentry — the two
structs disagree; the "// maybe wrong..." comment flags it.)
**Diagnostic committed (`<crit dump commit>`):** `GE_SAVELOG=1` now also dumps
each criterion's `type / ObjRefID / TextID / sizepropdef stride / first 3
words / currentstatus` (gated by `g_savelogObjOnce` so it only fires on the
`bossReturnTitleStage` call, not the per-frame poll).
**Next: user does ONE genuine full Dam completion (all objectives, walk out
the exit) under `GE_SAVELOG=1`; the `SAVELOG: crit ...` lines pin the exact
field/stride that's wrong.** Also noted but not chased:
`fileSetDifficultyStageTime` `offset = ((diff*20)+levelid)*10` indexes
`times[]` up to `[98/99]` but the array is `u8 times[76]` — OOB for
high level/difficulty combos (writes into the next slot's checksum). Not the
Dam/Agent case (index 0) but a latent save-corruption bug.
</details>
## Done this session (M-30) — user batch defect list; earlier items
User playtest batch report (5 items; #1 disregarded):
- **#4 main-menu hang (file-select → main menu → back → 5fps, no input, force-close).**
Crash log was D146 spam + `D63 [1479] from=... No stack`. **Root cause: the
D63 debug trail in `gfx_pc.cpp` fired ~10 `sysLogPrintf` lines + a full trail
walk on EVERY unknown GBI opcode, unconditionally** — on the D149 corrupt
front-end model DL that ran per-frame and collapsed the menu to ~5fps with
input starved. **FIXED `d0789358`:** stripped the D63 trail (statics,
per-G_DL recording, default-case dump); the rate-limited D146 "ending DL"
log (cap 20) stays. Headless: FILE_SELECT / MODE_SELECT / MISSION_SELECT all
boot + render 900+ frames clean, 0-1 D146 lines, no hang. `-level_09` 3/3
framediff, `-level_20` 91.6% unregressed. **User to re-verify the live
menu-nav transition** — the exact folder→mode→back path wasn't reproducible
headless, but the per-frame log flood (the "5fps no input") is gone.
- **#2 file-select mouse only moves side-to-side.** The front-end cursor is
stick-driven (`frontUpdateControlStickPosition` reads `joyGetStickX/Y`); the
port only fed mouse-X to the stick in menus (mouse-Y → C-buttons, ignored by
menus). **FIXED `<menu-pointer commit>`:** `port/src/input.c` — when
`current_menu != MENU_RUN_STAGE` (and `!= MENU_INVALID`, so bare `-level_XX`
in-game aim is untouched) mouse velocity drives stick X+Y as a pointer, no
C-buttons / aim band, LMB=A (select) RMB=B (back). Reads the game global
`current_menu` for UI context only (enum MENU is ABI int) — no logic change.
New knob `Input.MenuPointerSpeed` (%, default 100). **User to verify feel.**
- **#5 menu + saving for playtest.** EEPROM saves (`osEeprom*` →
`data/ge007.eep`, write-through, M-29 `bf5e4d3d`) audited — sound; progress
persists across restart *once the menu works* (#4). No change needed; verify
after #4.
- **#3 "interlaced" textures** (`screenshots/interlaced texture example.jpg`).
**RC2 (mip contamination) FIXED, default on** (`<rc2 commit>`,
`port/fast3d/gfx_pc.cpp`). New `GE_DTEX=1` probe confirmed it: every LOD
texture uploaded ~1.3x too tall (64x64 I4 → 64x87), the extra rows being mip
bytes rendered as image rows. `import_texture()` now clips a full-width LOD
block to the SETTILESIZE base height; GL builds correct mips from a correct
base. Knob `Video.FixMipTextures` (=0 → old behaviour, byte-identical to
golden). BUNKER1 side-wall textures visibly cleaner with it on; Silo
unregressed. **`-level_09` framediff phash on frames 320/440 is D117 noise,
not a regression** — M-30 verified: BUNKER1's intro camera pans continuously
for 1000+ frames and its speed varies run-to-run, so *any* fixed capture
frame lands on a different pan moment each run (a re-baseline attempt to a
"settled" 700-1000 window failed the same way — there is no static window in
the intro). Trust `nonclear` (coverage %, stays stable) + the 21-level sweep
for `-level_09`/`-level_20`; the golden phash frames are only meaningful for
gross breakage. Depot M-30 A/B: RC2 makes the wall panels mildly cleaner,
does NOT touch the ceiling blue-speckle (separate B2 light-shaft/UV bug),
makes nothing worse. **21/21 sweep PASS with the fix on.**
- **Still open:** the wallet-Bond *photo* garble is RC1 / D75 front-end model
family (the model GDL / `texLoadFromGdl` expansion is broken for front-end
models — fast3d walks into `0xfafbfbfc` garbage, not a missing opcode).
Separate larger track — see `docs/TEXTURE-GLITCH-ANALYSIS.md` §6b.
- RC4 (palette off-by-one, `gfx_pc.cpp:835`) and RC3 (wrap period) still
open — small, in the same doc §6.
- Regenerated `docs/LEVEL-OBJECTIVES.md` for all 21 solo levels (Track A);
committed loose reference docs/tools.
## Done this session (M-29) — race-to-release plan; audio DEFERRED; 2 commits
**Direction (user, M-29):** ship the solo campaign start→finish, fastest.
Three tracks: A = full 21-level playtest (release gate, user batch-reports a
defect list from a solo playthrough); B = **audio DEFERRED** — ship silent,
add sound post-launch (`docs/AUDIO-PLAN.md` stays valid for later, do NOT
start it); C = solo quick wins, priority: **B2 textures** > B3 blood-stain
converter > B5/B6 menu input > C2 (D75 models) > C4 (D148 cutscene) >
C1 (mirrored HUD — user unsure it's worth it). Full plan +
`docs/SMALL-FIXES.md` mapping in the `m29-release-push` memory.
- **`0bd0ceec`** — aim sensitivity (SMALL-FIXES B4 / BACKLOG B3):
`Input.MouseAimSpeed` default 50→25 (overshot), new `Input.AimBand` knob
(5..40, def 20). Port-only, `ge007.ini`-tunable. Needs playtest feel check.
- **`bf5e4d3d`** — file-backed EEPROM saves (BACKLOG B5 / Phase 4):
`osEeprom*` in `port/src/libultra.c` now back `data/ge007.eep` (2 KB /
16Kbit, lazy-load + write-through, PD pattern). `osEepromProbe` →
`EEPROM_TYPE_16K`. Verified: game writes the file (checksum @blk 0 +
save_data @blk 4), `-level_09` unregressed, persists across runs.
Campaign progress now survives quit.
- **SMALL-FIXES B1 (D74 wrap block) — DEFERRED.** The in-place one-liner
(`G_TX_CLAMP` test + `[t]` index) activates never-run code and boot-crashes
`-level_09` (0 frames). Left inert with a NOTE in `gfx_pc.cpp`. Needs the
hoist-out-of-vertex-loop rework + per-level visual check.
- **B1 3-point filtering — wired up as opt-in (`a418f4d0`), NOT default.**
The sm64ex 3-point shader path existed but was unreachable + its min-filter
row was mip-less. Fixed the row (trilinear), aniso default 0→4, exposed
`Video.TextureFilter=2`. **Default stays 1 (bilinear)** — 3-point softens
textures at normal distance and (tested) did NOT fix the Depot roof.
- **B2 (Depot wrong textures) — filtering RULED OUT, still not fixed.**
`docs/BRIEF-B2-depot-textures.md`. The roof renders as blue speckle +
radial rays converging to a point; identical with 3-point+trilinear+aniso
at 640×480 native, so NOT grazing aliasing. Decode path (TLUT bswap,
`palette_to_rgba32`, `import_texture_*`) audited clean. It's a texture-data
/ UV / light-shaft-effect bug on that surface. **Next step: `GE_DTEX`
probe** to identify what draws it (params in the brief). This is a
RenderDoc/probe-class investigation — D114/D116 rabbit-hole family, needs
a focused session, not inline.
- **`7d7f5fb2` D155 — Facility outro-cutscene "hang" FIXED.** User report.
`waitForNextFrame()` passed `deltaFrames` unclamped; on the port
`osGetCount()` is wall-clock (D117) so a real-time stall at the
cutscene→debrief asset load (worse with a local LLM thrashing the box)
made it balloon to hundreds → `g_ClockTimer` huge → `modelTickAnim`
`while(numticks--)` × N chrs + dozens of `for(i<g_ClockTimer)` sim loops
→ multi-second frame → heartbeat "hang" + spiral. Fix: `#ifdef PORT` clamp
to 6 in `frametiming.c`. **Likely fixes a whole class of post-slow-load
transition hangs** — de-risks the Track A campaign playtest (every level
boundary loads assets). Confidence high (stack + arithmetic agree).
- **`d656823e`+`b4a7fc2a` D156 — Facility outro hang, 2nd occurrence, the
ACTUAL fix.** After D155 the user re-ran the playthrough → hung again at
end of Facility, same stack but `frames=12962` **frozen constant** (true
infinite loop, not D155's spiral). The `while(1)` at `model.c:3131`
(`modelSetAnimFrame2WithChrStuff`) steps one anim frame at a time from
`framea` to `frameb`; `frameb` = `modelTickAnim`'s accumulated `frame`. A
cutscene anim transition with a near-zero blend/`timespeed`/`unkb0`
denominator → huge/NaN `model->speed`/`playspeed` → `frameb` huge/NaN →
`floorFloatToInt` garbage → ~2^31 iterations. `#ifdef PORT` guards: snap a
non-finite/`|x|>=1e6` `frameb` to `framea`, and fall back `frame`/`frame2`
to the pre-loop values in `modelTickAnim`. One-shot `stderr` diagnostic
dumps the model speed fields when it fires. **User to re-verify the
Facility outro.** If it still hangs OR the diagnostic prints, the NaN
source is upstream (suspect a D100/D140-style misaligned `Model` field, or
the cutscene data). §F D155+D156, PORT-LEARNINGS §E.
- **D153 reminder cost real time this session:** back-to-back `-level_09`
runs during builds boot-crash under machine thrash; only regression-test on
a settled machine (clean run = 600+ frames fine).
## Done this session (M-28) — D150 (watch page crash) + D151 (watch text blank)
- **D150** (`src/str.c`, `#ifdef PORT`, uncommitted) — user found a crash
opening **level objectives on the watch**. AV in `strcat` (`str.c:25`,
NULL src). The watch BRIEF/OBJECTIVES pages (`options.c:3940-4068`) build
text with `strcat(buf, langGet(id))`; `langGet` returns NULL on PC for the
briefing/objective string banks the in-level watch flow never loads
(same missing-bank issue as the D143 "briefing text renders blank" note).
Fix: NULL-tolerant `strcpy/strncpy/strcat` under `#ifdef PORT`. Note these
are `__nonnull__` builtins to GCC so a plain `if (src==NULL)` is optimised
away — the guard laundras the ptr through an empty `__asm__` (`GE_IS_NULL`).
Build green; `-level_09` 600+ frames @ 91.67% unregressed. **Watch-page
repro is interactive — needs user re-verify.** §F/§H D150, PORT-LEARNINGS §C.
- **D151** (`src/bondtypes.h`, `#ifdef PORT`, uncommitted) — the D150
"objective/briefing text renders blank" consequence, ROOT-CAUSED and fixed.
NOT a missing bank: the per-level lang bank IS loaded in-level
(`langLoadToAddr`, `prop.c:1274`). The propDef records that carry the watch
text (types 35 `WatchMenuObjectiveText`, 23 `ObjectiveStart`) store a plain
`s32` slot id in their 3rd word; `struct watchMenuObjectiveText` /
`struct objective_entry` decode it as `u16 reserved; u16 text;` and read
`text` at offset `0xA` — works only because on BE the id is in the low 16
bits. `d88_propdefs.py` `_bswap32`'s the whole word → `text` @0xA reads 0 →
`langGet(0)` → NULL → blank. Fix: `text` is a full `u32` at the word offset
under `#ifdef PORT` (N64 `u16 reserved; u16 text;` kept under `#else`). No
converter change / no sidecar regen. Compiles clean (link needs the running
game closed). **Interactive re-verify pending** — open the watch, all of
OBJECTIVES / mission background / M / Q / Moneypenny should now show text.
§F **D151**, PORT-LEARNINGS §C.
### D152 (OPEN) — mission-failed → permanent black screen = `osSetIntMask` lock deadlock
User killed Trevelyan in Facility (`Ctrl` = **fire**, no crouch bind) → failed
the objective → fade to black, never returns; process alive. **Live `gdb.txt`
inspected** (game left running under `debug.ps1`): NOT the front-end
mission-failed *screen* — it's a **deadlock on `s_imLock`** (the D147
recursive-mutex behind `osSetIntMask`). Rendering froze at frame 12600.
`mainThread` in `sndSetScalerApplyVolumeAllSfxSlot`→`alEvtqPostEvent`→
`osSetIntMask(OS_IM_NONE)`→`pthread_mutex_lock` (waiting); `amMain` in
`sndPlayerVoiceHandler`→`alEvtqNextEvent`→same, also waiting; **neither owns the
lock** → a leaked unbalanced `OS_IM_NONE` (libaudio early-return paths) or a
transient thread that acquired-and-exited holds it. The mission-failed **audio
fade-out** (`sndSetScalerApplyVolumeAllSfxSlot` posts `AL_SNDP_RELEASE_EVT` per
`ALSoundState` per frame) is what exercises the window. D147 family; D147's
"cannot deadlock" claim is now falsified. §F **D152**, PORT-LEARNINGS §D4.
**MITIGATED (M-28, `port/src/libultra.c`, port-only, uncommitted).**
`osSetIntMask` is now a **self-healing** logical lock: same recursive
acquire/release as the D147 mutex on the normal (microsecond) path, but a
waiter blocked **> 2 s** (`OS_IM_STUCK_NS`) declares the holder leaked it,
logs `LOG_ERROR` with the stale owner + the stealing caller's return
address, and steals the section. A leaked/unbalanced `OS_IM_NONE` (or an
acquire-and-exit transient thread) can no longer wedge the game forever —
worst case is a ~2 s audio hiccup + a log line that names the leak. Builds
clean. Still OPEN: the exact leaking call site (read it from the next
`D152: ... stealing from owner=` log) + the proper narrow fix (dedicated
`ALEventQueue` lock). Next: replay the Facility mission-fail; if the steal
fires, `addr2line` the logged caller.
Separate minor gap: no crouch keybind (BACKLOG B3/B4).
### D154 (WRITTEN, UNVERIFIED, UNCOMMITTED) — `bg.c` room hit-test GBI parser port
`bgTestRayIntersectionInRoom` (`src/game/bg.c:3331`) is the D135 sibling the
docs kept flagging: an unported N64 GBI parser (`((u8*)gdl)[k]` / `((u32*)gdl)[i]`
byte/word indexing) walking the 16-byte PC room DL as if it were 8-byte N64
`Gfx`. Fires on **shooting walls/floor** (`bgTestBulletHitBackground`), which no
level-sweep exercises. Ported under `#ifdef PORT` (N64 path verbatim under
`#else`): header fields via the `.dma` view like `bgBuildRoomVtxBounds`; G_TRI1
+ G_TRI4 vertex-index nibbles recovered from `(u32)gdl->words.w0/.w1`
(derivation table cross-checked twice); `texturenum` -> -1 like D135 (KSEG0
deref invalid for converted GDLs; only flavours the impact decal/sound, D77).
Builds clean. **NOT verified** — a no-input capture never calls this function,
so it needs a real firefight into a wall on an **idle** machine; couldn't get a
clean run this session (D153/driver-crash under machine thrash — see below).
Next session: build, `-level_09`, fire at a wall, confirm no crash + `-level_09`
framediff green, then commit. §F **D154**.
### D153 — `-level_09` frame ~900 crash = load flakiness, NOT a bug (closed)
Chased for ~30 min in M-28: `-level_09` crashed `0xc0000005` at a
nondeterministic frame 900↔1400, 6/6 runs — **but every one was while the
machine was hammered with concurrent builds + multi-run loops.** On a quiet
machine `-level_09` ran past frame 2400, 0 heartbeats, no crash. It's the
D117/D134 host-scheduling flakiness the docs warn about every session (M-25
Silo ~frame 300 is the same). Not a regression, not D150/D151/D152.
`romdataFixupMusicSeqTable: seqCount 63 exceeds blob capacity 1` is a benign
expected warning (header-only first call). **Lesson: never regression-test
while builds/other game runs are in flight.** §F **D153** (closed).
<details><summary>M-27 earlier — front-end/pause bucket: D140 + D141 FIXED</summary>
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.
**Update (M-27 end): the level-exit → MISSION COMPLETE → debrief →
auto-advance → next briefing → Start loop is DONE and playtested (D142–
D147).** D139 got exercised for free. Remaining bucket items (unpause /
watch-page nav) are minor.
</details>
## 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.