From 52ec6d837ce9e85b7aa4b5627c2092a294996fd9 Mon Sep 17 00:00:00 2001 From: James Dansereau <106631954+jkdansereau@users.noreply.github.com> Date: Sun, 30 Aug 2026 20:34:15 -0400 Subject: [PATCH] =?UTF-8?q?M-28:=20D154=20(WIP,=20unverified)=20=E2=80=94?= =?UTF-8?q?=20port=20bg.c=20room-geometry=20hit-test=20GBI=20parser?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bgTestRayIntersectionInRoom (bg.c) is the D135 sibling flagged since M-23: an unported N64 GBI parser walking the 16-byte PC room DL with raw ((u8*)gdl)[k] / ((u32*)gdl)[i] 8-byte-Gfx byte/word indexing. It runs on every shot that resolves against background geometry (bgTestBulletHitBackground), so it AVs the first time you shoot a wall or floor in a real playthrough -- invisible to the no-input level sweep. Ported #ifdef PORT (N64 path kept verbatim under #else): - vtxoff / vtxbase / op read through the .dma view, matching the already-ported bgBuildRoomVtxBounds. - G_TRI1 vertex indices from (w1 >> 16/8/0) & 0xff, /10. - the 12 G_TRI4 index nibbles recovered 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 not valid for the port's converted GDLs (same as D135); texnum only selects the bullet-impact decal/sound, parked with D77. Build: green (ntsc-final). NOT VERIFIED -- a no-input GE_PCDUMP capture never fires a bullet so it never enters this function; needs a firefight into a wall on an idle machine. This session's box was too thrashed to get a clean run (D153 driver-crash-under-load). Committed as WIP so the port work isn't lost; next session: idle-machine wall-shoot test + -level_09 framediff, then mark verified. §F/HANDOFF D154, PORT-LEARNINGS §B (D135 corollary). --- docs/HANDOFF.md | 16 ++++++++ docs/PCPortResearch.md | 1 + src/game/bg.c | 89 ++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 102 insertions(+), 4 deletions(-) diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index 20082a49..44b4a8b1 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -458,6 +458,22 @@ clean. Still OPEN: the exact leaking call site (read it from the next fires, `addr2line` the logged caller. Separate minor gap: no crouch keybind (BACKLOG B3/B4). +### D154 (WRITTEN, UNVERIFIED, UNCOMMITTED) — `bg.c` room hit-test GBI parser port +`bgTestRayIntersectionInRoom` (`src/game/bg.c:3331`) is the D135 sibling the +docs kept flagging: an unported N64 GBI parser (`((u8*)gdl)[k]` / `((u32*)gdl)[i]` +byte/word indexing) walking the 16-byte PC room DL as if it were 8-byte N64 +`Gfx`. Fires on **shooting walls/floor** (`bgTestBulletHitBackground`), which no +level-sweep exercises. Ported under `#ifdef PORT` (N64 path verbatim under +`#else`): header fields via the `.dma` view like `bgBuildRoomVtxBounds`; G_TRI1 ++ G_TRI4 vertex-index nibbles recovered from `(u32)gdl->words.w0/.w1` +(derivation table cross-checked twice); `texturenum` -> -1 like D135 (KSEG0 +deref invalid for converted GDLs; only flavours the impact decal/sound, D77). +Builds clean. **NOT verified** — a no-input capture never calls this function, +so it needs a real firefight into a wall on an **idle** machine; couldn't get a +clean run this session (D153/driver-crash under machine thrash — see below). +Next session: build, `-level_09`, fire at a wall, confirm no crash + `-level_09` +framediff green, then commit. §F **D154**. + ### D153 — `-level_09` frame ~900 crash = load flakiness, NOT a bug (closed) Chased for ~30 min in M-28: `-level_09` crashed `0xc0000005` at a nondeterministic frame 900↔1400, 6/6 runs — **but every one was while the diff --git a/docs/PCPortResearch.md b/docs/PCPortResearch.md index 38c610e6..c73bca42 100644 --- a/docs/PCPortResearch.md +++ b/docs/PCPortResearch.md @@ -835,6 +835,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | 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*`. PORT-LEARNINGS §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. PORT-LEARNINGS §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 / PORT-LEARNINGS §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). | +| 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). | UNVERIFIED — build green, `#ifdef PORT` only. Next: idle-machine wall-shoot test + `-level_09` framediff, then commit. PORT-LEARNINGS §B (D135 corollary). | | 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. PORT-LEARNINGS §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. | diff --git a/src/game/bg.c b/src/game/bg.c index 734efbe4..d852a799 100644 --- a/src/game/bg.c +++ b/src/game/bg.c @@ -3370,14 +3370,39 @@ bool bgTestRayIntersectionInRoom(coord3d *from, coord3d *to, coord3d *dir, RoomV gdl = (Gfx *) g_BgRoomInfo[roomnum].ptr_expanded_mapping_info; gdl = &gdl[point->gdlindex]; +#ifdef PORT + #define BGCMD(g) ((s32)(u8)(g)->dma.cmd) +#else + #define BGCMD(g) (*((u8 *) (g))) +#endif +#ifdef PORT + /* D154: this room-geometry ray/hit-test is an unported N64 GBI parser -- + * raw ((u8*)gdl)[k] / ((u32*)gdl)[i] byte/word indexing assumes the 8-byte + * N64 Gfx, but the room DL here is the 16-byte PC Gfx (D85 bgWidenRoomGdl + + * texLoadFromGdl). Same class + fix as D135 (propobj.c bgTestHitOnObj): + * recover the two N64 32-bit words from the low dwords of words.w0/.w1 and + * re-express every raw access; the G_VTX header fields go through the .dma + * view exactly like bgBuildRoomVtxBounds. Fires on shooting walls/floor. + * texturenum recovery (a KSEG0 deref 8 bytes before the texture data) is + * not valid for the port's converted GDLs -> return -1 like D135; it only + * flavours the bullet-impact decal/sound (parked with D77). */ + vtxoff = gdl->dma.par & 0xf; + temp.vertices = (Vertex *)g_BgRoomInfo[roomnum].vertices; + vtxbase = (Vertex *)((uintptr_t)temp.vertices + ((u32)gdl->words.w1 & 0x00ffffff)); +#else vtxoff = ((u8 *) gdl)[1] & 0xf; temp.vertices = (Vertex *)g_BgRoomInfo[roomnum].vertices; vtxbase = (Vertex *)((s32)temp.vertices + (((u32 *)gdl)[1] & 0x00ffffff)); +#endif temp.roominfo = &g_BgRoomInfo[roomnum]; bestScore = 0x7FFFFFFF; found = 0; gdl++; +#ifdef PORT + op = (s8) gdl->dma.cmd; +#else op = *((s8 *) gdl); +#endif if ((op != G_VTX) && (op != ((s8) G_ENDDL))) { @@ -3390,9 +3415,15 @@ bool bgTestRayIntersectionInRoom(coord3d *from, coord3d *to, coord3d *dir, RoomV bboxMin = D_80044868; bboxMax = D_80044874; score = dist; +#ifdef PORT + idx[0] = ((s32)(((u32)gdl->words.w1 >> 16) & 0xff) / 10) - vtxoff; + idx[1] = ((s32)(((u32)gdl->words.w1 >> 8) & 0xff) / 10) - vtxoff; + idx[2] = ((s32)(((u32)gdl->words.w1 ) & 0xff) / 10) - vtxoff; +#else idx[0] = (((u8 *) gdl)[5] / 10) - vtxoff; idx[1] = (((u8 *) gdl)[6] / 10) - vtxoff; idx[2] = (((u8 *) gdl)[7] / 10) - vtxoff; +#endif i = 0; do @@ -3450,12 +3481,12 @@ bool bgTestRayIntersectionInRoom(coord3d *from, coord3d *to, coord3d *dir, RoomV score = dist; found = 1; - if (((*((u8 *) gdl)) != G_SETTIMG) && (dist || vtxbase || 1) && (((Gfx *) local.roominfo->ptr_expanded_mapping_info) < gdl)) + if (((BGCMD(gdl)) != G_SETTIMG) && (dist || vtxbase || 1) && (((Gfx *) local.roominfo->ptr_expanded_mapping_info) < gdl)) { do { tcmd--; - if ((*((u8 *) tcmd)) == G_SETTIMG) + if ((BGCMD(tcmd)) == G_SETTIMG) { break; } @@ -3469,8 +3500,12 @@ bool bgTestRayIntersectionInRoom(coord3d *from, coord3d *to, coord3d *dir, RoomV } else { + #ifdef PORT + texnum = -1; /* D154: KSEG0 texnum deref invalid for converted GDLs (cf. D135) */ +#else temp.word = ((u32 *) tcmd)[1] - 8; texnum = *((u16 *) (temp.word | 0x80000000)); +#endif } if (check_if_imageID_is_light(texnum)) @@ -3510,6 +3545,40 @@ bool bgTestRayIntersectionInRoom(coord3d *from, coord3d *to, coord3d *dir, RoomV bboxMin2 = D_80044880; bboxMax2 = D_8004488C; +#ifdef PORT + /* D154: G_TRI4 vertex-DMEM index nibbles, recovered + * from the two N64 words (words.w0/.w1 low dwords). + * N64 byte k = w0[k] for k<4 (MSB-first), w1[k-4] + * for k>=4; N64 u16 j = w0/w1 halfword MSB-first. */ + { + const u32 gw0 = (u32) gdl->words.w0; + const u32 gw1 = (u32) gdl->words.w1; + if (s2 == 0) + { + idx2[0] = ((s32)( gw1 & 0xf)) - vtxoff; /* w1 & 0xf */ + idx2[1] = ((s32)((gw1 >> 4) & 0xf)) - vtxoff; /* byte7 >> 4 */ + idx2[2] = ((s32)( gw0 & 0xf)) - vtxoff; /* w0 & 0xf */ + } + else if (s2 == 1) + { + idx2[0] = ((s32)((gw1 >> 8) & 0xf)) - vtxoff; /* byte6 & 0xf */ + idx2[1] = ((s32)((gw1 >> 12) & 0xf)) - vtxoff; /* u16[3] >> 12 */ + idx2[2] = ((s32)((gw0 >> 4) & 0xf)) - vtxoff; /* byte3 >> 4 */ + } + else if (s2 == 2) + { + idx2[0] = ((s32)((gw1 >> 16) & 0xf)) - vtxoff; /* u16[2] & 0xf */ + idx2[1] = ((s32)((gw1 >> 20) & 0xf)) - vtxoff; /* byte5 >> 4 */ + idx2[2] = ((s32)((gw0 >> 8) & 0xf)) - vtxoff; /* byte2 & 0xf */ + } + else + { + idx2[0] = ((s32)((gw1 >> 24) & 0xf)) - vtxoff; /* byte4 & 0xf */ + idx2[1] = ((s32)((gw1 >> 28) & 0xf)) - vtxoff; /* w1 >> 28 */ + idx2[2] = ((s32)((gw0 >> 12) & 0xf)) - vtxoff; /* u16[1] >> 12 */ + } + } +#else if (s2 == 0) { idx2[0] = (((u32 *) gdl)[1] & 0xf) - vtxoff; @@ -3534,6 +3603,7 @@ bool bgTestRayIntersectionInRoom(coord3d *from, coord3d *to, coord3d *dir, RoomV idx2[1] = (((u32 *) gdl)[1] >> 28) - vtxoff; idx2[2] = (((u32) ((u16 *) gdl)[1]) >> 12) - vtxoff; } +#endif i = 0; @@ -3588,12 +3658,12 @@ bool bgTestRayIntersectionInRoom(coord3d *from, coord3d *to, coord3d *dir, RoomV score = dist; found = 1; - if (((*((u8 *) gdl)) != G_SETTIMG) && (dist || vtxbase || 1) && (((Gfx *) local.roominfo->ptr_expanded_mapping_info) < gdl)) + if (((BGCMD(gdl)) != G_SETTIMG) && (dist || vtxbase || 1) && (((Gfx *) local.roominfo->ptr_expanded_mapping_info) < gdl)) { do { tcmd--; - if ((*((u8 *) tcmd)) == G_SETTIMG) + if ((BGCMD(tcmd)) == G_SETTIMG) { break; } @@ -3607,8 +3677,12 @@ bool bgTestRayIntersectionInRoom(coord3d *from, coord3d *to, coord3d *dir, RoomV } else { + #ifdef PORT + texnum = -1; /* D154: KSEG0 texnum deref invalid for converted GDLs (cf. D135) */ +#else temp.word = ((u32 *) tcmd)[1] - 8; texnum = *((u16 *) (temp.word | 0x80000000)); +#endif } if (check_if_imageID_is_light(texnum)) @@ -3643,10 +3717,17 @@ bool bgTestRayIntersectionInRoom(coord3d *from, coord3d *to, coord3d *dir, RoomV } gdl++; +#ifdef PORT + op = (s8) gdl->dma.cmd; +#else op = *((s8 *) gdl); +#endif } while ((op != G_VTX) && (op != ((s8) G_ENDDL))); } +#ifdef PORT + #undef BGCMD +#endif return found; }