M-23: WS6 Facility playtest — D135/D137/D138/D139 (firefight, aim, freeze, unload)

First real human WS6 playthrough. Each crash is a subsystem the 21-level
boot sweep never exercises.

- D135 (propobj.c) bgTestHitOnObj: unported N64 GBI parser — raw 8-byte-Gfx
  byte/word indices desync on the PC 16-byte Gfx stream, walk off the DL.
  Ported #ifdef PORT to gdl->words.w0/.w1 shifts (mirrors pd_port bg.c:3635).
  Sub-fix: objHit:9717 div-by-0 — texnum recovery not portable, force -1.
- D137 (gunfire.c) gunDrawSight: s32 sp54 holds a Gfx*; 4-byte slot splices
  adjacent stack into the pointer -> wild DL write on right-mouse aim.
  #ifdef PORT: Gfx *sp54.
- D138 (snd.c) sndCreatePostEvent: parked audio never drains evtq->allocList,
  per-objTick positional-sound posts stall the main thread ("1 room from the
  end" freeze). #ifdef PORT no-op until audio lands (D77).
- D139 (cleanup_objects.c) cleanupObjects: read the propDef type via
  (u8)obj[0] — correct only big-endian (type = low byte of the header word).
  On LE that's extrascale -> walk never hits PROPDEF_END -> runs off the blob
  -> frees garbage -> stage-unload crash. #ifdef PORT uses pdef->type.
  UNVERIFIED (fast teardown test hit D140 first).

Open: D140 pause-menu watch-model NULL matrix; D118c aim-mode mouse-down
-> crouch. New harness tools_pc/repro_gdb.sh + build-pc/d136_cmds.txt.

-level_09 unregressed (91.65%). Docs: PCPortResearch §F D135-D140,
PORT-LEARNINGS §B/§C, HANDOFF M-23 + M-24 next-task.
This commit is contained in:
James Dansereau committed 2026-08-30 11:28:50 -04:00
1 parent 78c0f72157
commit 73820a0c24
9 files changed
+383 -18

No files matched your search

+111 -14
View File
@@ -36,8 +36,12 @@ Full workstream breakdown (WS1–WS6) in `docs/PLAN-linear-level-sweep.md`.
- **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. **Open polish bugs (deferred):** D118a mouse
yaw slower than pitch; D118b mouse-Y inverted; rebinding / gamepad
hotplug / `ge007.ini` generation still TODO.
yaw (analog) vs pitch (digital) mismatch; D118b mouse-Y inverted;
**D118c — in manual-aim mode, mouse-down → crouch** (mouse Y emits
C-down, which game logic maps to crouch/zoom in `insightaimmode`;
clean fix = the deferred analog-aim `#ifdef PORT` hook, §F D118);
rebinding / gamepad hotplug / `ge007.ini` generation 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).
@@ -150,7 +154,108 @@ Full workstream breakdown (WS1–WS6) in `docs/PLAN-linear-level-sweep.md`.
later pass. Bug class D24-implications + `osYieldThread` watch item added
to PORT-LEARNINGS §E.
## Next task
## 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. **All uncommitted** (folds into the
docs-restructure commit set with D121/D122/D126/D128/D130/D131/D134).
- **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.
## Next task (M-24 — Opus 5)
**Immediate:** confirm **D139** — launch `-level_34`, play a few seconds,
then die or pause→Abort Mission (avoid the watch/pause-menu render until
D140 is fixed — dying is cleaner). Clean drop to the debrief/next screen =
D139 confirmed. Then re-run Dam (`-level_33`) end-to-end — its M-22 crash
was the same `objFreeEmbedmentOrProjectile` site, should be D139.
**Then, in rough priority:**
1. **D140** — pause-menu / watch-model NULL (`field_23C`). Blocks WS6
objective-status checks. Start: is `field_23C` never-allocated or a
wrong `struct player` offset? gdb watchpoint / check `AUDIT-M6-player-
offsets.md`. Likely D75 family (watch is a 3D model).
2. **Re-run `tools_pc/level_sweep.sh`** — D135/D138 are cross-level (every
level with shootable props / ambient machinery). Sweep is boot+capture
only so it won't show combat regressions, but confirm no *boot* breakage
and refresh `docs/LEVEL-STATUS.md`.
3. **Resume WS6** `docs/LEVEL-PLAYTEST.md` — Facility first (finish the
objective checklist now that it's completable), then down the mission
list. Each new crash is one subsystem deeper; expect more `#ifdef PORT`
ABI fixes (D122/D126/D132/D135/D137/D139 pattern).
4. bg.c hit-test sibling port (follow-up above) — do it before a level
where wall-shooting matters (most).
**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,
@@ -160,21 +265,11 @@ Full workstream breakdown (WS1–WS6) in `docs/PLAN-linear-level-sweep.md`.
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.
**Consequence: re-run the full 21-level sweep** — several NO-FRAMES /
"hang" rows in `docs/LEVEL-STATUS.md` (incl. the M-20 Facility/Jungle
re-verify failures) are probably this and may now pass. `level_sweep.sh`
is also hardened this session (45 s watchdog, lockdir serialization,
stray-process kill, one retry on NO-FRAMES).
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). Verified `-level_09` framediff 3/3, `-level_20` == pre-D132
baseline (no regression), Archives/Streets/Dam load+render+no-crash.
**Now hand the user `docs/LEVEL-PLAYTEST.md` for WS6** (linked props —
switch-doors, dual weapons, locked doors, safes — need real input to
validate). Also cheap: harden `level_sweep.sh` (watchdog 24→45 s,
serialize) — sweep flakiness was severe this pass.
PORT).
1. **`-level_09` (BUNKER1) boot crash — DISPROVEN as a regression** (M-20,
coordinator: 3 runs / 1800 frames crash-free on `1fc3cff6`; re-confirmed
@@ -205,6 +300,8 @@ 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
+27 -1
View File
@@ -814,7 +814,8 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
| D108–D112 | skeletal models: `d43_emit.py put_f32` byte-reversal bug | resolved |
| D113–D116 | portal BFS ok · matrix chain ok · player raw-offset audit + `gunfire.c` THROW* · HUD text X-mirror | D116 OPEN (parked — DO NOT re-static-trace) |
| D117 | frame nondeterminism root-caused; `GE_DETERM` deferred; `framediff.py` structural | resolved (mode deferred) |
| D118 | SDL input layer (`port/src/input.c`); D118a/b mouse polish open | resolved (polish open) |
| D118 | SDL input layer (`port/src/input.c`); D118a/b/c mouse polish open (D118c: aim-mode mouse-down → crouch, digital C-button overload — needs the deferred analog-aim hook) | resolved (polish open) |
| 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 *`) |
| D119 · D120 | guard-attack `weapons_held[]->chr` pun crash · blood-stain `PointUsage[]` chain hang (guarded, `d43_emit.py` opcode-0x18 gap) | D119 fixed · D120 guarded |
| 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 |
@@ -825,6 +826,10 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
| 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). | 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. PORT-LEARNINGS §C. |
| D140 | **Pause-menu crash (M-23, OPEN).** `maybe_mp_interface` → `bondviewRenderWatch` (`bondview2.c:8604`) → `bondviewTransformManyPosToViewMatrix(g_CurrentPlayer->field_23C, objheader->numMatrices)` with `field_23C == NULL` (`arg1 == 9`) → `matrix_4x4_copy(src=0x0, …)` (`matrixmath.c:130`) AV. `struct player.field_23C` is the watch model's per-node view-matrix cache — never allocated on PC, or `field_23C` is at the wrong `struct player` offset (`docs/AUDIT-M6-player-offsets.md`). Blocks the WS6 pause-menu / `objective_status` checks + watch gadgets. D75 3D-model-transform family. Files: `src/game/bondview2.c` (`bondviewRenderWatch` ~8540-8608), `src/bondtypes.h` (`struct player`). | OPEN |
| 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. |
Phase 2 replaced the Phase-1 demo loop with the real `mainproc()` on real OS
@@ -3912,6 +3917,27 @@ TODO: (1) human playtest + tune `MouseAimSpeed`; (2) key rebinding;
(3) gamepad hotplug (opened only at init today); (4) optional analog-aim
`#ifdef PORT` hook in bondview.c for true mouse-look.
*Open polish bugs (from WS6 playtest, M-22):*
- **D118a** — mouse yaw (analog, `stick_x += aimDX`) vs pitch (digital
C-up/C-down pulse) feel very different; pitch is steppy/slow.
- **D118b** — mouse-Y may feel inverted (toggle: `ge007.ini [Input]
MouseInvertY`).
- **D118c** — **in manual-aim mode (right-mouse), moving the mouse down to
look down makes Bond crouch.** Root cause is the same digital bridge: mouse
Y emits `GE_CONT_D` (C-down), and `bondview2.c` `MoveData` maps
`C-down` in `insightaimmode` to `crouchDown` (`bondview2.c:5351`) / zoom
(`:5340`, DISABLE_CROUCH weapons) as well as free-aim pitch. The C-button
is genuinely overloaded by mode in game logic — the only clean fix is the
deferred analog-aim `#ifdef PORT` hook (TODO 4), which would drive pitch
without touching C-down. Low priority (aim-mode look-down is rare in
normal play). No cheap input-layer-only fix — suppressing C-down in aim
mode would break intentional crouch/zoom.
- **D137 (FIXED)** — right-mouse *crash* (not this crouch bug):
`gunDrawSight` `s32 sp54` truncated a `Gfx*`; see §F D137.
- Weapon switch on kbd/mouse = the A button (`Space`/`Z`/`E`), same as
action/use (GE overloads it). No dedicated key; mouse-wheel cycle is a
candidate quick-win.
**`data/` deletion + recovery (session M-3).** `git worktree remove
--force` on an agent worktree that had a directory *junction*
`worktree/data → main/data` followed the junction and deleted the real
+27
View File
@@ -102,6 +102,22 @@ expressed in N64 `Gfx`/`Vtx` units is **half-size** on PC.
- Instances: D50.6 (texCopyGdls copied only w0 of each 16-byte slot),
D58 (DL reserve 0x100→0x200), D85 (`bgWidenRoomGdl` 8→16 + bswap),
D95 (2× master-DL buffer + raised mempool ceiling).
- **D135 corollary — an unported GBI *parser* (not just a buffer size).** Code
that walks a DL command stream with raw byte/word indices into an 8-byte N64
`Gfx` (`*(s8*)gdl` for the cmd, `((u32*)gdl)[1]` for w1, `((u8*)gdl)[5..7]`
for `G_TRI1` vtx indices, `((u32*)gdl)[0/1]` nibble reads for `G_TRI4`) reads
the wrong bytes of the PC 16-byte `{u64 w0; u64 w1}` slot and desyncs on the
first command → walks off the DL → wild deref. The N64 32-bit words survive
in the LOW dword of each 64-bit field, so the mechanical port is: `u32 w0 =
(u32)gdl->words.w0; u32 w1 = (u32)gdl->words.w1;` then replace every raw
access with the equivalent shift/mask on w0/w1 (BE byte `i` of a word →
`(word >> (8*(3-i))) & 0xff`). `gdl++` (advances by `sizeof(Gfx)`) is already
correct. Watch `(s32)ptr` truncation in vtx-base math and `x | 0x80000000`
KSEG0 folds (identity on PC — just drop the OR, and guard segmented w1).
Instances: `bgTestHitOnObj` (`propobj.c`, FIXED); `bg.c` room-geometry
hit-test `sub_GAME_…` (`~3373-3646`, still latent — shooting walls).
PD ground truth: `pd_port` uses `gdl->dma.cmd` + `GFX_W0_BYTE(i)`/`GFX_W1_BYTE(i)`
macros (`3-i` / `11-i` on 64-bit LE).
- **Also bites RAW hardcoded struct-stride writes, not just Gfx/Vtx.**
D128: `sub_GAME_7F0B37EC` did `((u8*)g_BgPortals)[(portal<<3)+6] |= 2`
— the N64 `bg_portal_data_entry` is 8B (`ptr@0, cr1@4..cb2@7`); on PC
@@ -129,6 +145,17 @@ through a converter or a runtime bswap fixup reads scrambled.
(StandTile id/room, header mid/tail).
- **Rule:** prefer an offline sidecar converter (D43, D69, D88 pattern,
`tools_pc/d*_emit.py`) over a runtime fixup for a whole format.
- **D139 — `(u8)word` / `(s8)word` to grab "the first field" of a struct
reads the WRONG byte on LE.** N64 code that does `(u8)obj[0]` (obj a
`u32*`) to read a byte-3 field of a big-endian header word — e.g. the
propDef type in `[u16 extrascale][u8 state][u8 type]` — gets the low byte,
which is the *last* BE field. On LE it's `extrascale`. `cleanupObjects`
did this for its walk-termination + type dispatch → never saw
`PROPDEF_END` → ran off the blob → freed garbage → crash on stage unload.
Fix: use the struct member (`pdef->type`) like every other consumer.
Grep for `(u8)*`/`(s8)*`/`& 0xff` on a `u32`/`s32` that's really a
serialized multi-field word — especially GBI-word and header-word reads
that were never routed through a converter or `#ifdef PORT`.
- **Python slice-assignment width is a silent corruptor** (D125):
`out[a:a+8] = b"\x00\x00\x00\x00"` on a `bytearray` does NOT raise — it
deletes `(8 - len(rhs))` bytes, shrinking `out` and shifting everything
+18 -2
View File
@@ -7,15 +7,29 @@
#include "loadobjectmodel.h"
#ifdef PORT
/* D139: the N64 `(u8)obj[0]` extracts the propDef type because the header word
* `[u16 extrascale][u8 state][u8 type]` is big-endian, so `type` is the low
* byte. On little-endian PC the low byte is `extrascale`, so this walk never
* matched PROPDEF_END (ran off the propDefs blob) and dispatched the switch on
* garbage -> objFreePermanently() on non-ObjectRecords -> `obj->prop` reads
* float data -> crash in objFreeEmbedmentOrProjectile on stage unload. Every
* other propDef consumer (sizepropdef, the proplvreset walks) already uses
* `pdef->type` (struct member, offset 3); match them. */
#define CLEANUP_PDTYPE(o) (((PropDefHeaderRecord *)(o))->type)
#else
#define CLEANUP_PDTYPE(o) ((u8)(o)[0])
#endif
void cleanupObjects(s32 stage)
{
u32 *obj = (u32)g_CurrentSetup.propDefs;
if (obj)
{
while ((u8)obj[0] != PROPDEF_END)
while (CLEANUP_PDTYPE(obj) != PROPDEF_END)
{
switch ((u8)obj[0])
switch (CLEANUP_PDTYPE(obj))
{
case PROPDEF_DOOR:
case PROPDEF_PROP:
@@ -75,3 +89,5 @@ void cleanupObjects(s32 stage)
}
}
}
#undef CLEANUP_PDTYPE
+18
View File
@@ -6234,13 +6234,27 @@ void gunSetSightVisible(s32 reason, bool visible)
void gunDrawSight(s32 *gdl) {
#ifdef PORT
/* D137: sp54 holds a Gfx* the whole time (`sp54 = *gdl`, then passed as
* `&sp54` to texSelect/display_image_at_position which take Gfx**). As an
* N64 `s32` it is 4 bytes, so `*(Gfx**)&sp54` reads 4 bytes of adjacent
* stack as the pointer high word → wild `gdl` write in texSetRenderMode
* when the player raises the crosshair (right-mouse aim). §A. */
Gfx *sp54;
#else
s32 sp54;
#endif
f32 xypos[2];
f32 halfedxy[2];
if ((g_CurrentPlayer->gunsightmode == 0) && (g_CurrentPlayer->mpmenuon == FALSE)) {
#ifdef PORT
sp54 = *(Gfx **)gdl;
texSelect(&sp54, crosshairimage, 4, 0, 0);
#else
sp54 = *gdl;
texSelect(&sp54, crosshairimage, 4, 0, 0);
#endif
xypos[0] = g_CurrentPlayer->crosshair_angle.f[0];
xypos[1] = g_CurrentPlayer->crosshair_angle.f[1];
@@ -6254,7 +6268,11 @@ void gunDrawSight(s32 *gdl) {
halfedxy[1] = halfedxy[1] * g_GunSightAspectRatio;
#endif
display_image_at_position(&sp54, &xypos, &halfedxy, 0x20, 0x20, 0, 0, 1, 0xFF, 0xFF, 0xFF, 0x6E, (crosshairimage->level > 0), 0);
#ifdef PORT
*(Gfx **)gdl = sp54;
#else
*gdl = sp54;
#endif
}
}
+81 -1
View File
@@ -8481,7 +8481,19 @@ bool bgTestHitOnObj(coord3d *arg0, coord3d *arg1, coord3d *arg2, Gfx *gdl, Gfx *
while (1)
{
#ifdef PORT
/* D135: GE object bullet-hit GBI parser. On PC the model GDL is the
* 16-byte Gfx layout (tools_pc/d43_emit.py) with the two N64 32-bit
* words preserved in the low dwords of words.w0 / words.w1. Every raw
* byte/word index below assumed an 8-byte N64 slot, so it desynced on
* the first G_VTX/G_TRI1 and walked off the DL (§B, PORT-LEARNINGS).
* Read the words instead; mirrors PD bgTestHitOnObj (pd_port bg.c). */
u32 w0 = (u32) gdl->words.w0;
u32 w1 = (u32) gdl->words.w1;
op = (s8) (w0 >> 24);
#else
op = *((s8 *) gdl);
#endif
if (op == (s8)G_ENDDL)
{
if (gdl2 == 0)
@@ -8501,9 +8513,15 @@ bool bgTestHitOnObj(coord3d *arg0, coord3d *arg1, coord3d *arg2, Gfx *gdl, Gfx *
if (op == G_VTX)
{
#ifdef PORT
op = (w0 >> 16) & 0xf;
padC = w1 & 0x00ffffff;
vtxbase = (Vertex *) ((uintptr_t) vertices + (uintptr_t) padC - (op << 4));
#else
op = ((u8 *) gdl)[1] & 0xf;
padC = ((u32 *) gdl)[1] & 0x00ffffff;
vtxbase = (Vertex *) ((((s32) vertices) + padC) - (op << 4));
#endif
gdl++;
continue;
@@ -8514,9 +8532,15 @@ bool bgTestHitOnObj(coord3d *arg0, coord3d *arg1, coord3d *arg2, Gfx *gdl, Gfx *
bboxMin = D_8003204C;
bboxMax = D_80032058;
#ifdef PORT
idx[0] = ((w1 >> 16) & 0xff) / 10;
idx[1] = ((w1 >> 8) & 0xff) / 10;
idx[2] = (w1 & 0xff) / 10;
#else
idx[0] = ((u8 *) gdl)[5] / 10;
idx[1] = ((u8 *) gdl)[6] / 10;
idx[2] = ((u8 *) gdl)[7] / 10;
#endif
for (i = 0; i < 3; i++)
{
@@ -8560,6 +8584,28 @@ bool bgTestHitOnObj(coord3d *arg0, coord3d *arg1, coord3d *arg2, Gfx *gdl, Gfx *
dy = (f32) (((s32) hitbuf.hitpos.y) - ((s32) arg0->y));
dz = (f32) (((s32) hitbuf.hitpos.z) - ((s32) arg0->z));
tcmd = gdl;
#ifdef PORT
if (((u8) ((u32) gdl->words.w0 >> 24) != G_SETTIMG) && (cmdStart < gdl))
{
do
{
tcmd--;
if ((u8) ((u32) tcmd->words.w0 >> 24) == G_SETTIMG)
{
break;
}
} while (cmdStart < tcmd);
}
/* N64 recovers the texture number from *(s16*)(phys(w1) - 8).
* That layout is not guaranteed for the port's converted
* model textures / runtime-replaced collision GDLs, and a
* bogus value here indexes g_HitTypeSounds[] OOB (div-by-0
* at :9717). texturenum only flavours the bullet-impact
* sound/decal, so fall back to the generic hit (-1). */
texnum = -1;
(void) tcmd;
#else
if (((*((u8 *) gdl)) != 253) && (cmdStart < gdl))
{
do
@@ -8583,6 +8629,7 @@ bool bgTestHitOnObj(coord3d *arg0, coord3d *arg1, coord3d *arg2, Gfx *gdl, Gfx *
padC = ((u32 *) tcmd)[1] - 8;
texnum = *((u16 *) (padC | 0x80000000));
}
#endif
d = ((dx * dx) + (dy * dy)) + (dz * dz);
@@ -8614,6 +8661,32 @@ bool bgTestHitOnObj(coord3d *arg0, coord3d *arg1, coord3d *arg2, Gfx *gdl, Gfx *
bboxMin2 = D_80032070;
bboxMax2 = D_8003207C;
#ifdef PORT
if (s2 == 0)
{
idx2[0] = w1 & 0xf;
idx2[1] = (w1 & 0xff) >> 4;
idx2[2] = w0 & 0xf;
}
else if (s2 == 1)
{
idx2[0] = (w1 >> 8) & 0xf;
idx2[1] = (w1 & 0xffff) >> 12;
idx2[2] = (w0 & 0xff) >> 4;
}
else if (s2 == 2)
{
idx2[0] = (w1 >> 16) & 0xf;
idx2[1] = ((w1 >> 16) & 0xff) >> 4;
idx2[2] = (w0 >> 8) & 0xf;
}
else
{
idx2[0] = (w1 >> 24) & 0xf;
idx2[1] = w1 >> 28;
idx2[2] = (w0 & 0xffff) >> 12;
}
#else
if (s2 == 0)
{
idx2[0] = ((u32 *) gdl)[1] & 0xf;
@@ -8638,6 +8711,7 @@ bool bgTestHitOnObj(coord3d *arg0, coord3d *arg1, coord3d *arg2, Gfx *gdl, Gfx *
idx2[1] = ((u32 *) gdl)[1] >> 28;
idx2[2] = ((u32) ((u16 *) gdl)[1]) >> 12;
}
#endif
for (i = 0; i < 3; i++)
{
@@ -8681,7 +8755,12 @@ bool bgTestHitOnObj(coord3d *arg0, coord3d *arg1, coord3d *arg2, Gfx *gdl, Gfx *
dy = (f32) (((s32) hitbuf.hitpos.y) - ((s32) arg0->y));
dz = (f32) (((s32) hitbuf.hitpos.z) - ((s32) arg0->z));
tcmd = gdl;
#ifdef PORT
/* see the G_TRI1 note above — texturenum recovery from
* the G_SETTIMG w1 is not portable; use generic hit. */
texnum = -1;
(void) tcmd;
#else
if (((*((u8 *) gdl)) != G_SETTIMG) && (cmdStart < gdl))
{
do
@@ -8704,6 +8783,7 @@ bool bgTestHitOnObj(coord3d *arg0, coord3d *arg1, coord3d *arg2, Gfx *gdl, Gfx *
padC = ((u32 *) tcmd)[1] - 8;
texnum = *((u16 *) (padC | 0x80000000));
}
#endif
d = ((dx * dx) + (dy * dy)) + (dz * dz);
+13
View File
@@ -1037,6 +1037,18 @@ void sndCreatePostEvent(ALSoundState *state, s16 eventType, s32 arg2)
{
ALSndpEvent evt;
#ifdef PORT
/* D138: libaudio is Phase-3 parked. The audio thread never drains
* g_sndPlayerPtr->evtq->allocList, so every positional-sound tick
* (chrobjSndCreatePostEvent, called per objTick for each ambient-sound
* object) appends an item and alEvtqPostEvent walks the whole list to
* insert it -> O(n^2). Facility has enough machinery ambience that after
* a few thousand frames one insert takes seconds and the kernel-heartbeat
* watchdog trips ("no frame rendered"). Skip event posting until audio
* lands (D77); no gameplay effect. */
(void) evt; (void) eventType; (void) arg2; (void) state;
return;
#else
evt.common.type = eventType;
evt.common.state = state;
evt.unks32.val8 = arg2;
@@ -1045,6 +1057,7 @@ void sndCreatePostEvent(ALSoundState *state, s16 eventType, s32 arg2)
{
alEvtqPostEvent(&g_sndPlayerPtr->evtq, (ALEvent *)&evt, 0);
}
#endif
}
/**
+41
View File
@@ -0,0 +1,41 @@
#!/bin/bash
# Playtest launcher for docs/LEVEL-PLAYTEST.md (WS6 human validation).
# Usage: ./tools_pc/playtest.sh <XX> [difficulty-note]
# e.g. ./tools_pc/playtest.sh 33 "Agent"
# Launches the game bare (-level_XX, pools auto-injected per D121), tees all
# output to build-pc/playtest_L<XX>_<HHMMSS>.log, and reports crash status
# on exit. Human plays start-to-finish; close the window when done.
set -e
cd "$(dirname "$0")/.."
LEVEL="${1:?usage: playtest.sh <XX> [difficulty]}"
DIFF="${2:-Agent}"
export PATH="/c/msys64/mingw64/bin:$PATH"
if [ ! -x build-pc/ge007.x86_64.exe ]; then
echo "binary missing — building..."
./build-pc.sh ntsc-final
fi
LOG="build-pc/playtest_L${LEVEL}_$(date +%H%M%S).log"
echo "== playtest level ${LEVEL} (difficulty: ${DIFF}) =="
echo "== log: ${LOG}"
echo "== checklist: docs/LEVEL-PLAYTEST.md — spawn, geometry, guards,"
echo "== doors/pickups/objectives, alarms, exit -> MISSION COMPLETE, auto-advance"
set +e
./build-pc/ge007.x86_64.exe "-level_${LEVEL}" 2>&1 | tee "$LOG"
RC=${PIPESTATUS[0]}
set -e
echo "==============================="
if grep -q "FATAL: EXCEPTION" "$LOG"; then
echo "RESULT: CRASH (see log)"
grep -A3 "FATAL: EXCEPTION" "$LOG" | head -8
elif grep -qi "mission complete\|MISSION COMPLETE" "$LOG"; then
echo "RESULT: MISSION COMPLETE seen in log"
else
echo "RESULT: exited rc=${RC} (no crash marker; check log + your notes)"
fi
echo "log: ${LOG}"
+47
View File
@@ -0,0 +1,47 @@
#!/bin/bash
# Repro harness: launch a level under gdb-attach, dump a full backtrace on the
# first access violation (0xc0000005 / SIGSEGV). Human plays; close nothing --
# gdb prints the trace and detaches when it faults.
#
# Usage: ./tools_pc/repro_gdb.sh <XX> e.g. ./tools_pc/repro_gdb.sh 34
set -e
cd "$(dirname "$0")/.."
export PATH="/c/msys64/mingw64/bin:$PATH"
LEVEL="${1:?usage: repro_gdb.sh <XX>}"
STAMP=$(date +%H%M%S)
LOG="build-pc/repro_L${LEVEL}_${STAMP}.log"
GDBLOG="build-pc/repro_L${LEVEL}_${STAMP}.gdb.txt"
export GE_PCDUMP="1-99999:30" # rolling frame dump -> ppm/ (last few = pre-crash)
cat > "build-pc/repro_cmds_${STAMP}.txt" <<'EOF'
set pagination off
set width 0
set confirm off
handle SIGSEGV stop print nopass
continue
echo \n\n=================== FAULT CAUGHT ===================\n
bt
echo \n--- frame 0 ---\n
frame 0
info args
info locals
echo \n--- full backtrace ---\n
bt full
echo \n--- registers ---\n
info registers rax rbx rcx rdx rsi rdi rbp rsp r8 r9 r10 r11
echo \n===================================================\n
detach
quit
EOF
echo "== launching -level_${LEVEL} (nohup); play a firefight to trigger the crash =="
nohup ./build-pc/ge007.x86_64.exe "-level_${LEVEL}" > "$LOG" 2>&1 &
BASHPID=$!
sleep 6
# winpid = 4th column of ps -p <bashpid>
WINPID=$(ps -p "$BASHPID" | awk 'NR==2{print $4}')
echo "== bash pid=$BASHPID winpid=$WINPID attaching gdb =="
echo "== game log: $LOG"
echo "== gdb log: $GDBLOG"
gdb -batch -x "build-pc/repro_cmds_${STAMP}.txt" -p "$WINPID" 2>&1 | tee "$GDBLOG"
echo "== done. crash log: ge007.crash.log ; pre-crash frames: ppm/ =="