diff --git a/docs/PCPortResearch.md b/docs/PCPortResearch.md index f877614a..feaa8b60 100644 --- a/docs/PCPortResearch.md +++ b/docs/PCPortResearch.md @@ -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. | diff --git a/docs/PORT-LEARNINGS.md b/docs/PORT-LEARNINGS.md index 02d00c4a..c355c86c 100644 --- a/docs/PORT-LEARNINGS.md +++ b/docs/PORT-LEARNINGS.md @@ -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 diff --git a/src/game/bg.c b/src/game/bg.c index d852a799..ab71fb3b 100644 --- a/src/game/bg.c +++ b/src/game/bg.c @@ -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;