docs(findings): migrate fat §F rows D123-D151 to tail sections (ctx-opt P1, batch 1/8)

16 rows: topic/status cells moved verbatim to new '## Dxx' sections under a
provenance divider at file tail; table rows now carry a one-line summary +
pointer. CSV regenerated, label set unchanged (171). Nothing deleted.
This commit is contained in:
James Dansereau committed 2026-09-15 21:22:39 -04:00
1 parent d7d84c09f5
commit cd6f0b5e83
2 files changed
+124 -20

No files matched your search

+4 -4
View File
@@ -113,12 +113,12 @@ D123,FIXED,crash class C1: chrIsNotDeadOrShot NULL deref on 6 levels (Dam/Runway
D125,RESOLVED,crash classes C3+C6 (Aztec/Bunker2/Surface2):,resolved (M-16b) — line 374 → 8 NULs; all 21 sidecars regen'd; Aztec -level_28 now PASSES (was C3 CRASH).
D126,RESOLVED,"crash classes C3r/C4/C6 (Bunker2 -level_27 door7F054FB4 propobj.c:13523, Depot -level_30 prop.c:902, Surface2 -level_43 loadobjectmodel.c:393):",resolved — d88_propdefs.py PROPDEF_PC_BYTES[30/32/33/35]=24 + typed handler; loadobjectmodel.c sizepropdef PORT returns 6.
D124,FIXED,crash class C2: fast3d bad texture pointer.,Jungle fixed (port/src/gimgfixup.c); Facility → D130
D131,RESOLVED,crash class C2m (Jungle -level_37):,"resolved — seg_addr() restores the module high word for any fallthrough w1 in [0x40000000, 0x70000000) (module fixed-based at 0x140000000, no ASLR; DRAM/KSEG0/segmented/phys all handled earlier)."
D131,RESOLVED,crash class C2m (Jungle -level_37):,"resolved — seg_addr() restores the module high word for any fallthrough w1 in [0x40000000, 0x70000000) (module fixed-based at 0x140000000, no ASLR; DRAM/KSEG0/segmented/phys all handled earlier).…"
D133,RESOLVED,intro render triage (M-20):,"RESOLVED — not a regression, parked (D75/D76, GRAPHICS-BACKLOG.md)"
D134,FIXED,"Frame-2 boot hang / sweep ""NO-FRAMES"" flakiness —",FIXED (port/src/libultra.c)
D135,FIXED,"Firefight crash — bgTestHitOnObj (propobj.c:8446) is an unported N64 GBI parser (M-22, WS6 playtest).","FIXED (src/game/propobj.c, #ifdef PORT —"
D135,FIXED,"Firefight crash — bgTestHitOnObj (propobj.c:8446) is an unported N64 GBI parser (M-22, WS6 playtest).","FIXED (src/game/propobj.c, #ifdef PORT —…"
D136,RESOLVED,"Stage-unload crash — objFree obj->prop reads float garbage (M-22, WS6 Facility playthrough).",ROOT-CAUSED → D139 (it was NOT a layout bug —
D139,FIXED,Stage-unload crash root cause + fix (M-23).,"FIXED (src/game/cleanup_objects.c, #ifdef PORT —"
D139,FIXED,Stage-unload crash root cause + fix (M-23).,"FIXED (src/game/cleanup_objects.c, #ifdef PORT —…"
D140,FIXED,Pause-menu crash (M-23 open → M-27 fixed).,"FIXED (src/game/bondview.h, src/game/bondview2.c; #ifdef PORT)."
D141,FIXED,"Watch-menu item-model crash (M-27), exposed by D140.","FIXED (src/game/gunfire.c, #ifdef PORT)."
D152,OPEN,"Mission-failed / objective-failed → permanent black screen (M-28, user bug report —","MITIGATED (port/src/libultra.c, M-28, #ifdef PORT/port-only)."
@@ -126,7 +126,7 @@ D154,FIXED,"bg.c room-geometry ray/hit-test GBI parser port (M-28, WRITTEN / UNV
D156,PARTIAL,"Facility outro-cutscene ""hang"" —","GUARDED (src/game/model.c, #ifdef PORT):"
D155,FIXED,"Facility outro-cutscene end → ""hang"" = unclamped deltaFrames spiral (M-29, user bug report).","FIXED (src/game/frametiming.c, #ifdef PORT —"
D153,RESOLVED,"-level_09 frame ~900–1400 0xc0000005 = D117/D134 load flakiness, NOT a bug (M-28).",NOT A BUG — closed. Run anchors on a quiet machine.
D151,FIXED,Watch data all blank —,"FIXED (src/bondtypes.h, #ifdef PORT —"
D151,FIXED,Watch data all blank —,"FIXED (src/bondtypes.h, #ifdef PORT —…"
D130,RESOLVED,"crash class C2 (Facility -level_34, Runway -level_35):",resolved — stage all 6 N64 fields in a local f[6] before writing any.
D164,PROPOSED,"Disclaimer / legal screen only draws the first line (D76 root cause, M-31).","FIX PROPOSED, NOT APPLIED (front.c is owned by another agent this session)."
D165,FIXED,"Front-end menu cursor doesn't feel like a mouse pointer (M-31, user bug B5 follow-up).","FIXED (port/src/input.c; Input.MenuPointerMode, Input.HipfirePitchSpeed added)."
1 # GENERATED by tools_pc/gen_findings_index.py -- do not hand-edit.
113 D125
114 D126
115 D124
116 D131
117 D133
118 D134
119 D135
120 D136
121 D139
122 D140
123 D141
124 D152
126 D156
127 D155
128 D153
129 D151
130 D130
131 D164
132 D165
+120 -16
View File
@@ -492,33 +492,33 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
| D274 | **D236 wall, seventh pass — probe identity (`seq`) proven unreliable both mid-run (cache-hit staleness) and cross-run (shared runtime-arena address reuse); fixed with an unconditional per-slot address stamp.** Resolved the prior pass's open "content vs. decode" question as a bug in this session's own offline analysis (wrong palette byte order — native/little-endian, not big-endian), not a port bug: the port's existing CI palette decode, applied correctly, reproduces a plausible soft gradient on the real, address-confirmed wall-card texture. Confirmed mips are generated (`genmip=1`) and enabled (`tex_lod=1`) for that texture, with correct `GL_REPEAT` wrap — ruling out "mips absent" as the mechanism. Attempted class-isolation A/B was inconclusive (black frame). | `d267_slot_addr` fix live (always-on); content/decode question CLOSED; mip-presence CLOSED; wall still open, see D236/D276 |
| D273 | **D236 wall, fifth pass — user-supplied N64 reference screenshot obtained (discrete tree silhouettes, not a sheet); a pre-existing, undocumented `GE_D273` palette-decode A/B probe found in the uncommitted tree was tested against it and RULED OUT.** Standard-ARGB1555 vs. inherited-shifted CI palette decode: wall pattern visually unchanged either way; unrelated CI8 textures (snow path, viewmodel skin) corrupted by the swap. Not the wall's cause; `GE_D273` stays default-off. Flags a process gap: a live investigative lead sat in the working tree undocumented across a handoff boundary. | RULED OUT (`port/fast3d/gfx_pc.cpp` `palette_to_rgba32`, pre-existing/uncommitted `GE_D273`) — wall still open, see D236/D272 |
| D272 | **D236 wall, fourth pass — per-card S/T audit (HANDOFF next-step #1) RULES OUT "identical UV window" and "converter bug"; the wall cards genuinely sample a wide, per-instance-varying UV range, verified authored in ROM, not injected by `d43_emit.py`.** New `GE_D272` raw per-vertex UV+tile probe (`gfx_sp_tri1`), filtered to D269's exact two oml classes. Both are confirmed CI8 (fmt=2 siz=1, matches D269). Findings: (1) per-instance UV is **not identical** across cards sharing the same bound texture (`seq`) — small (~0.3 texel) jitter for the OPA class, larger (~1.6 texel) jitter for the XLU class — consistent with genuinely distinct authored per-vertex S/T, not a converter collapse; D265's "identical u:0..64/v:0..16" observation traces to a different (non-wall) draw class caught by the old unfiltered band gate, not these cards. (2) The XLU class's U range spans **~143–145 texels — ~4.5× the tile's 32-texel wrap period** (`mask=5,5`); the OPA class stays within roughly one tile width. (3) Read `tools_pc/d43_emit.py`'s vertex emit path (`emit_main_vtx`): `tc[0]/tc[1]` (offsets 8/10 of the 16-byte `Vtx`) go through a plain `be16` byteswap with no scale/offset — **no converter bug possible here**; the ROM must already encode this ~4.5× wrap. Reframes the leading hypothesis: this heavy horizontal wrap is by design (Rare tiling one texture across a long tree-line quad), so the "wall" comes from *how* the port renders that wrap, not from wrong UV data — TEXTLOD/mip generation and GL wrap-boundary sampling for a highly-minified, wide-wrapped CI8 tile is now the top suspect again (D269 hypothesis #2), ahead of fog/palette. | OPEN — probe `GE_D272` added to `port/fast3d/gfx_pc.cpp` (env-gated, uncommitted); wall still open, see D236 |
| D137 | right-mouse (aim-sight) crash: `gunDrawSight` (`gunfire.c:6235`) `s32 sp54` holds a `Gfx*` the whole function (`sp54 = *gdl`; `texSelect(&sp54,…)`/`display_image_at_position(&sp54,…)` take `Gfx**`). As N64 `s32` it is 4 B, so `*(Gfx**)&sp54` reads 4 B of the adjacent stack float as the pointer high word → `_g = 0x03e4fca0_70081220` → wild DL write in `texSetRenderMode` (`othermodemicrocode.c:177`) the moment the crosshair raises. §A. | FIXED (`src/game/gunfire.c` `#ifdef PORT` — `sp54` is `Gfx *`) |
| D137 | **right-mouse (aim-sight) crash:** — full `## D137` entry at file tail | FIXED (`src/game/gunfire.c` `#ifdef PORT` — `sp54` is `Gfx *`) |
| D119 · D120 | guard-attack `weapons_held[]->chr` pun crash · blood-stain `PointUsage[]` chain hang | D119 fixed · **D120 FIXED (M-30)** |
| D120 | **Blood-decal `PointUsage[]` chain — converter now emits it (M-30).** `d43_emit.py` reserved the opcode-0x18 `PointUsage` region in the layout pass (`add_region(puo, 2*nv)`) but the **emit pass never wrote it** — every entry stayed zero. `chr.c`'s decal walk `index = PointUsage[index]` then cycled 0→PointUsage[0]→0 forever (capped by the `#ifdef PORT` guard at `chr.c:3328`, so decals were missing/wrong rather than a true hang). Fix: `op24_pointusage[puo] = nv` in layout + a byteswap loop (`put_s16(remap(puo)+2k, be16(src, puo+2k))` for `nv` = `numVertices` entries — a negative-terminated `s16` index chain, no remap) in the emit pass. Full regen chain re-run, round-trip validation passes. The `chr.c` guard stays as a safety net (SMALL-FIXES B3). | FIXED (`tools_pc/d43_emit.py`). Interactive verify pending — blood decals on a BUNKER1 firefight should now appear + no hang. |
| D121 | WS1 frictionless per-level boot: bare `-level_XX` injects its `memallocstringtable[]` `-m*` row (`#ifdef PORT` in `boss.c`) | resolved |
| D122 | per-level prop/item model-load crash (Dam/Facility/Runway `modelLoad`/`modelInitRwData`): `d88_propdefs.py` had no handler for 6 ObjectRecord-derived propDef types (47/39/40/45/13/20) → generic arm half-swapped the `[s16 obj][s16 pad]` word → OOB `PitemZ_entries[]` | converter fixed; residual chr/fast3d crashes on those levels are separate |
| D123 | crash class C1: `chrIsNotDeadOrShot` NULL deref on 6 levels (Dam/Runway/Frigate/Statue/Streets/Cradle). D122's `OBJ_TAIL_DESC` zeroed the widened `VehichleRecord/AircraftRecord.ailist` slot (w32), but `prop.c:1764/1786` reads a pre-populated int AI-list id there → `ailistFindById(0)` → `GAILIST_AIM_AT_BOND` → `ai()` runs a CHR aim list with `ChrEntityp==NULL` | converter fixed (`OBJ_ID_WORDS`); C1 cleared on all 6, residual crashes are fast3d (C2) |
| D125 | crash classes C3+C6 (Aztec/Bunker2/Surface2): **root cause found (M-16)** — `tools_pc/d88_emit.py:374` assigns a 4-byte literal to an 8-byte slice (`out[dst_o+0x30:dst_o+0x38] = b"\x00…"`, the stan zero-fill in `emit_pad`); every pad/boundpad record silently shrinks the output bytearray by 4 B, and once the tail falls below the boundpad plink string blob, later verbatim leaf writes hit Python out-of-range slice semantics (insert at current end, not relocated offset) → boundpad names drift/truncate (Aztec pad33 `p138d2`→`8d2`) → `stanPackId()` reject → `getposstan()` NULL stan → `setupDoor` leaves door `model=NULL` → crash `propobj.c:13601`. M-14's "propDefs zeros in RAM" was a misread; sidecar propdefs were always correct (M-15) and post-load RAM matches the sidecar byte-for-byte (M-16) | **resolved (M-16b)** — line 374 → 8 NULs; all 21 sidecars regen'd; **Aztec `-level_28` now PASSES** (was C3 CRASH). Bunker2 falls through to a separate DOOR-tail `linkedDoor` layout bug (C3 residual) |
| D126 | crash classes C3r/C4/C6 (Bunker2 `-level_27` `door7F054FB4` propobj.c:13523, Depot `-level_30` prop.c:902, Surface2 `-level_43` loadobjectmodel.c:393): the 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 the setup walk (`set_parent_cur_obj_*` / `setup_briefing_text_entry_parent`) writes unconditionally. On PC that pointer widens 4→8B and lands 8-aligned at offset 16 → struct is 24B/6w (N64 16/20). `d88_propdefs.py` emitted them at N64 size via the generic arm → the runtime 8-byte `->next` write clobbered the *next* record's header → propdef walk desynced, command indices drifted ~100, `linkedDoorOffset + arg2` resolved to the wrong record → door `linkedDoor` chain walked into garbage. | resolved — `d88_propdefs.py` PROPDEF_PC_BYTES[30/32/33/35]=24 + typed handler; `loadobjectmodel.c` sizepropdef PORT returns 6. Bunker2/Depot/Surface2 now PASS (13→16/21) |
| D124 | crash class C2: fast3d bad texture pointer. **Jungle** (`0xabcd0824`): `gimgSyncCompiledGlobalDLs()` slot-detect keyed on the post-fixup marker, which `texLoad()` had already overwritten → compiled `globalDL_0xNNN` explosion DLs kept link-time `IMAGESEG` words → latent on every level, tripped by the first explosion-DL draw. **Facility** (`0x72181ee8`): see **D130** — the model-GDL-relocation hypothesis (M-14 addendum) was WRONG; real cause was `romdataFixupFont` | Jungle fixed (`port/src/gimgfixup.c`); Facility → D130 |
| D131 | crash class C2m (Jungle `-level_37`): fast3d `gfx_sp_matrix` AV on a wild matrix pointer `0x401c68e0` (`gfx_pc.cpp:1046`) ~frame 300, when the first explosion/smoke prop renders. `explosionRenderPropSmoke` builds `gSPMatrix(gdl++, osVirtualToPhysical((void*)&dword_CODE_bss_8007A100), …MODELVIEW)` (+ `applyRoomMatrixToDisplayList`). `osVirtualToPhysical()` is a `u32`-returning shim (`libultra.c:1207`) → truncates the compiled `.bss` symbol's `0x1_00000000` module high word → w1 = `0x40xxxxxx` → `seg_addr()` returns it raw. Same class as D94 (chraction 32-bit ptr truncation), but in a GBI DL word. The projection matrix in the same DL is fine (`get_BONDdata_field_10E0()` is a runtime `0x70xxxxxx` ptr, fits u32); the `gSPDisplayList(&globalDL_0xNNN)` refs are fine (port `Gwords.w1` is 64-bit `uintptr_t`, no macro truncation). ~30 `osVirtualToPhysical(<compiled matrix/vtx symbol>)` sites exist (`explosion.c`, `glass*.c`, `blood_animation.c`, `bondview2.c`) — all latent until that effect first draws. | resolved — `seg_addr()` restores the module high word for any fallthrough w1 in `[0x40000000, 0x70000000)` (module fixed-based at `0x140000000`, no ASLR; DRAM/KSEG0/segmented/phys all handled earlier). Jungle renders to frame 2400+ clean; `-level_20`/`-level_24` unregressed. **18→19/21.** (`-level_09` has a separate pre-existing boot crash, see below.) |
| D133 | intro render triage (M-20): the "intro renders mostly black" item in the M-18/M-19 handoff is **NOT a regression** — it is the D75/D76 parked-cosmetic steady state. M-17 (`9ec6121e`), whose handoff claimed "the entire intro renders — logos → gun barrel → cast", was built in a scratch worktree and captured with the same `GE_PCDUMP="20-900:20"` window: coverage is **pixel-identical** to HEAD `0b5f5d1a` (legal screen 6677 non-clear px / 2.17% on both; centred-logo frames ~7% on both; near-black between). 2D/text/texture layers draw; animated character-model layers (Nintendo-logo transform, gun-barrel Bond, cast models) never appear = D75 exactly. No cheap regression to fix; parked. Secondary: Facility `-level_34` + Jungle `-level_37` re-verify FAILED this pass (boot crash / frame-2 hang) — flagged in LEVEL-STATUS for a clean-machine re-check, not investigated (scope). | RESOLVED — not a regression, parked (D75/D76, `GRAPHICS-BACKLOG.md`) |
| D134 | **Frame-2 boot hang / sweep "NO-FRAMES" flakiness — root-caused and fixed (M-22).** `osSpTaskStartGo` (port/src/libultra.c) posted the SP/DP task-done events with `OS_MESG_NOBLOCK` into the scheduler's 8-slot `interruptQ`, which the 60 Hz VI pacemaker thread also fills with `VIDEO_MSG`. A gfx task runs the whole frame synchronously on the sched thread (fast3d); the first two frames take 30-80 ms, so the pacemaker queues several retraces meanwhile. Once the queue is full the RSP-done post is **silently dropped** -> `__scMain` never clears `sc->curRSPTask` -> the client never gets `OS_SC_DONE_MSG` -> `bossMainloop`'s `pendingGfx` never clears -> permanent stall (log: `frame 2 rendered`, then `kernel heartbeat ... frames=2`, retrace queue pinned at `valid=8/8 ret=-1`). Reproduced 2 of 3 `-level_09` boots on an idle machine, i.e. most of what M-13..M-21 wrote off as D117 nondeterminism / machine load. `OS_MESG_BLOCK` is NOT the fix (the done event is posted from the sched thread, the queue's only consumer -> self-deadlock): `portPostEventForce()` drops the OLDEST message (always a stale retrace) and retries, and `portPostVIEvent` now reserves 2 slots. Verified: 6/6 `-level_09` boots reach frame 600 with 0 heartbeats (pre-fix 1/3). | FIXED (`port/src/libultra.c`) |
| D135 | **Firefight crash — `bgTestHitOnObj` (`propobj.c:8446`) is an unported N64 GBI parser (M-22, WS6 playtest).** The object bullet-hit test walks a model's display list command stream with hardcoded 8-byte-N64-`Gfx` byte/word indices (`*(s8*)gdl`, `((u32*)gdl)[1]`, `((u8*)gdl)[5..7]`, the G_TRI4 nibble reads, the backward `G_SETTIMG` scan). On PC the model GDL is the 16-byte `Gfx` layout (`d43_emit.py`, `{u64 w0; u64 w1}`), so every access reads the wrong bytes → the walk desyncs on the first `G_VTX`/`G_TRI1` → `v->coord.x` deref off the end of the DL → AV (`0x709ae02a`, a near-miss past a live DRAM ptr = the §B signature). Triggered by `propobjFindHit` every time a shot ray resolves a hit on an object model (guards' dropped weapons, attached objects, shootable props) — invisible to `level_sweep.sh` (no weapon fire). Two sibling faults seen the same session (`texSelect`/`texSetRenderMode` wild `gdl`, `objFreeEmbedmentOrProjectile` `prop->obj`) are likely the same corrupted-object cascade. PD `bgTestHitOnObj` (`pd_port/src/game/bg.c:3635`) is the fully-ported ground truth. | FIXED (`src/game/propobj.c`, `#ifdef PORT` — reads `gdl->words.w0/.w1` shifts). **Second fault, same walker:** after the parser fix the Facility firefight then hit `0xc0000094` (int div-by-zero) at `objHit` `propobj.c:9717` `randomGetNext() % impact_sounds->thing2_len` — the ported texnum-recovery (`*(s16*)(phys(w1) − 8)`) returned a bogus positive `texturenum` (`(u32)` cast truncated a 64-bit `words.w1`; the N64 "texnum 8 bytes before the texture data" layout isn't guaranteed for the port's converted/replaced GDLs) → `g_HitTypeSounds[texnum & 0xf]` picked an entry with `thing2_len == 0`. Fixed: the PORT texnum branch now always returns `-1` (→ `g_HitTypeSounds[0]` = `isnd_default`, `thing2_len == 1`, safe). `texturenum` only flavours the bullet-impact sound/decal, so generic-hit is an acceptable degradation (park with D77). **Follow-up:** the BG room-geometry sibling `sub_GAME_…` (`bg.c:~3373-3646`, walks the D85-widened `ptr_expanded_mapping_info`) has the identical unported parser pattern — latent, fires on shooting walls/floor; not yet ported. |
| D136 | **Stage-unload crash — `objFree` `obj->prop` reads float garbage (M-22, WS6 Facility playthrough).** After D135 cleared the firefight crashes the level is completable; then on teardown `lvlUnloadStageTextData` → `cleanupObjects` (`cleanup_objects.c:42`) → `objFreePermanently` → `objFree` (`propobj.c:994`) → `objFreeEmbedmentOrProjectile(obj->prop)` faults at `:888` (`prop->obj` deref). Real gdb `bt`: `obj` = `0x701bd730` (valid) but `obj->prop` = `0x3e567400405c30c0` — two packed `f32` (~0.21, ~3.44), i.e. a `coord3d` read as a pointer → `ObjectRecord.prop` (N64 `/*0x10*/`, after `inherits PropDefHeaderRecord` + `s16 obj; s16 pad; u32 flags; u32 flags2;`) lands at the wrong PC offset for this object's propDef type, OR the record is undersized and setup's `->prop` store never happened / was clobbered. Same family as D122/D132 (`tools_pc/d88_propdefs.py` per-type record sizing vs the compiled `ObjectRecord`/subtype layout). Type not yet identified — needs a gdb run with a breakpoint on `objFree` to read `obj->type` (`cleanupObjects` frees WEAPON/AMMO/MAGAZINE/COLLECTABLE/MONITOR/RACK/AUTOGUN/HAT/ARMOUR/GAS_RELEASING/VEHICHLE/AIRCRAFT/GLASS/SAFE/TANK/TINTED_GLASS via this path). Mission completes; crash is before the next briefing loads. | ROOT-CAUSED → **D139** (it was NOT a layout bug — `cleanupObjects` read the type from the wrong byte on LE and walked off the propDef blob). |
| D139 | **Stage-unload crash root cause + fix (M-23).** `cleanupObjects` (`cleanup_objects.c`) walks `g_CurrentSetup.propDefs` with `(u8)obj[0]` as the record type — correct only because the header word `[u16 extrascale][u8 state][u8 type]` is **big-endian** on N64, so `type` is the low byte. On little-endian PC the low byte is `extrascale`, so `(u8)obj[0]` is never `PROPDEF_END` → the `while` runs off the end of the blob, and the `switch` dispatches on garbage → `objFreePermanently()` on records that aren't `ObjectRecord`s → `obj->prop`/`obj->model` read adjacent float data (`0x3e567400405c30c0`) → AV in `objFreeEmbedmentOrProjectile` (`propobj.c:888`). `sizepropdef()` and every other propDef consumer already use `pdef->type` (struct member, offset 3) — only this one spot used the raw byte cast. Not a `d88_propdefs.py` layout issue at all. | FIXED (`src/game/cleanup_objects.c`, `#ifdef PORT` — `CLEANUP_PDTYPE(o)` = `((PropDefHeaderRecord*)o)->type`). **UNVERIFIED** — the fast teardown test (pause→abort) hit D140 first; `-level_09` unregressed. porting-notes.md §C. |
| D140 | **Pause-menu crash (M-23 open → M-27 fixed).** `bondviewRenderWatch` (`bondview2.c:8604`) → `bondviewTransformManyPosToViewMatrix(g_CurrentPlayer->field_23C, objheader->numMatrices)` with `field_23C == NULL` → `matrix_4x4_copy(src=0x0)` AV. Root cause (NOT "cache never allocated"): `something_with_watch_object_instance` (N64 player +0x230) is a **`struct Model` (0xBC) + its RW-data pool (0xC8) punned into a 0x184-byte hole** (`field_234..field_3B0`); three of those "fields" are Model members the game reads by name — `watch_scale_destination`==`.scale`, `pause_watch_related_adjust`==`.animframe1`, **`field_23C`==`.render_pos`**. `render_pos` IS populated (by `instcalcmatrices` via `subcalcmatrices` at `:8524`), but on x86-64 `sizeof(struct Model)` grows (4→8B ptrs) so `field_23C` no longer overlaps `.render_pos` → reads Model padding (~0) → NULL. Same class as D100 (gait Model) / D102 (weapon Model) / D56 (this very watch Model's pool). Fix: `#ifdef PORT` makes `something_with_watch_object_instance` a real inline `struct Model` + `u32 watchRwPool[192]` in `struct player` (`bondview.h`); `bondview2.c` redirects the 3 named reads to the member via `GE_WATCH_{ANIMFRAME,SCALE,RENDERPOS}` macros (N64 `#else` = the verbatim field). The D56 branch of `sub_GAME_7F07E7CC` drops its `static u8 watchRwPool[0xC8]` for the inline pool. | FIXED (`src/game/bondview.h`, `src/game/bondview2.c`; `#ifdef PORT`). Exposed the next watch-render crash → **D141**. `-level_09`/`-level_20` unregressed (3/3). porting-notes.md §A. |
| D123 | **crash class C1: `chrIsNotDeadOrShot` NULL deref on 6 levels (Dam/Runway/Frigate/Statue/Streets/Cradle).** — full `## D123` entry at file tail | converter fixed (`OBJ_ID_WORDS`); C1 cleared on all 6, residual crashes are fast3d (C2) |
| D125 | **crash classes C3+C6 (Aztec/Bunker2/Surface2):** — full `## D125` entry at file tail | **resolved (M-16b)** — line 374 → 8 NULs; all 21 sidecars regen'd; **Aztec `-level_28` now PASSES** (was C3 CRASH). Bunker2 falls through to a separate DOOR-tail `linkedDoor` layout bug (C3 residual) |
| D126 | **crash classes C3r/C4/C6 (Bunker2 `-level_27` `door7F054FB4` propobj.c:13523, Depot `-level_30` prop.c:902, Surface2 `-level_43` loadobjectmodel.c:393):** — full `## D126` entry at file tail | resolved — `d88_propdefs.py` PROPDEF_PC_BYTES[30/32/33/35]=24 + typed handler; `loadobjectmodel.c` sizepropdef PORT returns 6. Bunker2/Depot/Surface2 now PASS (13→16/21) |
| D124 | **crash class C2: fast3d bad texture pointer.** — full `## D124` entry at file tail | Jungle fixed (`port/src/gimgfixup.c`); Facility → D130 |
| D131 | **crash class C2m (Jungle `-level_37`):** — full `## D131` entry at file tail | resolved — `seg_addr()` restores the module high word for any fallthrough w1 in `[0x40000000, 0x70000000)` (module fixed-based at `0x140000000`, no ASLR; DRAM/KSEG0/segmented/phys all handled earlier).… |
| D133 | **intro render triage (M-20):** — full `## D133` entry at file tail | RESOLVED — not a regression, parked (D75/D76, `GRAPHICS-BACKLOG.md`) |
| D134 | **Frame-2 boot hang / sweep "NO-FRAMES" flakiness — root-caused and fixed (M-22).** — full `## D134` entry at file tail | FIXED (`port/src/libultra.c`) |
| D135 | **Firefight crash — `bgTestHitOnObj` (`propobj.c:8446`) is an unported N64 GBI parser (M-22, WS6 playtest).** — full `## D135` entry at file tail | FIXED (`src/game/propobj.c`, `#ifdef PORT` —… |
| D136 | **Stage-unload crash — `objFree` `obj->prop` reads float garbage (M-22, WS6 Facility playthrough).** — full `## D136` entry at file tail | ROOT-CAUSED → **D139** (it was NOT a layout bug — `cleanupObjects` read the type from the wrong byte on LE and walked off the propDef blob). |
| D139 | **Stage-unload crash root cause + fix (M-23).** — full `## D139` entry at file tail | FIXED (`src/game/cleanup_objects.c`, `#ifdef PORT` —… |
| D140 | **Pause-menu crash (M-23 open → M-27 fixed).** — full `## D140` entry at file tail | FIXED (`src/game/bondview.h`, `src/game/bondview2.c`; `#ifdef PORT`). Exposed the next watch-render crash → **D141**. `-level_09`/`-level_20` unregressed (3/3). porting-notes.md §A. |
| D118d | **Watch-menu list over-scrolls with keyboard (M-27, user bug report).** `game_options_inventory_navigation` / `sub_GAME_7F0A611C` (`options.c`) have a "slam the stick" fast-scroll: a raw *level* check `joyGetStickY(PLAYER_1) < -0x46` / `>= 0x47` that steps `watch_inventory_cursor_pos` by 1 **every frame** it's held. An N64 stick rarely sustains past ~0x46 on-axis so a human taps it; keyboard W/S (and SDL pads pegged at 0x80) sit there every frame → one keypress skips several items (breaks item selection needed to finish the game). Fix: `#ifdef PORT` macro `GE_WATCH_STICK_FAST{UP,DOWN}` → `0` for the port — stick list-nav then goes only through the latched single-step path (`watch_stick_y_pressed_*`, one step per press, existing game mechanism) and the -0x1e..-0x45 smooth-scroll band; D-pad/C-buttons keep edge-repeat. N64 `#else` = verbatim. | FIXED (`src/game/options.c`, `#ifdef PORT`). Build green, `-level_09` unregressed. **Feel to be confirmed in playtest** (probe couldn't reach the INVENTORY watch page headlessly). D118 input family. Latent sibling: `front.c` menu nav uses `joyGetStick*InRange` level checks the same way. |
| D141 | **Watch-menu item-model crash (M-27), exposed by D140.** Pressing Start → watch page renders → `set_enviro_fog_for_items_in_solo_watch_menu` (`gunfire.c:1620`) → `modelGetNodeRwData(&model, root)` with `root` garbage → AV reading `root->Opcode` (`model.c:478`). `gunfire.c:1720-1743` walks `bodymodel->Switches[]` (a `ModelNode*` array) with **raw byte offsets** `((u8*)Switches) + j + 0x48` / `+ 0x5c` and `j += 4` — assumes 4-byte N64 pointers. `0x48/4=18`, `0x5c/4=23`, `k=j>>2=0..4` → intent is `Switches[18+k]` and `Switches[23+k]`. PC's 8-byte stride makes the raw math read misaligned garbage → bogus non-NULL `ModelNode*`. porting-notes.md §B (raw hardcoded stride into a pointer array, cf. D128). Fix: `#ifdef PORT` uses `bodymodel->Switches[18 + (j>>2)]` / `[23 + (j>>2)]`. | FIXED (`src/game/gunfire.c`, `#ifdef PORT`). Watch page now renders (weapon page verified — mirrored text D114/D116, dark weapon model D75, both parked cosmetic). `-level_09`/`-level_20` unregressed. |
| D150 | **Watch objective/briefing page crash (M-28).** Opening the watch → BRIEF (any subpage) or OBJECTIVES page → AV in `strcat` (`str.c:25`, `Rdi/src = 0`). The pages assemble their display text with `strcpy`/`strcat(buf, langGet(id))` (`options.c:3940-4068`, `front.c:3538-3562`, `propobj.c`). `langGet()` returns **NULL** on PC for a string bank the menu flow never loaded (D129/D143 guards — briefing/objective banks in the in-level watch flow), so `strcat(buf, NULL)` derefs NULL. On N64 those ids always resolve so the decomp never guards. Twist: `str.c`'s `strcpy/strncpy/strcat` are `__nonnull__` **builtins** to GCC, so a plain `if (src == NULL)` on the parameter is optimised away as provably-dead (verified: `-Og` elided it, no `test` in the disassembly). Fix: `#ifdef PORT` NULL guards that launder the pointer through an empty `__asm__` (`GE_IS_NULL()` / `ge_launder_ptr()`) so the check survives — NULL src → treat as empty string (blank text, not a crash), NULL dst → return. Same philosophy as the D143 textRender/textMeasure NULL guards, one layer down. Inert on N64. | FIXED (`src/str.c`, `#ifdef PORT`). Build green (`ntsc-final`); `-level_09` 600+ frames @ 91.67% unregressed. Watch-page repro is interactive — user to re-verify. porting-notes.md §C. |
| D141 | **Watch-menu item-model crash (M-27), exposed by D140.** — full `## D141` entry at file tail | FIXED (`src/game/gunfire.c`, `#ifdef PORT`). Watch page now renders (weapon page verified — mirrored text D114/D116, dark weapon model D75, both parked cosmetic). `-level_09`/`-level_20` unregressed. |
| D150 | **Watch objective/briefing page crash (M-28).** — full `## D150` entry at file tail | FIXED (`src/str.c`, `#ifdef PORT`). Build green (`ntsc-final`); `-level_09` 600+ frames @ 91.67% unregressed. Watch-page repro is interactive — user to re-verify. porting-notes.md §C. |
| D152 | **Mission-failed / objective-failed → permanent black screen (M-28, user bug report — LIVE process inspected, not yet fixed).** User killed Trevelyan in Facility (Ctrl = fire, no crouch bind) → failed the objective → screen faded to black and never came back; process stayed alive under `debug.ps1`. `gdb.txt` (kept running): rendering stops dead at frame 12600 (`frames=12657` frozen across 206 heartbeat dumps / ~7 real min), VI pacemaker still posts. **Deadlock on `s_imLock`** — the D147 recursive-mutex that backs `osSetIntMask`. Both consumer threads are blocked in `pthread_mutex_lock(&s_imLock)` and neither owns it: `mainThread` = `sndSetScalerApplyVolumeAllSfxSlot` → `sndApplyVolumeAllSfxSlot` → `sndSetSfxSlotVolume` → `alEvtqPostEvent` (`event.c:82`) → `osSetIntMask(OS_IM_NONE)` (`libultra.c:1232`); `amMain`/audioThread = `amHandleFrameMessage` → `alAudioFrame` → `sndPlayerVoiceHandler` → `alEvtqNextEvent` (`event.c:47`) → `osSetIntMask(OS_IM_NONE)`. `sndSetScalerApplyVolumeAllSfxSlot` is the **mission-failed audio fade-out** (ramps `g_sndSfxVolumeScale` → 0 each frame, `AL_SNDP_RELEASE_EVT` posted per active `ALSoundState` per frame) — so the fade-to-black *is* the intended fail transition, and it hammers the event queue hard enough that the `osSetIntMask` lock is left owned by a party not in the 3-thread dump (a leaked unbalanced `OS_IM_NONE` — libaudio has early-return mask paths — or a transient thread that acquired it and exited; `gdb.txt` shows `New Thread`/`exited` churn every few k frames). D147's "the decomp never blocks while holding OS_IM_NONE → cannot deadlock" assumption is **falsified** by this path. Same family as D147 / porting-notes.md §D4. | MITIGATED (`port/src/libultra.c`, M-28, `#ifdef PORT`/port-only). `osSetIntMask` is now a **self-healing** logical lock: `OS_IM_NONE`/`OS_IM_ALL` acquire/release a process-wide recursive critical section tracked by `s_imHeld`/`s_imOwner`/`s_imDepth` under a short bookkeeping mutex `s_imMx` + condvar. Normal (balanced, microsecond) audio critical sections behave exactly as the D147 recursive mutex did. A waiter that blocks **> 2 s** (`OS_IM_STUCK_NS`) concludes the holder leaked it, logs `LOG_ERROR` with the stale `s_imOwner` + the stealing caller's return address, and **steals** the section — so 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 naming the leak. Builds clean. Still OPEN: the exact leaking call site (find it from the `D152: ... stealing from owner=` log next time it fires) and the proper narrow fix (dedicated `ALEventQueue` lock, or restore-on-thread-exit). Regression check deferred — `-level_09`/`-level_20` both flaky under the current machine load (D117/D134), and clean `master` itself has a separate nondeterministic `-level_09` frame ~900-1400 `0xc0000005` (see below). |
| D152+ | **D152 addendum — static audit + narrow fix (M-31).** Audited every `osSetIntMask(OS_IM_NONE)` in the *compiled* audio code (`event.c` `alEvtqNextEvent`/`alEvtqPostEvent`/`alEvtqFlush`/`alEvtqFlushType`, `csplayer.c` `__CSPRepostEvent`, `synaddplayer.c` `alSynAddPlayer`, `snd.c` `sndRemoveEvents`/`sndSetupSound`/`sndDeactivateAllSfxByFlag`): **all balanced on every path** — `alEvtqPostEvent`'s lone early `return` (`event.c:86`, freeList empty) restores the mask first. The §F D152 guess "libaudio has unbalanced early-return mask paths" is **not** borne out. Two real defects found + fixed `#ifdef PORT` (N64 verbatim `#else`): **(1)** `sndSetSfxSlotVolume` (`snd.c`) walks the live `ALSoundState` list and posts `AL_SNDP_RELEASE_EVT` to the shared `ALEventQueue` **without holding `OS_IM_NONE`** — unlike its structural twin `sndDeactivateAllSfxByFlag`, which wraps its identical walk. On PC that's an unguarded walk racing `amMain` *and* one lock acquire/release per matching sound; the mission-failed fade (`sndSetScalerApplyVolumeAllSfxSlot`→`sndApplyVolumeAllSfxSlot`→ this, every frame, every active sound) turns it into a per-frame lock-acquire storm on `mainThread` = exactly the §F dump's "hammers the event queue hard enough that the lock is left owned." Fix: hold `OS_IM_NONE` once across the whole walk (nested `alEvtqPostEvent` hits the recursive depth++ fast path, no new contention window); also wrap `sndApplyVolumeAllSfxSlot`'s slot loop so the whole volume update is one recursive hold (1 real acquire/frame instead of `SFX_SLOT_COUNT × active-sounds`). **(2)** The actual leak matches the §F dump's *other* hypothesis — "a transient thread acquired `OS_IM_NONE` and exited" (`gdb.txt` shows `New Thread`/`exited` churn). `port/src/libultra.c` `portThreadWrapper` now calls new `imThreadExitRelease()` after the thread entry returns: if that thread still owns `s_imHeld`, release the orphan immediately (+ `LOG_ERROR` naming it). This removes the wedge *and* the ~2 s steal hitch, and stops the **re-wedge** that occurs when a new host thread reuses the dead thread's pthread id and `imAcquire` mis-detects recursion (`s_imOwner == self`). The M-28 steal-lock stays as the last-resort backstop for a genuinely unbalanced *same-thread* path (none found, but cheap insurance). Build green (240/240); `-level_09` frame 300 renders 91.7% non-clear, crash-free; `-level_20` boots crash-free (frame-count flaky under load per D117/D134, no `D152:` log). Fade-out repro is **playtest-gated** — not reproducible headless, so not end-to-end verified; user should replay the mission-fail and confirm no black screen + no `D152:` steal/orphan log lines. | FIXED (`src/snd.c`, `port/src/libultra.c`, `#ifdef PORT`). porting-notes.md §D4. |
| D154 | **`bg.c` room-geometry ray/hit-test GBI parser port (M-28, WRITTEN / UNVERIFIED / UNCOMMITTED).** `bgTestRayIntersectionInRoom` (`bg.c:3331`, called by `bgTestBulletHitBackground` on every shot that resolves against background geometry) is the D135 sibling flagged since M-23: raw `((u8*)gdl)[k]` / `((u32*)gdl)[i]` byte/word indexing into what is now the 16-byte PC `Gfx` room DL (D85 `bgWidenRoomGdl` + `texLoadFromGdl`) → desync → OOB `vtxbase[idx]` / walk-off AV the first time you shoot a wall or floor. Ported `#ifdef PORT` (N64 verbatim under `#else`): `vtxoff`/`vtxbase`/`op` via the `.dma` view (matches the already-ported `bgBuildRoomVtxBounds`); G_TRI1 indices from `(w1>>16/8/0)&0xff /10`; the 12 G_TRI4 nibble extractions from `(u32)gdl->words.w0/.w1` (each mapped from the N64 byte/halfword layout — table in the code comment, cross-checked twice); both `texturenum` recoveries → `-1` (the KSEG0 `*(u16*)(w1-8)` deref is invalid for the port's converted GDLs, same as D135; texnum only picks the impact decal/sound, parked D77). Builds clean. **Verification blocked** — a no-input `GE_PCDUMP` capture never fires a bullet so it never calls this function; needs a real firefight into a wall on an idle machine (this session's box was thrashed → D153/driver crash on every run). | **M-30 re-audit + fixes (still playtest-gated for final verify).** Cross-checked the ported G_TRI1 + all 12 G_TRI4 nibble recoveries twice against the N64 `#else` and the raw BE-word bit derivation — **G_TRI1 and G_TRI4 are correct**. **Two bugs fixed:** (1) `vtxoff` — the port read `gdl->dma.par & 0xf`, but the PC `Gdma_le` shim (`port/shim/PR/gbi.h`) maps `.par` to bits 0-23 of word0 (the packed *length*, always 16-aligned) not the N64 byte-1 params field at bits 16-23, so vtxoff was silently forced to 0 (breaks any G_VTX batch with `v0 != 0`, i.e. multi-batch rooms → wrong `vtxbase[idx]` → OOB). Now `((u32)gdl->words.w0 >> 16) & 0xf`. (2) **Sibling ported** — `bgTestBulletHitBackground` tail (`bg.c:~3841`, the post-hit G_SETTILE back-scan for `tileformat`/`tilesize`) was itself an unported raw `((u8*)gdl)[0]`/`[1]` + 64-bit `words.w0 << 11 >> 30` parser over the same widened DL; wrapped `#ifdef PORT` (opcode from bits 24-31, byte1 from bits 16-23, `(u32)`-forced 32-bit width for the tilesize shift). **Diagnostic:** `GE_D154=1` → `osSyncPrintf` of the parsed cmd/vtx-index stream (call header + per-TRI1/TRI4 `idx`) for the first 64 invocations, for the user/integrator to eyeball. Build green (`ntsc-final`, 242/242). Runtime unverifiable in a worktree (no `data/`); still needs a firefight-into-wall on an idle machine + `-level_09` framediff. Confidence: HIGH on the mechanical correctness (bit-for-bit vs N64), MEDIUM that nothing else in the walk still bites. porting-notes.md §B (D135 corollary). |
| D156 | **Facility outro-cutscene "hang" — the actual fix (M-29, user bug report, 2nd occurrence after D155).** After D155 shipped the user re-ran the playthrough and it hung again at the end of Facility — same stack (`modelSetAnimFrame2WithChrStuff` ← `modelTickAnim` ← `chrUpdateAnim` ← `chrTick` ← `playerTick` ← `propsTick` ← `lvlRender` ← `bossMainloop`) but this time **`frames=12962` frozen CONSTANT across ~200 heartbeat dumps / minutes** — a true infinite loop, not the D155 slow-catch-up spiral. Root: the `while (1)` at `model.c:3131` in `modelSetAnimFrame2WithChrStuff` steps the root-motion accumulation one anim frame at a time from `framea` (`model->animframe1`) to `frameb` (`frame`, accumulated in `modelTickAnim` as `frame += playspeed * speed` per tick). If `frameb` arrives **NaN or blown up** — a cutscene anim whose blend/transition math produces a huge/non-finite `model->speed`/`playspeed`/`timespeed` (`model.c:3436` `/ model->unkb0`, `:3477` `/ model->timespeed`, both guarded `> 0` but not against tiny values) — then `floorFloatToInt(frameb)` yields garbage, `endframe - curframe` ≈ 2^31, and the loop (each iteration: `sub_GAME_7F06D3F4` joint transform + `cosf`/`sinf` + `modelConstrainOrWrapAnimFrame`) runs effectively forever. **Likely deeper cause (unconfirmed, needs the repro):** these speed/blend fields are `Model` struct members; if this model is punned/inline (D100/D140 family) and `sizeof(Model)` growth misaligned them, they read garbage exactly when a cutscene sets an unusual anim transition. | GUARDED (`src/game/model.c`, `#ifdef PORT`): (1) in `modelSetAnimFrame2WithChrStuff`, if `frameb` is non-finite or `|frameb| >= 1e6` snap it to `framea` (loop then does ~0 iterations; the `modelConstrainOrWrapAnimFrame` at :3211 still clamps `model->frameb` to a valid frame); (2) in `modelTickAnim` after the tick loop, if `frame`/`frame2` went non-finite/absurd fall back to the pre-loop `model->animframe1`/`animframe2` so the NaN can't poison model state every frame. `!(x > -1e6f && x < 1e6f)` catches NaN too. Anim frames are 0..few-hundred so 1e6 is a safe ceiling. Verified: build green; `-level_09` frame 200 unregressed (guards never fire in normal play). **User to re-verify the Facility outro.** If it still hangs, the NaN source is upstream — dump `model->speed/playspeed/timespeed/unkb0` in `modelTickAnim` during the cutscene. porting-notes.md §E. |
| D155 | **Facility outro-cutscene end → "hang" = unclamped `deltaFrames` spiral (M-29, user bug report).** User watched the Facility outro cutscene; at the end the game froze (process alive, kernel-heartbeat dumps). Symbolicated `mainThread` stack from the port's own hang dump: `modelConstrainOrWrapAnimFrame` ← `modelTickAnim` ← `chrUpdateAnim` ← `chrTick` ← `playerTick` ← `propsTick` ← `lvlRender`, sampled there across *every* heartbeat dump. Root cause: `waitForNextFrame()` (`src/game/frametiming.c`) sets `nextFrameTime = (osGetCount() − prev + 387937) / 775875` and passes it **unclamped** to `updateFrameCounters` as `deltaFrames`. `osGetCount()` is wall-clock on the port (D117), not VI-locked, so a real-time stall — the asset load at the cutscene→debrief boundary, made far worse by the user running a local LLM on the same box — makes `nextFrameTime` balloon to hundreds/thousands. That becomes `speedgraphframes` → `g_ClockTimer`, which drives `modelTickAnim(model, g_ClockTimer, 1)`'s `while (numticks-- > 0)` loop once per character per render, plus dozens of `for (i = 0; i < g_ClockTimer; i++)` sim loops (`bondhead.c`, `bondview2.c`, `explosion.c`, `options.c`…). One render then does thousands of anim ticks × N characters → multi-second frame → heartbeat trips, and the slow frame feeds an even bigger delta next time → unrecoverable spiral. The N64 was physically VI-bound and never produced `deltaFrames > ~2`. Timing-compensation class, cf. D117/D134. | FIXED (`src/game/frametiming.c`, `#ifdef PORT` — clamp `nextFrameTime` to `FRAMETIMING_PORT_MAX_CATCHUP` = 6). After a hitch the sim just resumes at real-time-ish pace instead of spiralling, exactly as the console did when it dropped frames under load. N64 path verbatim. Confidence: high (stack + arithmetic both point here). porting-notes.md §E. |
| D153 | **`-level_09` frame ~900–1400 `0xc0000005` = D117/D134 load flakiness, NOT a bug (M-28).** Reproduced 6/6 while the machine was under heavy concurrent build+run load; on a **quiet** machine `-level_09` ran past frame 2400 with 0 heartbeats, no crash. Same false-positive class the docs warn about every session (M-13..M-22, M-25 Silo ~frame 300 freeze). The crash PC was in a system DLL (a `memcpy` off a buffer) and the frame varied run-to-run — both consistent with the port's host-scheduling jitter under load (`osGetCount` is wall-clock, variable-timestep sim), not a deterministic defect. `romdataFixupMusicSeqTable: seqCount 63 exceeds blob capacity 1` is an EXPECTED benign warning on the header-only first `romdataFixupMusicSeqTable` call (music.c:736; the full-size second call at :742 is silent) — not related. **Lesson: don't regression-test while builds/other runs are in flight.** | NOT A BUG — closed. Run anchors on a quiet machine. |
| D151 | **Watch data all blank — objectives / mission background / M / Q / Moneypenny (M-28, user bug report).** Not a crash (D150 fixed that); every line renders empty. The watch text lives in `propDefs` record types **35** (`WatchMenuObjectiveText`) and **23** (`ObjectiveStart`). In the setup stream each record's 3rd word is a **plain 32-bit language slot id** (see `assets/obseg/setup/Usetup*Z.c` `propDefs[]`: `_mkword(0,_mkshort(0,35)), menu, 11281, 0`). The decomp structs `struct watchMenuObjectiveText` / `struct objective_entry` decode that word as `u16 reserved; u16 text;` and read `text` at offset `0xA` — which only works on the N64 because the id sits in the **low 16 bits of a big-endian word**. On LE, `tools_pc/d88_propdefs.py` `_bswap32`'s the whole word (correct for a 32-bit field), moving the id to the low bytes, so `text` (u16 @0xA) reads `0x0000` → `langGet(0)` → NULL → `get_ptr_text_for_watch_breifing_page` / `get_text_for_objective` return NULL/blank. This is the "briefing/objective text renders blank" side-effect left open under D143 / D150. The per-level lang bank itself IS loaded in-level (`langLoadToAddr(langGetLangBankIndexFromStagenum(stageId))`, `prop.c:1274`). | FIXED (`src/bondtypes.h`, `#ifdef PORT` — `text` is a full `u32` covering the whole word in both structs; N64 `u16 reserved; u16 text;` kept under `#else`). No converter change, no sidecar regen. `nextentry`/`difficulty`/`unkD` unchanged. porting-notes.md §C. Build compiles clean; interactive re-verify pending (open the watch in any level). |
| D130 | crash class C2 (Facility `-level_34`, Runway `-level_35`): `import_texture_i8` AV on a wild texture pointer (`0x72181ee8`), baked into the HUD glyph DL by `gDPLoadTextureBlock(gdl, curchar->pixeldata, …)` when the level-title string ("Chemical Warfare Facility #2") renders `#`/`"`. Root cause: `romdataFixupFont` (`port/src/romdata.c`) converts the N64 24B fontchar array to the PC 32B array **in place**, forward-field, backward-glyph. PC−N64 stride = 8, so for glyphs 0/1/2 `d` overlaps `s` by less than the 24B read span and an early field write clobbers a later field's source mid-`for k<5` loop → glyph 1's `width` = `bswap(index)` = `0x01000000`, glyph 2's `pixeldata` = `pcPixOff+(index−pixStart)` ≈ `0x020002e8` → `+= font_base` → wild → fast3d AV. On N64 the array is read-only rodata, never re-laid-out, so no bug there. NOT the model-GDL relocation (`sub_GAME_7F0762E0` / `texLoadFromGdl`) the D124-Facility addendum suspected — `gdl` there is still a segmented `0x05xxxxxx` value so the `& 0x00ffffff` masks are correct, and `texLoadFromGdl` never copies a `G_SETTIMG` for these levels. | resolved — stage all 6 N64 fields in a local `f[6]` before writing any. Facility + Runway now PASS (16→18/21). Probes reverted. |
| D151 | **Watch data all blank — objectives / mission background / M / Q / Moneypenny (M-28, user bug report).** — full `## D151` entry at file tail | FIXED (`src/bondtypes.h`, `#ifdef PORT` —… |
| D130 | **crash class C2 (Facility `-level_34`, Runway `-level_35`):** — full `## D130` entry at file tail | resolved — stage all 6 N64 fields in a local `f[6]` before writing any. Facility + Runway now PASS (16→18/21). Probes reverted. |
| D164 | **Disclaimer / legal screen only draws the first line (D76 root cause, M-31).** `constructor_menu00_legalscreen` (`src/game/front.c:1523`) renders the 12-entry `legalpage_text_array[]` with `legal_text_end = (struct legal_screen_text *)&legalscreen_MRD;` then `do { render; ptr++; } while (ptr < legal_text_end);`. This is a **linker-adjacency assumption**: on N64 `.data` the two file-scope globals are emitted in source order so `&legalscreen_MRD == legalpage_text_array + 12` and the loop count is exactly the array length. mingw/GCC reorders them — in `build-pc/ge007.x86_64.exe` `legalscreen_MRD` links at `0x1401305e0`, **0x60 bytes before** `legalpage_text_array` at `0x140130640` (verified via `nm`). So `legal_text_end < legal_text_ptr` from the start; the `do/while` runs the body **once** (entry 0, "TWYCROSS BOARD OF GAME CLASSIFICATION" — long + CENTER_ALIGN so it wraps to ~2 visual lines = the "classification line + one below" the user saw), then exits. The other 11 legal lines never render. NOT a D68 image-table bug — the legal screen references **zero** `sImageTableEntry`s; it is pure `langGet()` font text via `display_aligned_white_text_to_screen`→`textRender`, plus the 3D `logoinst` model (`subdraw`, absent — D75 family, parked). The D68 fixup and the `GE_IMGT` avenue in the brief are both dead ends here. | **FIX PROPOSED, NOT APPLIED** (`front.c` is owned by another agent this session). One-line `#ifdef PORT` in `constructor_menu00_legalscreen`: `legal_text_end = legalpage_text_array + ARRAY_COUNT(legalpage_text_array);` (N64 `&legalscreen_MRD` cast under `#else`). Same linker-layout class as the D75 loop-bound / D142 enum family. Confidence: **high** — mechanism confirmed by symbol addresses. |
| D159 | **Front-end wallet-Bond photo (and other large front-end I4/I8/IA8 textures) render "interlaced" / combed (M-31, user bug RC1 / D149).** The user's menu showed a garbled venetian-blind Bond photo on the folder / mode-select screen; earlier the same model DL walked into garbage (`D146` opcode spam) — that crash is already contained by the D144/D146 guards, so what's left is purely the texture. Root-caused with `GE_DTEX`: the photo is a **65×65 I4** (also seen: 65×65 I8, 95×32 IA8) with a full mip chain; `import_texture_i4` uploads it `line_size_bytes*2` (= padded stride) wide and `size_bytes/line` tall — geometry fine. The comb is **`texSwapAltRowBytes` (`src/game/image.c:2191`)**: it pre-swaps every **odd** texture row in 8-byte (one-`u32`) groups to compensate for the N64 RDP's odd-line TMEM address XOR (bit 2) during 4-byte-word `gDPLoadBlock` loads. fast3d does **not** emulate that XOR (no odd-row handling anywhere in `gfx_pc.cpp`), so the pre-swapped odd rows stay scrambled in the uploaded GL image. It's called on *every* level of *every* texture on *every* path (§4), but only bites where the texture is large enough and viewed 1:1 to see the 8-texel comb — glaring on the ~big front-end photos, ~invisible on small HUD glyphs / distant in-level surfaces (which is why it was written off as "front-end model" work for many sessions). Same class as the K0-fold / interrupt-mask hardware-quirk shims — belongs in `port/`, not game logic. | **FIXED** (`src/game/image.c`, `#ifdef PORT` early-`return` in `texSwapAltRowBytes` — fast3d wants a plain linear image; N64 body kept verbatim under it). Verified: wallet-Bond photo renders as a clean recognisable grayscale portrait (still 180°-rotated — D75 model-transform family, parked); `-level_09` frame 200 coverage 91.62 vs 91.66 golden (D117 noise) and a visual frame is clean (tiled floor / monitors / world-map screen all correct). Confidence: **high** (GE_DTEX + before/after screenshots). The residual over-tall-mip (RC2) and non-PoT wrap (RC3) issues are separate and still parked; this only removes the row-swap. porting-notes.md §C. |
@@ -10192,3 +10192,107 @@ do {
**Not root-caused.** No distinguishing log line immediately precedes the stall (just routine `D51 vi post` lines) — nothing points at a specific subsystem yet. Caverns is one of the larger/more complex levels (cross-ref the D250 "FPS inconsistent" lineage, though D250's own symptom was sustained sub-60fps, not a multi-second total stall, so likely a different mechanism — a one-time heavy room/asset stream-in stall is the leading guess, unconfirmed). A single occurrence with no repro trigger identified; **do not spend session time trying to force a repro** without a better lead. If it recurs, the next capture should note (a) exact in-level location/trigger if any pattern emerges, (b) whether `Rooms`/`roomsDrawn` or a texture/model load was in flight via existing debug logging, and (c) whether it's Caverns-specific or level-general (test other large/open levels — Streets, Egyptian — the D249/D250 neighbourhood).
**Also fixed in passing (cosmetic, no behavior change):** the heartbeat log's unit label — `now - g_lastFrameUs` is computed in microseconds but was printed as `%llu ms` without converting; now divided by 1000 so the log actually reads correct milliseconds. | OPEN — one user-confirmed real freeze on Caverns (~8s, self-recovered, no crash), not root-caused, no repro trigger identified; distinct from D255/D285. Log-unit cosmetic fix landed alongside.
---
## Entries migrated from §F table (2026-09-15)
> Migrated verbatim from the §F index table by `migrate_bucket_a.py` (ctx-opt P1, bucket A): topic/status cell
> content moved unchanged; the table row now carries a one-line
> summary + pointer. Nothing deleted.
## D137 — right-mouse (aim-sight) crash:
right-mouse (aim-sight) crash: `gunDrawSight` (`gunfire.c:6235`) `s32 sp54` holds a `Gfx*` the whole function (`sp54 = *gdl`; `texSelect(&sp54,…)`/`display_image_at_position(&sp54,…)` take `Gfx**`). As N64 `s32` it is 4 B, so `*(Gfx**)&sp54` reads 4 B of the adjacent stack float as the pointer high word → `_g = 0x03e4fca0_70081220` → wild DL write in `texSetRenderMode` (`othermodemicrocode.c:177`) the moment the crosshair raises. §A.
**Status (from §F table):** FIXED (`src/game/gunfire.c` `#ifdef PORT` — `sp54` is `Gfx *`)
## D123 — crash class C1: `chrIsNotDeadOrShot` NULL deref on 6 levels (Dam/Runway/Frigate/Statue/Streets/Cradle).
crash class C1: `chrIsNotDeadOrShot` NULL deref on 6 levels (Dam/Runway/Frigate/Statue/Streets/Cradle). D122's `OBJ_TAIL_DESC` zeroed the widened `VehichleRecord/AircraftRecord.ailist` slot (w32), but `prop.c:1764/1786` reads a pre-populated int AI-list id there → `ailistFindById(0)` → `GAILIST_AIM_AT_BOND` → `ai()` runs a CHR aim list with `ChrEntityp==NULL`
**Status (from §F table):** converter fixed (`OBJ_ID_WORDS`); C1 cleared on all 6, residual crashes are fast3d (C2)
## D125 — crash classes C3+C6 (Aztec/Bunker2/Surface2):
crash classes C3+C6 (Aztec/Bunker2/Surface2): **root cause found (M-16)** — `tools_pc/d88_emit.py:374` assigns a 4-byte literal to an 8-byte slice (`out[dst_o+0x30:dst_o+0x38] = b"\x00…"`, the stan zero-fill in `emit_pad`); every pad/boundpad record silently shrinks the output bytearray by 4 B, and once the tail falls below the boundpad plink string blob, later verbatim leaf writes hit Python out-of-range slice semantics (insert at current end, not relocated offset) → boundpad names drift/truncate (Aztec pad33 `p138d2`→`8d2`) → `stanPackId()` reject → `getposstan()` NULL stan → `setupDoor` leaves door `model=NULL` → crash `propobj.c:13601`. M-14's "propDefs zeros in RAM" was a misread; sidecar propdefs were always correct (M-15) and post-load RAM matches the sidecar byte-for-byte (M-16)
**Status (from §F table):** **resolved (M-16b)** — line 374 → 8 NULs; all 21 sidecars regen'd; **Aztec `-level_28` now PASSES** (was C3 CRASH). Bunker2 falls through to a separate DOOR-tail `linkedDoor` layout bug (C3 residual)
## D126 — crash classes C3r/C4/C6 (Bunker2 `-level_27` `door7F054FB4` propobj.c:13523, Depot `-level_30` prop.c:902, Surface2 `-level_43` loadobjectmodel.c:393):
crash classes C3r/C4/C6 (Bunker2 `-level_27` `door7F054FB4` propobj.c:13523, Depot `-level_30` prop.c:902, Surface2 `-level_43` loadobjectmodel.c:393): the 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 the setup walk (`set_parent_cur_obj_*` / `setup_briefing_text_entry_parent`) writes unconditionally. On PC that pointer widens 4→8B and lands 8-aligned at offset 16 → struct is 24B/6w (N64 16/20). `d88_propdefs.py` emitted them at N64 size via the generic arm → the runtime 8-byte `->next` write clobbered the *next* record's header → propdef walk desynced, command indices drifted ~100, `linkedDoorOffset + arg2` resolved to the wrong record → door `linkedDoor` chain walked into garbage.
**Status (from §F table):** resolved — `d88_propdefs.py` PROPDEF_PC_BYTES[30/32/33/35]=24 + typed handler; `loadobjectmodel.c` sizepropdef PORT returns 6. Bunker2/Depot/Surface2 now PASS (13→16/21)
## D124 — crash class C2: fast3d bad texture pointer.
crash class C2: fast3d bad texture pointer. **Jungle** (`0xabcd0824`): `gimgSyncCompiledGlobalDLs()` slot-detect keyed on the post-fixup marker, which `texLoad()` had already overwritten → compiled `globalDL_0xNNN` explosion DLs kept link-time `IMAGESEG` words → latent on every level, tripped by the first explosion-DL draw. **Facility** (`0x72181ee8`): see **D130** — the model-GDL-relocation hypothesis (M-14 addendum) was WRONG; real cause was `romdataFixupFont`
**Status (from §F table):** Jungle fixed (`port/src/gimgfixup.c`); Facility → D130
## D131 — crash class C2m (Jungle `-level_37`):
crash class C2m (Jungle `-level_37`): fast3d `gfx_sp_matrix` AV on a wild matrix pointer `0x401c68e0` (`gfx_pc.cpp:1046`) ~frame 300, when the first explosion/smoke prop renders. `explosionRenderPropSmoke` builds `gSPMatrix(gdl++, osVirtualToPhysical((void*)&dword_CODE_bss_8007A100), …MODELVIEW)` (+ `applyRoomMatrixToDisplayList`). `osVirtualToPhysical()` is a `u32`-returning shim (`libultra.c:1207`) → truncates the compiled `.bss` symbol's `0x1_00000000` module high word → w1 = `0x40xxxxxx` → `seg_addr()` returns it raw. Same class as D94 (chraction 32-bit ptr truncation), but in a GBI DL word. The projection matrix in the same DL is fine (`get_BONDdata_field_10E0()` is a runtime `0x70xxxxxx` ptr, fits u32); the `gSPDisplayList(&globalDL_0xNNN)` refs are fine (port `Gwords.w1` is 64-bit `uintptr_t`, no macro truncation). ~30 `osVirtualToPhysical(<compiled matrix/vtx symbol>)` sites exist (`explosion.c`, `glass*.c`, `blood_animation.c`, `bondview2.c`) — all latent until that effect first draws.
**Status (from §F table):** resolved — `seg_addr()` restores the module high word for any fallthrough w1 in `[0x40000000, 0x70000000)` (module fixed-based at `0x140000000`, no ASLR; DRAM/KSEG0/segmented/phys all handled earlier). Jungle renders to frame 2400+ clean; `-level_20`/`-level_24` unregressed. **18→19/21.** (`-level_09` has a separate pre-existing boot crash, see below.)
## D133 — intro render triage (M-20):
intro render triage (M-20): the "intro renders mostly black" item in the M-18/M-19 handoff is **NOT a regression** — it is the D75/D76 parked-cosmetic steady state. M-17 (`9ec6121e`), whose handoff claimed "the entire intro renders — logos → gun barrel → cast", was built in a scratch worktree and captured with the same `GE_PCDUMP="20-900:20"` window: coverage is **pixel-identical** to HEAD `0b5f5d1a` (legal screen 6677 non-clear px / 2.17% on both; centred-logo frames ~7% on both; near-black between). 2D/text/texture layers draw; animated character-model layers (Nintendo-logo transform, gun-barrel Bond, cast models) never appear = D75 exactly. No cheap regression to fix; parked. Secondary: Facility `-level_34` + Jungle `-level_37` re-verify FAILED this pass (boot crash / frame-2 hang) — flagged in LEVEL-STATUS for a clean-machine re-check, not investigated (scope).
**Status (from §F table):** RESOLVED — not a regression, parked (D75/D76, `GRAPHICS-BACKLOG.md`)
## D134 — Frame-2 boot hang / sweep "NO-FRAMES" flakiness — root-caused and fixed (M-22).
**Frame-2 boot hang / sweep "NO-FRAMES" flakiness — root-caused and fixed (M-22).** `osSpTaskStartGo` (port/src/libultra.c) posted the SP/DP task-done events with `OS_MESG_NOBLOCK` into the scheduler's 8-slot `interruptQ`, which the 60 Hz VI pacemaker thread also fills with `VIDEO_MSG`. A gfx task runs the whole frame synchronously on the sched thread (fast3d); the first two frames take 30-80 ms, so the pacemaker queues several retraces meanwhile. Once the queue is full the RSP-done post is **silently dropped** -> `__scMain` never clears `sc->curRSPTask` -> the client never gets `OS_SC_DONE_MSG` -> `bossMainloop`'s `pendingGfx` never clears -> permanent stall (log: `frame 2 rendered`, then `kernel heartbeat ... frames=2`, retrace queue pinned at `valid=8/8 ret=-1`). Reproduced 2 of 3 `-level_09` boots on an idle machine, i.e. most of what M-13..M-21 wrote off as D117 nondeterminism / machine load. `OS_MESG_BLOCK` is NOT the fix (the done event is posted from the sched thread, the queue's only consumer -> self-deadlock): `portPostEventForce()` drops the OLDEST message (always a stale retrace) and retries, and `portPostVIEvent` now reserves 2 slots. Verified: 6/6 `-level_09` boots reach frame 600 with 0 heartbeats (pre-fix 1/3).
**Status (from §F table):** FIXED (`port/src/libultra.c`)
## D135 — Firefight crash — `bgTestHitOnObj` (`propobj.c:8446`) is an unported N64 GBI parser (M-22, WS6 playtest).
**Firefight crash — `bgTestHitOnObj` (`propobj.c:8446`) is an unported N64 GBI parser (M-22, WS6 playtest).** The object bullet-hit test walks a model's display list command stream with hardcoded 8-byte-N64-`Gfx` byte/word indices (`*(s8*)gdl`, `((u32*)gdl)[1]`, `((u8*)gdl)[5..7]`, the G_TRI4 nibble reads, the backward `G_SETTIMG` scan). On PC the model GDL is the 16-byte `Gfx` layout (`d43_emit.py`, `{u64 w0; u64 w1}`), so every access reads the wrong bytes → the walk desyncs on the first `G_VTX`/`G_TRI1` → `v->coord.x` deref off the end of the DL → AV (`0x709ae02a`, a near-miss past a live DRAM ptr = the §B signature). Triggered by `propobjFindHit` every time a shot ray resolves a hit on an object model (guards' dropped weapons, attached objects, shootable props) — invisible to `level_sweep.sh` (no weapon fire). Two sibling faults seen the same session (`texSelect`/`texSetRenderMode` wild `gdl`, `objFreeEmbedmentOrProjectile` `prop->obj`) are likely the same corrupted-object cascade. PD `bgTestHitOnObj` (`pd_port/src/game/bg.c:3635`) is the fully-ported ground truth.
**Status (from §F table):** FIXED (`src/game/propobj.c`, `#ifdef PORT` — reads `gdl->words.w0/.w1` shifts). **Second fault, same walker:** after the parser fix the Facility firefight then hit `0xc0000094` (int div-by-zero) at `objHit` `propobj.c:9717` `randomGetNext() % impact_sounds->thing2_len` — the ported texnum-recovery (`*(s16*)(phys(w1) − 8)`) returned a bogus positive `texturenum` (`(u32)` cast truncated a 64-bit `words.w1`; the N64 "texnum 8 bytes before the texture data" layout isn't guaranteed for the port's converted/replaced GDLs) → `g_HitTypeSounds[texnum & 0xf]` picked an entry with `thing2_len == 0`. Fixed: the PORT texnum branch now always returns `-1` (→ `g_HitTypeSounds[0]` = `isnd_default`, `thing2_len == 1`, safe). `texturenum` only flavours the bullet-impact sound/decal, so generic-hit is an acceptable degradation (park with D77). **Follow-up:** the BG room-geometry sibling `sub_GAME_…` (`bg.c:~3373-3646`, walks the D85-widened `ptr_expanded_mapping_info`) has the identical unported parser pattern — latent, fires on shooting walls/floor; not yet ported.
## D136 — Stage-unload crash — `objFree` `obj->prop` reads float garbage (M-22, WS6 Facility playthrough).
**Stage-unload crash — `objFree` `obj->prop` reads float garbage (M-22, WS6 Facility playthrough).** After D135 cleared the firefight crashes the level is completable; then on teardown `lvlUnloadStageTextData` → `cleanupObjects` (`cleanup_objects.c:42`) → `objFreePermanently` → `objFree` (`propobj.c:994`) → `objFreeEmbedmentOrProjectile(obj->prop)` faults at `:888` (`prop->obj` deref). Real gdb `bt`: `obj` = `0x701bd730` (valid) but `obj->prop` = `0x3e567400405c30c0` — two packed `f32` (~0.21, ~3.44), i.e. a `coord3d` read as a pointer → `ObjectRecord.prop` (N64 `/*0x10*/`, after `inherits PropDefHeaderRecord` + `s16 obj; s16 pad; u32 flags; u32 flags2;`) lands at the wrong PC offset for this object's propDef type, OR the record is undersized and setup's `->prop` store never happened / was clobbered. Same family as D122/D132 (`tools_pc/d88_propdefs.py` per-type record sizing vs the compiled `ObjectRecord`/subtype layout). Type not yet identified — needs a gdb run with a breakpoint on `objFree` to read `obj->type` (`cleanupObjects` frees WEAPON/AMMO/MAGAZINE/COLLECTABLE/MONITOR/RACK/AUTOGUN/HAT/ARMOUR/GAS_RELEASING/VEHICHLE/AIRCRAFT/GLASS/SAFE/TANK/TINTED_GLASS via this path). Mission completes; crash is before the next briefing loads.
**Status (from §F table):** ROOT-CAUSED → **D139** (it was NOT a layout bug — `cleanupObjects` read the type from the wrong byte on LE and walked off the propDef blob).
## D139 — Stage-unload crash root cause + fix (M-23).
**Stage-unload crash root cause + fix (M-23).** `cleanupObjects` (`cleanup_objects.c`) walks `g_CurrentSetup.propDefs` with `(u8)obj[0]` as the record type — correct only because the header word `[u16 extrascale][u8 state][u8 type]` is **big-endian** on N64, so `type` is the low byte. On little-endian PC the low byte is `extrascale`, so `(u8)obj[0]` is never `PROPDEF_END` → the `while` runs off the end of the blob, and the `switch` dispatches on garbage → `objFreePermanently()` on records that aren't `ObjectRecord`s → `obj->prop`/`obj->model` read adjacent float data (`0x3e567400405c30c0`) → AV in `objFreeEmbedmentOrProjectile` (`propobj.c:888`). `sizepropdef()` and every other propDef consumer already use `pdef->type` (struct member, offset 3) — only this one spot used the raw byte cast. Not a `d88_propdefs.py` layout issue at all.
**Status (from §F table):** FIXED (`src/game/cleanup_objects.c`, `#ifdef PORT` — `CLEANUP_PDTYPE(o)` = `((PropDefHeaderRecord*)o)->type`). **UNVERIFIED** — the fast teardown test (pause→abort) hit D140 first; `-level_09` unregressed. porting-notes.md §C.
## D140 — Pause-menu crash (M-23 open → M-27 fixed).
**Pause-menu crash (M-23 open → M-27 fixed).** `bondviewRenderWatch` (`bondview2.c:8604`) → `bondviewTransformManyPosToViewMatrix(g_CurrentPlayer->field_23C, objheader->numMatrices)` with `field_23C == NULL` → `matrix_4x4_copy(src=0x0)` AV. Root cause (NOT "cache never allocated"): `something_with_watch_object_instance` (N64 player +0x230) is a **`struct Model` (0xBC) + its RW-data pool (0xC8) punned into a 0x184-byte hole** (`field_234..field_3B0`); three of those "fields" are Model members the game reads by name — `watch_scale_destination`==`.scale`, `pause_watch_related_adjust`==`.animframe1`, **`field_23C`==`.render_pos`**. `render_pos` IS populated (by `instcalcmatrices` via `subcalcmatrices` at `:8524`), but on x86-64 `sizeof(struct Model)` grows (4→8B ptrs) so `field_23C` no longer overlaps `.render_pos` → reads Model padding (~0) → NULL. Same class as D100 (gait Model) / D102 (weapon Model) / D56 (this very watch Model's pool). Fix: `#ifdef PORT` makes `something_with_watch_object_instance` a real inline `struct Model` + `u32 watchRwPool[192]` in `struct player` (`bondview.h`); `bondview2.c` redirects the 3 named reads to the member via `GE_WATCH_{ANIMFRAME,SCALE,RENDERPOS}` macros (N64 `#else` = the verbatim field). The D56 branch of `sub_GAME_7F07E7CC` drops its `static u8 watchRwPool[0xC8]` for the inline pool.
**Status (from §F table):** FIXED (`src/game/bondview.h`, `src/game/bondview2.c`; `#ifdef PORT`). Exposed the next watch-render crash → **D141**. `-level_09`/`-level_20` unregressed (3/3). porting-notes.md §A.
## D141 — Watch-menu item-model crash (M-27), exposed by D140.
**Watch-menu item-model crash (M-27), exposed by D140.** Pressing Start → watch page renders → `set_enviro_fog_for_items_in_solo_watch_menu` (`gunfire.c:1620`) → `modelGetNodeRwData(&model, root)` with `root` garbage → AV reading `root->Opcode` (`model.c:478`). `gunfire.c:1720-1743` walks `bodymodel->Switches[]` (a `ModelNode*` array) with **raw byte offsets** `((u8*)Switches) + j + 0x48` / `+ 0x5c` and `j += 4` — assumes 4-byte N64 pointers. `0x48/4=18`, `0x5c/4=23`, `k=j>>2=0..4` → intent is `Switches[18+k]` and `Switches[23+k]`. PC's 8-byte stride makes the raw math read misaligned garbage → bogus non-NULL `ModelNode*`. porting-notes.md §B (raw hardcoded stride into a pointer array, cf. D128). Fix: `#ifdef PORT` uses `bodymodel->Switches[18 + (j>>2)]` / `[23 + (j>>2)]`.
**Status (from §F table):** FIXED (`src/game/gunfire.c`, `#ifdef PORT`). Watch page now renders (weapon page verified — mirrored text D114/D116, dark weapon model D75, both parked cosmetic). `-level_09`/`-level_20` unregressed.
## D150 — Watch objective/briefing page crash (M-28).
**Watch objective/briefing page crash (M-28).** Opening the watch → BRIEF (any subpage) or OBJECTIVES page → AV in `strcat` (`str.c:25`, `Rdi/src = 0`). The pages assemble their display text with `strcpy`/`strcat(buf, langGet(id))` (`options.c:3940-4068`, `front.c:3538-3562`, `propobj.c`). `langGet()` returns **NULL** on PC for a string bank the menu flow never loaded (D129/D143 guards — briefing/objective banks in the in-level watch flow), so `strcat(buf, NULL)` derefs NULL. On N64 those ids always resolve so the decomp never guards. Twist: `str.c`'s `strcpy/strncpy/strcat` are `__nonnull__` **builtins** to GCC, so a plain `if (src == NULL)` on the parameter is optimised away as provably-dead (verified: `-Og` elided it, no `test` in the disassembly). Fix: `#ifdef PORT` NULL guards that launder the pointer through an empty `__asm__` (`GE_IS_NULL()` / `ge_launder_ptr()`) so the check survives — NULL src → treat as empty string (blank text, not a crash), NULL dst → return. Same philosophy as the D143 textRender/textMeasure NULL guards, one layer down. Inert on N64.
**Status (from §F table):** FIXED (`src/str.c`, `#ifdef PORT`). Build green (`ntsc-final`); `-level_09` 600+ frames @ 91.67% unregressed. Watch-page repro is interactive — user to re-verify. porting-notes.md §C.
## D151 — Watch data all blank — objectives / mission background / M / Q / Moneypenny (M-28, user bug report).
**Watch data all blank — objectives / mission background / M / Q / Moneypenny (M-28, user bug report).** Not a crash (D150 fixed that); every line renders empty. The watch text lives in `propDefs` record types **35** (`WatchMenuObjectiveText`) and **23** (`ObjectiveStart`). In the setup stream each record's 3rd word is a **plain 32-bit language slot id** (see `assets/obseg/setup/Usetup*Z.c` `propDefs[]`: `_mkword(0,_mkshort(0,35)), menu, 11281, 0`). The decomp structs `struct watchMenuObjectiveText` / `struct objective_entry` decode that word as `u16 reserved; u16 text;` and read `text` at offset `0xA` — which only works on the N64 because the id sits in the **low 16 bits of a big-endian word**. On LE, `tools_pc/d88_propdefs.py` `_bswap32`'s the whole word (correct for a 32-bit field), moving the id to the low bytes, so `text` (u16 @0xA) reads `0x0000` → `langGet(0)` → NULL → `get_ptr_text_for_watch_breifing_page` / `get_text_for_objective` return NULL/blank. This is the "briefing/objective text renders blank" side-effect left open under D143 / D150. The per-level lang bank itself IS loaded in-level (`langLoadToAddr(langGetLangBankIndexFromStagenum(stageId))`, `prop.c:1274`).
**Status (from §F table):** FIXED (`src/bondtypes.h`, `#ifdef PORT` — `text` is a full `u32` covering the whole word in both structs; N64 `u16 reserved; u16 text;` kept under `#else`). No converter change, no sidecar regen. `nextentry`/`difficulty`/`unkD` unchanged. porting-notes.md §C. Build compiles clean; interactive re-verify pending (open the watch in any level).
## D130 — crash class C2 (Facility `-level_34`, Runway `-level_35`):
crash class C2 (Facility `-level_34`, Runway `-level_35`): `import_texture_i8` AV on a wild texture pointer (`0x72181ee8`), baked into the HUD glyph DL by `gDPLoadTextureBlock(gdl, curchar->pixeldata, …)` when the level-title string ("Chemical Warfare Facility #2") renders `#`/`"`. Root cause: `romdataFixupFont` (`port/src/romdata.c`) converts the N64 24B fontchar array to the PC 32B array **in place**, forward-field, backward-glyph. PC−N64 stride = 8, so for glyphs 0/1/2 `d` overlaps `s` by less than the 24B read span and an early field write clobbers a later field's source mid-`for k<5` loop → glyph 1's `width` = `bswap(index)` = `0x01000000`, glyph 2's `pixeldata` = `pcPixOff+(index−pixStart)` ≈ `0x020002e8` → `+= font_base` → wild → fast3d AV. On N64 the array is read-only rodata, never re-laid-out, so no bug there. NOT the model-GDL relocation (`sub_GAME_7F0762E0` / `texLoadFromGdl`) the D124-Facility addendum suspected — `gdl` there is still a segmented `0x05xxxxxx` value so the `& 0x00ffffff` masks are correct, and `texLoadFromGdl` never copies a `G_SETTIMG` for these levels.
**Status (from §F table):** resolved — stage all 6 N64 fields in a local `f[6]` before writing any. Facility + Runway now PASS (16→18/21). Probes reverted.