D115: fix gunfire THROW* raw struct player offsets; M-6 D113/D114 findings

D115: THROWMTX/THROWPOS/THROWPREV in gunfire.c were raw byte offsets
((u8*)g_CurrentPlayer + handnum*sizeof(struct hand) + 0xAD8/0xB08/0xB48).
Both the stride (PC sizeof(struct hand) ~0x968 vs N64 0x3B8, 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 casing-ejecting shot. The offsets are exactly
hands[handnum].throw_item_pos_related[_prev] and its translation row -
macros now use those field accessors under #ifdef PORT; N64 build
unchanged. Full survey in docs/AUDIT-M6-player-offsets.md (10 sites,
3 HIGH fixed here, 2 MED open incl. the likely 'weapon model doesn't
draw' cause).

D113 (investigation, no code change): the 'portal-BFS under-reach / void'
premise is stale - the BFS output is correct for BUNKER1's topology
(rooms 26/27 are behind PORTALFLAG_DISABLED closed-door portals, culled
identically on N64). The void is the closed-door models obj 153/154 not
rendering in their doorway -> D114 track.

D114 (investigation, no code change): joint-matrix chain + d43_emit.py
float emission verified clean post-D112; basemtx is a proper
right-handed +Y-up lookat, joint render_pos coherent and upright. All
plan hypotheses disproven. Residual = a horizontal mirror in the shared
port/fast3d viewport / MP-matrix path vs GE's left-handed screen
convention (also explains the open D75 mirrored-text / logo items).
Still open.

Plan + agent reports: docs/PLAN-M6-playable.md. §F: D113/D114/D115.
This commit is contained in:
James Dansereau committed 2026-08-29 09:53:00 -04:00
1 parent 64698f6b23
commit 7bdb19e96f
5 files changed
+342 -13

No files matched your search

+125
View File
@@ -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.”*
+27 -13
View File
@@ -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):**
+82
View File
@@ -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
+94
View File
@@ -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="<range>:<stride>"` +
`tools_pc/pixcount.py` / `tools_pc/pixcount.py`.
- Sidecar regen (if a converter changes): `python tools_pc/d43_emit.py
ntsc-final`.
+14
View File
@@ -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;