diff --git a/docs/AUDIT-M6-player-offsets.md b/docs/AUDIT-M6-player-offsets.md new file mode 100644 index 00000000..87041c99 --- /dev/null +++ b/docs/AUDIT-M6-player-offsets.md @@ -0,0 +1,125 @@ +# AUDIT-M6 — `struct player` / `struct hand` raw-offset pass + +Survey only (Track 3 of `docs/PLAN-M6-playable.md`). No source changed, no +build. Feeds a follow-up fix session. Companion to §F D98/D100/D101/D102. + +## Method + +Grep of `src/` for: `handoffset`, `(u8 *)g_CurrentPlayer`, `g_CurrentPlayer + 0x`, +`+ 0x[3-hex]`, `sizeof(struct hand|player)`, `* sizeof(struct`, hardcoded +`0x2A80`/`0x2A70`, `->hands[i] +`, punned `->m[` / inline-Model accesses, +`bondheadmatrices`, `copy_of_body_obj_header`, `field_59C..field_6CC`, +`field_654`, `gaitRwData`. Cross-checked every hit against the live +`struct player` / `struct hand` definitions in `src/game/bondview.h` +(player @ line 314, hand @ line 119). + +## PC layout facts (from bondview.h + bondtypes.h) + +- `struct hand` (bondview.h:119): N64 `sizeof` ≈ **0x3B8** (base 0x870→0xC28 + inside `player`). PC adds: `audioHandle` +4, `rocket` (`AttachedObj*`) +4, + `mtxlist` (`Mtxf*`) +4, and the D102 `#ifdef PORT` tail + `struct Model weaponModel` (~0x2A8) + `u32 weaponRwPool[192]` (0x300). + **PC `sizeof(struct hand)` ≈ 0x968** — roughly **+0x5B0** vs N64, and *not* + a clean multiple, so `handnum * sizeof(struct hand)` de-syncs for `handnum==1`. +- `struct player` pointer growth **below N64 offset 0x594** (contradicts the + D100 note that “anything below 0x594 is unaffected”): `cameratile` (0x34), + `prop` (0xA8), `bodyModel` (0xD4), `autoaim_target_y` (0x130), + `autoaim_target_x` (0x140) → **+0x14** by offset ~0x144. `field_10C4`.. + and the `bloodImg*` / `InvItem*` / `textoverride*` blocks add much more + higher up. +- `model` (N64 0x598): D100 made it an **inline `struct Model` (~0x2A8)** + replacing an 8-byte pointer → **+0x2A0** for everything after it, before + `field_59C..field_6CC` (which D100 grep-verified dead except `field_654`). +- `ptr_hand_weapon_buffer[2]` +0x8; `copy_of_body_obj_header[2]` — inline + `ModelFileHeader` grew 0x20→0x30 (4 pointers ×+4) → **+0x20**; + `item_related[2]` `struct texpool` pointer growth; `bondheadmatrices` / + `viewports` unchanged. +- Net delta from `player` base to `hands[]`: **≈ +0x2E0** (0x14 + 0x2A0 + + 0x08 + 0x20 + texpool). So N64 `hands[0]` @ 0x870 is at PC **≈ 0xB50**; + N64 `hands[0].throw_item_pos_related` @ 0x870+0x268 = **0xAD8** is at PC + **≈ 0xDB8**. + +## Findings table + +| # | file:line | accessor | targets (N64 field) | N64 off | PC off (real) | PORT-adj? | BUNKER1 FP reachable? | severity | recommended fix | +|---|-----------|----------|---------------------|---------|---------------|-----------|----------------------|----------|-----------------| +| 1 | `src/game/gunfire.c:4960` `THROWMTX` (macro) used @ 5043,5047,5070,5126,5360,5364,5387,5443 | `(Mtxf*)((u8*)g_CurrentPlayer + handoffset + 0xAD8)` (`0xAD0` EU) | `hands[handnum].throw_item_pos_related` | 0xAD8 (h0) / 0xAD8+0x3B8 (h1) | ≈0xDB8 (h0) / ≈0xDB8+0x968 (h1) | **NO** | **YES** — `sub_GAME_7F068508` runs on every shot with a cartridge (PP7/KF7/…), solo only (`getPlayerCount()>=2` early-out) | **HIGH — memory corruption.** Raw 0xAD8 on PC lands inside the inline `model` / `field_59C` region of `struct player`; `matrix_4x4_copy(THROWMTX,…)` writes 64 bytes there. `handnum==1` (dual wield) additionally uses the wrong stride. Corrupts the gait `Model` / bondhead matrices → wrong Bond-body render or a delayed fault in model code. | Replace macro body with `(&g_CurrentPlayer->hands[handnum].throw_item_pos_related)`. Drop `THROWMTX_OFFSET`, `handoffset`. | +| 2 | `src/game/gunfire.c:4961` `THROWPOS(k)` used @ 5112-5114, 5164-5166, 5429-5431, 5481-5483 | `((f32*)((u8*)g_CurrentPlayer + handoffset + 0xB08))[k]` (`0xB00` EU) | `hands[handnum].throw_item_pos_related.m[3][k]` (translation row) | 0xB08 | ≈0xDE8 | **NO** | **YES** (same path as #1) | **HIGH — silent-wrong + reads corrupted memory.** Feeds `casing->vel` from the frame-to-frame delta of a garbage location → casings fly wrong / NaN, and the read is OOB-ish relative to intent. | `g_CurrentPlayer->hands[handnum].throw_item_pos_related.m[3][k]` | +| 3 | `src/game/gunfire.c:4962` `THROWPREV(k)` used @ 5112-5114, 5164-5166, 5429-5431, 5481-5483 | `((f32*)((u8*)g_CurrentPlayer + handoffset + 0xB48))[k]` (`0xB40` EU) | `hands[handnum].throw_item_pos_related_prev.m[3][k]` | 0xB48 | ≈0xE28 | **NO** | **YES** (same path) | **HIGH** (same as #2) | `g_CurrentPlayer->hands[handnum].throw_item_pos_related_prev.m[3][k]` | +| 4 | `src/game/gunfire.c:5031` and `:5348` | `handoffset = handnum * sizeof(struct hand)` | array stride for `hands[handnum]` | 0x3B8 | 0x968 (and non-uniform) | **NO** (the raw math is only consumed by #1–#3) | YES | **HIGH** — root of #1–#3’s `handnum==1` error; harmless once #1–#3 use `hands[handnum].field`. | delete; fold into the field accessors above. | +| 5 | `src/game/gunfire.c:600-661` weapon-model setup (`copy_of_body_obj_header[handnum]` → `modelInit(HAND_WEAPON_MODEL, mdlhdr, HAND_WEAPON_RWPOOL)`; `hand->mtxlist = rwmtx`; `render_pos = (RenderPosView*)rwmtx`) | field-name access + D102 macros | `hands[h].weaponModel` / `weaponRwPool` / `mtxlist`; `copy_of_body_obj_header[h]` (inline `ModelFileHeader`) | — | — | **YES (D102)** for the Model/pool pun; `copy_of_body_obj_header` is a plain inline struct (layout-consistent) | YES — this *is* the 1P weapon draw path | **MED — prime suspect for “weapon model doesn’t draw.”** `rwmtx` is `dynAllocate`’d (transient per-frame arena) but `weaponModel.render_pos` is set to it once at setup and consumed at render (`gunRenderFirstPersonGunModels:1605`); on N64 `render_pos` aliased the *persistent* `mtxlist` field, here it can point at reused arena memory. Also `flashvisptr = HAND_WEAPON_RWPOOL + node->…RwDataIndex` (gunfire.c:640) assumes N64 word indices into a pool whose PC records are ~2×. | Verify `dynAllocate` lifetime vs. render; consider persisting the matrix list. Re-check `weaponRwPool[192]` sizing against `modelCalculateRwDataLen(mdlhdr)` on PC. Confirm `subdraw` matrix path (shared with Track 2). | +| 6 | `src/game/bondview2.c:3155-3162` (PORT branch of `sub_GAME_7F07E7CC`) | `(Model*)&g_CurrentPlayer->something_with_watch_object_instance`; `static u8 watchRwPool[0xC8]`; `g_CurrentPlayer->step_in_view_watch_animation = 0` | watch-menu item preview Model punned at N64 player+0x230, pool at +0x2EC | 0x230 / 0x2EC | 0x244 / 0x300-ish | **YES (D56)** but suspect | YES — open watch menu / cycle to an item page | **MED — under-reservation.** D56 hosts the pool in a `0xC8` static (N64 capacity) but PC RW records are wider; and `animInit` writes a full inline `struct Model` (~0x2A8) starting at `&something_with_watch_object_instance`, overrunning `field_234..watch_scale_destination..` (N64 those were the tail of the 0xBC Model hole; on PC they are live watch fields). | Give the watch Model real inline storage in `struct player` under `#ifdef PORT` (D100 pattern) + size `watchRwPool` from `modelCalculateRwDataLen`. | +| 7 | `src/game/gunfire.c:4951-4957` | `#define THROWMTX_OFFSET 0xAD8` etc. | (the constants behind #1–#3) | — | — | NO (EU/non-EU split only, both N64) | — | INFO — remove with #1–#3. | delete block. | +| 8 | `src/game/player.c:136` | `mempAllocBytesInBank((sizeof(struct player)+0xF)&~0xF, …)` | player allocation | — | — | **YES (D98)** | — | OK — listed for completeness; correct. | none | +| 9 | `src/game/initBondDATAdefaults.c:103` | `animInit(&g_CurrentPlayer->model, &player_gait_object_header, g_CurrentPlayer->gaitRwData)` | inline gait Model + `gaitRwData[256]` | — | — | **YES (D100)** | YES | OK. | none | +| 10 | `src/game/bondhead.c:217-480`, `bondview2.c:922-9102` | `&g_CurrentPlayer->model`, `bondheadmatrices[0].m[..]` (field-name) | inline gait Model + head matrices | — | — | **YES** (D100 makes field access correct; single TU layout) | YES | OK — no raw offsets; safe as long as D100 inline-Model stays. | none | + +### Not player/hand but same class (noted, out of scope for the fix list) + +- `src/game/gunfire.c:5513` `render_data.mtxlist = (Mtxf*)model_matrices` — local, fine. +- `initexplosioncasing.c`, `initobjects.c`, `propobj.c`, `glass2.c`, + `initunk_005520.c` — all `N * sizeof(struct X)` allocations, already + `sizeof`-correct (D57 covered the rwdata capacities). +- `ramromreplay.c` `+0x110/+0x1F8/+0x21E` — `ramromfilestructure`, handled by D66/D87, unrelated. + +## Total + +**10 distinct offset sites; 3 live HIGH-severity bugs (findings #1–#3, one +underlying cause #4), 1 dead constant block (#7), 2 MED items to verify +(#5, #6), 4 already-correct (#8–#10).** + +All 3 HIGH bugs are the same construct in `gunfire.c` and are reachable in +BUNKER1 first-person the instant the player fires a weapon that ejects a +casing (every ballistic weapon). They are NOT reachable before the first +shot, so they do not explain a weapon model that is missing from frame 1 — +but once firing starts they corrupt the inline gait `Model` region and will +produce cascading render/anim breakage or a fault. + +## Where the “1P weapon model doesn’t draw” bug most likely lives + +Not in a surviving raw offset — D102 already gave the weapon `Model` and its +RW pool dedicated storage, and `gunRenderFirstPersonGunModels` / +`gunTickHandState` use field names. Best candidates, in order: + +1. **Finding #5 — the D102 wiring in `gunfire.c:600-661`.** `weaponModel.render_pos` + is pointed at a `dynAllocate`’d transient (`rwmtx`) that on N64 aliased the + *persistent* `hand->mtxlist` field; if the arena is recycled between the + hand-tick setup and `gunRenderFirstPersonGunModels`, the model draws with + garbage / off-screen matrices (looks like “not drawn”). Check `dynAllocate` + lifetime and whether `weaponModel` should own its matrix list on PC. +2. **The `subdraw` / `subcalcmatrices` / `process_02_position` matrix + composition** — shared with Track 2 (inverted characters). A handedness / + lookat-sign error there hides the weapon model just as it inverts guards. +3. **`field_87F` gating** (`gunfire.c:520`, `:587-598`) — if the setup branch + that sets `field_87F = 1` and calls `modelInit` isn’t taken (e.g. + `get_ptr_weapon_model_header_line(item)` returns NULL because + `ptr_hand_weapon_buffer` / weapon-model load failed), the render loop + `continue`s and nothing is emitted. Cheap to instrument. + +## Recommended fix order + +1. **#1–#4 & #7 (gunfire.c THROW macros)** — one mechanical change, no layout + work: rewrite `THROWMTX`/`THROWPOS`/`THROWPREV` as + `g_CurrentPlayer->hands[handnum].throw_item_pos_related[_prev]` field + accesses (`.m[3][k]` for the POS/PREV variants), delete `handoffset` and + the `THROW*_OFFSET` defines. Wrap in `#ifdef PORT` keeping the N64 macro + for the N64 build (or, since the field access is identical semantics on + both, use it unconditionally — verify byte-match). Log as §F “D115”. +2. **#5** — investigate the weapon-model `render_pos` / `mtxlist` lifetime and + `weaponRwPool` sizing; this is the likely playability blocker. May fold + into the Track-2 matrix work. +3. **#6** — give the watch preview Model inline PORT storage + sized pool + (only matters once the player opens the watch in-level). +4. Add a debug assert in `gunfire.c` weapon-model setup logging + `mdlhdr->numRecords` vs `weaponRwPool` capacity and `field_87F` state, to + settle candidate 3 above. + +## §F pointer + +Add to `docs/PCPortResearch.md` §F after D112: *“**D115 (audit)** — +`struct player`/`struct hand` raw-offset pass: see +`docs/AUDIT-M6-player-offsets.md`. Live HIGH bugs = `gunfire.c` THROWMTX/POS/PREV +macros (`+handoffset+0xAD8/0xB08/0xB48`), not PORT-adjusted; fix = field +accessors. MED = D102 weapon `render_pos` transient-arena aliasing (likely +the ‘weapon model doesn’t draw’ cause) and the D56 watch-Model pool +under-reservation.”* diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index 3c04a0ae..6075cca1 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -54,20 +54,34 @@ screens draw content. See §F "D108–D112". complete leaf DLs (~15k/run, valid ptrs; fast3d transforms them). The storage-room void is NOT closed doors failing to render. -**Real remaining work, in order:** -1. **Portal-BFS under-reach in the storage area** — per-frame visible - room count drops to 1–2 in rooms 27–29 (`GE_D104`/probes now removed). - `projmatrix` + `screensize` verified valid; the 200→3 `g_RoomLoadBudget` - drop is intentional N64 FP-cam behaviour. Likely `bgQueuePortalTraversal` - / `bgDetermineVisibleRooms` portal-projection. This is the void. -2. **Character pose / orientation polish** — post-D112 a BUNKER guard - renders but appears inverted; suspect a matrix-handedness / lookat - sign item in the joint base matrix. Also the mispositioned static - crate prop (top-left in the control room). -3. **`struct player` offset pass** — weapon model doesn't draw; gate to - playability (landmine below + D102). +**Real remaining work, in order (updated session M-6):** +1. **Shared fast3d screen mirror (D114 → D75 class)** — the single most + likely cause of: inverted BUNKER guard, mispositioned Nintendo logo, + mirrored front-end text, AND the storage-room "void" (D113 proved the + portal BFS is correct — the void is the closed-door models obj 153/154 + rendering outside their doorway). The joint-matrix chain and the model + converter were **verified clean** in M-6 (see §F D114); the residual is + an X (± Y) flip in `port/fast3d`'s viewport / MP-matrix path vs GE's + left-handed screen convention (`gmain.s` / `bondview.c:726`). Suspects: + `gfx_calc_and_set_viewport` / `gfx_adjust_viewport_or_scissor` + (`gfx_pc.cpp:1739/1771`), `MP_matrix` (`gfx_pc.cpp:1122`). **Rebuild + with D112+D115 and eyeball a guard first** — D115 stops a per-shot + 64-byte scribble that could itself present as "inverted". +2. **Weapon model doesn't draw (D115 #5)** — D102 pointed `weaponModel. + render_pos` at a transient `dynAllocate` arena; on N64 it aliased the + persistent `hand->mtxlist`. Gate to playability. May fold into #1. +3. **Rest of the `struct player` offset pass** — D115 fixed the HIGH + `gunfire.c` THROW* bugs; #6 (watch-preview Model pool) + the broader + audit remain (`docs/AUDIT-M6-player-offsets.md`). 4. Add an f32 value spot-check to `d43_emit.py`'s verify pass (it only - checks pointers/opcodes today — this bug passed "ALL CHECKS PASSED"). + checks pointers/opcodes today — the D112 bug passed "ALL CHECKS PASSED"). + +**Session M-6 (this session) — see §F D113/D114/D115, `docs/PLAN-M6-playable.md`, +`docs/AUDIT-M6-player-offsets.md`, `docs/AGENT-WORKFLOW.md`, +`docs/PORT-LEARNINGS.md`.** D113: portal BFS is correct, not the void. +D114: matrix chain + converter verified clean, residual = shared fast3d +mirror (open). D115: player raw-offset audit + `gunfire.c` THROW* fix +(uncommitted). Build green. **This session's fixes (all committed, master):** diff --git a/docs/PCPortResearch.md b/docs/PCPortResearch.md index 481f8aad..7cb2fac7 100644 --- a/docs/PCPortResearch.md +++ b/docs/PCPortResearch.md @@ -3441,6 +3441,88 @@ values — a float spot-check is worth adding. Probes `GE_D96GATE / D96DL / D108 / D109 / D110 / D111 / D112 / GE_TRICNT` were all removed after the fix. +**D113 (session M-6) — "portal-BFS under-reach / storage-room void" was a +STALE premise; there is no BFS bug.** A subagent dumped BUNKER1's runtime +portal table and traced every accept/reject decision of `sub_GAME_7F0B7F84` +(non-EU/LEFTOVERDEBUG copy, `bg.c:4091`; queue = 5-arg `bgQueuePortalTraversal` +`bg.c:3843` — confirmed the compiled ones). With the camera in room 29 the +BFS visits portals 25,26,27,28: portals 25 (r29↔r26) and 26 (r29↔r27) +carry `PORTALFLAG_DISABLED` (`controlbytes1 & 1`) because door props obj +153/154 spawn `openPosition==0` → `doorDeactivatePortal`, and nothing +re-opens them in the fly-through — so they are culled at `bg.c:4138`, a +site that is **not** `#ifdef PORT`-guarded and behaves identically on N64. +Result: a stable 3 visible rooms {29,28,25} (D104 probe agrees), which is +the correct answer for this topology, not "1–2". No s32/pointer +truncation, no `min>max`/NaN reaching an accept flip, no depth-cap +(`D_8004489C=0xF`) or visited-set (`>=9`) early-out firing. `sub_GAME_7F0B5864` +is fine post-D106. **The void = the two closed-door models (obj 153/154) +filling rooms 26/27's doorways not rendering where they should** — +`GE_D96` shows room 29's prop pass emits their leaf DLs, so the door +geometry exists but its world transform puts it outside the doorway, +leaving the wall hole unoccluded → sky. This is the D114 model-transform +track, not visibility. No `bg.c` change. Probes reverted. + +**D114 (session M-6) — character inversion / mispositioned crate: +OPEN, matrix chain verified sane, residual is a shared fast3d mirror +(D75 class).** A subagent statically traced the whole joint-matrix chain +(`chr.c:2564` → `subcalcmatrices` → `modelUpdateMatrices` → +`process_02_position` `model.c:1662` → `modelBuildGroupMatrices` +`model.c:1318` → `matrix_4x4_multiply_homogeneous` → +`bondviewTransformManyPosToViewMatrix` → `matrix_4x4_f32_to_s32` +`matrixmath.c:495` → `gfx_sp_matrix`) and an env-gated `GE_D114` probe +(24 joint builds, 3+ BUNKER guards). Findings: `basemtx` from +`camGetWorldToScreenMtxf()` is a proper orthonormal right-handed lookat +(+Y up, `up ≈ (-0.09, 0.96, -0.26)`, `right = up × forward`), scaled by +`bgGetLevelVisibilityScale()` = 0.1 (also applied to the room path — +expected); joint `render_pos` are coherent and **upright** (head y ≈ 280 +> pelvis ≈ 230 > limb ends ≈ 132); view-space z negative (in front, +correct). Converter clean: `put_f32` is now the only float writer and is +correct post-D112; `matrix_4x4_f32_to_s32` is byte-identical to +`guMtxF2L`; `matrix_4x4_set_lookat` / `guPerspectiveF` / +`matrix_4x4_multiply_homogeneous` match N64 and are native-LE. So the +plan's hypotheses (converter axis/sign, lookat handedness, compose +order, F2L) are all **disproven**. Leading residual: a horizontal (± +vertical) mirror in the **shared fast3d viewport / MP-matrix path**, not +the model path — consistent with the open D75 notes ("front-end text +draws mirrored", "Nintendo logo mispositioned"): a flip is only visible +on asymmetric content (humanoid, text, logo) and invisible on a boxy +room. GE bakes a left-handed screen convention into its own transforms +(`transform3Dto2DCoords`, `bondview.c:726`: `screenX = center − +x·invz·scale`); if GE's RSP (`rsp/graphics/gmain.s`) applies an X flip +that `port/fast3d` does not, every model and room is mirrored. Suspects: +`gfx_calc_and_set_viewport` / `gfx_adjust_viewport_or_scissor` +(`gfx_pc.cpp:1739/1771`), `MP_matrix` vertex transform +(`gfx_pc.cpp:1122`). This is a fast3d-correctness gap → write-up, not a +narrow ABI patch. Also: **rebuild with D112+D115 and eyeball a guard +first** — the D115 `gunfire.c` fix stops a per-shot 64-byte scribble into +the inline-Model / bondhead-matrix region that could itself have caused +"inverted". The crate: static-prop placement comes via a per-prop +`basemtx` from the Usetup `PropRecord` position/rotation — check +`d88_emit.py` separately from the character question. Probe reverted; +re-apply snippet in the M-6 agent report. + +**D115 (session M-6) — `struct player` / `struct hand` raw-offset audit + +first fix.** Full survey in `docs/AUDIT-M6-player-offsets.md` (10 offset +sites; 3 live HIGH, 1 dead const block, 2 MED, 4 already-correct). +**Fixed:** `gunfire.c:4960-4962` `THROWMTX` / `THROWPOS(k)` / `THROWPREV(k)` +were raw byte offsets — `(u8*)g_CurrentPlayer + handoffset + 0xAD8/0xB08/ +0xB48` with `handoffset = handnum * sizeof(struct hand)`. Both the stride +(`sizeof(struct hand)` ≈ 0x968 PC vs 0x3B8 N64, D102) and the base offset +are N64-sized, so on x86-64 the address lands inside the inline gait +`Model` / bondhead-matrix region of `struct player`, and +`matrix_4x4_copy(THROWMTX, …)` scribbles 64 bytes of live render state on +**every shot from a casing-ejecting weapon** (`sub_GAME_7F068508`, +solo only). The offsets are exactly +`hands[handnum].throw_item_pos_related[_prev]` and its translation row, so +the macros now use those field accessors under `#ifdef PORT` (N64 build +keeps the byte-offset macros unchanged). **Still open (MED):** #5 — the +D102 weapon-model `render_pos` is pointed at a `dynAllocate`'d transient +arena that on N64 aliased the persistent `hand->mtxlist`; likely the +"1P weapon model doesn't draw" cause, may fold into D114. #6 — the D56 +watch-preview Model pool (`watchRwPool[0xC8]`) is N64-sized and the +inline Model overruns live watch fields. See the audit doc for the full +table and recommended fix order. + **`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/PLAN-M6-playable.md b/docs/PLAN-M6-playable.md new file mode 100644 index 00000000..bfead3c9 --- /dev/null +++ b/docs/PLAN-M6-playable.md @@ -0,0 +1,94 @@ +# Plan — Session M-6: toward a playable, low-glitch BUNKER1 PC build + +Baseline: D112 committed (`67d674da`). Build green. `-level_09` renders +BUNKER1 continuously, crash-free. Three tracks below, in priority order. +Each track: **investigate → root-cause → narrow `#ifdef PORT` fix or +write-up** per AGENTS.md non-negotiables (no game-logic changes; ABI / +layout / format exceptions only, each documented in +`docs/PCPortResearch.md` §F). + +## Track 1 — Portal-BFS under-reach ("the void") [highest visual impact] + +**Symptom:** in BUNKER storage area (rooms 27–29) the per-frame visible +room count collapses to 1–2; large sky-fill void where an adjacent room +should show through an open doorway. Portal *visibility* is otherwise +healthy post-D106 (room 18 → 8 rooms deep on enabled doorways). + +**Known-good / ruled out:** `currentPlayerGetProjectionMatrix()`, +`g_CurrentPlayer->screensize`, the intentional `g_RoomLoadBudget` 200→3 +FP-cam drop, `zfar`, the plane-side metric cull (`bg.c:4135/4144`). + +**Suspects:** `sub_GAME_7F0B5864` screen-space portal projection +(`bg.c:1619`) — sibling of the D106 z==0 near-plane case, but for +partially-off-screen (not straddling) portals; `bgQueuePortalTraversal` +(`bg.c:3814/3843`) depth/dedup; `bgDetermineVisibleRooms` (`bg.c:4615`) +seed set; `sub_GAME_7F0B5528` clip-point generation (`bg.c:1551`); +`transform3Dto2DWithZScaling` (`bondview.c:730`). + +**Deliverable:** root-cause writeup in §F ("D113"); if a narrow PORT fix +is obviously correct (e.g. another degenerate-box / clamp mismatch), +apply it, else stop and document. + +**RESULT (Track 1 agent):** NOT a portal-BFS bug. The BFS output is +correct for BUNKER1's topology — rooms 26/27 sit behind portals 25/26 +which carry `PORTALFLAG_DISABLED` (closed doors), culled identically on +N64. Stable 3 visible rooms {29,28,25}, not "1–2". The void is the +**closed-door prop models (obj 153/154) rendering with a wrong world +transform** — their leaf DLs are emitted (`GE_D96`) but land outside the +doorway, leaving the wall hole unoccluded → sky. **Folds into Track 2** +(same model-matrix defect as the inverted guard / mislocated crate). No +`bg.c` change. Probes reverted, tree clean. + +## Track 2 — Character pose / orientation + +**Symptom:** post-D112 a BUNKER guard renders as a coherent humanoid but +appears **inverted**. Also a mispositioned static crate prop (top-left in +the control room). + +**Known-good:** joint *rotations* from the anim bitstream; `render_pos[]` +positions now sane (~±60 units) after the D112 f32 fix; `basemtx` from +`camGetWorldToScreenMtxf()` (`bondview.c:824`) was a clean orthonormal +lookat. + +**Suspects:** matrix handedness / lookat sign in the joint base matrix; +`modelBuildGroupMatrices` / `process_02_position` / `subcalcmatrices` +composition order on LE; a converter sign/axis issue in `d43_emit.py` +group `Origin` or matrix rodata that survived the D112 fix (byte order +right, axis convention wrong); guMtxF2L / Mtxf→Mtx path. + +**Deliverable:** root-cause writeup in §F ("D114"); narrow fix if +obviously correct. + +## Track 3 — `struct player` raw-offset audit [gate to playability] + +**Symptom:** 1P weapon model doesn't draw. Landmine (§F D100): raw +hardcoded-offset accessors into `struct player` / `struct hand` are NOT +PORT-adjusted and are already PC-wrong. + +**Known instances:** `gunfire.c` `THROWMTX_OFFSET 0xAD8` / `THROWPOS` +`0xB08` / `THROWPREV` `0xB48` (all `g_CurrentPlayer + handoffset + K`); +plus D102 (weapon Model pool) already fixed. + +**Task:** enumerate *every* raw byte-offset / hardcoded-size access into +`struct player`, `struct hand`, and siblings across `src/game/` (grep for +`+ 0x`, `handoffset`, `(u8 *)`, hardcoded `0x2A80`-class sizes, pointer +arithmetic on player). For each: N64 offset vs PC offset (struct is +0x2A8-class larger per D100), whether it's on a BUNKER1-reachable path, +severity. Produce a table + recommended fix order. Do NOT fix yet — +this is a survey feeding a follow-up. + +**Deliverable:** `docs/AUDIT-M6-player-offsets.md` table + §F pointer. + +## Execution + +- Tracks 1, 2, 3 dispatched as parallel investigation subagents + (read-mostly; each returns a writeup + optional narrow patch). +- Integrator (this session) reviews each, applies/adjusts verified narrow + fixes, rebuilds (`export PATH="/c/msys64/mingw64/bin:$PATH" && + ./build-pc.sh ntsc-final`), spot-checks `-level_09`, commits per-track + with §F docs. +- Repro: `./build-pc/ge007.x86_64.exe -level_09 -ml0 -me0 -mgfx100 + -mvtx50 -mt700 -ma150`. Frame capture: `GE_PCDUMP=":"` + + `tools_pc/pixcount.py` / `tools_pc/pixcount.py`. +- Sidecar regen (if a converter changes): `python tools_pc/d43_emit.py + ntsc-final`. diff --git a/src/game/gunfire.c b/src/game/gunfire.c index cb435cb2..ed81c4c1 100644 --- a/src/game/gunfire.c +++ b/src/game/gunfire.c @@ -4957,9 +4957,23 @@ CasingRecord* casingCreate(ModelFileHeader* header, Mtxf* mtx) #define THROWPOS_PREV_OFFSET 0xB48 #endif +#ifdef PORT +/* D115: these were raw byte offsets into `struct player` (via `handoffset = + * handnum * sizeof(struct hand)` + a hardcoded intra-hand offset). Both the + * stride and the base offset are N64-sized; on x86-64 `struct player` and + * `struct hand` are much larger (D98/D100/D102), so the raw math lands inside + * the inline gait Model / bondhead-matrix region and `matrix_4x4_copy(THROWMTX, + * ..)` scribbles 64 bytes of live state on every shot. The offsets are exactly + * `hands[handnum].throw_item_pos_related[_prev]` and its translation row, so + * use the field accessors directly. See docs/AUDIT-M6-player-offsets.md. */ +#define THROWMTX (&g_CurrentPlayer->hands[handnum].throw_item_pos_related) +#define THROWPOS(k) (g_CurrentPlayer->hands[handnum].throw_item_pos_related.m[3][k]) +#define THROWPREV(k) (g_CurrentPlayer->hands[handnum].throw_item_pos_related_prev.m[3][k]) +#else #define THROWMTX ((Mtxf *) ((u8 *) g_CurrentPlayer + handoffset + THROWMTX_OFFSET)) #define THROWPOS(k) (((f32 *) ((u8 *) g_CurrentPlayer + handoffset + THROWPOS_OFFSET))[k]) #define THROWPREV(k) (((f32 *) ((u8 *) g_CurrentPlayer + handoffset + THROWPOS_PREV_OFFSET))[k]) +#endif extern const f32 g_CasingSwitchScale; extern const f32 g_PistolCasingHorizontalSpeed;