diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index f2744fc0..0bc75bc4 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -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 ` — 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. + +
M-22 and earlier "Next task" (superseded — §H authoritative) -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). +
+ **Regen gotcha:** `d69_emit.py` rewrites `data/pccg-/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 diff --git a/docs/PCPortResearch.md b/docs/PCPortResearch.md index aa491e9a..79a01887 100644 --- a/docs/PCPortResearch.md +++ b/docs/PCPortResearch.md @@ -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()` 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 diff --git a/docs/PORT-LEARNINGS.md b/docs/PORT-LEARNINGS.md index 1d6582b9..eee2df5b 100644 --- a/docs/PORT-LEARNINGS.md +++ b/docs/PORT-LEARNINGS.md @@ -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 diff --git a/src/game/cleanup_objects.c b/src/game/cleanup_objects.c index a2562cb4..9050721d 100644 --- a/src/game/cleanup_objects.c +++ b/src/game/cleanup_objects.c @@ -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 diff --git a/src/game/gunfire.c b/src/game/gunfire.c index ed81c4c1..224de6b9 100644 --- a/src/game/gunfire.c +++ b/src/game/gunfire.c @@ -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 } } diff --git a/src/game/propobj.c b/src/game/propobj.c index 446e0d0a..34bd594d 100644 --- a/src/game/propobj.c +++ b/src/game/propobj.c @@ -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); diff --git a/src/snd.c b/src/snd.c index 4309aa69..1f40b3ff 100644 --- a/src/snd.c +++ b/src/snd.c @@ -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 } /** diff --git a/tools_pc/playtest.sh b/tools_pc/playtest.sh new file mode 100644 index 00000000..5face4aa --- /dev/null +++ b/tools_pc/playtest.sh @@ -0,0 +1,41 @@ +#!/bin/bash +# Playtest launcher for docs/LEVEL-PLAYTEST.md (WS6 human validation). +# Usage: ./tools_pc/playtest.sh [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_.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 [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}" diff --git a/tools_pc/repro_gdb.sh b/tools_pc/repro_gdb.sh new file mode 100644 index 00000000..37702191 --- /dev/null +++ b/tools_pc/repro_gdb.sh @@ -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 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 }" +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 +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/ =="