M-30: D154 re-audit — fix vtxoff, port bgTestBulletHitBackground G_SETTILE sibling, add GE_D154 diagnostic

Re-audited the M-28 bgTestRayIntersectionInRoom GBI-parser port (bg.c):

- G_TRI1 (w1>>16/8/0 & 0xff /10) and all 12 G_TRI4 nibble recoveries
  cross-checked twice against the N64 #else path and the raw BE-word bit
  layout -- both CORRECT, no change.
- BUG: vtxoff read gdl->dma.par & 0xf. The PC Gdma_le shim maps .par to
  bits 0-23 of word0 (packed length, 16-aligned) not the N64 byte-1
  params field at bits 16-23, so vtxoff was silently 0 -> wrong
  vtxbase[idx] for any G_VTX batch with v0 != 0. Now
  ((u32)gdl->words.w0 >> 16) & 0xf.
- Sibling ported: bgTestBulletHitBackground tail (post-hit G_SETTILE
  back-scan for tileformat/tilesize) was itself an unported raw
  ((u8*)gdl)[0]/[1] + 64-bit words.w0<<11>>30 parser over the same
  D85-widened 16-byte room DL. #ifdef PORT: opcode from bits 24-31,
  byte1 from bits 16-23, (u32)-forced 32-bit width for the tilesize
  shift. N64 path kept under #else.
- Diagnostic: GE_D154=1 logs the parsed cmd / vtx-index stream (call
  header + per-TRI1/TRI4 idx) for the first 64 invocations so the
  integrator/user can eyeball the parse. Verification is playtest-gated
  (shoot a wall/floor on an idle machine) -- the no-input level sweep
  never enters this function.

Build green (ntsc-final, 242/242). ABI/GBI-parse only, no game-logic
change. docs/PCPortResearch.md D154 + PORT-LEARNINGS §B updated.
This commit is contained in:
James Dansereau committed 2026-08-31 12:48:16 -04:00
1 parent 6f45df9dde
commit 75641fb0b7
3 files changed
+93 -10

No files matched your search

+1 -1
View File
@@ -843,7 +843,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). |
| D154 | **`bg.c` room-geometry ray/hit-test GBI parser port (M-28, WRITTEN / UNVERIFIED / UNCOMMITTED).** `bgTestRayIntersectionInRoom` (`bg.c:3331`, called by `bgTestBulletHitBackground` on every shot that resolves against background geometry) is the D135 sibling flagged since M-23: raw `((u8*)gdl)[k]` / `((u32*)gdl)[i]` byte/word indexing into what is now the 16-byte PC `Gfx` room DL (D85 `bgWidenRoomGdl` + `texLoadFromGdl`) → desync → OOB `vtxbase[idx]` / walk-off AV the first time you shoot a wall or floor. Ported `#ifdef PORT` (N64 verbatim under `#else`): `vtxoff`/`vtxbase`/`op` via the `.dma` view (matches the already-ported `bgBuildRoomVtxBounds`); G_TRI1 indices from `(w1>>16/8/0)&0xff /10`; the 12 G_TRI4 nibble extractions from `(u32)gdl->words.w0/.w1` (each mapped from the N64 byte/halfword layout — table in the code comment, cross-checked twice); both `texturenum` recoveries → `-1` (the KSEG0 `*(u16*)(w1-8)` deref is invalid for the port's converted GDLs, same as D135; texnum only picks the impact decal/sound, parked D77). Builds clean. **Verification blocked** — a no-input `GE_PCDUMP` capture never fires a bullet so it never calls this function; needs a real firefight into a wall on an idle machine (this session's box was thrashed → D153/driver crash on every run). | **M-30 re-audit + fixes (still playtest-gated for final verify).** Cross-checked the ported G_TRI1 + all 12 G_TRI4 nibble recoveries twice against the N64 `#else` and the raw BE-word bit derivation — **G_TRI1 and G_TRI4 are correct**. **Two bugs fixed:** (1) `vtxoff` — the port read `gdl->dma.par & 0xf`, but the PC `Gdma_le` shim (`port/shim/PR/gbi.h`) maps `.par` to bits 0-23 of word0 (the packed *length*, always 16-aligned) not the N64 byte-1 params field at bits 16-23, so vtxoff was silently forced to 0 (breaks any G_VTX batch with `v0 != 0`, i.e. multi-batch rooms → wrong `vtxbase[idx]` → OOB). Now `((u32)gdl->words.w0 >> 16) & 0xf`. (2) **Sibling ported** — `bgTestBulletHitBackground` tail (`bg.c:~3841`, the post-hit G_SETTILE back-scan for `tileformat`/`tilesize`) was itself an unported raw `((u8*)gdl)[0]`/`[1]` + 64-bit `words.w0 << 11 >> 30` parser over the same widened DL; wrapped `#ifdef PORT` (opcode from bits 24-31, byte1 from bits 16-23, `(u32)`-forced 32-bit width for the tilesize shift). **Diagnostic:** `GE_D154=1` → `osSyncPrintf` of the parsed cmd/vtx-index stream (call header + per-TRI1/TRI4 `idx`) for the first 64 invocations, for the user/integrator to eyeball. Build green (`ntsc-final`, 242/242). Runtime unverifiable in a worktree (no `data/`); still needs a firefight-into-wall on an idle machine + `-level_09` framediff. Confidence: HIGH on the mechanical correctness (bit-for-bit vs N64), MEDIUM that nothing else in the walk still bites. PORT-LEARNINGS §B (D135 corollary). |
| D156 | **Facility outro-cutscene "hang" — the actual fix (M-29, user bug report, 2nd occurrence after D155).** After D155 shipped the user re-ran the playthrough and it hung again at the end of Facility — same stack (`modelSetAnimFrame2WithChrStuff` ← `modelTickAnim` ← `chrUpdateAnim` ← `chrTick` ← `playerTick` ← `propsTick` ← `lvlRender` ← `bossMainloop`) but this time **`frames=12962` frozen CONSTANT across ~200 heartbeat dumps / minutes** — a true infinite loop, not the D155 slow-catch-up spiral. Root: the `while (1)` at `model.c:3131` in `modelSetAnimFrame2WithChrStuff` steps the root-motion accumulation one anim frame at a time from `framea` (`model->animframe1`) to `frameb` (`frame`, accumulated in `modelTickAnim` as `frame += playspeed * speed` per tick). If `frameb` arrives **NaN or blown up** — a cutscene anim whose blend/transition math produces a huge/non-finite `model->speed`/`playspeed`/`timespeed` (`model.c:3436` `/ model->unkb0`, `:3477` `/ model->timespeed`, both guarded `> 0` but not against tiny values) — then `floorFloatToInt(frameb)` yields garbage, `endframe - curframe` ≈ 2^31, and the loop (each iteration: `sub_GAME_7F06D3F4` joint transform + `cosf`/`sinf` + `modelConstrainOrWrapAnimFrame`) runs effectively forever. **Likely deeper cause (unconfirmed, needs the repro):** these speed/blend fields are `Model` struct members; if this model is punned/inline (D100/D140 family) and `sizeof(Model)` growth misaligned them, they read garbage exactly when a cutscene sets an unusual anim transition. | GUARDED (`src/game/model.c`, `#ifdef PORT`): (1) in `modelSetAnimFrame2WithChrStuff`, if `frameb` is non-finite or `|frameb| >= 1e6` snap it to `framea` (loop then does ~0 iterations; the `modelConstrainOrWrapAnimFrame` at :3211 still clamps `model->frameb` to a valid frame); (2) in `modelTickAnim` after the tick loop, if `frame`/`frame2` went non-finite/absurd fall back to the pre-loop `model->animframe1`/`animframe2` so the NaN can't poison model state every frame. `!(x > -1e6f && x < 1e6f)` catches NaN too. Anim frames are 0..few-hundred so 1e6 is a safe ceiling. Verified: build green; `-level_09` frame 200 unregressed (guards never fire in normal play). **User to re-verify the Facility outro.** If it still hangs, the NaN source is upstream — dump `model->speed/playspeed/timespeed/unkb0` in `modelTickAnim` during the cutscene. PORT-LEARNINGS §E. |
| D155 | **Facility outro-cutscene end → "hang" = unclamped `deltaFrames` spiral (M-29, user bug report).** User watched the Facility outro cutscene; at the end the game froze (process alive, kernel-heartbeat dumps). Symbolicated `mainThread` stack from the port's own hang dump: `modelConstrainOrWrapAnimFrame` ← `modelTickAnim` ← `chrUpdateAnim` ← `chrTick` ← `playerTick` ← `propsTick` ← `lvlRender`, sampled there across *every* heartbeat dump. Root cause: `waitForNextFrame()` (`src/game/frametiming.c`) sets `nextFrameTime = (osGetCount() − prev + 387937) / 775875` and passes it **unclamped** to `updateFrameCounters` as `deltaFrames`. `osGetCount()` is wall-clock on the port (D117), not VI-locked, so a real-time stall — the asset load at the cutscene→debrief boundary, made far worse by the user running a local LLM on the same box — makes `nextFrameTime` balloon to hundreds/thousands. That becomes `speedgraphframes` → `g_ClockTimer`, which drives `modelTickAnim(model, g_ClockTimer, 1)`'s `while (numticks-- > 0)` loop once per character per render, plus dozens of `for (i = 0; i < g_ClockTimer; i++)` sim loops (`bondhead.c`, `bondview2.c`, `explosion.c`, `options.c`…). One render then does thousands of anim ticks × N characters → multi-second frame → heartbeat trips, and the slow frame feeds an even bigger delta next time → unrecoverable spiral. The N64 was physically VI-bound and never produced `deltaFrames > ~2`. Timing-compensation class, cf. D117/D134. | FIXED (`src/game/frametiming.c`, `#ifdef PORT` — clamp `nextFrameTime` to `FRAMETIMING_PORT_MAX_CATCHUP` = 6). After a hitch the sim just resumes at real-time-ish pace instead of spiralling, exactly as the console did when it dropped frames under load. N64 path verbatim. Confidence: high (stack + arithmetic both point here). PORT-LEARNINGS §E. |
| D153 | **`-level_09` frame ~900–1400 `0xc0000005` = D117/D134 load flakiness, NOT a bug (M-28).** Reproduced 6/6 while the machine was under heavy concurrent build+run load; on a **quiet** machine `-level_09` ran past frame 2400 with 0 heartbeats, no crash. Same false-positive class the docs warn about every session (M-13..M-22, M-25 Silo ~frame 300 freeze). The crash PC was in a system DLL (a `memcpy` off a buffer) and the frame varied run-to-run — both consistent with the port's host-scheduling jitter under load (`osGetCount` is wall-clock, variable-timestep sim), not a deterministic defect. `romdataFixupMusicSeqTable: seqCount 63 exceeds blob capacity 1` is an EXPECTED benign warning on the header-only first `romdataFixupMusicSeqTable` call (music.c:736; the full-size second call at :742 is silent) — not related. **Lesson: don't regression-test while builds/other runs are in flight.** | NOT A BUG — closed. Run anchors on a quiet machine. |
+18 -2
View File
@@ -119,10 +119,26 @@ expressed in N64 `Gfx`/`Vtx` units is **half-size** on PC.
`(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).
Instances: `bgTestHitOnObj` (`propobj.c`, FIXED); `bgTestRayIntersectionInRoom`
+ `bgTestBulletHitBackground` tail (`bg.c`, D154 — ported M-28, re-audited +
bug-fixed M-30, playtest-gated).
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).
- **D154 corollary — the PC `Gdma_le` shim's `.par` is NOT the N64 params byte.**
When porting a room/model DL GBI parser, only `.dma.cmd` is safe to read
through the `port/shim/PR/gbi.h` `Gfx` union: that shim lays `Gdma_le` out as
`par:24` (bits 0-23 of word0 = the *packed length* from `gDma1p`), `cmd:8`
(bits 24-31). The N64 `Gdma` has `cmd:8` (byte 0) then `par:8` (**byte 1**,
bits 16-23 = the `((n-1)<<4)|v0` G_VTX params). So an N64 `((u8*)gdl)[1]` /
N64-semantics `.dma.par` becomes `((u32)gdl->words.w0 >> 16) & 0xff` on PC —
**`gdl->dma.par` gives bits 0-23 (length), and `& 0xf` on it is always 0**
because `len == 16*n`. The original D154 port had exactly this bug (vtxoff
forced to 0). Same trap latent in `bgBuildRoomVtxBounds` (`gdl.dma.par>>4&0xf`
reads PC bits 4-7, not N64 bits 20-23 — currently tolerated). Rule: for
anything but the opcode, extract the bit-field explicitly from
`(u32)gdl->words.w0` / `.w1`, don't trust the named `.dma.*` sub-fields.
Also: a `words.w0 << k >> m` bit-extract idiom that relied on 32-bit
truncation on N64 must be `(u32)`-cast first on PC (`words.w0` is 64-bit).
- **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
+74 -7
View File
@@ -3386,7 +3386,13 @@ bool bgTestRayIntersectionInRoom(coord3d *from, coord3d *to, coord3d *dir, RoomV
* 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;
/* D154 addendum (M-30): the N64 line reads BYTE 1 of word0 -- bits 16-23,
* the G_VTX params byte ((n-1)<<4 | v0) -- so vtxoff = v0. The prior port
* used gdl->dma.par, but the PC Gdma_le shim maps `par` to bits 0-23 of
* word0 (the packed length field), so `& 0xf` yielded 0 (len == 16*n is
* always 16-aligned), silently forcing vtxoff = 0. Extract byte 1
* explicitly from the recovered 32-bit word0 instead. */
vtxoff = ((u32)gdl->words.w0 >> 16) & 0xf;
temp.vertices = (Vertex *)g_BgRoomInfo[roomnum].vertices;
vtxbase = (Vertex *)((uintptr_t)temp.vertices + ((u32)gdl->words.w1 & 0x00ffffff));
#else
@@ -3404,6 +3410,23 @@ bool bgTestRayIntersectionInRoom(coord3d *from, coord3d *to, coord3d *dir, RoomV
op = *((s8 *) gdl);
#endif
#ifdef PORT
/* D154 diagnostic: GE_D154=1 logs the parsed command / vtx-index stream
* for the first 64 invocations. This function only runs when a bullet
* resolves against background geometry, so it is invisible to the
* no-input level sweep -- verification of the GBI parse is playtest-gated
* (shoot a wall/floor on an idle machine and eyeball this stream). */
{
static s32 d154_calls = 0;
d154_calls++;
if (d154_calls <= 64 && getenv("GE_D154")) {
osSyncPrintf("D154 call#%d room=%d gdlidx=%d vtxoff=%d op=%d hdr w0=%08x w1=%08x\n",
d154_calls, (int)roomnum, (int)point->gdlindex, (int)vtxoff,
(int)op, (unsigned)(u32)gdl[-1].words.w0, (unsigned)(u32)gdl[-1].words.w1);
}
}
#endif
if ((op != G_VTX) && (op != ((s8) G_ENDDL)))
{
local.roominfo = temp.roominfo;
@@ -3423,6 +3446,11 @@ bool bgTestRayIntersectionInRoom(coord3d *from, coord3d *to, coord3d *dir, RoomV
idx[0] = (((u8 *) gdl)[5] / 10) - vtxoff;
idx[1] = (((u8 *) gdl)[6] / 10) - vtxoff;
idx[2] = (((u8 *) gdl)[7] / 10) - vtxoff;
#endif
#ifdef PORT
if (getenv("GE_D154"))
osSyncPrintf("D154 TRI1 w1=%08x idx=%d,%d,%d\n",
(unsigned)(u32)gdl->words.w1, (int)idx[0], (int)idx[1], (int)idx[2]);
#endif
i = 0;
@@ -3604,6 +3632,12 @@ bool bgTestRayIntersectionInRoom(coord3d *from, coord3d *to, coord3d *dir, RoomV
idx2[2] = (((u32) ((u16 *) gdl)[1]) >> 12) - vtxoff;
}
#endif
#ifdef PORT
if (getenv("GE_D154"))
osSyncPrintf("D154 TRI4 s2=%d w0=%08x w1=%08x idx2=%d,%d,%d\n",
(int)s2, (unsigned)(u32)gdl->words.w0, (unsigned)(u32)gdl->words.w1,
(int)idx2[0], (int)idx2[1], (int)idx2[2]);
#endif
i = 0;
@@ -3838,28 +3872,61 @@ bool bgTestBulletHitBackground(coord3d *from, coord3d *to, s32 roomnum, struct H
{
point = (RoomVtxBatchBounds *)hit->tricmd;
if (((u8 *)((Gfx*)point))[0] != G_SETTILE)
#ifdef PORT
/* D154 sibling (M-30): unported N64 GBI parser walking the same
* D85-widened 16-byte room DL. On N64 ((u8*)gdl)[0] is the opcode
* byte and ((u8*)gdl)[1] is byte 1 of word0; on PC those raw indices
* read the low bytes of the 64-bit word0 (garbage). Recover the
* opcode from bits 24-31 of the 32-bit word0, byte 1 from bits 16-23.
* tilesize keeps the N64 (w0 << 11) >> 30 = bits 19-20 idiom but must
* force 32-bit width first (words.w0 is 64-bit on PC). ABI/GBI-parse
* only, no behaviour change. */
if ((((u32)((Gfx*)point)->words.w0 >> 24) & 0xff) != G_SETTILE)
{
while ((Gfx *)g_BgRoomInfo[roomnum].ptr_expanded_mapping_info < ((Gfx*)point))
while ((Gfx *)g_BgRoomInfo[roomnum].ptr_expanded_mapping_info < ((Gfx*)point))
{
point = (RoomVtxBatchBounds *)((Gfx *)point - 1);
if (((u8 *)((Gfx*)point))[0] == G_SETTILE)
if ((((u32)((Gfx*)point)->words.w0 >> 24) & 0xff) == G_SETTILE)
{
break;
}
}
}
if (((Gfx*)point) == g_BgRoomInfo[roomnum].ptr_expanded_mapping_info)
if (((Gfx*)point) == g_BgRoomInfo[roomnum].ptr_expanded_mapping_info)
{
hit->tileformat = -1;
hit->tilesize = -1;
}
else
}
else
{
hit->tileformat = (((u32)((Gfx*)point)->words.w0 >> 16) & 0xff) >> 5;
hit->tilesize = ((u32)((Gfx*)point)->words.w0 << 11) >> 30;
}
#else
if (((u8 *)((Gfx*)point))[0] != G_SETTILE)
{
while ((Gfx *)g_BgRoomInfo[roomnum].ptr_expanded_mapping_info < ((Gfx*)point))
{
point = (RoomVtxBatchBounds *)((Gfx *)point - 1);
if (((u8 *)((Gfx*)point))[0] == G_SETTILE)
{
break;
}
}
}
if (((Gfx*)point) == g_BgRoomInfo[roomnum].ptr_expanded_mapping_info)
{
hit->tileformat = -1;
hit->tilesize = -1;
}
else
{
hit->tileformat = ((u32)((u8 *)((Gfx*)point))[1]) >> 5;
hit->tilesize = (((Gfx*)point)->words.w0 << 11) >> 30;
}
#endif
}
return found;