M-13: 21-level sweep matrix + D123 (C1 fix) + D124 (C2 Jungle fix)

WS4 level sweep driven to completion: 12/21 solo levels PASS
(load + render + no-crash). Full matrix in docs/LEVEL-STATUS.md,
reusable runner tools_pc/level_sweep.sh, WS6 checklist docs/LEVEL-PLAYTEST.md.

Fixes:
- D123 (tools_pc/d88_propdefs.py): crash class C1 on 6 levels
  (Dam/Runway/Frigate/Statue/Streets/Cradle). D122 fallout - the widened
  Vehichle/AircraftRecord.ailist slot was zeroed instead of carrying its
  read-before-write integer AI-list id, so prop.c -> ailistFindById(0) ->
  GAILIST_AIM_AT_BOND -> ai() ran a CHR aim list with a NULL chr ->
  chrIsNotDeadOrShot(NULL). New OBJ_ID_WORDS arm. 4 levels -> PASS,
  Runway falls through to C2. BUNKER1/Silo unregressed.
- D124 (port/src/gimgfixup.c): crash class C2 on Jungle. Compiled
  explosion-DL pointer-sync keyed on an IMAGESEG marker that texLoad()
  had already erased -> compiled globalDL_0xNNN arrays kept link-time
  IMAGESEG words -> 0xABCDxxxx into fast3d on first explosion draw.
  Latent on every level. Detect the slot from the compiled array instead.

Diagnosed, NOT fixed (write-ups in PCPortResearch.md SF):
- C2-GDL (D124-Facility addendum): Facility/Runway - model texture-pool
  path yields a garbage tex->data; every model file loads at an odd
  bank base (mempAllocBytesInBank does no alignment). Suspect
  d43_emit.py/pcmodels sidecar texture-blob offsets (N64 8B vs PC 16B Gfx).
- C3/C6 (D125): Aztec/Bunker2/Surface2 - converted propDefs blob in RAM
  doesn't match d88_propdefs.py offline output (garbage after record 0);
  runtime sizepropdef() walk drifts ~100 records -> wrong door/modelid.
  d88_propdefs.py convert_stream + sizepropdef() PORT strides verified
  correct; suspect a dest-offset/pd_end bug in d88_emit.py propdefs emit.

Remaining: 9 crashes / 6 classes (C2-GDL, C3/C6, C2m Jungle G_MTX,
C5 Control BG portal, C4 Depot BG tile/room, C7 Surface1 sndSetupSound).
Priority order in docs/LEVEL-STATUS.md.

Also folds in the in-progress docs restructure (HANDOFF/AGENTS/CLAUDE,
PCPortResearch D121/D122 SF/SH entries, PLAN-linear-level-sweep,
HANDOFF-ARCHIVE, BACKLOG).
This commit is contained in:
James Dansereau committed 2026-08-29 16:36:18 -04:00
1 parent a8b174a829
commit 9d118e0b13
20 files changed
+2058 -493

No files matched your search

+21 -5
View File
@@ -4,7 +4,7 @@
- `n64decomp/007`: WIP decompilation of GoldenEye 007 (N64), byte-matches US/EU/JP ROMs.
- Active work: **PC port** modelled on the Perfect Dark PC port (same Rare "Indy" engine family).
- **Read `docs/PCPortResearch.md` first** — architecture, GE-specific RSP deltas, phased plan, and review findings (A1–D33 in §11; current handoff + plan in §H).
- **`docs/PCPortResearch.md` is the reference** — architecture, GE-specific RSP deltas, phased plan (§1–§10), and the Dxx finding log (§F + §H, indexed at the top of §F). Read the section you need; don't linear-read it. Current task lives in `docs/HANDOFF.md`.
- **Starting a fresh session?** Read `docs/HANDOFF.md` — the paste-ready brief with the immediate task, standing procedure, and environment gotchas.
- **Dispatching subagents?** `docs/AGENT-WORKFLOW.md` — task budgets/deadlines, file partitioning, pre-flight, the standard brief template. Every investigation subagent reads `docs/PORT-LEARNINGS.md` (recurring bug classes) first and appends to it.
@@ -20,7 +20,7 @@
| File | Role |
|---|---|
| `docs/PCPortResearch.md` | Research + plan + findings (A1–C2). Read first. |
| `docs/PCPortResearch.md` | Research + plan (§1–§10) + Dxx finding log (§F/§H, indexed at top of §F). Reference, not a linear read. |
| `CMakeLists.txt` | PC build (parallel to the N64 Makefile). Source list + `REGION_DEFS` live here. |
| `port/src/` | Shims: `libultra.c` (OS API), `gesched.c` (scheduler), `n64stubs.c` (boot/TLB/FPU/rmon), `random.c` (PRNG ported verbatim from `random.s`), `ucode.c` (microcode segment markers), `main.c`, `video.c`, … |
| `port/fast3d/` | Software RSP (adapted from the PD port). The main Phase 2 work. |
@@ -41,7 +41,7 @@ Needs CMake + SDL2 + zlib + OpenGL (MSYS2 packages: see `build-pc.sh` header). R
2. **Duplicates.** No symbol defined twice across the compiled set (watch `sp_*` stacks, `rmon*`, `os*` shims, segment markers).
3. **Syntax.** Every touched file must parse; once the toolchain is installed, `./build-pc.sh` is the final word.
Run `/linkcheck` for this sweep. Record new findings in `docs/PCPortResearch.md` §11 style (next label after the last used).
Run `/linkcheck` for this sweep. Record new findings in `docs/PCPortResearch.md` §F/§H style (next Dxx label after the last used) and add the label to the §F index.
## Phase status
@@ -49,6 +49,22 @@ Run `/linkcheck` for this sweep. Record new findings in `docs/PCPortResearch.md`
- **Compile+link milestone:** all ~235 TUs compile and `ge007.x86_64.exe` links clean (`ninja ge007 -k 0`, 236/236). Findings D7–D17 in `docs/PCPortResearch.md` §11; asset stubs in `port/src/assetstubs.c`, fast3d no-ops in `port/src/gfxstub.c` (delete when real fast3d lands).
- **Phase 1 (boot to window):** done — ROM loads/validates/maps at cart base 0x10000000 (`romdata.c`), SDL2 window + GL clear loop (`video.c`), absolute asset symbols from `scripts/gen_romassets.py` → `port/src/romassets_<r>.s` (findings D18–D23).
- **Phase 1.5 (boot to first frame):** DONE — boot chain complete AND real frames render (`[NOTE] frame N rendered in … us`). D34–D42 closed the blockers on top of D31–D33: ANIM_DATA lvalues, music seq-table ABI+endianness, music-heap sizing, libaudio bank-tree re-layout, implicit-declaration prototype shim (D38), Globalimagetable rebasing + true segment size (D39), ModelHitEntry pool sizing (D40), cross-thread GL context release (D41), rsp task-settings toggle (D42). ROM dump verified against the No-Intro good dump (byte-identical, header CRCs re-checked — `tools_pc/romverify.c`).
- **Phase 2 (rendering):** IN PROGRESS — fast3d integrated + real frames render. **Plan B EXECUTED (D50, committed):** all 512 NTSC model files converted offline by `tools_pc/d43_emit.py` → `data/pcmodels-ntsc-final/pcmodels.bin` + manifest.csv (single concatenated RZ image; regenerate per region with the script — sidecars are gitignored), served through the existing load path via a port-layer table patch (`port/src/pcmodels.c`, romdata cart-extension, one-shot `pcmodelsPatchTable()` + poolRemaining reset hook in `load_object_fill_header`); runtime C fixups for language-bank BE offset tables + font re-layout (D50.3/D50.4); legal-screen UB seed (D50.5); **D50.6 fixed the first model-render crash** — `texCopyGdls` copied only w0 of each 16-byte Gfx slot on PC (the union's trailing `long long` is 8 bytes), leaving stale mempool w1s in the compacted GDLs; byte-proven against PlegalpageZ, fixed with a full-slot copy under #ifdef PORT. Model GDLs now execute to completion (PlegalpageZ sub-DLs). **D51 RESOLVED** (font `pixeldata` fixup wrote the pointer at the wrong offsets — frame-5 `import_texture_i8` crash gone; GE opcode space decoded along the way, G_TEXRECT=0xE4/0xE5 non-F3DEX numbering), **D52 RESOLVED** (`osGetCount` now advances at the N64 RSP counter rate ≈ 46.5525 ticks/µs — fixed the non-deterministic post-frame-2 hang), **D53 RESOLVED** (model RW-data pool addressing: D53.1 `Model.datas` word-stride → `u32 *` under PORT + casts; D53.2 ModelSlot/AnimModelSlot re-laid on top of the PC struct Model — in-use@0x10 / pool@0x20, slot size = sizeof(Model) = 0xE8 — plus PORT-guarded heap-fallback bumps in model.c), **D54 RESOLVED** (cseq ALCMidiHdr BE header: port-layer `romdataFixupCseq` bswap32 of the first 17 u32s, called from music.c after each decompress), **D54b RESOLVED** (synth param slots sized for ALStartParamAlt = 0x28 in synthesizer.c alSynInit under PORT), **D55 RESOLVED** (RLE folder-menu background BE w/h header: bswap32 of the first word at `(u32 *)&unknown2` at cart-map time in romdataInit, guarded). Build GREEN; game boots, plays intro music, renders through logo/gun-barrel, instantiates Brosnan, and executes the gun-barrel hole DL clean to ENDDL. **D56 RESOLVED** (watch Model/RW-pool raw offsets: field-name Model access + static 0xC8-byte pool under PORT in bondview2.c), **D57 RESOLVED** (pointer-grown rwdata records overflowed N64-sized spare pools — Brosnan PC numRecords 153 > 140: capacities grown in initunk_005520.c + dynamic slot+pool fallback in model.c), **D58 RESOLVED** (gun-barrel DL: vertex-pointer K0 idiom rebuilt via `OS_K0_TO_PHYSICAL(…) | 0x80000000` in title.c + DL reserve 0x100→0x200 because `sizeof(Gfx)` is 16 on PC). **D59–D68 RESOLVED (committed):** the "GL DLL" crash was D64's unbounded blood-RLE write (sentinel section-adjacency lost on PC) clobbering the gun-barrel sub-DL; then D60–D62 (DMA target validation, OSMesgQueue layout), D65/D65b (HEADS/BODIES enum signed sentinels under PORT), D66 (romCopyAligned + ramrom replay 64-bit pointer width), D67 (struct image_entry N64 layout), **D68 (Globalimagetable BE→LE fixup** — `port/src/gimgfixup.c` bswaps IMAGESEG Gfx w1 words + sImageTableEntry.index after texReset's romCopy; compiled globalDL shadows synced for explosion.c). The game now renders the **entire intro** (logo → gun barrel w/ Brosnan → cast; ~frame 2100, ≈2 min wall-clock at the current ~20 fps clean-run rate). **D69 RESOLVED (committed):** `load_bg_file` no longer faults on stage load. BG-file headers/records are N64 big-endian segment-0x0F offsets with internal self-relative offsets; fixed via offline sidecar conversion (`tools_pc/d69_emit.py` → `data/pccg-<region>/pccg.bin` + manifest, same Plan-B pattern as D43/pcmodels, port-layer wiring in `port/src/pccg.c`) plus four ABI layout fixes surfaced along the way: D78 (`StandTile` id/room bitfield packing), D79 (`bg_room_data` pointer-width), D83 (`StandTileHeaderMid`/`Tail` bitfield packing — same class as D78, caused an infinite loop in `stanBuildRoomData` until fixed), D84 (hand-inlined `BG_SEG_TO_PTR`-style folds in room-DL loaders needed the same `(u32)`-cast 32-bit wrap the macro already has). **D85 OPEN, D86/D87 RESOLVED, D88 OPEN (current blocker) — found during/after D69 verification:** D85 — room primary/secondary DL binaries decompress correctly but `texLoadFromGdl` (the already-working model-GDL runtime converter) produces garbage from their content, not yet root-caused; safety-netted (bounds-checked in `bgBuildRoomVtxBounds` so it can't segfault, but geometry is wrong/empty). D86 (RESOLVED) — `modelInitRwData` crash was a single `(int)`-truncated pointer in `initplayergaitobject.c`'s embedded player gait-model init; fixed under `#ifdef PORT`. D87 (RESOLVED) — the front end's genuine attract-mode demo playback crashed on an unswapped big-endian `ramromfilestructure`; fixed with a `#ifdef PORT` byteswap fixup in `ramromreplay.c` (D54-cseq pattern).Line truncated
- **Phase 3 (audio + input):** libaudio → SDL — largely copy-and-adapt from PD (`mixer.c` 722 lines vs our 31-line stub; `input.c` SDL_GameController backend; §2.4); decide the ASP strategy (C2: `aspMain*` dummies in `ucode.c`).
- **Phase 2 (rendering):** IN PROGRESS. fast3d integrated; the **entire
intro renders** (logos → gun barrel → cast). BUNKER1 (`-level_09`)
**loads, renders recognisably** (textured rooms, skeletal guards) and
**survives a firefight** crash-free. Now pivoted **breadth-first** —
see `docs/PLAN-linear-level-sweep.md` (the plan of record): make
boot→menu→level-select functional, then sweep all 21 solo levels for
load+render+no-crash.
- **Cross-level blocker:** D88.4 — `Usetup*Z` `propDefs` record stream
not byteswapped by `tools_pc/d88_emit.py` (crashes in `setupDoor`).
- **Cosmetic defects are parked** in `docs/GRAPHICS-BACKLOG.md`
(D75 3D model transforms, D76 disclaimer, D77 audio, D114/D116 text
X-mirror). Below crash/level work.
- **Recurring sub-task:** ABI/layout fixes per the D32 procedure
(§H) as new asset types are touched — the classes are catalogued in
`docs/PORT-LEARNINGS.md`.
- Full finding history: `docs/PCPortResearch.md` §F + §H (D31–D121,
indexed at top of §F); session narrative in `docs/HANDOFF-ARCHIVE.md`.
- **Phase 3 (audio + input):** input layer DONE (D118 — `port/src/input.c`, SDL keyboard/mouse/gamepad → N64 pad; polish bugs D118a/b open). Audio remains: libaudio → SDL, copy-and-adapt from PD (`mixer.c` 722 lines vs our 31-line stub; §2.4); decide the ASP strategy (C2: `aspMain*` dummies in `ucode.c`).
- **Phase 4 (saves + polish):** file-backed EEPROM — build on PD's `fs.c` (file-backed save dir); PFS/motor already no-op stubs.
+24 -3
View File
@@ -6,13 +6,34 @@ checklists that were being skipped.
@AGENTS.md
## Doc load order — read deliberately, not everything
Context is scarce. Load by tier; do not blind-read whole files.
- **Tier 0 — always (auto-loaded):** `CLAUDE.md`, `AGENTS.md`. Nothing
else auto-loads.
- **Tier 1 — every session start, read fully (both are short):**
`docs/HANDOFF.md` (current state + next task + environment),
`docs/PORT-LEARNINGS.md` (recurring bug classes).
- **Tier 2 — on demand only, do NOT read start-to-finish:**
- `docs/PCPortResearch.md` §1–§10 — architecture / RSP deltas / phased
plan. Read the section you need.
- `docs/PCPortResearch.md` §F + §H — the Dxx finding log. **Jump to a
specific `Dxx` via the index at the top of §F.** Never linear-read.
- `docs/HANDOFF-ARCHIVE.md` — prior-session narrative (M-2…). Only when
tracing the history of one fix.
- `docs/AGENT-WORKFLOW.md` — before dispatching a subagent (also Tier 1
if you may dispatch this session).
- `docs/PLAN-linear-level-sweep.md` — the active plan of record.
## Session preflight — do this in your FIRST few tool calls, before acting
Having a concrete "next step" from the handoff is NOT license to skip this.
1. Read `docs/HANDOFF.md` — immediate task, standing procedure, environment gotchas.
2. Read `docs/PORT-LEARNINGS.md` — recurring bug classes (you will re-derive
bugs already catalogued here if you don't).
1. Read the Tier 1 docs: `docs/HANDOFF.md` (immediate task, standing
procedure, environment gotchas) and `docs/PORT-LEARNINGS.md`
(recurring bug classes — you will re-derive catalogued bugs if you don't).
2. Skim `docs/PLAN-linear-level-sweep.md` — the active plan of record.
3. If you may dispatch a subagent this session, read `docs/AGENT-WORKFLOW.md`
NOW — not at dispatch time.
+7
View File
@@ -0,0 +1,7 @@
All of these items should be completed once the main original game is playable from start to finish on PC with no crashing or major issues.
PC graphics settings - higher resolutions, anti aliasing, etc. Any rendering improvements but make them all optional, use Nightdive remasters or things like the Quake 1 remaster as a reference of offered features.
Co Op - find a way to implement local co op for the game on the singleplayer levels. The game already has local split screen multiplayer, but we'd want to extend netplay at least for LAN so that two PCs could run the port and play co op together.
Deathmatch - use the co op netplay to make deathmatch / regular multiplayer playable across 4 instances of the PC port on a local network
+122
View File
@@ -0,0 +1,122 @@
# BRIEF C1 — `chrIsNotDeadOrShot` crash on 6 levels (blocks the level sweep)
Repo: `REPO_ROOT` (git, branch `master`, Windows;
PowerShell + Bash tool).
## READ FIRST
- `docs/HANDOFF.md` (top) — state, environment, repro line.
- `docs/PORT-LEARNINGS.md` — **§A (pointer-width struct growth) and §C
(BE rodata on LE) are the prime suspects.** Also §A D119
(`weapons_held[]->chr` type-pun) — closest prior instance.
- `docs/PCPortResearch.md` §F entries **D119, D115, D101, D102, D56, D57**
(jump via the index — do NOT linear-read).
- `docs/AUDIT-M6-player-offsets.md` — the raw-offset landmine class.
- `AGENTS.md` non-negotiables (#2: narrow `#ifdef PORT` ABI/layout only,
N64 line kept under `#else`, logged in §F).
## SYMPTOM (WS4 sweep, this session — `docs/LEVEL-STATUS.md`)
`./build-pc/ge007.x86_64.exe -level_XX` (bare — pools auto-injected, D121),
crashes **early, before frame 1**, deterministically, at:
```
chrIsNotDeadOrShot src/game/chraction.c:4483 s8 currentaction = self->actiontype;
```
`EXCEPTION 0xc0000005`, FAULT ADDR `0x8` (Dam). Crash **`self` = the same
value as backtrace frame #1 = `0x1401296a0`** — an address *inside the
compiled binary's rodata*, in the `chraidata.c` global-AI-list blob
(`m_AimAtBond[]` / `m_*[]` at `chraidata.c:61+`). So a `ChrRecord *self`
is pointing at AI-list bytecode rodata — a pointer swap / pun / wrong
struct-offset read, not mere corruption.
Affected: **Dam 33, Runway 35, Frigate 26, Statue 22, Streets 29,
Cradle 41** (6). NOT Bunker1 09 / Silo 20 / Archives 24 / Train 25 /
Caverns 39 / Egypt 32 / Cuba 54 (those 7 PASS, render ~91%).
## TASK
**One question: why is a `ChrRecord*` actually an AI-list rodata pointer
on these 6 levels, and what is the narrow fix?**
Leading hypotheses:
1. A per-level `Usetup*Z` guard record (`GuardRecord` type 9 /
`GuardAttributeRecord` type 18, or the chr-spawn record) has a pointer
or `s16/s16` packed field converted wrongly — or not at all — by
`tools_pc/d88_propdefs.py` (cf. D122: the "generic" fallback arm
word-swaps sub-word fields). Enumerate every propDef `type` byte the 6
crashing levels emit vs the 7 passing ones
(`tools_pc/d88_propdef_scan.py` histogram) — the delta type is the
suspect. The 7 pass / 6 fail split is the strongest lead.
2. The AI-list bytecode pointers in `chraidata.c` `.rodata`
(`SetChrAiList`, list-table entries) are BE and consumed raw — a chr
gets `aiList` set to a byteswapped pointer, then a subsequent tick
treats it as / derefs it as a `ChrRecord*`. Check `chraifunc.c` /
`chraidata.c` list dispatch and any `AiListRecord`-style table.
3. `ChrRecord` / `GuardRecord` struct is pointer-width-grown on x86-64
and a per-level record is read from ROM bytes at N64 offsets (D56/D57
class) — `self->actiontype` at the wrong byte.
Determine which. Prefer an **offline converter extension**
(`d88_propdefs.py` / `d88_emit.py`, D43/D88 pattern) over a runtime fixup;
else a narrow `#ifdef PORT` ABI edit.
## FILES YOU MAY TOUCH
- `src/game/chraction.c`, `src/game/chr.c`, `src/game/chraidata.c`,
`src/game/chraifunc.c` (narrow `#ifdef PORT` only).
- `tools_pc/d88_propdefs.py`, `tools_pc/d88_emit.py`,
`tools_pc/d88_layoutprobe.c`, `tools_pc/d88_propdef_scan.py`.
- New probe files `tools_pc/c1_*.py` / env-gated `GE_C1` blocks.
- `docs/PORT-LEARNINGS.md` (append), `docs/PCPortResearch.md` §F (write up
as the next Dxx after D122 — check the §F index for the current max —
add to the index), `docs/LEVEL-STATUS.md` (flip the rows you fix).
- **NOT** `port/fast3d/*` or `src/game/propobj.c` (other tracks).
## KNOWN-GOOD / RULED OUT — do not re-investigate
- D121 per-level pool injection — works, committed.
- D122 propDef `obj` half-swap — committed `f2beae4b`; your bug is a
*different* record type/field (BUNKER1/Silo unaffected).
- BUNKER1 + Silo + Archives + Train + Caverns + Egypt + Cuba chr/AI setup
— works. Use them as the "good" control for the propDef-type diff.
- The intermittent BUNKER1 `loadobjectmodel.c:393` crash — D117
nondeterminism, unrelated.
## PRE-FLIGHT ATTACHED
- `./build-pc.sh ntsc-final` green (`export PATH="/c/msys64/mingw64/bin:$PATH"` first).
- Sidecars current for `f2beae4b`. Regen after any converter change:
`python tools_pc/d43_emit.py ntsc-final && python tools_pc/d69_emit.py ntsc-final && python tools_pc/d88_emit.py ntsc-final --regen`
- Repro: `./build-pc/ge007.x86_64.exe -level_33` (Dam). Crash log
`ge007.crash.log` at repo root. Symbolicate:
`addr2line -e build-pc/ge007.x86_64.exe -f -C 0x<PC>` (PATH first;
image base 0x140000000, PC in log is absolute).
- Frame capture `GE_PCDUMP="80-260:40"` → `./ppm/`; `python tools_pc/pixcount.py ppm/<last>.ppm`.
- **`timeout` does NOT kill the game on Windows.** Launch `... &`,
`sleep 24`, `taskkill //F //IM ge007.x86_64.exe`.
- gdb attach is fast: game running via `nohup ... &`, then
`gdb -batch -x cmds.txt -p <winpid>` (winpid = 4th col of `ps -p <bashpid>`).
Hardware watchpoint on the chr's `aiList` field, or on the bad `self`,
catches the corrupting write.
- PD analogue: check `PD_PORT_CHECKOUT` chr / guard
setup + its propdef converter before writing anything new.
## BUDGET
Structural → **~12 build→run→inspect cycles or ~90 min.** On expiry:
revert temp probes (leave only committed env-gated ones), write up spec
progress + best hypothesis in §F with an explicit confidence rating, list
every file you touched. A good half-solved write-up is a deliverable.
## CONSTRAINTS
No game-logic / control-flow changes. ABI/layout/format only, each
`#ifdef PORT` with the N64 line verbatim under `#else`, each documented in
§F. Prefer converter extension over runtime fixup. Revert your own temp
probes before reporting. Do not touch other tracks' uncommitted edits
(none expected).
## REPORT
1. Root cause — `file:line` evidence, which hypothesis, why the 6 differ
from the 7 passing levels.
2. Fix — the diff, or why-not if unresolved.
3. Verification — `-level_33/35/26/22/29/41` each boot to ≥1
non-degenerate frame (`pixcount.py` > a few %), AND `-level_09` +
`-level_20` unregressed (`tools_pc/framediff.py`).
4. Probes left in the tree (env-gated, capped).
5. Confidence rating.
6. Generalisable quirk appended to `docs/PORT-LEARNINGS.md`.
+81
View File
@@ -0,0 +1,81 @@
# BRIEF C2 — fast3d bad texture pointer on Facility + Jungle
Repo: `REPO_ROOT` (git, `master`, Windows;
PowerShell + Bash).
## READ FIRST
- `docs/HANDOFF.md` (top), `docs/PORT-LEARNINGS.md` (§C BE rodata, §D N64
hardware idioms fast3d doesn't emulate).
- `docs/PCPortResearch.md` §F entries **D69, D68, D39, D67, D107, D84,
D58** (texture / segment-address history — jump via §F index).
- `AGENTS.md` non-negotiables. `rsp/graphics/gmain.s` = GBI ground truth.
- PD analogue: `PD_PORT_CHECKOUT` `port/fast3d/`
texture import path — check first.
## SYMPTOM (WS4 sweep — `docs/LEVEL-STATUS.md`)
Bare `./build-pc/ge007.x86_64.exe -level_XX`, early deterministic crash:
| Level | # | Site | Fault addr |
|---|---|---|---|
| Facility | 34 | `import_texture_i8` `port/fast3d/gfx_pc.cpp:821` | `0x72181ee8` (looks like an unresolved N64 segment addr) |
| Jungle | 37 | `gfx_tex_normalize_source` `port/fast3d/gfx_pc.cpp:644` | `0xabcd0824` (sentinel / uninit value) |
fast3d is handed a garbage texture address. 7 levels render fine
(Bunker1/Silo/Archives/Train/Caverns/Egypt/Cuba) so the common path is
sound — Facility/Jungle pull in a texture asset or a GBI texture-image
segment address the others don't.
## TASK
Root-cause both (likely the same class) and apply the narrowest fix.
Hypotheses:
1. A per-level texture asset mis-converted / missing —
`tools_pc/d69_emit.py` (pccg path) or `port/src/pccg.c`. `0xabcd....`
is a classic uninit-slot sentinel — a texture slot that was never
filled because the converter skipped a format/opcode.
2. A GBI `gsDPSetTextureImage` segment address not resolved to a real
pointer before `import_texture_*` (segment-fold class — D58/D84;
`(u32)` 32-bit wrap on `BG_SEG_TO_PTR` / K0 folds). `0x72181ee8` is in
the N64 segment-mapped range.
3. A texture LOD / detail-tile path (D107) hit only by these levels.
## FILES YOU MAY TOUCH
- `port/fast3d/gfx_pc.cpp` (narrow — a resolve/guard at the import site).
- `tools_pc/d69_emit.py` (+ `tools_pc/d69_*` helpers), `port/src/pccg.c`,
`port/src/romdata*.c` (segment resolution).
- New probe files `tools_pc/c2_*` / env-gated `GE_C2` blocks.
- `docs/PORT-LEARNINGS.md` (append), `docs/PCPortResearch.md` §F (next Dxx
after the current §F-index max; add to index), `docs/LEVEL-STATUS.md`.
- **NOT** `src/game/chr*.c` or `src/game/propobj.c` (other tracks).
## KNOWN-GOOD / RULED OUT
- D121 pool injection, D122 propDef fix — committed, work.
- 7 levels render clean — the base texture path is fine.
- HUD/text X-mirror (D114/D116) — parked, do NOT re-trace (PORT-LEARNINGS §D2).
## PRE-FLIGHT
- `export PATH="/c/msys64/mingw64/bin:$PATH" && ./build-pc.sh ntsc-final` — green.
- Repro `./build-pc/ge007.x86_64.exe -level_34`. Crash log
`ge007.crash.log`. `addr2line -e build-pc/ge007.x86_64.exe -f -C 0x<PC>`.
- `GE_PCDUMP="80-260:40"` → `./ppm/`; `python tools_pc/pixcount.py`.
- `timeout` does NOT kill on Windows: `... &`, `sleep 24`,
`taskkill //F //IM ge007.x86_64.exe`.
- Regen sidecars after converter change:
`python tools_pc/d43_emit.py ntsc-final && python tools_pc/d69_emit.py ntsc-final && python tools_pc/d88_emit.py ntsc-final --regen`.
## BUDGET
**~10 build→run→inspect cycles or ~75 min.** On expiry: revert probes,
write up in §F with confidence rating + files touched.
## CONSTRAINTS
No game-logic changes; ABI/layout/format / port-layer only. Prefer
converter fix over runtime. Revert temp probes. Don't touch other tracks'
uncommitted edits.
## REPORT
1. Root cause + `file:line` evidence, why Facility/Jungle differ.
2. Fix diff or why-not.
3. Verification: `-level_34` + `-level_37` boot to ≥1 non-degenerate
frame; `-level_09` + `-level_20` unregressed (`framediff.py`).
4. Probes left in tree.
5. Confidence.
6. Quirk appended to `docs/PORT-LEARNINGS.md`.
+95
View File
@@ -0,0 +1,95 @@
# BRIEF C2-GDL — model/prop GDL runtime relocation misaligns dst (Facility, Runway)
Repo: `REPO_ROOT` (git `master`, Windows).
## READ FIRST
- `docs/HANDOFF.md` top, `docs/PORT-LEARNINGS.md` **§B (16-byte PC Gfx vs
8-byte N64 — this is the class)** + §D.
- `docs/PCPortResearch.md` §F **D124** (the C2 split — this brief is the
unfinished Facility half), **D80, D82, D83, D85, D95, D58, D50.6**
(Gfx-stride / GDL history — jump via §F index).
- `AGENTS.md` non-negotiables. `rsp/graphics/gmain.s` = GBI ground truth.
- PD analogue: `PD_PORT_CHECKOUT` — its model-GDL /
texture relocation path. Check before writing anything.
## SYMPTOM (WS4 re-sweep — `docs/LEVEL-STATUS.md`)
Facility `-level_34` (fault 0x72181ee8) and Runway `-level_35` (fault
0x721b8ee8) both crash in `import_texture_i8` (`port/fast3d/gfx_pc.cpp:821`)
— fast3d fed a garbage `G_SETTIMG` w1.
Per the D124 investigation: the bad `G_SETTIMG` (`fmt=4 siz=2 w=0`,
preceded by an unconsumed `0xba` GE tex-macro opcode) sits in a
model/prop GDL relocated at runtime by `texLoadFromGdl()` via
`sub_GAME_7F0762E0` (`objecthandler_2.c:82`) — NOT a room GDL, NOT the
global bank. A `GE_C2` probe showed `texLoadFromGdl` writing to
**non-16-byte-aligned `dst`** (`0x701eac01`, `0x701eb7d5`, …): the
`replacementgdl` / `name`(=srcsize) offset arithmetic still mixes N64
8-byte and PC 16-byte `Gfx` strides, so converted commands land mid-slot
and a subsequent `G_SETTIMG` w1 reads as garbage.
12 levels PASS — they either don't hit this model-GDL path or hit it with
a GDL whose misalignment happens to be benign.
## TASK
Pin the exact stride/offset error in the model-GDL runtime relocation and
fix it narrowly (`#ifdef PORT`, N64 line under `#else`, §F-documented).
Likely spots:
- `src/game/tex.c` `texLoadFromGdl()` — src/dst pointer advance per Gfx.
- `src/game/objecthandler_2.c` `sub_GAME_7F0762E0` — `replacementgdl` /
`srcsize` math, GDL buffer sizing.
- `src/game/model.c` `modelNodeReplaceGdl` / rwdata GDL slot stride.
- The `0xba` (`G_GEGBIMACRO_*` / GE tex-load macro) decode — fast3d may
need to consume its extra word (cf. gmain.s).
Use the existing `GE_D86` / model-node probes and add a capped `GE_C2GDL`
probe if needed.
## FILES YOU MAY TOUCH
`src/game/tex.c`, `src/game/objecthandler_2.c`, `src/game/model.c`
(narrow `#ifdef PORT`); `port/fast3d/gfx_pc.cpp` (only a guard / the 0xba
consume, if that's the cause); new `tools_pc/c2gdl_*` probes;
`docs/PORT-LEARNINGS.md`, `docs/PCPortResearch.md` §F (extend D124 or new
Dxx), `docs/LEVEL-STATUS.md`.
**NOT** `src/game/propobj.c` / `prop.c` / `tools_pc/d88*` (C3 agent owns
those), **NOT** `port/src/gimgfixup.c` (D124-Jungle, done).
## KNOWN-GOOD / RULED OUT
- D123 (C1), D124-Jungle (`gimgfixup.c`) — landed, work.
- 12 levels PASS — the common GDL path is fine.
- Global-bank / explosion-DL sync (D124) — separate, fixed.
- HUD/text X-mirror (D114/D116) — parked, do NOT re-trace.
## PRE-FLIGHT
- `export PATH="/c/msys64/mingw64/bin:$PATH" && ./build-pc.sh ntsc-final` — green.
- Repro `./build-pc/ge007.x86_64.exe -level_34`. `ge007.crash.log`;
`addr2line -e build-pc/ge007.x86_64.exe -f -C 0x<PC>`.
- `GE_PCDUMP="80-260:40"` → `./ppm/`; `python tools_pc/pixcount.py`.
- `timeout` does NOT kill on Windows: `... &`, `sleep 24`,
`taskkill //F //IM ge007.x86_64.exe`.
- gdb attach fast (`gdb -batch -x cmds.txt -p <winpid>`). HW watchpoint on
the `dst` cursor in `texLoadFromGdl` catches the mis-advance.
- Regen sidecars after any converter change (none expected here):
`python tools_pc/d43_emit.py ntsc-final && python tools_pc/d69_emit.py ntsc-final && python tools_pc/d88_emit.py ntsc-final --regen`.
## BUDGET
Structural / shared-infra → **~14 build→run→inspect cycles or ~2 h.** On
expiry: revert temp probes, write up the stride spec + best hypothesis in
§F with a confidence rating + files touched. A good half-solved write-up
is a deliverable.
## CONSTRAINTS
No game-logic / control-flow changes. ABI/layout/format only, `#ifdef
PORT` with N64 line under `#else`, §F-documented. Revert your own probes.
Don't touch the C3 agent's files.
## REPORT
1. Root cause — `file:line`, the exact stride/offset bug, why 12 levels
are unaffected.
2. Fix diff or why-not.
3. Verification — `-level_34` + `-level_35` boot to ≥1 non-degenerate
frame (`pixcount.py` > a few %); `-level_09` + `-level_20` unregressed
(`framediff.py`); re-check Jungle `-level_37` + Streets `-level_29`
(may improve).
4. Probes left in tree.
5. Confidence rating.
6. Generalisable quirk appended to `docs/PORT-LEARNINGS.md`.
+62
View File
@@ -0,0 +1,62 @@
# BRIEF C3+C6 — prop/door/item model-load crashes (Aztec, Bunker2, Surface2)
Dispatch AFTER C1 lands (shares `tools_pc/d88_propdefs.py` +
`src/game/prop*.c` with C1 — do not run concurrently with C1).
Repo: `REPO_ROOT` (git `master`, Windows).
## READ FIRST
- `docs/HANDOFF.md` top, `docs/PORT-LEARNINGS.md` §A + §C (esp. the D122
"polymorphic-record converter with a forgotten type" note).
- `docs/PCPortResearch.md` §F **D122, D120, D112, D53.2, D57, D67**
(jump via index).
- `docs/BRIEF-D122-per-level-modelload.md` — direct predecessor, same class.
- `AGENTS.md` non-negotiables.
## SYMPTOM (WS4 sweep — `docs/LEVEL-STATUS.md`)
| Level | # | Site | Note |
|---|---|---|---|
| Aztec | 28 | `propobj.c:13601` `door->model->obj->RootNode->Child->Child` | fault 0x10 |
| Bunker2 | 27 | `propobj.c:13523` door displacement `linkedDoor` list walk | fault 0x8 |
| Surface2 | 43 | `loadobjectmodel.c:393` `PitemZ_entries[modelid].header->RootNode` | fault 0x0 — direct D122 continuation |
All three: a prop/door/item model pointer or model-id from a `Usetup*Z`
propDef record is wrong. D122 fixed the `[s16 obj][s16 pad]` half-swap for
6 `inherits ObjectRecord` types; these levels emit a door subtype or
another record type whose `model` / `linkedDoor` / `modelid` field is
still converted wrong (or a placeholder-sized type:
`VEHICLE`/`AIRCRAFT`/`TANK`/`AMMO`/`DEPOSIT_IN_ROOM` per
`d88_propdefs.py`). Run `tools_pc/d88_propdef_scan.py` for the per-level
type histogram; diff Aztec/Bunker2/Surface2 against the 7 passing levels.
## TASK
Root-cause all three (likely one converter gap), extend
`tools_pc/d88_propdefs.py` (+ `sizepropdef()` `#ifdef PORT` stride in
`src/game/prop.c` to match, per the D122 pattern). Resolve any
placeholder `PROPDEF_PC_BYTES` via `tools_pc/d88_layoutprobe.c`.
## FILES YOU MAY TOUCH
`tools_pc/d88_propdefs.py`, `d88_emit.py`, `d88_layoutprobe.c`,
`d88_propdef_scan.py`; `src/game/prop.c`, `src/game/propobj.c`,
`src/game/loadobjectmodel.c` (narrow `#ifdef PORT`); new `tools_pc/c3_*`
probes; `docs/PORT-LEARNINGS.md`, `docs/PCPortResearch.md` §F,
`docs/LEVEL-STATUS.md`.
## KNOWN-GOOD / RULED OUT
D121, D122 (committed `f2beae4b`), the 7 passing levels' prop/model load,
C1's chr fix (landed first — rebase on it).
## PRE-FLIGHT / ENV / BUDGET / CONSTRAINTS / REPORT
Same as `docs/BRIEF-C1-chr-airecord.md` (repro `-level_28` / `-level_27` /
`-level_43`; `taskkill //F //IM ge007.x86_64.exe`; regen all 3 sidecars
after converter change; `#ifdef PORT` with N64 line under `#else`; verify
the 3 levels boot to a frame + `-level_09`/`-level_20` unregressed).
Budget: **~12 cycles / ~90 min.**
## STILL OPEN AFTER THIS (singletons, own briefs later)
- **C4** Depot 30 — `prop.c:902` `sp4C->room` after
`walkTilesBetweenPoints_NoCallback` (BG tile/room table).
- **C5** Control 23 — `bg.c:5723` `portal_pts->numPoints`
(`g_BgPortals[].offset_portal` unresolved/BE), via `chrprop.c:62`.
- **C7** Surface1 36 — `snd.c:653` `sndSetupSound` (audio subsystem is
parked; may just need a narrow load-time guard until Phase 3).
+3
View File
@@ -1,3 +1,6 @@
> **RESOLVED — historical.** D69 is closed (see PCPortResearch.md §F/§H
> D69, D78–D84). Kept for the bg/stan format spec. Not active work.
# Brief — M1 / D69: Stage (BG/stan) loading for PC
_Paste-ready milestone brief. Authoritative context: `AGENTS.md`,
+520
View File
@@ -0,0 +1,520 @@
# HANDOFF ARCHIVE — session-by-session narrative
> **Reference only.** Current state, next task, and environment live in
> `docs/HANDOFF.md`; per-finding detail in `docs/PCPortResearch.md` §F/§H
> (indexed at the top of §F). This file is the frozen accumulation of
> prior sessions' handoff briefs (through session M-11), kept so the
> reasoning behind a fix is recoverable. Everything below is as-written
> at the time and may be superseded.
---
# Handoff brief — GoldenEye 007 PC port (Phase 2: Session M-3 —
# STAGE-LOAD → RENDER crash chain CLEAR; BUNKER1 renders, next is D75 model quality)
## PREFLIGHT — before touching anything (a concrete next-step below is NOT a reason to skip this)
1. `CLAUDE.md` (auto-loaded) + `AGENTS.md` non-negotiables.
2. This file, top-to-bottom.
3. `docs/PORT-LEARNINGS.md` — recurring bug classes.
4. Dispatching a subagent? `docs/AGENT-WORKFLOW.md` FIRST. Every brief needs
FILES / BUDGET / ON-EXPIRY / CONSTRAINTS / REPORT (see CLAUDE.md "Dispatch preflight").
_Paste-ready brief. Authoritative context: `AGENTS.md`,
`docs/PCPortResearch.md` §F (D69, D78-D102), `docs/BRIEF-D69-stage-load.md`._
**NEW DIRECTION (2026-08-29): `docs/PLAN-linear-level-sweep.md`** — pivot
to breadth-first: fix the boot→intro→menu→level-select path to
*functional, not crashing* (cosmetics parked in `GRAPHICS-BACKLOG.md`),
then sweep all 21 solo levels for load+render+no-crash. Start with WS1
(auto-inject `memallocstringtable` args for bare `-level_XX`).
## READ THIS FIRST — crash chain CLEAR; viewport fixed; room geometry is next
`-level_09 -ml0 -me0 -mgfx100 -mvtx50 -mt700 -ma150` **boots BUNKER1
and renders continuously with no fault** (60 s+, 5000+ VI posts at full
framerate; attract mode also clean). The stage-load → first-frame →
in-level crash chain (blocker since D69) is resolved.
**D103 (session M-4) fixed the "~46 %, lower half" symptom** — it was a
single native-resolution/viewport bug (`osViSetMode` hard-coded height
480 → `RATIO_Y` half of `RATIO_X`), not per-model. Frame is now full
(91.7 %, correct letterbox + HUD placement). See §F "D103".
**D105 (session M-4) fixed room geometry** — `zbufClearCurrentPlayer`
(`viewport.c`) used the N64 "fill the Z buffer as a colour image" idiom,
which fast3d doesn't emulate, and nothing emitted `G_CLEAR_DEPTH_EXT`, so
the depth buffer was never cleared and all ~160 k room triangles failed
the Z test (only `skyRender`'s background fill survived). PORT branch now
emits `G_CLEAR_DEPTH_EXT`. See §F "D104"/"D105". BUNKER1 now renders
recognisably — textured walls, storage racks, floor (2585 colours, sky
16 %), 70 s crash-free.
**D106 (session M-4)** — the big sky-void through doorways was the portal
BFS dropping the next room: a portal straddling the camera near-plane
projects z==0 clip points to ±1e20 screen coords, and on x86-64 that
garbage came back `min>max` on one axis, slipping past the
degenerate-box check that on N64 clamps it to full-screen. PORT guard in
`sub_GAME_7F0B5864` treats non-finite / out-of-range bounds as
full-screen. Visible rooms/frame 1–3 → 2–4. See §F "D106".
**D107 (session M-4)** — blurry ceilings/wall-panels were fast3d sampling
GE's first mip (render tile 1), whose single-`G_LOADBLOCK` TMEM slot
fast3d never registers, so `import_texture` fabricated a 16×16 crop of
the base image and magnified it. `gfx_lod_tile_offset` now returns 0
(base tile) when detail textures are off. BUNKER1 rooms render crisp.
See §F "D107".
**Skeletal models — FIXED (session M-5, D112).** The "3D line" / missing
characters were `tools_pc/d43_emit.py`'s `put_f32` reversing the wrong 4
bytes (`src[doff+4:doff:-1]` → bytes doff+1..doff+4, dropping the MSB),
corrupting every f32 in converted model rodata (joint `Origin`s, LOD
distances, BSP planes). Compiled-in front-end models were unaffected
(native LE), hence "logo fine, every guard broken". Fix: one line,
`src[doff:doff+4][::-1]`; regen with `python tools_pc/d43_emit.py
ntsc-final`. BUNKER guards now render as coherent humanoids, monitor
screens draw content. See §F "D108–D112".
**"props emit no geometry" was a STALE premise** — props/doors emit
complete leaf DLs (~15k/run, valid ptrs; fast3d transforms them). The
storage-room void is NOT closed doors failing to render.
**Priority (user, M-11): get BUNKER1 — then every level — playable
start-to-finish with no crashes. Fix crashes/hangs first; cosmetic
rendering (D74/D75/D76/D114/D116) is logged in `docs/GRAPHICS-BACKLOG.md`
and comes later.** D120 (blood-stain converter) is the nearest real
converter gap; extending `d43_emit.py` for opcode-0x18 is a good
subagent brief. After BUNKER1: walk the objective/exit path to a level
transition, then the next stage.
**Real remaining work, in order (updated session M-8):**
1. **Input layer (Phase 3)** — IN PROGRESS (M-8 agent, §F D118). `port/src/
input.c` was a stub; only a minimal keyboard path in `libultra.c`
(`contSnapshotFromKeyboard`) worked. No C-buttons/mouse-look/gamepad/
rebinding. This is the top playability blocker — you cannot aim or
properly move. Reference: `pd_port/port/src/input.c`.
2. **Weapon model doesn't draw (AUDIT-M6 #5)** — NOT STARTED (M-8 agent
hit the account session rate-limit before any work; tree untouched).
Plan: instrument `gunfire.c` weapon setup (~600-661) + `field_87F`
gating (~520/587-598) FIRST with a `GE_D119` printf — does setup even
run? Then check `weaponModel.render_pos` = `dynAllocate`'d `rwmtx`
lifetime vs the `gunRenderFirstPersonGunModels` (~1605) consumer (on
N64 it aliased the persistent `hand->mtxlist`; D102 gave it own
storage → arena may recycle). Also `weaponRwPool[192]` capacity vs
`modelCalculateRwDataLen(mdlhdr)` on PC, and `flashvisptr` word-index
assumption (~640). Narrow `#ifdef PORT` lifetime/sizing fix only — if
it needs a behavioural arena change, STOP + write up. Direct `-level_09`
boot may spawn Bond holstered — may need forced weapon state or the
attract path to get the gun on screen. Gate to playability.
3. **The HUD/text X-mirror (D116)** — DEPRIORITISED, parked in
`docs/GRAPHICS-BACKLOG.md`. M-11 confirmed the ammo digits use the
SAME `textrelated.c` path (no separate renderer — the M-8 premise was
wrong) and re-hit the same contradiction: every stage verified
non-mirrored, glyphs still flip. **Do not re-static-trace.** Next
attempt needs a RenderDoc/apitrace capture or the asymmetric-1-texel
experiment — nothing else. All cosmetic rendering issues (D74 dead
wrap-block, D75/D76/D77) now tracked in `docs/GRAPHICS-BACKLOG.md`;
they rank BELOW level-progression and crash work.
4. **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`).
5. Add an f32 value spot-check to `d43_emit.py`'s verify pass (it only
checks pointers/opcodes today — the D112 bug passed "ALL CHECKS PASSED").
6. **GE_DETERM fixed-tick mode (§F D117)** — deferred as not-narrow; would
unlock exact-diff regression testing + reproducible timing bugs. Design
is written up. Worth doing once the above land.
**Session M-7 — see §F D116 + "D116 probe results".** Ran the glyph
probe (overseer, after killing a subagent that was drifting toward a
global texrect S-swap). Corrected finding: the HUD text mirror is a
**per-quad texture-U flip that is NOT path-specific** — the ammo digits
"83" are mirrored too (`ppm/frame_000320.ppm`), refuting M-7's earlier
"proportional-font-specific" claim. Rect *positions* are correct; only
texture content is X-flipped. Every fast3d stage probed clean (glyph
bitmap in memory correct, texrect `ul.u=0→lr.u=max`, `import_texture_i8`
linear, GL shader UV pass-through) — so the flip is in the GL
vertex-buffer/draw layer OR a shared clip-space-X vs U desync on rect
quads. **This re-opens D114's shared-mirror hypothesis** (now per-quad
U/X, not screen-space) and plausibly also explains inverted guards /
mislocated door props. NEXT: shader/vertex-buffer probe — dump per-vertex
(x,u) for one glyph quad; render a 1-texel asymmetric test texture to see
which axis inverts. `GE_D116`-gated probes left in `textrelated.c` +
`gfx_pc.cpp` (zero-cost).
**Session M-8 wrap — state for next session.** Build GREEN at
`587c6856`, tree clean. Committed: D116 part-3 writeup + `[D116/vbo]`
probe (`5ff012b4`), `CLAUDE.md`/preflight (`fd25628a`), D117
framediff+nondeterminism (`44d9ae98`), D118 input layer
(`4ac11fe7`/`587c6856`), handoff reprioritisation. `GE_D116` and
`GE_INPUTLOG` probes are in-tree, `getenv`-gated, zero-cost. Weapon-model
(#5) agent never ran (rate-limit) — task untouched, plan in item 2 above.
Pre-existing stash `stash@{0} "d59 probes WIP"` (~300 lines,
gfx_pc/gfx_opengl/crash.c) is NOT from M-8 — provenance unknown, left as
found. Next: human input playtest (tune `ge007.ini [Input] MouseAimSpeed`),
then weapon-model #5.
**Session M-8 (continued) — D116 part 3 + input layer.** Killed the
drifting D116 agent, ran the probe as overseer: `buf_vbo` (x,u) for
glyph quads and the GL texture upload are both runtime-verified
non-mirrored (x-left<->u=0), GL shader is `gl_Position=aVtxPos` +
UV-passthrough — every stage clean, yet glyphs render X-flipped
(contradiction, §F "D116 runtime probe part 3"). Deprioritised as
cosmetic. `GE_D116` `[D116/vbo]` probe kept in `gfx_pc.cpp`. Then
dispatched the **input-layer agent** (§F D118, in progress) — top
playability blocker. `framediff.py` structural mode is the interim
regression gate (`tools_pc/golden/` is a nondeterministic D115 capture).
**Session M-8 — see §F D117.** Visual-regression tooling. Root-caused the
frame-to-frame nondeterminism: pure variable-timestep frame pacing
(`osGetCount()` = wall clock on PC → `frametiming.c waitForNextFrame`
advances logic a real-time-dependent number of 60 Hz ticks per render).
PRNG seeding verified correct (`random.c`), not a source. A `GE_DETERM=1`
fixed-tick mode was assessed **not narrow** (redesigns VI retrace/tick
semantics, deadlock risk in load-screen loops) and deferred with a full
design in §F D117 — NOT implemented. Added `tools_pc/framediff.py`
(structural/tolerant: 16×12 grid mean-colour + non-clear-% + aHash,
`--mask` for HUD, `--exact` for a future deterministic build, `--update`
to refresh goldens). Validated against the D115 golden set. No C changes,
no probes left in tree.
**Session M-10 — input playtest + guard-firefight crash chain.**
- **D119 — guard-attack crash FIXED** (`chraction.c`). Every prior
`-level_09` run segfaulted ~frame 1200 the instant a BUNKER guard
opened fire: `bondwalkItemGetAutomaticFiringRate` (`gun.c:1334`) ←
`chrlvInitActAttack`. ~28 sites pun `weapons_held[]->chr` (really a
`WeaponObjRecord*`) as `ChrRecord*` and read `.act_<x>.attack_item`,
which on N64 aliases `WeaponObjRecord.weaponnum` (act union @0x2C + 84
== 0x80). Pointer widening moves the act union to ~0x38 on PC → garbage
negative item id → OOB `g_ItemStats` → crash. Fix: `PUN_ATTACK_ITEM()`
macro reads `weaponnum` directly; `#else` branch is textually identical
to the original → N64 build unchanged. Class-A (D53.2 type-pun) bug.
- **D120 — blood-stain hang GUARDED, not fixed** (`chr.c:3322`). Next
blocker, reachable only after D119: first guard bullet-hit →
`chrCreateBloodStain` `PointUsage[]` negative-terminated chain walk
cycles forever (VI thread keeps posting, logic thread spins, kernel
heartbeat trips). Root cause: `tools_pc/d43_emit.py`'s opcode-0x18
`ModelRoData_DisplayList_CollisionRecord` conversion is incomplete —
6 pointer fields widen the struct and `PointUsage`/`CollisionVertices`
sub-array endianness+stride aren't handled (only `CollisionRelatedNode`
got the D43/D45 `u32`-vma treatment). **Interim:** `#ifdef PORT` caps
both walk loops at `numVertices+8` iters + bounds-checks `index` → game
survives, blood decals may be missing/wrong. **Real fix (next session,
own subagent brief):** byte-spec the opcode-0x18 record + PointUsage +
CollisionVertex sub-arrays vs a converted guard model, extend
`d43_emit.py`. Same shape as D69/D88 converter work.
- Result: `-level_09` now survives the guard firefight — 45 s+
crash-free, frames past 1200 (was: hard segfault ~1200 every run).
**Session M-10 — input playtested (human).** Fixed in `port/src/input.c`:
(a) mouse-look was a draining accumulator → flicks stayed "pressed" ~8
frames after you stopped (stuck looking up/down) — now per-poll delta,
no carryover; (b) horizontal mouse was routed to C-left/right = GE
*sidestep*, and A/D drove analog stick-X = GE *turn* — swapped: mouse X
→ stick-X turn (`MOUSE_TURN_GAIN 6.0`), A/D → C-left/right strafe, mouse
Y → C-up/down look. Core aim/move now works.
**Open input bugs (documented, deferred — fix in a later pass):**
- **D118a — mouse yaw slower than pitch.** Horizontal turn (analog
stick-X via `MOUSE_TURN_GAIN`) is visibly slower than vertical look
(digital C-up/down). Different transfer curves (analog rate-limited vs
digital full-press). Needs `MOUSE_TURN_GAIN` bump and/or a matched
pitch path; ideally a single tunable `MouseAimSpeed`.
- **D118b — mouse Y inverted.** Mouse up → looks down, mouse down →
looks up (X unaffected). `aimDY`→C-button sign is backwards for GE's
pitch convention (or SDL dy sign assumption wrong). Flip the
`aimDY >=`/`<=` C-up/C-down assignment (or default `MouseInvertY`).
One-line fix once confirmed against GE pitch sign.
- Still TODO from D118: rebinding, gamepad hotplug, `ge007.ini` not yet
created (config defaults are hardcoded).
**Session M-9 (Phase 3) — see §F D118.** SDL input layer implemented.
`port/src/input.c` is now real: keyboard+mouse and SDL_GameController,
mapped to GE's N64 pad (analog stick = move, C-buttons = aim, mouse-look
bridged to digital C-buttons via a clamped accumulator, RMB/LT = aim
mode, LMB/RT = fire). `libultra.c`'s SI section delegates to it (single
source of controller state; still driven by `osContStartReadData`, no
new frame hook). Config: `ge007.ini [Input]` MouseEnabled/MouseAimSpeed/
MouseInvertY. Build GREEN, boots `-level_09` crash-free 35 s.
**Owed: a human playtest** — live input is untested from the headless
agent. Manual checklist: (1) WASD moves Bond, mouse turns/looks (tune
`MouseAimSpeed` in ge007.ini if too fast/slow), LMB fires, RMB enters
aim mode, Enter opens the pause menu; (2) plug an Xbox pad — left stick
moves, right stick aims, triggers fire/aim; (3) `GE_INPUTLOG=1` prints
each nonzero OSContPad poll. Rebinding + gamepad hotplug are TODO (§F
D118).
**Session M-6 — 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):**
| # | commit | one-liner |
|---|--------|-----------|
| D93 | `164d7f99` | null-room (room 0) NULL-deref guards |
| D85 | `493c9838` | `bgWidenRoomGdl` (8→16 `Gfx` + `bswap32`) + `bgSwapRoomVtx`; room DLs decode to real GBI; `bg.c:2448` size-cast bug |
| D85 | `6f0208d6` | `ptr_texture_alloc_start` → real `struct texpool` storage (pool looked exhausted → ~630 room textures now resolve; cleared `Bad size for RGBA texture`) |
| D94 | `63204a27` | `chrlvInitActAttack` `(s32)`-truncated anim-table index |
| D95 | `f35eba91` `933ba52b` | 2× `g_GfxBuffers` (16-byte PC `Gfx`) + raise PC mempool ceiling `0x702F4400→0x70700000` (reclaim ~4 MB DRAM) so it doesn't OOM `MEMPOOL_STAGE` |
| D96 | `d86ec483` | `PROPRECORD_STAN_ROOM_LEN` 4→8 (PORT) — prop room-list stack overflow (guards span ≥4 rooms → no terminator → smashed `chrpropsRenderPass` frame; *this* was the "runaway GDL append") |
| D97 | `2fbcc556` | clamp negative `damagetype` (US-only OOB `g_DamageTypes[]` read) |
| D98 | `000ed6af` | `initBONDdataforPlayer` allocate real PC `sizeof(struct player)` (hardcoded `0x2A80` under-alloc scribbled the master DL) |
| D99 | `253caa23` | `Model.animflipfunc` `s32` fn-ptr truncation (flag + direct call, D92 pattern) |
| D100 | `8eaad547` | `struct player.model` is an inline `struct Model`, not a `Model*` + 45 filler s32s (PC struct 0x2A8 ≫ 0xB8 hole → `animInit` overran); + dedicated `gaitRwData[]` |
| D101 | `b2234f82` | `sub_GAME_7F06DB5C` stashed `ModelNode*`/`RenderPosView*` through an `s32` (the idiom its sibling was already fixed for) |
| D102 | `1b078f6d` | 1P weapon `Model` + RW pool punned onto `struct hand` (PC `Model` 0xE8 ≫ N64 0xBC → `modelInit` aliased `datas` onto the pool); dedicated `weaponModel`/`weaponRwPool` |
**Recurring pattern this session:** the decomp pun-allocates `struct
Model` (and `struct player`, `PropRecord`, `struct texpool`, …) into
N64-sized holes / hardcoded byte counts. Every one is bigger on x86-64
→ overrun → corruption. Fix = give it real inline storage or
`sizeof()`-based alloc under `#ifdef PORT`. **Landmine still open**
(§F D100): `struct player` / `struct hand` have raw hardcoded-offset
accessors (`gunfire.c` `THROWMTX` at `+0xAD8`, …) that are NOT
PORT-adjusted — already PC-wrong, a real `struct player` offset pass is
owed before grenade/knife-throw code works.
- Build: `export PATH="/c/msys64/mingw64/bin:$PATH" && ./build-pc.sh ntsc-final`
- Sidecar regen: `python tools_pc/d43_emit.py ntsc-final && python
tools_pc/d69_emit.py ntsc-final && python tools_pc/d88_emit.py
ntsc-final --regen`. `data/` is gitignored; if missing,
`cp baserom.u.z64 data/ge007.ntsc-final.z64` first (§F "`data/`
deletion + recovery").
- Repro: `./build-pc/ge007.x86_64.exe -level_09 -ml0 -me0 -mgfx100
-mvtx50 -mt700 -ma150` — `-m*` are BUNKER1's `boss.c` per-level sizes;
a bare `-level_09` skips them and crashes early (TODO: auto-inject).
## Next: D75 — 3D model rendering quality
BUNKER1 renders but character/skeletal models are wrong (see the D75
section further down, still accurate). `GE_PCDUMP` + `tools_pc/pixcount.py`
to measure; the animated-model path (`animInit` + `struct player` raw
offsets, `drawjointlist`, `modelApplyHeadRelations` head/body splice) is
the prime suspect. `struct tex` headers are 24 B vs 16 B on PC — a
separate open pool-pressure item; a real PC memory-budget pass is owed.
## What this session (M-2) fixed — all committed
1. **`-level_09` now works.** `osPiReadIo` was stubbed to 0, so the token
string was always empty and N64 debug switches were ignored (only
attract mode could reach a level). Now built from `argv[1..]`.
2. **Sidecar tables patched in `obInit()`** instead of lazily at first
model load — a direct stage boot loaded raw big-endian ROM before.
3. **D88.5** — stan tile-name byte-swap in the converter (0/276 →
273/273 name matches during pad setup).
4. **D88.6** — intro CAMERA `lang1c` is a `u16` pair, not an `s32`
(fixed the `langGet` NULL-bank crash).
5. **D89** — `init_path_table_links` `[-3]` OOB → SIGILL fix; NULL-tile
guard in `sub_GAME_7F0B0914`.
6. **D90** — `stanTileDistanceRelated`'s 80-byte zero-fill was clobbering
the caller's live stan-tile local (16B struct, 80B clear). The
player's spawn stan was fine all along.
7. **D91** — `(s32)&D_800442FC[portalnum]` truncation in bg portal cull.
8. **D92** — `chrAllocate` `s32` param truncated the ailist pointer;
`Model.unka0` `s32` field truncated a stored function pointer.
**Next steps for the resuming session (render milestone):**
1. **D85** — root cause confirmed (8-byte N64 `Gfx` vs 16-byte PC `Gfx`
in the unconverted per-room DL blob). Runtime widen+bswap fixup being
implemented; verify with `GE_D69BB=1` (real GBI opcodes, not
00/01/02/52), then `GE_PCDUMP` + `tools_pc/pixcount.py` for
non-degenerate BUNKER1 geometry. See §F "D85 root cause CONFIRMED".
Fallback if the widen stalls: soften the four `sysFatalError("Bad
size…")` guards in `gfx_pc.cpp` to skip-with-warning.
2. Then the front-end **render bugs (D75)**.
**D88.4 loose end (still open):** `PROPDEF_PC_BYTES` for `VEHICHLE`/
`AIRCRAFT`/`TANK`/`AMMO`/`DEPOSIT_IN_ROOM` in `d88_propdefs.py` are
placeholder guesses (BUNKER1 doesn't use them). Probe real sizes via
`d88_layoutprobe.c` before loading levels that use those types.
## Known rendering bugs (D75 — still open, orthogonal to the D88 crash)
Even once the crash chain is cleared, the front end has **broken 3D model
rendering** (user-confirmed this session):
- Rareware logo: correct (fixed in D73/D74).
- **Nintendo logo**: renders but **mispositioned**.
- **Gun-barrel intro**: the **James Bond character model is missing
entirely**.
- **Intro credits / cast roll**: the per-character 3D models **do not
appear at all** (names draw, models don't).
Pattern: textures/text draw; **animated/skeletal character models never
appear**; static 3D (logos) appears but with a bad transform. Leading
hypothesis is D75(b) — the animated-model path (`animInit` + raw offsets
into `struct player`, cf. D56) is broken independently of the D73 matrix
sin/cos fix. Full triage plan in `PCPortResearch.md` §F D75.
The rest of this document (below) is the **last known-good, committed**
status as of commit `8c9c6a2c` (D86+D87 resolved) — still accurate except
D88 is now further along (D88.1–3 done/verified, D88.4 is the live crash).
## Where things stand
**D69 (the original "stage load faults" milestone blocker) remains
RESOLVED.** `load_bg_file` (bg.c) doesn't fault on BUNKER1. Since then,
two more crashes further down the load chain were found and fixed this
session (D86, D87), and a third — the current blocker — was root-caused
but not fixed (D88).
**D86 RESOLVED.** `modelInitRwData` crash (`model.c:6174`) was a single
truncating pointer cast in the player's embedded gait/arm model:
`src/game/initplayergaitobject.c:5` did
`player_gait_object_header.RootNode = (int)&player_gait_hdr;` — a
same-width no-op on N64 that truncates+zero-extends a real 64-bit pointer
on PC. Fixed with a narrow `#ifdef PORT` branch assigning the pointer
directly. Root-caused via a new node-walk trace (`GE_D86=1`, left in
place, gated in `model.c`/`objecthandler_2.c`).
**D87 RESOLVED.** Once D86 stopped blocking progress, an idle (no-input)
run eventually triggers the front-end's genuine attract-mode demo
playback (`select_ramrom_to_play()` picks a random compiled-in demo —
this is shipped retail behavior, not a debug feature) and crashed in
`ramrom_replay_handler` (`ramromreplay.c`). Root cause: `ramromfilestructure`
is a real ROM-compiled asset (big-endian, like everything else) loaded via
`romCopyAligned()` — a raw byte copy by design (D66) — with **no
byteswap**, so every multi-byte field read back scrambled (e.g. `size_cmds`
2 → 33554432) and drove wild pointer arithmetic. Fixed with a `#ifdef PORT`
`ramromFixupEndian()` called once after the load (same pattern as the D54
cseq fixup). Not BUNKER1-specific — attract mode picks any of 7 demo
locations at random, so don't rely on it for BUNKER1-specific testing (see
`-level_09` below).
**D88 — SUPERSEDED, see "READ THIS FIRST" at top.** D88.1–D88.3 (header +
sub-table width/endian conversion) are now done and verified; D88.4
(`propDefs` byteswap) is the live blocker. The paragraph below is the
original root-cause writeup, kept for context.
**D88 (original writeup) — root-caused.** Launch with
`-level_09` (NTSC `LEVELID_BUNKER1 = 9`; `boss.c:199-339` decodes
`-level_XX` into `g_StageNum`, bypassing the front end/attract-mode
entirely — fast, deterministic BUNKER1 repro, crashes in well under a
minute instead of waiting ~2 min for attract mode to maybe pick Bunker).
Crash: `proplvreset2` (`prop.c:1306`) segfaults reading
`g_CurrentSetup.pathwaypoints[i1].padID`. Root cause: the per-level
`"Usetup<name>Z"` file (`prop.c:1267`, `struct stagesetup` in
`bondtypes.h:4091`) is loaded as raw ROM bytes and has **zero PC porting
work done on it** — unlike bg/stan (D69/D80-82) and models (D43/D50).
Two compounding problems, not just one:
1. The 10 top-level fields (`pathwaypoints`/`waypointgroups`/`intro`/
`propDefs`/`patrolpaths`/`ailists`/`pads`/`boundpads`/`padnames`/
`boundpadnames`) are declared as real pointers in the live C struct,
so on PC they're 8 bytes each (an 80-byte header) instead of the
file's real 4-byte-each (40-byte) N64 layout — same class as D79
(`bg_room_data` pointer growth). Field 0 reads fine; everything after
it is reading the wrong bytes entirely.
2. The 4 meaningful bytes each field *does* store are big-endian (the
code's own comment: "stores every internal reference as a byte offset
from the start of the file") and nothing byte-swaps them — same class
as D87.
No PORT/byteswap handling exists anywhere in `prop.c` (confirmed by grep).
**Not fixed this session** — this is D69-scale format-conversion work: a
byte-accurate spec of the whole `Usetup*Z` format (top-level header +
every nested sub-table: `waypoint`/`waygroup`/`PropDefHeaderRecord`/
`PathRecord`/`AIListRecord`/`PadRecord`/`BoundPadRecord`/`pname`, each
likely with its own internal offsets not yet audited) plus either an
offline converter sidecar (preferred pattern per AGENTS.md, same shape as
`tools_pc/d69_emit.py`) or a careful runtime fixup pass that parses the
raw 40-byte N64-packed header by explicit byte offset, byte-swaps each
field, and writes results into the PC-widened struct.
**Net effect vs. last session:** the game now runs substantially further
— all the way through room-streaming setup, past the intro's model
pipeline, and into per-level "Usetup" data — before hitting D88. The
"loads without fault" acceptance bar is **still not met**, but the
remaining blocker is now narrowly scoped and has a fast, deterministic
repro (`-level_09`, no attract-mode wait, no depending on which random
demo attract-mode picks).
## Recommended next steps, in order
1. **D88.** Get a byte-level spec of `Usetup*Z` (start from BUNKER1's
file; `strResource` is built as `"U" + "sev" + "Z"`-style name in
`prop.c:1253-1265` — check `setup_text_pointers[LEVELID_BUNKER1]` for
the exact literal). Decide offline-converter vs. runtime-fixup (D69's
bg/stan work is the template for the former; D54's cseq fixup is the
template for the latter — given the struct-width mismatch on top of
the byteswap, a runtime fixup that manually walks the *raw* 40-byte
N64 header by hand (not through the live `stagesetup` struct) into a
freshly-populated `g_CurrentSetup` is probably simpler than a full
sidecar here, but verify against a raw ROM hex dump either way).
2. Once BUNKER1 loads past `proplvreset2`, re-check for further crashes
in the same vein (this session found 3 in a row — D86, D87, D88 — each
only reachable after the previous one was fixed; expect more).
3. Once BUNKER1 reaches a rendered frame with no fault, revisit **D85**
(room primary/secondary GDL binaries decode to garbage via
`texLoadFromGdl`) — use `GE_PCDUMP="<range>:10"` + `tools_pc/pixcount.py`
to confirm non-black, non-degenerate content per the original D69
acceptance bar. (This session's `GE_PCDUMP` captures around frame
2100-2400 during attract-mode-driven "loading" were still on a HUD/menu
screen, not real 3D geometry — don't read too much into pixel counts
from before D88 is fixed.)
4. Do NOT touch D75/D76/D77 (parked, lower priority, unrelated).
## Debug tooling added this session (kept, env-gated, zero cost when unset)
- `GE_D86=1` — node-walk trace in `modelInitRwData` (model.c) + a
load-identity probe in `load_object_fill_header` (objecthandler_2.c).
Resolved the D86 crash; left in place since the same
load_object_fill_header/modelInitRwData pipeline could surface new
edge cases as more of the game becomes reachable.
- `GE_D87=1` — block-setup trace in `iterate_ramrom_entries_handle_camera_out`
and consumer trace in `ramrom_replay_handler` (ramromreplay.c). Resolved
the D87 crash; left in place as it's a rarely-exercised path (attract
mode) worth having visibility into if it acts up again.
- Pre-existing `GE_D69STAN=1`, `GE_D69BB=1`, `GE_D69=1` — unchanged, still
useful for the bg/stan/D85 load path (see prior session's HANDOFF
entries, preserved in git history, for exactly what each logs).
## New: fast, deterministic BUNKER1 repro (no attract-mode wait)
Launch with `-level_09` as a program argument
(`./build-pc/ge007.x86_64.exe -level_09`) to skip the front end/attract
mode and load BUNKER1 directly — `boss.c:199-339` decodes `-level_XX`
(the two digit-chars are consumed as raw ASCII bytes:
`g_StageNum = tokenFindLevel[0]*10 + tokenFindLevel[1] - 0x210`; NTSC
`LEVELID_BUNKER1 = 9` → `"09"` since `'0'*10 + '9' - 0x210 = 9`). This is
now the preferred way to test BUNKER1-specific load/render work — it's
faster (crashes/completes in well under a minute vs. ~2+ min waiting on
attract mode) and deterministic (not dependent on which of 7 random demo
locations attract mode happens to pick).
## Environment / build
- `export PATH="/c/msys64/mingw64/bin:$PATH" && ./build-pc.sh ntsc-final`
(~5 s). Build is GREEN with all D86/D87 work included.
- Run from the **repo root**, not `build-pc/`.
- Regenerate sidecars if missing: `python tools_pc/d69_emit.py ntsc-final`
(bg/stan) and the D43/D50 model sidecar generator (pcmodels) — both
gitignored, not checked in. (Both were already present in this dev
environment this session.)
- `GE_PCDUMP="<start>-<end>:<stride>"` + `tools_pc/pixcount.py` for frame
captures once D88 is fixed and a frame actually renders real BUNKER1
geometry.
- Crash log: `ge007.crash.log` (repo root); symbolicate with
`addr2line -e build-pc/ge007.x86_64.exe -f -C <pc>` (image base
`0x140000000`).
- gdb **launch** mode is too slow for timing-dependent bugs (unchanged
guidance). **New this session:** gdb **attach** mode
(`gdb -batch -x cmds.txt -p <winpid>`, `<winpid>` = 4th column of
`ps -p <bashpid>`) works well and is fast for "watch a global for a
legitimate vs. corrupted write" questions on an already-running,
not-yet-crashed process — see `docs/PCPortResearch.md` §F environment
reminders for the exact recipe used to root-cause D87.
## Non-negotiables (unchanged, see AGENTS.md)
1. N64 build files untouched.
2. Game logic unmodified except narrow, documented `#ifdef PORT`
ABI/layout exceptions (D86/D87 this session, both logged in
`PCPortResearch.md` §F). D88 is explicitly **not** patched with a
quick inline hack — it needs the same disciplined
spec-then-convert/fixup treatment as D69, logged as an open finding
instead per AGENTS.md's "stop and write it up" guidance for anything
beyond a narrow, obviously-correct exception.
3. Offline sidecar conversion preferred over runtime fixup for whole
ROM-asset formats (D69/D80-82 pattern) — likely the right call for
D88 too, though a careful runtime fixup is also plausible; decide
after the byte-level spec work.
+3
View File
@@ -1,3 +1,6 @@
> **RESOLVED — historical.** D7 closed at the compile+link milestone.
> Kept for the AI X-macro analysis. Not active work.
# HANDOFF — AI X-macro system (D7), the last build blocker
## Status
+3
View File
@@ -1,3 +1,6 @@
> **RESOLVED — historical.** D7/D8 closed at the compile+link milestone.
> Kept for the IDO-vs-GCC preprocessor analysis. Not active work.
# HANDOFF — SWITCH/CALL macro port (D7/D8) — IN PROGRESS
## Status: BLOCKED on SWITCH expansion mangling (root cause now fully understood)
+152 -475
View File
@@ -1,503 +1,180 @@
# Handoff brief — GoldenEye 007 PC port (Phase 2: Session M-3 —
# STAGE-LOAD → RENDER crash chain CLEAR; BUNKER1 renders, next is D75 model quality)
# HANDOFF — GoldenEye 007 PC port (Phase 2, breadth-first level sweep)
## PREFLIGHT — before touching anything (a concrete next-step below is NOT a reason to skip this)
Tier 1 doc. Current state + next task + environment. Finding history is
`docs/PCPortResearch.md` §F/§H (indexed at top of §F); session narrative
is `docs/HANDOFF-ARCHIVE.md`.
1. `CLAUDE.md` (auto-loaded) + `AGENTS.md` non-negotiables.
2. This file, top-to-bottom.
3. `docs/PORT-LEARNINGS.md` — recurring bug classes.
4. Dispatching a subagent? `docs/AGENT-WORKFLOW.md` FIRST. Every brief needs
FILES / BUDGET / ON-EXPIRY / CONSTRAINTS / REPORT (see CLAUDE.md "Dispatch preflight").
## Preflight (do not skip because you have a "next step")
_Paste-ready brief. Authoritative context: `AGENTS.md`,
`docs/PCPortResearch.md` §F (D69, D78-D102), `docs/BRIEF-D69-stage-load.md`._
1. `CLAUDE.md` (auto) + `AGENTS.md` non-negotiables.
2. This file, top to bottom.
3. `docs/PORT-LEARNINGS.md` — recurring bug classes (you WILL re-derive
catalogued bugs otherwise).
4. `docs/PLAN-linear-level-sweep.md` — the plan of record.
5. Dispatching a subagent? `docs/AGENT-WORKFLOW.md` first — every brief
needs FILES / BUDGET / ON-EXPIRY / CONSTRAINTS / REPORT.
## READ THIS FIRST — crash chain CLEAR; viewport fixed; room geometry is next
## Direction (2026-08-29)
`-level_09 -ml0 -me0 -mgfx100 -mvtx50 -mt700 -ma150` **boots BUNKER1
and renders continuously with no fault** (60 s+, 5000+ VI posts at full
framerate; attract mode also clean). The stage-load → first-frame →
in-level crash chain (blocker since D69) is resolved.
**Breadth-first pivot.** Stop the depth-first BUNKER1 + graphics
rabbit-holes. Instead:
**D103 (session M-4) fixed the "~46 %, lower half" symptom** — it was a
single native-resolution/viewport bug (`osViSetMode` hard-coded height
480 → `RATIO_Y` half of `RATIO_X`), not per-model. Frame is now full
(91.7 %, correct letterbox + HUD placement). See §F "D103".
1. Make boot → intro → menu → level-select **functional (not crashing)** —
cosmetics parked in `docs/GRAPHICS-BACKLOG.md`.
2. Sweep all **21 solo levels** for load + render + no-crash →
`docs/LEVEL-STATUS.md`.
**D105 (session M-4) fixed room geometry** — `zbufClearCurrentPlayer`
(`viewport.c`) used the N64 "fill the Z buffer as a colour image" idiom,
which fast3d doesn't emulate, and nothing emitted `G_CLEAR_DEPTH_EXT`, so
the depth buffer was never cleared and all ~160 k room triangles failed
the Z test (only `skyRender`'s background fill survived). PORT branch now
emits `G_CLEAR_DEPTH_EXT`. See §F "D104"/"D105". BUNKER1 now renders
recognisably — textured walls, storage racks, floor (2585 colours, sky
16 %), 70 s crash-free.
**D106 (session M-4)** — the big sky-void through doorways was the portal
BFS dropping the next room: a portal straddling the camera near-plane
projects z==0 clip points to ±1e20 screen coords, and on x86-64 that
garbage came back `min>max` on one axis, slipping past the
degenerate-box check that on N64 clamps it to full-screen. PORT guard in
`sub_GAME_7F0B5864` treats non-finite / out-of-range bounds as
full-screen. Visible rooms/frame 1–3 → 2–4. See §F "D106".
**D107 (session M-4)** — blurry ceilings/wall-panels were fast3d sampling
GE's first mip (render tile 1), whose single-`G_LOADBLOCK` TMEM slot
fast3d never registers, so `import_texture` fabricated a 16×16 crop of
the base image and magnified it. `gfx_lod_tile_offset` now returns 0
(base tile) when detail textures are off. BUNKER1 rooms render crisp.
See §F "D107".
**Skeletal models — FIXED (session M-5, D112).** The "3D line" / missing
characters were `tools_pc/d43_emit.py`'s `put_f32` reversing the wrong 4
bytes (`src[doff+4:doff:-1]` → bytes doff+1..doff+4, dropping the MSB),
corrupting every f32 in converted model rodata (joint `Origin`s, LOD
distances, BSP planes). Compiled-in front-end models were unaffected
(native LE), hence "logo fine, every guard broken". Fix: one line,
`src[doff:doff+4][::-1]`; regen with `python tools_pc/d43_emit.py
ntsc-final`. BUNKER guards now render as coherent humanoids, monitor
screens draw content. See §F "D108–D112".
**"props emit no geometry" was a STALE premise** — props/doors emit
complete leaf DLs (~15k/run, valid ptrs; fast3d transforms them). The
storage-room void is NOT closed doors failing to render.
**Priority (user, M-11): get BUNKER1 — then every level — playable
start-to-finish with no crashes. Fix crashes/hangs first; cosmetic
rendering (D74/D75/D76/D114/D116) is logged in `docs/GRAPHICS-BACKLOG.md`
and comes later.** D120 (blood-stain converter) is the nearest real
converter gap; extending `d43_emit.py` for opcode-0x18 is a good
subagent brief. After BUNKER1: walk the objective/exit path to a level
transition, then the next stage.
**Real remaining work, in order (updated session M-8):**
1. **Input layer (Phase 3)** — IN PROGRESS (M-8 agent, §F D118). `port/src/
input.c` was a stub; only a minimal keyboard path in `libultra.c`
(`contSnapshotFromKeyboard`) worked. No C-buttons/mouse-look/gamepad/
rebinding. This is the top playability blocker — you cannot aim or
properly move. Reference: `pd_port/port/src/input.c`.
2. **Weapon model doesn't draw (AUDIT-M6 #5)** — NOT STARTED (M-8 agent
hit the account session rate-limit before any work; tree untouched).
Plan: instrument `gunfire.c` weapon setup (~600-661) + `field_87F`
gating (~520/587-598) FIRST with a `GE_D119` printf — does setup even
run? Then check `weaponModel.render_pos` = `dynAllocate`'d `rwmtx`
lifetime vs the `gunRenderFirstPersonGunModels` (~1605) consumer (on
N64 it aliased the persistent `hand->mtxlist`; D102 gave it own
storage → arena may recycle). Also `weaponRwPool[192]` capacity vs
`modelCalculateRwDataLen(mdlhdr)` on PC, and `flashvisptr` word-index
assumption (~640). Narrow `#ifdef PORT` lifetime/sizing fix only — if
it needs a behavioural arena change, STOP + write up. Direct `-level_09`
boot may spawn Bond holstered — may need forced weapon state or the
attract path to get the gun on screen. Gate to playability.
3. **The HUD/text X-mirror (D116)** — DEPRIORITISED, parked in
`docs/GRAPHICS-BACKLOG.md`. M-11 confirmed the ammo digits use the
SAME `textrelated.c` path (no separate renderer — the M-8 premise was
wrong) and re-hit the same contradiction: every stage verified
non-mirrored, glyphs still flip. **Do not re-static-trace.** Next
attempt needs a RenderDoc/apitrace capture or the asymmetric-1-texel
experiment — nothing else. All cosmetic rendering issues (D74 dead
wrap-block, D75/D76/D77) now tracked in `docs/GRAPHICS-BACKLOG.md`;
they rank BELOW level-progression and crash work.
4. **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`).
5. Add an f32 value spot-check to `d43_emit.py`'s verify pass (it only
checks pointers/opcodes today — the D112 bug passed "ALL CHECKS PASSED").
6. **GE_DETERM fixed-tick mode (§F D117)** — deferred as not-narrow; would
unlock exact-diff regression testing + reproducible timing bugs. Design
is written up. Worth doing once the above land.
**Session M-7 — see §F D116 + "D116 probe results".** Ran the glyph
probe (overseer, after killing a subagent that was drifting toward a
global texrect S-swap). Corrected finding: the HUD text mirror is a
**per-quad texture-U flip that is NOT path-specific** — the ammo digits
"83" are mirrored too (`ppm/frame_000320.ppm`), refuting M-7's earlier
"proportional-font-specific" claim. Rect *positions* are correct; only
texture content is X-flipped. Every fast3d stage probed clean (glyph
bitmap in memory correct, texrect `ul.u=0→lr.u=max`, `import_texture_i8`
linear, GL shader UV pass-through) — so the flip is in the GL
vertex-buffer/draw layer OR a shared clip-space-X vs U desync on rect
quads. **This re-opens D114's shared-mirror hypothesis** (now per-quad
U/X, not screen-space) and plausibly also explains inverted guards /
mislocated door props. NEXT: shader/vertex-buffer probe — dump per-vertex
(x,u) for one glyph quad; render a 1-texel asymmetric test texture to see
which axis inverts. `GE_D116`-gated probes left in `textrelated.c` +
`gfx_pc.cpp` (zero-cost).
**Session M-8 wrap — state for next session.** Build GREEN at
`587c6856`, tree clean. Committed: D116 part-3 writeup + `[D116/vbo]`
probe (`5ff012b4`), `CLAUDE.md`/preflight (`fd25628a`), D117
framediff+nondeterminism (`44d9ae98`), D118 input layer
(`4ac11fe7`/`587c6856`), handoff reprioritisation. `GE_D116` and
`GE_INPUTLOG` probes are in-tree, `getenv`-gated, zero-cost. Weapon-model
(#5) agent never ran (rate-limit) — task untouched, plan in item 2 above.
Pre-existing stash `stash@{0} "d59 probes WIP"` (~300 lines,
gfx_pc/gfx_opengl/crash.c) is NOT from M-8 — provenance unknown, left as
found. Next: human input playtest (tune `ge007.ini [Input] MouseAimSpeed`),
then weapon-model #5.
**Session M-8 (continued) — D116 part 3 + input layer.** Killed the
drifting D116 agent, ran the probe as overseer: `buf_vbo` (x,u) for
glyph quads and the GL texture upload are both runtime-verified
non-mirrored (x-left<->u=0), GL shader is `gl_Position=aVtxPos` +
UV-passthrough — every stage clean, yet glyphs render X-flipped
(contradiction, §F "D116 runtime probe part 3"). Deprioritised as
cosmetic. `GE_D116` `[D116/vbo]` probe kept in `gfx_pc.cpp`. Then
dispatched the **input-layer agent** (§F D118, in progress) — top
playability blocker. `framediff.py` structural mode is the interim
regression gate (`tools_pc/golden/` is a nondeterministic D115 capture).
**Session M-8 — see §F D117.** Visual-regression tooling. Root-caused the
frame-to-frame nondeterminism: pure variable-timestep frame pacing
(`osGetCount()` = wall clock on PC → `frametiming.c waitForNextFrame`
advances logic a real-time-dependent number of 60 Hz ticks per render).
PRNG seeding verified correct (`random.c`), not a source. A `GE_DETERM=1`
fixed-tick mode was assessed **not narrow** (redesigns VI retrace/tick
semantics, deadlock risk in load-screen loops) and deferred with a full
design in §F D117 — NOT implemented. Added `tools_pc/framediff.py`
(structural/tolerant: 16×12 grid mean-colour + non-clear-% + aHash,
`--mask` for HUD, `--exact` for a future deterministic build, `--update`
to refresh goldens). Validated against the D115 golden set. No C changes,
no probes left in tree.
**Session M-10 — input playtest + guard-firefight crash chain.**
- **D119 — guard-attack crash FIXED** (`chraction.c`). Every prior
`-level_09` run segfaulted ~frame 1200 the instant a BUNKER guard
opened fire: `bondwalkItemGetAutomaticFiringRate` (`gun.c:1334`) ←
`chrlvInitActAttack`. ~28 sites pun `weapons_held[]->chr` (really a
`WeaponObjRecord*`) as `ChrRecord*` and read `.act_<x>.attack_item`,
which on N64 aliases `WeaponObjRecord.weaponnum` (act union @0x2C + 84
== 0x80). Pointer widening moves the act union to ~0x38 on PC → garbage
negative item id → OOB `g_ItemStats` → crash. Fix: `PUN_ATTACK_ITEM()`
macro reads `weaponnum` directly; `#else` branch is textually identical
to the original → N64 build unchanged. Class-A (D53.2 type-pun) bug.
- **D120 — blood-stain hang GUARDED, not fixed** (`chr.c:3322`). Next
blocker, reachable only after D119: first guard bullet-hit →
`chrCreateBloodStain` `PointUsage[]` negative-terminated chain walk
cycles forever (VI thread keeps posting, logic thread spins, kernel
heartbeat trips). Root cause: `tools_pc/d43_emit.py`'s opcode-0x18
`ModelRoData_DisplayList_CollisionRecord` conversion is incomplete —
6 pointer fields widen the struct and `PointUsage`/`CollisionVertices`
sub-array endianness+stride aren't handled (only `CollisionRelatedNode`
got the D43/D45 `u32`-vma treatment). **Interim:** `#ifdef PORT` caps
both walk loops at `numVertices+8` iters + bounds-checks `index` → game
survives, blood decals may be missing/wrong. **Real fix (next session,
own subagent brief):** byte-spec the opcode-0x18 record + PointUsage +
CollisionVertex sub-arrays vs a converted guard model, extend
`d43_emit.py`. Same shape as D69/D88 converter work.
- Result: `-level_09` now survives the guard firefight — 45 s+
crash-free, frames past 1200 (was: hard segfault ~1200 every run).
**Session M-10 — input playtested (human).** Fixed in `port/src/input.c`:
(a) mouse-look was a draining accumulator → flicks stayed "pressed" ~8
frames after you stopped (stuck looking up/down) — now per-poll delta,
no carryover; (b) horizontal mouse was routed to C-left/right = GE
*sidestep*, and A/D drove analog stick-X = GE *turn* — swapped: mouse X
→ stick-X turn (`MOUSE_TURN_GAIN 6.0`), A/D → C-left/right strafe, mouse
Y → C-up/down look. Core aim/move now works.
**Open input bugs (documented, deferred — fix in a later pass):**
- **D118a — mouse yaw slower than pitch.** Horizontal turn (analog
stick-X via `MOUSE_TURN_GAIN`) is visibly slower than vertical look
(digital C-up/down). Different transfer curves (analog rate-limited vs
digital full-press). Needs `MOUSE_TURN_GAIN` bump and/or a matched
pitch path; ideally a single tunable `MouseAimSpeed`.
- **D118b — mouse Y inverted.** Mouse up → looks down, mouse down →
looks up (X unaffected). `aimDY`→C-button sign is backwards for GE's
pitch convention (or SDL dy sign assumption wrong). Flip the
`aimDY >=`/`<=` C-up/C-down assignment (or default `MouseInvertY`).
One-line fix once confirmed against GE pitch sign.
- Still TODO from D118: rebinding, gamepad hotplug, `ge007.ini` not yet
created (config defaults are hardcoded).
**Session M-9 (Phase 3) — see §F D118.** SDL input layer implemented.
`port/src/input.c` is now real: keyboard+mouse and SDL_GameController,
mapped to GE's N64 pad (analog stick = move, C-buttons = aim, mouse-look
bridged to digital C-buttons via a clamped accumulator, RMB/LT = aim
mode, LMB/RT = fire). `libultra.c`'s SI section delegates to it (single
source of controller state; still driven by `osContStartReadData`, no
new frame hook). Config: `ge007.ini [Input]` MouseEnabled/MouseAimSpeed/
MouseInvertY. Build GREEN, boots `-level_09` crash-free 35 s.
**Owed: a human playtest** — live input is untested from the headless
agent. Manual checklist: (1) WASD moves Bond, mouse turns/looks (tune
`MouseAimSpeed` in ge007.ini if too fast/slow), LMB fires, RMB enters
aim mode, Enter opens the pause menu; (2) plug an Xbox pad — left stick
moves, right stick aims, triggers fire/aim; (3) `GE_INPUTLOG=1` prints
each nonzero OSContPad poll. Rebinding + gamepad hotplug are TODO (§F
D118).
**Session M-6 — 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):**
| # | commit | one-liner |
|---|--------|-----------|
| D93 | `164d7f99` | null-room (room 0) NULL-deref guards |
| D85 | `493c9838` | `bgWidenRoomGdl` (8→16 `Gfx` + `bswap32`) + `bgSwapRoomVtx`; room DLs decode to real GBI; `bg.c:2448` size-cast bug |
| D85 | `6f0208d6` | `ptr_texture_alloc_start` → real `struct texpool` storage (pool looked exhausted → ~630 room textures now resolve; cleared `Bad size for RGBA texture`) |
| D94 | `63204a27` | `chrlvInitActAttack` `(s32)`-truncated anim-table index |
| D95 | `f35eba91` `933ba52b` | 2× `g_GfxBuffers` (16-byte PC `Gfx`) + raise PC mempool ceiling `0x702F4400→0x70700000` (reclaim ~4 MB DRAM) so it doesn't OOM `MEMPOOL_STAGE` |
| D96 | `d86ec483` | `PROPRECORD_STAN_ROOM_LEN` 4→8 (PORT) — prop room-list stack overflow (guards span ≥4 rooms → no terminator → smashed `chrpropsRenderPass` frame; *this* was the "runaway GDL append") |
| D97 | `2fbcc556` | clamp negative `damagetype` (US-only OOB `g_DamageTypes[]` read) |
| D98 | `000ed6af` | `initBONDdataforPlayer` allocate real PC `sizeof(struct player)` (hardcoded `0x2A80` under-alloc scribbled the master DL) |
| D99 | `253caa23` | `Model.animflipfunc` `s32` fn-ptr truncation (flag + direct call, D92 pattern) |
| D100 | `8eaad547` | `struct player.model` is an inline `struct Model`, not a `Model*` + 45 filler s32s (PC struct 0x2A8 ≫ 0xB8 hole → `animInit` overran); + dedicated `gaitRwData[]` |
| D101 | `b2234f82` | `sub_GAME_7F06DB5C` stashed `ModelNode*`/`RenderPosView*` through an `s32` (the idiom its sibling was already fixed for) |
| D102 | `1b078f6d` | 1P weapon `Model` + RW pool punned onto `struct hand` (PC `Model` 0xE8 ≫ N64 0xBC → `modelInit` aliased `datas` onto the pool); dedicated `weaponModel`/`weaponRwPool` |
**Recurring pattern this session:** the decomp pun-allocates `struct
Model` (and `struct player`, `PropRecord`, `struct texpool`, …) into
N64-sized holes / hardcoded byte counts. Every one is bigger on x86-64
→ overrun → corruption. Fix = give it real inline storage or
`sizeof()`-based alloc under `#ifdef PORT`. **Landmine still open**
(§F D100): `struct player` / `struct hand` have raw hardcoded-offset
accessors (`gunfire.c` `THROWMTX` at `+0xAD8`, …) that are NOT
PORT-adjusted — already PC-wrong, a real `struct player` offset pass is
owed before grenade/knife-throw code works.
- Build: `export PATH="/c/msys64/mingw64/bin:$PATH" && ./build-pc.sh ntsc-final`
- Sidecar regen: `python tools_pc/d43_emit.py ntsc-final && python
tools_pc/d69_emit.py ntsc-final && python tools_pc/d88_emit.py
ntsc-final --regen`. `data/` is gitignored; if missing,
`cp baserom.u.z64 data/ge007.ntsc-final.z64` first (§F "`data/`
deletion + recovery").
- Repro: `./build-pc/ge007.x86_64.exe -level_09 -ml0 -me0 -mgfx100
-mvtx50 -mt700 -ma150` — `-m*` are BUNKER1's `boss.c` per-level sizes;
a bare `-level_09` skips them and crashes early (TODO: auto-inject).
## Next: D75 — 3D model rendering quality
BUNKER1 renders but character/skeletal models are wrong (see the D75
section further down, still accurate). `GE_PCDUMP` + `tools_pc/pixcount.py`
to measure; the animated-model path (`animInit` + `struct player` raw
offsets, `drawjointlist`, `modelApplyHeadRelations` head/body splice) is
the prime suspect. `struct tex` headers are 24 B vs 16 B on PC — a
separate open pool-pressure item; a real PC memory-budget pass is owed.
## What this session (M-2) fixed — all committed
1. **`-level_09` now works.** `osPiReadIo` was stubbed to 0, so the token
string was always empty and N64 debug switches were ignored (only
attract mode could reach a level). Now built from `argv[1..]`.
2. **Sidecar tables patched in `obInit()`** instead of lazily at first
model load — a direct stage boot loaded raw big-endian ROM before.
3. **D88.5** — stan tile-name byte-swap in the converter (0/276 →
273/273 name matches during pad setup).
4. **D88.6** — intro CAMERA `lang1c` is a `u16` pair, not an `s32`
(fixed the `langGet` NULL-bank crash).
5. **D89** — `init_path_table_links` `[-3]` OOB → SIGILL fix; NULL-tile
guard in `sub_GAME_7F0B0914`.
6. **D90** — `stanTileDistanceRelated`'s 80-byte zero-fill was clobbering
the caller's live stan-tile local (16B struct, 80B clear). The
player's spawn stan was fine all along.
7. **D91** — `(s32)&D_800442FC[portalnum]` truncation in bg portal cull.
8. **D92** — `chrAllocate` `s32` param truncated the ailist pointer;
`Model.unka0` `s32` field truncated a stored function pointer.
**Next steps for the resuming session (render milestone):**
1. **D85** — root cause confirmed (8-byte N64 `Gfx` vs 16-byte PC `Gfx`
in the unconverted per-room DL blob). Runtime widen+bswap fixup being
implemented; verify with `GE_D69BB=1` (real GBI opcodes, not
00/01/02/52), then `GE_PCDUMP` + `tools_pc/pixcount.py` for
non-degenerate BUNKER1 geometry. See §F "D85 root cause CONFIRMED".
Fallback if the widen stalls: soften the four `sysFatalError("Bad
size…")` guards in `gfx_pc.cpp` to skip-with-warning.
2. Then the front-end **render bugs (D75)**.
**D88.4 loose end (still open):** `PROPDEF_PC_BYTES` for `VEHICHLE`/
`AIRCRAFT`/`TANK`/`AMMO`/`DEPOSIT_IN_ROOM` in `d88_propdefs.py` are
placeholder guesses (BUNKER1 doesn't use them). Probe real sizes via
`d88_layoutprobe.c` before loading levels that use those types.
## Known rendering bugs (D75 — still open, orthogonal to the D88 crash)
Even once the crash chain is cleared, the front end has **broken 3D model
rendering** (user-confirmed this session):
- Rareware logo: correct (fixed in D73/D74).
- **Nintendo logo**: renders but **mispositioned**.
- **Gun-barrel intro**: the **James Bond character model is missing
entirely**.
- **Intro credits / cast roll**: the per-character 3D models **do not
appear at all** (names draw, models don't).
Pattern: textures/text draw; **animated/skeletal character models never
appear**; static 3D (logos) appears but with a bad transform. Leading
hypothesis is D75(b) — the animated-model path (`animInit` + raw offsets
into `struct player`, cf. D56) is broken independently of the D73 matrix
sin/cos fix. Full triage plan in `PCPortResearch.md` §F D75.
The rest of this document (below) is the **last known-good, committed**
status as of commit `8c9c6a2c` (D86+D87 resolved) — still accurate except
D88 is now further along (D88.1–3 done/verified, D88.4 is the live crash).
Full workstream breakdown (WS1–WS6) in `docs/PLAN-linear-level-sweep.md`.
## Where things stand
**D69 (the original "stage load faults" milestone blocker) remains
RESOLVED.** `load_bg_file` (bg.c) doesn't fault on BUNKER1. Since then,
two more crashes further down the load chain were found and fixed this
session (D86, D87), and a third — the current blocker — was root-caused
but not fixed (D88).
- **BUNKER1 (`-level_09`) is playable-ish.** Loads, renders recognisably
(textured rooms, storage racks, floor; skeletal guards render as
humanoids), and **survives a guard firefight** — 45 s+ crash-free
(D103–D120). **Silo (`-level_20`) also loads + renders clean.**
Committed through `f2beae4b` (M-12: D121 WS1 boot, D122 propDef fix).
- **Input layer (Phase 3) landed and playtested** (D118, M-9/M-10).
`port/src/input.c` is real: keyboard+mouse + SDL_GameController → N64
pad. Core aim/move works. **Open polish bugs (deferred):** D118a mouse
yaw slower than pitch; D118b mouse-Y inverted; rebinding / gamepad
hotplug / `ge007.ini` generation still TODO.
- **Recent fixes (M-10, `d1e93b76`):** D119 guard-attack crash
(`weapons_held[]->chr` type-pun) fixed; D120 blood-stain hang guarded
(not fixed — `d43_emit.py` opcode-0x18 converter gap).
- **Cosmetic backlog (parked, `docs/GRAPHICS-BACKLOG.md`):** D75 front-end
3D model transforms (Nintendo logo misplaced, gun-barrel Bond absent,
cast models absent), D76 disclaimer screen partial, D77 no audio,
D114/D116 HUD/text X-mirror (**DO NOT re-static-trace** — see
PORT-LEARNINGS §D2), D74 dead wrap-block.
**D86 RESOLVED.** `modelInitRwData` crash (`model.c:6174`) was a single
truncating pointer cast in the player's embedded gait/arm model:
`src/game/initplayergaitobject.c:5` did
`player_gait_object_header.RootNode = (int)&player_gait_hdr;` — a
same-width no-op on N64 that truncates+zero-extends a real 64-bit pointer
on PC. Fixed with a narrow `#ifdef PORT` branch assigning the pointer
directly. Root-caused via a new node-walk trace (`GE_D86=1`, left in
place, gated in `model.c`/`objecthandler_2.c`).
## Done this session (M-12) — 2 commits
**D87 RESOLVED.** Once D86 stopped blocking progress, an idle (no-input)
run eventually triggers the front-end's genuine attract-mode demo
playback (`select_ramrom_to_play()` picks a random compiled-in demo —
this is shipped retail behavior, not a debug feature) and crashed in
`ramrom_replay_handler` (`ramromreplay.c`). Root cause: `ramromfilestructure`
is a real ROM-compiled asset (big-endian, like everything else) loaded via
`romCopyAligned()` — a raw byte copy by design (D66) — with **no
byteswap**, so every multi-byte field read back scrambled (e.g. `size_cmds`
2 → 33554432) and drove wild pointer arithmetic. Fixed with a `#ifdef PORT`
`ramromFixupEndian()` called once after the load (same pattern as the D54
cseq fixup). Not BUNKER1-specific — attract mode picks any of 7 demo
locations at random, so don't rely on it for BUNKER1-specific testing (see
`-level_09` below).
- **WS1 / D121 (`02c12068`)** — bare `./build-pc/ge007.x86_64.exe
-level_XX` auto-injects that stage's `memallocstringtable[]` `-m*` pool
row (`#ifdef PORT` in `boss.c bossInitMainthreadData`). No manual `-m*`
args any more. Gotchas recorded: setting `g_DebugAndUpdateStageFlag=1`
is the WRONG fix (routes boot through the title-stage intro), and
`tokenSetString` *replaces* the whole token buffer so the injected
string must carry `-level_XX` forward. See §H D121.
- **D88.4 was already resolved** (committed `ff563812`/`4ccc0d3c` weeks
ago). The "OPEN — cross-level blocker / `setupDoor` crash" note in the
old §F index / plan was **stale**. `d88_emit.py --regen` = 21/21 pass.
- **D122 (`f2beae4b`)** — the real cross-level blocker was prop/item
**model loading**. `tools_pc/d88_propdefs.py` had a per-type handler
table + a generic fallback; 6 `inherits ObjectRecord` propDef types
(47 TINTED_GLASS, 39 VEHICHLE, 40 AIRCRAFT, 45 TANK, 13 AUTOGUN, 20
AMMO/MultiAmmoCrate) had **no handler** → fell to the generic
word-granular `bswap32`, which swapped the `[s16 obj][s16 pad]` word as
one u32 → model id `obj` in the wrong half → OOB `PitemZ_entries[]`
deref → crash in `modelLoad` (`loadobjectmodel.c:393`) /
`modelInitRwData` (`model.c:6249`). Fixed in the converter
(`OBJ_TAIL_DESC` handler) + matching `sizepropdef()` `#ifdef PORT`
stride. BUNKER1/Silo were never affected (they don't emit those types).
Full write-up: §F/§H **D122**. Confidence: crash fix high; types
39/40/45 tail layout medium (structs have "locs unconfirmed" notes —
nudge if a level actually drives a tank/vehicle).
**D88 — SUPERSEDED, see "READ THIS FIRST" at top.** D88.1–D88.3 (header +
sub-table width/endian conversion) are now done and verified; D88.4
(`propDefs` byteswap) is the live blocker. The paragraph below is the
original root-cause writeup, kept for context.
**Uncommitted docs** (tangled with the in-progress big docs restructure —
`HANDOFF.md`, `AGENTS.md`, `CLAUDE.md`): the D121 + D122 entries in
`docs/PCPortResearch.md` §F/§H and this file's edits. Commit or fold in.
**D88 (original writeup) — root-caused.** Launch with
`-level_09` (NTSC `LEVELID_BUNKER1 = 9`; `boss.c:199-339` decodes
`-level_XX` into `g_StageNum`, bypassing the front end/attract-mode
entirely — fast, deterministic BUNKER1 repro, crashes in well under a
minute instead of waiting ~2 min for attract mode to maybe pick Bunker).
Crash: `proplvreset2` (`prop.c:1306`) segfaults reading
`g_CurrentSetup.pathwaypoints[i1].padID`. Root cause: the per-level
`"Usetup<name>Z"` file (`prop.c:1267`, `struct stagesetup` in
`bondtypes.h:4091`) is loaded as raw ROM bytes and has **zero PC porting
work done on it** — unlike bg/stan (D69/D80-82) and models (D43/D50).
Two compounding problems, not just one:
1. The 10 top-level fields (`pathwaypoints`/`waypointgroups`/`intro`/
`propDefs`/`patrolpaths`/`ailists`/`pads`/`boundpads`/`padnames`/
`boundpadnames`) are declared as real pointers in the live C struct,
so on PC they're 8 bytes each (an 80-byte header) instead of the
file's real 4-byte-each (40-byte) N64 layout — same class as D79
(`bg_room_data` pointer growth). Field 0 reads fine; everything after
it is reading the wrong bytes entirely.
2. The 4 meaningful bytes each field *does* store are big-endian (the
code's own comment: "stores every internal reference as a byte offset
from the start of the file") and nothing byte-swaps them — same class
as D87.
No PORT/byteswap handling exists anywhere in `prop.c` (confirmed by grep).
**Not fixed this session** — this is D69-scale format-conversion work: a
byte-accurate spec of the whole `Usetup*Z` format (top-level header +
every nested sub-table: `waypoint`/`waygroup`/`PropDefHeaderRecord`/
`PathRecord`/`AIListRecord`/`PadRecord`/`BoundPadRecord`/`pname`, each
likely with its own internal offsets not yet audited) plus either an
offline converter sidecar (preferred pattern per AGENTS.md, same shape as
`tools_pc/d69_emit.py`) or a careful runtime fixup pass that parses the
raw 40-byte N64-packed header by explicit byte offset, byte-swaps each
field, and writes results into the PC-widened struct.
## Next task — WS4/WS5: drive the 21-level sweep
**Net effect vs. last session:** the game now runs substantially further
— all the way through room-streaming setup, past the intro's model
pipeline, and into per-level "Usetup" data — before hitting D88. The
"loads without fault" acceptance bar is **still not met**, but the
remaining blocker is now narrowly scoped and has a fast, deterministic
repro (`-level_09`, no attract-mode wait, no depending on which random
demo attract-mode picks).
**WS4 matrix DONE** — `docs/LEVEL-STATUS.md`. **7 / 21 PASS** (Bunker1 09,
Silo 20, Archives 24, Train 25, Caverns 39, Egypt 32, Cuba 54). 14 crashes
in 7 classes C1–C7. Reusable sweep runner: `tools_pc/level_sweep.sh`
(bare `-level_XX`, `GE_PCDUMP="80-260:40"`, 24 s watchdog,
`taskkill //F //IM ge007.x86_64.exe`; **`export PATH=".../mingw64/bin:$PATH"`
before `addr2line`** — the script's own symbolication no-op'd without it).
## Recommended next steps, in order
| Class | Levels | Site |
|---|---|---|
| **C1** | Dam 33, Runway 35, Frigate 26, Statue 22, Streets 29, Cradle 41 | `chrIsNotDeadOrShot` chraction.c:4483 — `self` = AI-list rodata ptr |
| **C2** | Facility 34, Jungle 37 | `import_texture_i8`/`gfx_tex_normalize_source` — bad tex ptr |
| **C3** | Aztec 28, Bunker2 27 | `propobj.c` door model / `linkedDoor` walk |
| **C4** | Depot 30 | `prop.c:902` `sp4C->room` after `walkTilesBetweenPoints` |
| **C5** | Control 23 | `bg.c:5723` `portal_pts->numPoints` |
| **C6** | Surface2 43 | `loadobjectmodel.c:393` `PitemZ_entries[modelid].header` (D122 cont.) |
| **C7** | Surface1 36 | `snd.c:653` `sndSetupSound` (audio — parked subsystem) |
1. **D88.** Get a byte-level spec of `Usetup*Z` (start from BUNKER1's
file; `strResource` is built as `"U" + "sev" + "Z"`-style name in
`prop.c:1253-1265` — check `setup_text_pointers[LEVELID_BUNKER1]` for
the exact literal). Decide offline-converter vs. runtime-fixup (D69's
bg/stan work is the template for the former; D54's cseq fixup is the
template for the latter — given the struct-width mismatch on top of
the byteswap, a runtime fixup that manually walks the *raw* 40-byte
N64 header by hand (not through the live `stagesetup` struct) into a
freshly-populated `g_CurrentSetup` is probably simpler than a full
sidecar here, but verify against a raw ROM hex dump either way).
2. Once BUNKER1 loads past `proplvreset2`, re-check for further crashes
in the same vein (this session found 3 in a row — D86, D87, D88 — each
only reachable after the previous one was fixed; expect more).
3. Once BUNKER1 reaches a rendered frame with no fault, revisit **D85**
(room primary/secondary GDL binaries decode to garbage via
`texLoadFromGdl`) — use `GE_PCDUMP="<range>:10"` + `tools_pc/pixcount.py`
to confirm non-black, non-degenerate content per the original D69
acceptance bar. (This session's `GE_PCDUMP` captures around frame
2100-2400 during attract-mode-driven "loading" were still on a HUD/menu
screen, not real 3D geometry — don't read too much into pixel counts
from before D88 is fixed.)
4. Do NOT touch D75/D76/D77 (parked, lower priority, unrelated).
**Done M-13:** C1 fixed (**D123**, `tools_pc/d88_propdefs.py` — the C1
crash was D122 fallout: widened `Vehichle/AircraftRecord.ailist` slot
zeroed instead of carrying its read-before-write int id). C2 split:
Jungle's texture crash fixed (**D124**, `port/src/gimgfixup.c` — compiled
explosion-DL sync keyed on an already-erased marker); Facility+Runway
diagnosed (model-GDL relocation misaligns `dst`, `objecthandler_2.c` /
`texLoadFromGdl`, D80/D82/D83 area) — NOT fixed.
## Debug tooling added this session (kept, env-gated, zero cost when unset)
Full re-sweep after both: **12 / 21 PASS** (`docs/LEVEL-STATUS.md`).
9 crashes remain in 6 classes: **C2** Runway+Facility (model-GDL align —
biggest), **C3** Aztec+Bunker2 (`docs/BRIEF-C3-C6-prop-model.md`),
**C2m** Jungle (explosion-DL `G_MTX`), **C6** Surface2 (`PitemZ` modelid),
**C5** Control (BG portal), **C4** Depot (BG tile/room), **C7** Surface1
(`sndSetupSound`). Priority order + files in `docs/LEVEL-STATUS.md` "Next".
- `GE_D86=1` — node-walk trace in `modelInitRwData` (model.c) + a
load-identity probe in `load_object_fill_header` (objecthandler_2.c).
Resolved the D86 crash; left in place since the same
load_object_fill_header/modelInitRwData pipeline could surface new
edge cases as more of the game becomes reachable.
- `GE_D87=1` — block-setup trace in `iterate_ramrom_entries_handle_camera_out`
and consumer trace in `ramrom_replay_handler` (ramromreplay.c). Resolved
the D87 crash; left in place as it's a rarely-exercised path (attract
mode) worth having visibility into if it acts up again.
- Pre-existing `GE_D69STAN=1`, `GE_D69BB=1`, `GE_D69=1` — unchanged, still
useful for the bg/stan/D85 load path (see prior session's HANDOFF
entries, preserved in git history, for exactly what each logs).
Uncommitted M-13 edits: `tools_pc/d88_propdefs.py`, `port/src/gimgfixup.c`,
§F/§H D123+D124, `docs/LEVEL-STATUS.md`, `docs/LEVEL-PLAYTEST.md`,
`docs/PORT-LEARNINGS.md`, `tools_pc/level_sweep.sh`, the BRIEF-C* files,
this file. Fold into the docs-restructure commit set.
## New: fast, deterministic BUNKER1 repro (no attract-mode wait)
Re-run `tools_pc/level_sweep.sh` after each fix; keep `-level_09` +
`-level_20` green (`framediff.py`). All-21-PASS → hand the user
`docs/LEVEL-PLAYTEST.md` for WS6.
Launch with `-level_09` as a program argument
(`./build-pc/ge007.x86_64.exe -level_09`) to skip the front end/attract
mode and load BUNKER1 directly — `boss.c:199-339` decodes `-level_XX`
(the two digit-chars are consumed as raw ASCII bytes:
`g_StageNum = tokenFindLevel[0]*10 + tokenFindLevel[1] - 0x210`; NTSC
`LEVELID_BUNKER1 = 9` → `"09"` since `'0'*10 + '9' - 0x210 = 9`). This is
now the preferred way to test BUNKER1-specific load/render work — it's
faster (crashes/completes in well under a minute vs. ~2+ min waiting on
attract mode) and deterministic (not dependent on which of 7 random demo
locations attract mode happens to pick).
**Verification per fix:** target level boots to ≥1 non-degenerate frame
(`tools_pc/pixcount.py` > a few %), AND `-level_09` + `-level_20`
unregressed (`tools_pc/framediff.py`). Regen sidecars after any converter
change: `python tools_pc/d43_emit.py ntsc-final && python
tools_pc/d69_emit.py ntsc-final && python tools_pc/d88_emit.py ntsc-final
--regen`.
**Parked (do NOT start here):** WS2 front-end menu playtest (needs a
human driving; cosmetics D75/D76/D77/D114/D116 all out of scope —
`GRAPHICS-BACKLOG.md`). Audio (Phase 3), saves (Phase 4).
## Environment / build
- `export PATH="/c/msys64/mingw64/bin:$PATH" && ./build-pc.sh ntsc-final`
(~5 s). Build is GREEN with all D86/D87 work included.
- Run from the **repo root**, not `build-pc/`.
- Regenerate sidecars if missing: `python tools_pc/d69_emit.py ntsc-final`
(bg/stan) and the D43/D50 model sidecar generator (pcmodels) — both
gitignored, not checked in. (Both were already present in this dev
environment this session.)
- `GE_PCDUMP="<start>-<end>:<stride>"` + `tools_pc/pixcount.py` for frame
captures once D88 is fixed and a frame actually renders real BUNKER1
geometry.
- Crash log: `ge007.crash.log` (repo root); symbolicate with
```sh
export PATH="/c/msys64/mingw64/bin:$PATH" && ./build-pc.sh ntsc-final # ~5 s
```
Run from the **repo root**, not `build-pc/`.
- **Repro (BUNKER1, skips attract mode):**
`./build-pc/ge007.x86_64.exe -level_09`
(per-level `-m*` pool sizes are now auto-injected — D121/WS1; pass them
by hand only to override). `boss.c:337` decodes `-level_XX` as
`d0*10 + d1 - 0x210`; LEVELID == the `-level_XX` number.
- **Sidecar regen** (after any converter change; `data/` is gitignored):
```sh
python tools_pc/d43_emit.py ntsc-final && \
python tools_pc/d69_emit.py ntsc-final && \
python tools_pc/d88_emit.py ntsc-final --regen
```
If `data/` is missing: `cp baserom.u.z64 data/ge007.ntsc-final.z64`
first (see §F "`data/` deletion + recovery").
- **Frame capture:** `GE_PCDUMP="<start>-<end>:<stride>"` → `./ppm/`
(gitignored). Analyse with `tools_pc/pixcount.py` (non-black content
check) and `tools_pc/framediff.py <ppmdir>` (structural regression vs
`tools_pc/golden/` — the port is **NOT frame-deterministic**, D117; use
`--mask` for the HUD, not an exact compare). `tools_pc/ppm2bmp.py` to
view.
- **Crashes:** `ge007.crash.log` (repo root). Symbolicate
`addr2line -e build-pc/ge007.x86_64.exe -f -C <pc>` (image base
`0x140000000`).
- gdb **launch** mode is too slow for timing-dependent bugs (unchanged
guidance). **New this session:** gdb **attach** mode
(`gdb -batch -x cmds.txt -p <winpid>`, `<winpid>` = 4th column of
`ps -p <bashpid>`) works well and is fast for "watch a global for a
legitimate vs. corrupted write" questions on an already-running,
not-yet-crashed process — see `docs/PCPortResearch.md` §F environment
reminders for the exact recipe used to root-cause D87.
`0x140000000`). Frames past the true chain may be stale (D56).
- **gdb:** launch mode is too slow for timing-dependent faults. **Attach**
mode is fast: `gdb -batch -x cmds.txt -p <winpid>` (`<winpid>` = 4th
column of `ps -p <bashpid>`; game must already be running, e.g. `nohup
… &`). A hardware watchpoint on a global catches a bad write in < 1 min.
- Standalone probe compiles need `-std=c11`.
## Non-negotiables (unchanged, see AGENTS.md)
## Non-negotiables (full list in AGENTS.md)
1. N64 build files untouched.
1. N64 build files (`Makefile`, `tools/`, `rsp/`, `ld/`) untouched.
2. Game logic unmodified except narrow, documented `#ifdef PORT`
ABI/layout exceptions (D86/D87 this session, both logged in
`PCPortResearch.md` §F). D88 is explicitly **not** patched with a
quick inline hack — it needs the same disciplined
spec-then-convert/fixup treatment as D69, logged as an open finding
instead per AGENTS.md's "stop and write it up" guidance for anything
beyond a narrow, obviously-correct exception.
3. Offline sidecar conversion preferred over runtime fixup for whole
ROM-asset formats (D69/D80-82 pattern) — likely the right call for
D88 too, though a careful runtime fixup is also plausible; decide
after the byte-level spec work.
ABI/layout/format exceptions — each logged as a Dxx in §F/§H with the
N64 line kept verbatim under `#else`. Anything beyond a narrow,
obviously-correct exception: **stop and write it up** with a confidence
rating, don't hack it in.
3. Prefer an **offline sidecar converter** (`tools_pc/d*_emit.py`,
D43/D69/D88 pattern) over a runtime fixup for a whole ROM-asset format.
+70
View File
@@ -0,0 +1,70 @@
# LEVEL-PLAYTEST — WS6 human completion-validation checklist
Run **after** `docs/LEVEL-STATUS.md` is all-21 load+render PASS. This is
the part that needs real input — a human plays each level start to finish
and confirms it matches retail. Findings feed a WS5-style triage round
(log each as a crash-class row or a new Dxx candidate).
## How to run one level
```sh
export PATH="/c/msys64/mingw64/bin:$PATH" && ./build-pc.sh ntsc-final
./build-pc/ge007.x86_64.exe -level_XX # bare — pools auto-injected (D121)
```
Controls: `port/src/input.c` — keyboard+mouse or SDL gamepad. Known input
polish gaps (do not re-log): D118a mouse yaw slower than pitch, D118b
mouse-Y may feel inverted. Difficulty for a bare `-level_XX` boot defaults
to Agent; use the menu path (mission select → difficulty) to test 00/007.
Objective status: `src/game/objective_status.c`
(`get_status_of_objective()` `:161`); objective propDefs are
`PROPDEF_OBJECTIVE_*` (`bondconstants.h:4332`).
## Per-level checklist (copy this block per level)
```
### <Level> (-level_XX) difficulty tested: [ ] Agent [ ] Secret Agent [ ] 00 Agent
- [ ] Spawn point + start orientation match retail
- [ ] Geometry / textures / lighting recognisable, no missing rooms
- [ ] Guards spawn, path, react, take/deal damage, die correctly
- [ ] Doors / lifts / switches / destructibles work
- [ ] Pickups (ammo, armour, key items, gadgets) present and collectable
- [ ] Each difficulty-gated objective reachable AND registers COMPLETE
(objective_status)
- [ ] Alarms / reinforcements / scripted events fire
- [ ] Level exit trigger fires → MISSION COMPLETE (not a hang / wrong screen)
- [ ] Auto-advance to next briefing works
- [ ] Frame rate + timing acceptable
- Notes / defects:
```
## 21 solo levels (mission order — numbers = `-level_XX`)
| # | Level | -level | Briefing asset |
|---|---|---|---|
| 1 | Dam | 33 | UbriefdamZ |
| 2 | Facility | 34 | UbriefarkZ |
| 3 | Runway | 35 | UbriefrunZ |
| 4 | Surface 1 | 36 | UbriefsevxZ |
| 5 | Bunker 1 | 09 | UbriefsevbunkerZ |
| 6 | Silo | 20 | UbriefsiloZ |
| 7 | Frigate | 26 | UbriefdestZ |
| 8 | Surface 2 | 43 | UbriefsevxbZ |
| 9 | Bunker 2 | 27 | UbriefsevbZ |
| 10 | Statue | 22 | UbriefstatueZ |
| 11 | Archives | 24 | UbriefarchZ |
| 12 | Streets | 29 | UbriefpeteZ |
| 13 | Depot | 30 | UbriefdepoZ |
| 14 | Train | 25 | UbrieftraZ |
| 15 | Jungle | 37 | UbriefjunZ |
| 16 | Control | 23 | UbriefcontrolZ |
| 17 | Caverns | 39 | UbriefcaveZ |
| 18 | Cradle | 41 | UbriefcradZ |
| 19 | Aztec | 28 | UbriefaztZ |
| 20 | Egypt | 32 | UbriefcrypZ |
| 21 | Cuba | 54 | (post-Egypt unlock) |
(Mission table: `front.c:433`. Cuba = `LEVELID_CUBA`, not in the standard
folder list — unlocked after Egypt.)
+160
View File
@@ -0,0 +1,160 @@
# LEVEL-STATUS — 21 solo-level load+render sweep (WS4)
Method: bare `./build-pc/ge007.x86_64.exe -level_XX` (per-level `-m*` pools
auto-injected, D121), `GE_PCDUMP="80-260:40"`, ~24 s watchdog then
`taskkill //F //IM ge007.x86_64.exe`, `ge007.crash.log` symbolicated with
`addr2line -e build-pc/ge007.x86_64.exe -f -C <PC>` (image base
0x140000000). "render %" = `tools_pc/pixcount.py` non-clear on the last
captured frame. Binary at `f2beae4b` + this session's build.
Last full re-sweep: 2026-08-29, after D123 (C1) + D124-Jungle (C2) landed.
Build = `f2beae4b` + `tools_pc/d88_propdefs.py` (D123) + `port/src/gimgfixup.c` (D124).
| Level | # | Status | Crash site | Class |
|---|---|---|---|---|
| Bunker1 | 09 | **PASS** 91.7% | — | — |
| Silo | 20 | **PASS** 91.7% | — | — |
| Archives | 24 | **PASS** 91.1% | — | — |
| Train | 25 | **PASS** 87.0% | — | — |
| Caverns | 39 | **PASS** 91.6% | — | — |
| Egypt | 32 | **PASS** 90.9% | — | — |
| Cuba | 54 | **PASS** 92.5% | — | — |
| Dam | 33 | **PASS** 83.3% | — (C1 fixed, D123) | — |
| Frigate | 26 | **PASS** 90.4% | — (C1 fixed, D123) | — |
| Statue | 22 | **PASS** 80.1% | — (C1 fixed, D123) | — |
| Streets | 29 | **PASS** 91.6% | — (C1 fixed, D123; no C2 crash this run — timing) | — |
| Cradle | 41 | **PASS** 55.4% (low — partial render, no crash) | — (C1 fixed, D123) | — |
| Runway | 35 | CRASH | `import_texture_i8` gfx_pc.cpp:821 (fault 0x721b8ee8) — same as Facility | **C2** |
| Facility | 34 | CRASH | `import_texture_i8` gfx_pc.cpp:821 (fault 0x72181ee8) — model-GDL relocation align bug, D124, NOT fixed | **C2** |
| Jungle | 37 | CRASH | `gfx_sp_matrix` gfx_pc.cpp:1046 (fault 0x401c68e0) — C2 texture crash fixed (D124), now explosion-DL `G_MTX` (D75/matrix family) | **C2m** |
| Aztec | 28 | CRASH | `door7F054FB4` propobj.c:13601 (`door->model->obj->RootNode->Child->Child`) | **C3** |
| Bunker2 | 27 | CRASH | `door7F054FB4` propobj.c:13523 (door displacement list walk) | **C3** |
| Depot | 30 | CRASH | `sub_GAME_7F00324C` prop.c:902 (`sp4C->room` after `walkTilesBetweenPoints`) | **C4** |
| Control | 23 | CRASH | `sub_GAME_7F0BA2D4` bg.c:5723 (`portal_pts->numPoints`, via chrprop.c:62) | **C5** |
| Surface2 | 43 | CRASH | `modelLoad` loadobjectmodel.c:393 (`PitemZ_entries[modelid].header->RootNode`) | **C6** |
| Surface1 | 36 | CRASH | `sndSetupSound` snd.c:653 (fault 0x56220001…, ascii-ish) | **C7** |
**12 / 21 PASS.** D123 cleared C1 on all 6 (Dam/Frigate/Statue/Cradle/Streets
PASS; Runway falls through to C2). D124 cleared Jungle's texture crash but
it now hits an explosion-DL matrix crash. 9 crashes remain in 6 classes.
Notes: Streets passed this run but the C1 agent saw a `gfx_sp_matrix`
crash — likely the same explosion-DL `G_MTX` (C2m) as Jungle, timing-
dependent (D117 nondeterminism). Cradle renders only 55% — watch for a
partial-load issue when it gets a playtest. Frigate/Statue vary 68–90%
run to run.
## Crash classes (most-impactful first)
### C1 — chr / AI-record pointer deref — FIXED (D123)
**RESOLVED.** Root cause: `tools_pc/d88_propdefs.py` (D122's `OBJ_TAIL_DESC`)
zeroed the widened `VehichleRecord/AircraftRecord.ailist` slot instead of
carrying its pre-populated int AI-list id, so `prop.c:1764/1786` →
`ailistFindById(0)` → `GAILIST_AIM_AT_BOND` → `ai()` ran a CHR aim list
against `ChrEntityp==NULL`. Fixed via `OBJ_ID_WORDS`. See §F/§H D123.
Dam/Frigate/Statue/Cradle now PASS; Runway/Streets fall through to C2.
<details><summary>original triage notes</summary>
`chrIsNotDeadOrShot(ChrRecord *self)` faults on `self->actiontype` with
`self` bogus (Dam: Rdi=0x1401296a0 → an image address; fault addr 0x8).
Reached from `chraidata.c:61` (`m_AimAtBond[]` AI-list region — backtrace
frame is inside the global AI-list rodata blob, i.e. the return address
was corrupted OR an AI-list entry is being called as a function pointer).
Pointer-width / BE-rodata family (PORT-LEARNINGS §A / §C): a per-level
`Usetup*Z` guard record (`GuardRecord` type 9 / `GuardAttributeRecord`
type 18) or chr-spawn pointer not converted / not pointer-width-adjusted,
OR the AI-list bytecode pointers in `chraidata.c` rodata are BE and get
called raw. **Biggest single win — 6 levels.**
</details>
### C2 — fast3d bad texture pointer (D124) — SPLIT into two causes
**Jungle (`0xabcd0824`) — FIXED (D124).** `gimgSyncCompiledGlobalDLs()`
never copied the resolved texture pointers into the compiled
`globalDL_0xNNN` explosion DLs (slot-detect keyed on a marker `texLoad()`
had already erased); the arrays kept link-time `IMAGESEG` words and the
first explosion-DL draw fed `0xABCDxxxx` into fast3d. Latent on every
level. Fix: `port/src/gimgfixup.c` detects the slot from the compiled
array. Jungle now renders ~300 frames then hits a *separate* explosion-DL
`G_MTX` crash (`gfx_sp_matrix` gfx_pc.cpp:1046 — D75/matrix family).
**Facility (`0x72181ee8`) — NOT fixed, separate cause.** Bogus `G_SETTIMG`
w1 in a model/prop GDL produced by the runtime `texLoadFromGdl()`
relocation in `sub_GAME_7F0762E0` (`objecthandler_2.c:82`): its model-GDL
output `dst` pointers are non-16-aligned (N64 8B vs PC 16B `Gfx` stride
mix in the `replacementgdl`/`name` offset math). Open D80/D82/D83
"model/room GDL runtime conversion unverified" area — shared infra, needs
a dedicated pass. Files: `src/game/objecthandler_2.c`, `src/game/tex.c`
(`texLoadFromGdl`), `src/game/model.c` (`modelNodeReplaceGdl`).
**M-14 update (partial, still NOT fixed):** crash cmd pinned —
`G_SETTIMG w0=0xfd900000 w1=0x72181ee8` where `w1` is a garbage
`tex->data` from the texture-pool path (`texLoadFromModelFileHeader` →
`texLoad`), not solely the GDL command-stream copy. Every model file
loads at an odd (`&15==1`) `objheader->Switches` base
(`mempAllocBytesInBank` does no alignment) and `delta&15` != 0.
Full write-up + next-step probe plan: PCPortResearch §F D124-Facility
addendum. Likely fix site: the `d43_emit.py`/`pcmodels` sidecar's model
texture-blob offsets (N64 8B-Gfx vs PC 16B-Gfx GDL extent).
### C3/C6 update (M-14, D125) — NOT a converter type-gap
Root cause narrowed: the converted `propDefs` blob in RAM does **not** match
`tools_pc/d88_propdefs.py`'s offline output (after record 0 it is
zeros/garbage), so the runtime `sizepropdef()` walk drifts +101 records by
the first door → `setupDoor`/`modelLoad` get the wrong `pdefIndex`/modelid.
Histogram diff is clean (no type absent from passing levels). Suspect a
destination-offset / `pd_end` / header-reloc bug in `tools_pc/d88_emit.py`'s
propdefs emit path (retrofitted post-D122; docstring still says "opaque
passthrough"). Not fixed — see §F/§H D125. **d88_propdefs.py `convert_stream`
itself and `sizepropdef()` PORT strides are verified correct.**
### C3 — door model / ModelNode walk (2 levels: Aztec, Bunker2)
`propobj.c:13601` derefs `door->model->obj->RootNode->Child->Child`;
`:13523` walks the `linkedDoor` list. Fault addrs 0x10 / 0x8 → a null-ish
base + field offset. Door prop model not loaded, or `door->model` /
`->linkedDoor` pointer-width mis-set. Same family as D122 (propDef obj
half-swap) — Aztec/Bunker2 emit a door subtype BUNKER1/Silo don't, or the
door propDef `linkedDoor`/`model` field is pointer-width-sized wrong.
Files: `src/game/propobj.c`, `tools_pc/d88_propdefs.py` (door handler),
`src/game/prop.c`.
### C4 — prop tile-walk room deref (1: Depot)
`prop.c:902` — `*arg1 = sp4C->room` where `sp4C` came back bad from
`walkTilesBetweenPoints_NoCallback`. Likely a BG tile/room table not
converted for Depot. Files: `src/game/prop.c`, `src/game/bg.c`, BG
sidecar.
### C5 — bg portal points (1: Control)
`bg.c:5723` — `portal_pts = g_BgPortals[idx].offset_portal; portal_pts->numPoints`.
`offset_portal` unresolved / BE. Via `chrprop.c:62` (chr line-of-sight
through portals). Files: `src/game/bg.c`, BG portal sidecar.
### C6 — modelLoad PitemZ header (1: Surface2)
`loadobjectmodel.c:393` — `PitemZ_entries[modelid].header->RootNode`,
`header` NULL / `modelid` OOB. Direct D122 continuation (prop/item model
id from a propDef record). Fault addr 0x0. Files: `tools_pc/d88_propdefs.py`,
`tools_pc/d43_emit.py`, `src/game/loadobjectmodel.c`.
### C7 — sndSetupSound (1: Surface1)
`snd.c:653`. Audio is a parked subsystem (Phase 3) but this is a hard
crash on load, not silence. May be dodge-able with a narrow guard until
audio lands. Files: `src/snd.c`, `port/src/` audio stubs.
## Next (9 crashes, 6 classes — priority order)
1. **C2** Runway + Facility — model-GDL relocation align bug in
`objecthandler_2.c`/`texLoadFromGdl` (D80/D82/D83 area). Shared infra,
2 levels + likely helps Jungle/Streets. Biggest win.
2. **C3** Aztec + Bunker2 — door model / `linkedDoor`; brief
`docs/BRIEF-C3-C6-prop-model.md` (dispatch after any `d88_propdefs.py`
work settles).
3. **C2m** Jungle (+ Streets intermittent) — explosion-DL `G_MTX`
(`gfx_sp_matrix`), D75/matrix family.
4. **C6** Surface2 — `PitemZ_entries` modelid (D122/D123 continuation).
5. **C5** Control — BG portal `offset_portal` resolve.
6. **C4** Depot — BG tile/room table.
7. **C7** Surface1 — `sndSetupSound`; parked-audio, may just need a narrow
load guard.
Re-run `tools_pc/level_sweep.sh` after each class fix; keep
`-level_09`/`-level_20` green (`tools_pc/framediff.py`).
+394 -2
View File
@@ -790,6 +790,37 @@ duration. Asset symbol spot-checks against the ROM confirm correct offsets
### F. Phase 2 runtime findings (threads, DRAM, addresses)
**Dxx index — jump to a finding, do not linear-read this section.** §F
covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
| Range | Topic | Status |
|---|---|---|
| D24–D30 | green threads / kernel / crash handler | resolved |
| D31–D42 | boot ABI: ANIM_DATA, music seq-table, libaudio banks, Globalimagetable rebase, GL context, rsp toggle | resolved |
| D43–D50 | model-file loading → offline sidecar "Plan B" (`d43_emit.py`); D50.6 texCopyGdls full-slot copy | resolved |
| D51–D58 | font pixeldata, osGetCount tick rate, model RW pools, cseq BE header, synth slots, RLE menu bg, watch raw offsets, gun-barrel DL | resolved |
| D59–D68 | intro render: blood-RLE clobber, DMA validate, OSMesgQueue, HEADS/BODIES sentinels, romCopy width, image_entry, Globalimagetable BE→LE | resolved |
| D69 · D78–D84 | stage load (`load_bg_file`): bg/stan offline sidecar (`d69_emit.py`) + StandTile/bg_room_data ABI | resolved |
| D70–D74 | intro-logo pixels: C-array bswap, UV path, sinf/cosf `DVAL()`, texture-import truncation | resolved |
| D75 · D76 · D77 | front-end 3D model transforms · disclaimer screen · audio | OPEN (parked, `GRAPHICS-BACKLOG.md`) |
| D85 | room primary/secondary DL → `texLoadFromGdl` garbage | OPEN (safety-netted; widen/pool fixes landed, geometry now renders) |
| D86 · D87 | modelInitRwData truncated ptr · attract-demo BE `ramromfilestructure` | resolved |
| D88.1–D88.3 · D88.5–D88.6 | `Usetup*Z` header + sub-table width/endian conversion (`d88_emit.py`) | resolved |
| D88.4 | `propDefs` polymorphic record stream not byteswapped → `setupDoor` crash | **OPEN — cross-level blocker** |
| D89–D92 | stage-load→frame: stan zero-fill overrun, portal address trunc, chr/AI spawn ptrs | resolved |
| D93–D102 | struct-pun / hardcoded-size pass: player alloc, player.model inline Model, weapon Model pun, master-DL buffer | resolved (D95 pool bump partial) |
| D103–D107 | BUNKER1 viewport height, depth-buffer clear (`G_CLEAR_DEPTH_EXT`), portal near-plane, LOD mip tile | resolved |
| D108–D112 | skeletal models: `d43_emit.py put_f32` byte-reversal bug | resolved |
| D113–D116 | portal BFS ok · matrix chain ok · player raw-offset audit + `gunfire.c` THROW* · HUD text X-mirror | D116 OPEN (parked — DO NOT re-static-trace) |
| D117 | frame nondeterminism root-caused; `GE_DETERM` deferred; `framediff.py` structural | resolved (mode deferred) |
| D118 | SDL input layer (`port/src/input.c`); D118a/b mouse polish open | resolved (polish open) |
| D119 · D120 | guard-attack `weapons_held[]->chr` pun crash · blood-stain `PointUsage[]` chain hang (guarded, `d43_emit.py` opcode-0x18 gap) | D119 fixed · D120 guarded |
| D121 | WS1 frictionless per-level boot: bare `-level_XX` injects its `memallocstringtable[]` `-m*` row (`#ifdef PORT` in `boss.c`) | resolved |
| D122 | per-level prop/item model-load crash (Dam/Facility/Runway `modelLoad`/`modelInitRwData`): `d88_propdefs.py` had no handler for 6 ObjectRecord-derived propDef types (47/39/40/45/13/20) → generic arm half-swapped the `[s16 obj][s16 pad]` word → OOB `PitemZ_entries[]` | converter fixed; residual chr/fast3d crashes on those levels are separate |
| D123 | crash class C1: `chrIsNotDeadOrShot` NULL deref on 6 levels (Dam/Runway/Frigate/Statue/Streets/Cradle). D122's `OBJ_TAIL_DESC` zeroed the widened `VehichleRecord/AircraftRecord.ailist` slot (w32), but `prop.c:1764/1786` reads a pre-populated int AI-list id there → `ailistFindById(0)` → `GAILIST_AIM_AT_BOND` → `ai()` runs a CHR aim list with `ChrEntityp==NULL` | converter fixed (`OBJ_ID_WORDS`); C1 cleared on all 6, residual crashes are fast3d (C2) |
| D125 | crash classes C3+C6 (Aztec/Bunker2/Surface2): the converted `propDefs` blob in RAM ≠ `d88_propdefs.py` offline output — after record 0 it is zeros/garbage, so the `sizepropdef()` walk drifts (+101 records by the first door) and every `pdefIndex` lookup (`setupDoor`→`linkedDoor`, `modelLoad` modelid) hits the wrong record. NOT a forgotten-type gap (histogram diff clean). Suspect a destination-offset / `pd_end` / header-reloc bug in `tools_pc/d88_emit.py`'s propdefs emit, not in the converter | **not fixed** — investigation cut for usage reset; next: diff pccg.bin bytes vs `convert_stream`, trace `pd_start/pd_end/do` in d88_emit.py |
| D124 | crash class C2: fast3d bad texture pointer. **Jungle** (`0xabcd0824`): `gimgSyncCompiledGlobalDLs()` slot-detect keyed on the post-fixup marker, which `texLoad()` had already overwritten → compiled `globalDL_0xNNN` explosion DLs kept link-time `IMAGESEG` words → latent on every level, tripped by the first explosion-DL draw. **Facility** (`0x72181ee8`): separate — model/prop GDL from the `texLoadFromGdl()`/`sub_GAME_7F0762E0` relocation path writes non-16-aligned `dst` (N64 8B vs PC 16B `Gfx` stride mix in `objecthandler_2.c`), open D80/D82/D83 area | Jungle fixed (`port/src/gimgfixup.c`); Facility diagnosed, not fixed |
Phase 2 replaced the Phase-1 demo loop with the real `mainproc()` on real OS
threads, compiled GE's real `src/sched.c`, and brought in PD's fast3d software
RSP (`port/fast3d/`). The boot path now runs: ROM map → DRAM reserve → video
@@ -2193,7 +2224,10 @@ word1=0x14 room-fileposition list), bg_room_data records carry more
stanZ files go through stanDetermineEOF/stanLoadFile. TEMP D69 probe in
ob.c (GE_D69) logs name/index/rom_size/hw_address per BG load.
### G. Phase 2 status (current)
### G. Phase 2 status snapshot (HISTORICAL — frozen ~D74)
> Stale point-in-time snapshot. **Current status: `docs/HANDOFF.md`.**
> Finding detail: §F index above + §H below.
Done through **D68**: PD fast3d integrated (`port/fast3d/`); GE's real
`src/sched.c` + pthread kernel; dual-mapped DRAM; ROM mapped at cart base;
@@ -2243,7 +2277,14 @@ stripped, but previously committed TEMP diagnostics (D63 blocks, GE_D71LOG,
and the older D51–D66 leftovers) are still in the tree — strip list in
HANDOFF Task 3. Build is GREEN.
### H. Handoff & plan (current session)
### H. Finding log continued (D32 procedure, D70–D121)
> This section began as a per-session "handoff & plan" and grew into the
> D70–D121 finding log. The **D32 repeatable fix procedure** below is
> permanent reference; the rest is the finding archive (use the §F index).
> **Current task and plan: `docs/HANDOFF.md` +
> `docs/PLAN-linear-level-sweep.md`.** Session narrative:
> `docs/HANDOFF-ARCHIVE.md`.
Full paste-ready brief: **docs/HANDOFF.md** (primary thread: D69 BG/stan
stage loading — the milestone blocker; secondary: final pixel check of the
@@ -3931,3 +3972,354 @@ module, as in D56, means unwind depth is limited — confirm callers by code-
path analysis + behavior). Standalone probe compiles need `-std=c11` (without
it `typedef s32 bool` in bondtypes.h breaks under gnu23) and pointer-difference
arithmetic instead of offsetof (include/stddef.h is #if 0'd).
---
**D121 (session M-12, WS1) — frictionless per-level boot.** A bare
`./build-pc/ge007.x86_64.exe -level_XX` with no `-m*` args booted every
level with the default memory pools and OOM-crashed early in the load
sequence; you had to hand-copy the per-level `-ml -me -mgfx -mvtx -mt -ma`
row from `memallocstringtable[]` (`boss.c:101`) onto the command line.
*Root cause.* On N64 the per-stage auto-inject loop in `bossMainloop`
(`boss.c` ~line 425: `tokenSetString(memallocstringtable[i].string)`)
supplies that row, but it is gated on `g_DebugAndUpdateStageFlag`. With
`-level_` present that flag is left to `rmonGetToken()` (`boss.c:178`),
which the PC stub (`port/src/n64stubs.c`) returns 0 for, so the loop never
runs.
*Rejected fix.* Setting `g_DebugAndUpdateStageFlag = 1` when `-level_` is
present (the "mirror `boss.c:199-202`" idea in the old HANDOFF) is **not
behaviour-preserving on PC**: it makes boot render the full title-stage
intro (logos → gun-barrel → cast) instead of loading the level directly.
The flag does more than pick the token row.
*Fix (`#ifdef PORT` in `bossInitMainthreadData`, after the flag's own
`tokenSetString` block).* When `-level_` is present and no `-m` token was
given, look up the stage's `memallocstringtable[]` row (same match the N64
loop does) and `tokenSetString` it — **prefixed with `-level_XX` (and
`-hard N` if present)**, because `tokenSetString` (`src/token.c:41`)
`strcpy`s over the whole token buffer; without the prefix `bossMainloop`'s
`tokenFind(1, "-level_")` returns NULL and it boots the title stage. No
flag change, no control-flow change.
*Verified.* Bare `-level_09` now loads BUNKER1 directly (85–92 % non-clear
frame content from frame 30, no intro), matching the old
`-level_09 -ml0 -me0 -mgfx100 -mvtx50 -mt700 -ma150` repro. Residual
BUNKER1 prop/`modelLoad` crash is intermittent (D117 nondeterminism +
D88.4) and pre-existing — out of WS1 scope. HANDOFF repro line updated to
drop the manual `-m*` list.
**D122 (session M-12) — per-level prop/item model-load crash: propDef
converter missing handlers for ObjectRecord-derived record types.**
`-level_33` (Dam), `-level_34` (Facility), `-level_35` (Runway) crashed
deterministically before frame 1: `modelLoad` (`loadobjectmodel.c:393`,
`PitemZ_entries[modelid].header->RootNode` with an OOB `modelid` — `Rax`
held ASCII) and `modelInitRwData` (`model.c:6249`). BUNKER1 (`-level_09`)
and Silo (`-level_20`) were fine.
*Root cause.* The D88.4 propDef stream converter (`tools_pc/d88_propdefs.py
convert_record`) had per-type handlers for most record layouts but **four
`inherits ObjectRecord` types fell through to the generic arm**: 47
TINTED_GLASS, 39 VEHICHLE, 40 AIRCRAFT, 45 TANK (plus 13 AUTOGUN and 20
AMMO/MultiAmmoCrate, exposed once the first four were fixed). The generic
arm bswap32's N64 word 1 as a single 32-bit value, but for an
ObjectRecord that word is `[s16 obj][s16 pad]` — it needs `_hh_word`
(swap each half in place). Result: `obj` (the model id, read by
`domakedefaultobj` at `prop.c:155` → `modelLoad`) landed in the wrong
half → garbage/OOB index into `PitemZ_entries[]`. The generic arm also
never widened the ObjectRecord's embedded pointer slots (prop*/model* 4→8
B), so every tail field was mis-offset too. BUNKER1/Silo use none of
these six types (verified with a per-level `PROPDEF_N64_WORDS` type
histogram), which is why they were unaffected — the stream *walk* was
self-consistent for all 21 levels (`convert_stream` validates region
length), only the *field contents* of these records were wrong.
*Fix (converter only — `tools_pc/d88_propdefs.py`).* Added `OBJ_TAIL_DESC`
= {type: (ptr_word_set, hh_word_set)} for types 47/39/40/45/13/20 and a
handler that emits the real 144-byte PC ObjectRecord prefix
(`_emit_object_prefix`, which does `obj` correctly) followed by the tail
with pointer members widened 4→8 B and 8-aligned, `_hh_word` for u16-pair
words (vehicle aioffset/aireturnlist; ammo-crate `slots[]`), `_bswap32`
elsewhere. Updated `PROPDEF_PC_BYTES` (39: 176→208, 40: 180→208, 45:
224→248; 47/13/20 already correct) and the matching
`sizepropdef()` `#ifdef PORT` arms in `loadobjectmodel.c` (VEHICHLE
44→52, AIRCRAFT 45→52, TANK 56→62 words; N64 values kept in the trailing
comment / `#else` `#if 1` switch). No game-logic change. Regenerate:
`python tools_pc/d88_emit.py ntsc-final --regen` (21/21, ALL CHECKS
PASSED).
*Verified.* `-level_33/34/35` all get past the model-load chain now (no
`loadobjectmodel.c:393` / `model.c:6249` crash). BUNKER1 unregressed
(83–92 % non-clear frame content, frames 80–200). Silo unregressed
(exercises the new type-20 handler — no crash).
*Residual (separate blockers, out of D122 scope — hand to the level
sweep).* Dam + Runway now crash later in `chrIsNotDeadOrShot`
(`chraction.c:4483`, `self` = an image rodata address — guard/chr setup,
not propDef-related). Facility crashes in `import_texture_i8`
(`gfx_pc.cpp:821`, bad texture pointer — fast3d). Neither is touched by
this fix.
*Tail-layout confidence: medium* for 39/40/45 (VehichleRecord/
AircraftRecord/TankRecord field offsets have "locs need confirming" notes
in `bondtypes.h`; the pointer-word sets and total sizes are derived from
the struct as written and may need a nudge if a level actually drives
tank/vehicle motion). **High** for 47/13/20 (offsets are known) and for
the crash fix itself (the `obj`-field half-swap is unambiguous).
---
**D123 (session M-13) — crash class C1: `chrIsNotDeadOrShot` NULL deref on
6 levels (Dam 33, Runway 35, Frigate 26, Statue 22, Streets 29, Cradle
41). Converter zeroed the vehicle/aircraft `ailist` id. FIXED
(`tools_pc/d88_propdefs.py` only).**
*Symptom.* Bare `-level_33` etc. crashed before frame 1 in
`chrIsNotDeadOrShot` (`chraction.c:4483`, `self->actiontype`), fault addr
0x8 → `self ≈ NULL`. (The log's frame #1 `0x1401296a0` / `chraidata.c:61`
was a stale stack-scan hit, not a real frame — misled the original
triage.)
*Real backtrace (gdb):* `objTick` (`propobj.c:5504`) →
`ai((PropDefHeaderRecord *)poTruck, PROP_TYPE_OBJ)` →
`ai()` sees `Entityp->type == 39` (PROPDEF_VEHICHLE) so sets
`VehichleEntityp`, `AiListp = VehichleEntityp->ailist`, `ChrEntityp =
NULL`. `AiListp` resolved to `m_AimAtBond` (`GAILIST_AIM_AT_BOND` == id 0).
Its first opcode is `AI_TRYFireOrAimAtTarget` →
`actor_aim_at_actor(ChrEntityp=NULL,…)` → `chrIsNotDeadOrShot(NULL)`.
*Root cause.* `prop.c:1764` does `pdef_veh->ailist =
ailistFindById(pdef_veh->ailist)` — it reads the **pre-populated integer
AI-list id** out of the ROM record (VehichleRecord.ailist @ N64 0x80;
Dam's is `0x040a` = 1034, a per-level list). But D122's `OBJ_TAIL_DESC`
put word 32 in `ptr_word_set`, whose handler emits **8 zero bytes** for
the widened 4→8B slot (correct for `path*`/`Sound*`, which `prop.c`
overwrites with 0 — but wrong for `ailist`, read before overwrite).
Result: `ailistFindById(0)` → `isGlobalAIListID(0)` true → global list id 0
= `GAILIST_AIM_AT_BOND` → a vehicle runs a CHR-only aim list with a NULL
chr. `AircraftRecord.ailist` (`prop.c:1786`) is identical. BUNKER1/Silo
emit no vehicle/aircraft propDef → unaffected. All 6 crashing levels have
a vehicle or aircraft prop.
*Fix (converter only).* New `OBJ_ID_WORDS = {39:{32}, 40:{32}}` in
`tools_pc/d88_propdefs.py`; the `OBJ_TAIL_DESC` walk now emits
`_bswap32(w)` into the low 4 bytes of the 8-aligned slot for id words
(high 4 stay zero), so the u16/s32 id survives into the widened pointer
field. Words 32 removed from the 39/40 `ptr_word_set`. Struct size
unchanged → no `sizepropdef()` edit. TANK (45, `collision*`) and AUTOGUN
(13, `unkC4/unkC8/beam`) left as ptr/zero — verified genuinely
runtime-populated (`prop.c:1712` / `setupAutogun` `prop.c:694`). Regen:
`python tools_pc/d88_emit.py ntsc-final --regen` (21/21 ALL CHECKS
PASSED).
*Verified.* `chrIsNotDeadOrShot` crash gone on all 6. Dam 83%, Statue
80%, Frigate 68–90%, Cradle 77% non-clear frames, no crash log.
BUNKER1 (91.7%) + Silo (91.7%) unregressed. Runway and Streets now crash
**later** in fast3d (`palette_to_rgba32`/`import_texture_i4`
`gfx_pc.cpp:851`; `gfx_sp_matrix` `gfx_pc.cpp:1046`) — class C2/other
track, not C1. Frigate/Statue/Cradle/Dam reach a frame cleanly.
*Confidence: high* — root cause reproduced in gdb, fix is a 1-field
converter change matching the exact `prop.c` read pattern, and the id
value (0x040a) matches the N64 `UsetupdamZ.c` Vehicle record tail.
**D124 (session M-13) — crash class C2: fast3d handed a garbage texture
pointer on Facility (`-level_34`) and Jungle (`-level_37`).** Symptom:
early deterministic crash — Facility `import_texture_i8` `gfx_pc.cpp:821`
fault `0x72181ee8`; Jungle `gfx_tex_normalize_source` `gfx_pc.cpp:644`
fault `0xabcd0824` (= `IMAGESEG(0x824)`). Two *different* root causes:
*Jungle — FIXED.* `gimgSyncCompiledGlobalDLs()` (`port/src/gimgfixup.c`,
D68) is meant to copy the texture pointers that `texLoadFromDisplayList()`
patched into the ROM copy of the Globalimagetable back into the compiled
`globalDL_0xNNN` shadow arrays (`assets/oddtextures.c`) that `explosion.c`
executes via `g_ExplosionDisplayLists[]`. Its slot-detect test was
`p[6]==0xCD && p[7]==0xAB` — the *post-fixup marker* — but by the time it
runs `texLoad()` has **already replaced every IMAGESEG w1 in the ROM copy
with a real pointer**, so that test never matches and the sync copied
*nothing*. The compiled arrays kept their link-time `IMAGESEG(id) =
0xABCD0000|id` words; the first explosion/smoke/particle DL that
Facility/Jungle render feeds `0xABCDxxxx` straight into fast3d
`seg_addr()` (falls through to `return (void*)w1`) → `import_texture`
deref. The 7 "working" levels are only working because nothing triggered
an explosion-DL draw inside the ~22 s capture — **the bug was latent in
every level.** *Fix:* detect the slot from the **compiled** array instead
(`dst[j]` is a `G_SETTIMG` whose w1 is still `0xABCDxxxx`), then copy the
ROM copy's resolved `*(u32*)(p+4)`. `port/src/gimgfixup.c` only; no
converter/sidecar change. Verified: Jungle now renders ~300 frames
(91.7% non-clear) before a *separate* downstream crash (`gfx_sp_matrix`
`gfx_pc.cpp:1046`, bad G_MTX seg addr in the same explosion-DL stream —
those DLs also carry unconverted `G_MTX` words; follow-up, D75/matrix
family). BUNKER1 (83%) + Silo (91.7%) unregressed.
*Facility — NOT fixed (separate class).* Fault `0x72181ee8` is a bogus
`G_SETTIMG` w1 (`fmt=4 siz=2 w=0`, prev cmd an unconsumed `0xba` GE
tex-macro opcode) in a DL living in V1 DRAM — a **model/prop GDL from the
runtime `texLoadFromGdl()` relocation path** in `sub_GAME_7F0762E0`
(`objecthandler_2.c:82`), not a room GDL and not the global bank.
`GE_C2` probe of `texLoadFromGdl` shows its model-GDL calls (second batch,
`src≈0x706a****`) write to **non-16-aligned `dst`** (e.g.
`0x701eac01`, `0x701eb7d5`) — the `replacementgdl`/`name`(=srcsize)
offset arithmetic in `objecthandler_2.c` still mixes N64 8-byte and PC
16-byte `Gfx` strides, so converted commands land mid-slot and a
`G_SETTIMG` w1 reads as garbage. This is the open D80/D82/D83
"model/room GDL runtime conversion not verified" area — shared infra
touched by every level; out of C2 budget, left for a dedicated pass.
BUNKER1/Silo/Jungle don't hit it because their visible prop models
happen to convert cleanly; Facility's do not.
*Probes:* none left in tree — `GE_C2` scratch prints removed after
root-cause. *Files touched:* `port/src/gimgfixup.c` (fix),
`docs/PORT-LEARNINGS.md`, `docs/PCPortResearch.md`, `docs/LEVEL-STATUS.md`.
*Confidence:* Jungle fix **high** (mechanism proven with a `[C2sync]`
trace showing 32 slots going `abcd08xx -> 700e****`, verified render).
Facility diagnosis **medium** (probe evidence strong; exact off-by in
`objecthandler_2.c` not yet pinned).
**D125 (session M-14) — crash classes C3 + C6: the converted `propDefs`
blob in RAM does not match `tools_pc/d88_propdefs.py`'s offline output;
the `sizepropdef()` walk drifts, so every `pdefIndex`-keyed lookup
(`setupDoor` → `linkedDoor`, `weaponAssignToHome`, `modelLoad(modelid)`)
gets the wrong record.** NOT root-caused; investigation cut for a usage
reset. Affects Aztec (`-level_28`, `propobj.c:13523/:13601`), Bunker2
(`-level_27`, same `door7F054FB4`), Surface2 (`-level_43`,
`loadobjectmodel.c:393` `PitemZ_entries[modelid].header`).
*Evidence.* Instrumented the `proplvreset2` propDef walk
(`prop.c:1865`, `#if defined(PORT)` `getenv("GE_C3")` print of
`pdefIndex / phead->type / sizepropdef(phead) / phead / g_CurrentSetup.propDefs`)
and the `setupDoor` `linkedDoor` resolve (`prop.c:1206`). On `-level_27`
(`UsetupsevbZ`, `setup_text_pointers[27]`):
- `g_CurrentSetup.propDefs = 0x701ce814`.
- Runtime walk: idx0 `type=35` (WATCH_MENU, correct), then idx1–4
`type=0`, idx6 `type=112`, idx15 `type=26`, … — i.e. after the first
record the blob is **zeros / garbage**, but `sizepropdef` still
strides the phantom `type=35` at idx0/5/10 forward 16 B each.
- By the first real DOOR the walk has counted **147** records where the
offline converter (`convert_stream(UsetupsevbZ)`) puts it at **46**
(verified: offline output = 346 records, DOOR at idx 46, self-
consistent with `PROPDEF_PC_BYTES` == `sizepropdef()` PORT strides —
every type cross-checked against `d88_layoutprobe` sizes, all match).
- `setupDoor` then gets `arg2=147` for a stream-index-46 door;
`linkedDoorOffset(-1) + 147 = 146` → `setupGetPtrToCommandByIndex(146)`
returns a `type=0` record → `door->linkedDoor` garbage → fault
walking the linked-door list in `door7F054FB4`. Surface2's C6 is the
same mechanism one step earlier: a drifted `pdefIndex`/`modelid`.
*What is NOT the bug (ruled out).* (a) `d88_propdefs.py` `convert_stream`
itself — self-test passes, offline walk of all 3 crashing levels'
converted output is clean, DOOR/linkedDoorOffset values correct, stream
tiles `[propDefs, intro)` exactly (`end=OK` for all 21). (b)
`sizepropdef()` PORT strides — every `case` equals
`PROPDEF_PC_BYTES[type]/4` and equals the compiler `sizeof` of the widened
struct (checked via `d88_layoutprobe`). (c) DOOR converter layout —
`linkedDoorOffset@144`, `linkedDoor@216`, `unkcc@224`, size 296 all match
`d88_layoutprobe`; tail pointer-widening walk is correct end-to-end. (d)
The per-type histogram diff (brief's suggested approach): the 3 crashing
levels emit no propDef type absent from the 12 passing levels — TINTED_GLASS
and AUTOGUN appear in passing Caverns/Control too. **This is not a
forgotten-type converter gap like D122/D123.**
*Best hypothesis (confidence: mechanism high, exact fault medium).* The
converted `propdefs_pc` bytes produced in `tools_pc/d88_emit.py`
(`convert_propdefs`, line 320) are **not landing in the sidecar at the
offset `g_CurrentSetup.propDefs` resolves to**, OR the `propDefs` header
field is not being relocated to the converted blob's new offset, OR
`pd_end` (the `'propdefs'` region end from the region-tiling at
`d88_emit.py:315-318`) is computed wrong so only the first record is
converted and the rest of the region is left as `bytearray(total_new)`
zero-fill. The "idx0 correct, then zeros" signature points hard at a
destination-offset / length bug in `d88_emit.py`'s propdefs emit
(lines 266-267, 308-338, 471-476), not in `d88_propdefs.py`. Note
`d88_emit.py`'s module docstring still describes propDefs as an "opaque
passthrough blob" (stale post-D122) — the emit path was retrofitted and
may not have been fully reconciled with the region-tiling / delta-reloc
logic.
*Next step.* Dump the actual bytes at `data/pccg-ntsc-final/pccg.bin` for
`UsetupsevbZ`'s propDefs region and compare to `convert_stream` output;
add a `[D88emit]` print of `pd_start / pd_end / len(propdefs_pc) / do`
(destination offset) in `d88_emit.py`; check whether the `propDefs`
header word in the emitted file points at `do`. Likely a 1-region
off-by-one in the sorted region list (intro vs propDefs ordering) or the
converted blob written at the pre-growth offset.
*Probes:* **none left in tree** — the two `#if defined(PORT)`/`GE_C3`
scratch prints in `src/game/prop.c` (lines ~1206 and ~1865) and the
temp scan scripts `tools_pc/c3_scan.py` / `tools_pc/c3_doors.py` were all
reverted/deleted. *Files touched:* only `docs/PCPortResearch.md` (this
entry) + `docs/LEVEL-STATUS.md` (one line). No code, no converter, no
sidecar change — **sidecars do NOT need regen**, tree builds clean at
`f2beae4b` + M-13 uncommitted set.
**D124-Facility addendum (session M-14 — partial, NOT fixed, out of time).**
Re-instrumented with a `GE_C2GDL` probe in `sub_GAME_7F0762E0`
(`objecthandler_2.c`) + a bad-`G_SETTIMG` catcher in fast3d
(`gfx_pc.cpp` G_SETTIMG case). Both probes **reverted** — tree is clean.
Findings:
1. *Exact crash cmd.* `-level_34` faults in `import_texture_ia16`
(`gfx_pc.cpp:757`, via `import_texture` :955) — NOT `import_texture_i8`
as the old note said. The offending command in the relocated model GDL
(dList base `0x7007b870`, cmd at `0x70080490`) is:
`w0=0xfd900000` (`G_SETTIMG`, fmt=7/rgba? siz per bits, w=0),
`w1=0x72181ee8`. `seg_addr(0x72181ee8)` returns it unchanged (top byte
0x72 is not a segment) → deref of unmapped DRAM → AV. Preceding slot is
`0xba 00 0e 02` (GE tex-load macro, w1=0) then a `00000000` slot.
2. *`Gfx` stride confirmed.* `Gwords{ uintptr_t w0; uintptr_t w1; }` →
on x86-64 `w0`@+0 (8B), `w1`@**+8** (8B), `sizeof(Gfx)==16`. The probe
dump (8-byte granular) shows w0 at slot+0 and w1 at slot+8, i.e. the
command is genuinely `{0xfd900000, 0x72181ee8}` — the converter wrote a
real 16-byte slot, it is not a half-slot artefact.
3. *`w1` is a bad `tex->data`.* This `G_SETTIMG` is emitted by
`gDPSetTextureImage(gdl++, .., tex->data)` inside
`texWriteLoadToTmemAddr` / `texWriteLoadToTmemZero` (`tex.c`). So the
**texture-pool** entry `tex` returned by `texFindInPool()` has a
garbage `.data` (should be a `0x05xxxxxx` seg-5 ref, resolved later by
fast3d `segmentPointers[5]`). i.e. the fault is (at least partly) in
the `texLoadFromModelFileHeader` → `texLoad` texture-pool path, driven
off `objheader->Textures`, **not** solely the `texLoadFromGdl`
command-stream copy. This partially contradicts the original
D124-Facility hypothesis.
4. *Alignment smoking gun.* `GE_C2GDL` probe shows **every** model file's
`objheader->Switches` base has `((uintptr_t)Switches & 15) == 1`
(odd!). `mempAllocBytesInBank` (`memp.c`) does **zero** alignment —
`allocation = pool->pos; pool->pos += bytes;` — so a single earlier
odd-sized bank alloc leaves the STAGE pool cursor permanently odd and
every subsequent model file loads at an odd address. `delta`
(`romremaining - pcremaining`, the scratch-relocation offset) is also
not 16-aligned (`&15` = 3, 7, 11 across files). `sub_GAME_7F0762E0`
then forms `dst = Switches + (replacementgdl & 0x00ffffff)` and
`src = Switches + gdloff + delta`; observed `dst&15` = 5, 7, 9, 13.
The N64 code assumes the file base is Gfx-aligned (8 on N64). The
converted model GDLs the sidecar carries also have per-DL offsets that
are only 4/8-aligned (`gdloff0 & 15` = 4, 8, 12), so even a 16-aligned
base would not make every DL 16-aligned.
- *Note:* an odd/misaligned base is self-consistent between the
converter writer (`Switches+off`) and the fast3d reader (seg-5 =
`Switches`, `+off`), so it does **not by itself** corrupt command
*content* on x86 (unaligned `uintptr_t` loads are tolerated). The
bad `tex->data` (#3) must come from an *offset* miscalc, likely the
model texture-blob offset assuming N64-sized (8B-Gfx) GDL extent
while the PC sidecar's GDLs are pre-expanded to 16B — the texture
data then sits at a stale/too-large offset (`0x72181ee8` is ~0x1.9M
past the bank end `~0x706a8000`), OR `texLoad` reads a `textures[i]`
descriptor at a wrong stride off the odd `objheader->Textures` base
(`(u8*)filedata + sizeof(ModelNode*)*numSwitches`, D43/D45 PORT line).
- *Next step:* probe `texLoad` (`image.c`) — dump `textures[i]`
`TextureID` / offset / resulting `tex->data` for `-level_34`, and
dump `objheader->Textures`, `numtextures`, `numSwitches`, and the
sidecar's texture-section layout. Compare against `d43_emit.py` /
`pcmodels` sidecar builder: does it emit model texture blob offsets
in N64 (8B-Gfx) or PC (16B-Gfx) GDL-extent terms? That converter is
the likely fix site (offline sidecar per the D43/D69/D88 rule),
with a fallback `#ifdef PORT` 16-align of the STAGE pool cursor /
the model file base in `mempAllocBytesInBank` or
`load_object_fill_header`.
5. *Files touched this session:* none committed; both probe edits
(`src/game/objecthandler_2.c`, `port/fast3d/gfx_pc.cpp`) reverted via
`git checkout`. Docs only: this entry + `docs/LEVEL-STATUS.md`.
*Confidence:* crash cmd + `Gfx` stride **high**; `tex->data`-is-the-bad-
value **high**; root cause of *why* `tex->data` is bad **low–medium**
(two candidate mechanisms, neither proven).
+254
View File
@@ -0,0 +1,254 @@
# PLAN — Linear pass: boot → menu → every solo level loads & renders
Paste-ready brief for a fresh session. Pivot from depth-first
(BUNKER1 + graphics rabbit-holes) to **breadth-first**: make the normal
player path work end-to-end (intro → credits → main menu → mission/level
select), then confirm **all 21 solo levels load and render** without
crashing. Turns the remaining work into a scoped list.
## Preflight (unchanged)
`CLAUDE.md` + `AGENTS.md` non-negotiables → `docs/HANDOFF.md` →
`docs/PORT-LEARNINGS.md` → `docs/AGENT-WORKFLOW.md` before any subagent.
## Scope decisions (locked with the user)
- **Intro / credits / menus:** bar is *functional, not crashing*. Menus
navigable start to finish; the intro is skippable with any button.
Cosmetic defects are **out of scope** here — log them in
`docs/GRAPHICS-BACKLOG.md`: gun-barrel Bond model missing (D75),
Nintendo-logo transform (D75), mirrored glyphs (D114/D116), no audio
(D77), partial disclaimer screen (D76).
- **Text X-mirror (D114/D116):** stays parked. Menus are navigable with
mirrored text.
- **Level sweep depth:** *load + render + no immediate crash* only. A
"soak" is pointless with no input (level triggers never fire). Play-to-
completion validation is a **separate human step** (complex input),
driven by a per-level checklist this plan produces (WS6).
- **N64 reference captures:** deferred. Use PC golden frames +
`tools_pc/framediff.py`; eyeball against memory/video for "matches
original". `tools_pc/mupen64/` is available if a level looks wrong.
## Facts established (exploration, this session)
**Front end is a stage** — `LEVELID_TITLE` (0x5A). Logic in
`src/game/front.c` (~8700 lines) + `src/game/title.c` (gun-barrel/logo
3D) + `src/game/ramromreplay.c` (attract demos). State machine = `MENU_*`
enum (`src/bondconstants.h:1831`), driven per frame by `menu_init()`
(`front.c:8536`) through three switch tables (`update_/init_/interface_
menuXX_*` at `front.c:8598/8637/8666`); rendered by
`menu_jump_constructor_handler()` (`front.c:8718`). Transitions:
`frontChangeMenu(menu, reload)` (`front.c:8513`). Boot entry:
`initgamedata.c:9` sets `menu_update = MENU_LEGAL_SCREEN`.
**Intro states** (each: `init_` loads assets, `interface_` advances on
timer OR any button, `constructor_` renders):
`MENU_LEGAL_SCREEN` → `NINTENDO_LOGO` → `RAREWARE_LOGO` → `EYE_INTRO`
(gun-barrel walk, "007", blood) → `GOLDENEYE_LOGO` → `DISPLAY_CAST`
(credit roll) → idle → `select_ramrom_to_play()` (attract demo).
Any button during intro sets `prev_keypresses` → logos short-circuit to
`MENU_FILE_SELECT`, cast roll skipped. Gun-barrel sub-state machine is
`gunbarrel_mode` (u8) in `title.c` (`insert_bond_eye_intro` `title.c:297`,
`renderGunbarrelEyeIntroSequence` `title.c:608`).
**Menu → level launch:** mission select →
`selected_stage = mission_folder_setup_entries[…].stage_id`
(`front.c:3301`) → `MENU_DIFFICULTY` → `MENU_BRIEFING` /
`MENU_007_OPTIONS` → `init_menu0B_runstage()` (`front.c:6900`) →
`bossSetLoadedStage(selected_stage)` → `g_MainStageNum` → `bossMainloop`
inner loop exits → `g_StageNum = g_MainStageNum` (`boss.c:647`) →
`lvlStageLoad`. **Level → level always routes back through the front end**
(`MENU_MISSION_COMPLETE`, `interface_menu0D_missioncomplete`
`front.c:7119`; auto-advance scans `mission_folder_setup_entries` for the
next `stage_id >= 0`). No direct in-stage jump.
**21 solo levels; LEVELID value == `-level_XX` number** (`boss.c:337`
decode `d0*10 + d1 - 0x210`). Mission order + numbers:
Dam 33, Facility 34, Runway 35, Surface1 36, Bunker1 09, Silo 20,
Frigate 26, Surface2 43, Bunker2 27, Statue 22, Archives 24, Streets 29,
Depot 30, Train 25, Jungle 37, Control 23, Caverns 39, Cradle 41,
Aztec 28, Egypt 32, Cuba 54. Enum: `src/bondconstants.h:1633-1685`;
mission table `mission_folder_setup_entries[]` `front.c:434-466`.
**Per-level memory args already have a table** — `memallocstringtable[]`
(`boss.c:101-142`), one row/LEVELID (`-ml -me -mgfx -mvtx -mt -ma`). The
auto-inject loop `boss.c:369-414` already selects the right row per stage
load — but it is gated on `g_DebugAndUpdateStageFlag`, which the port
only sets when `-level_` is **absent** (`boss.c:199-202`;
`rmonGetToken()` → 0 on PC, `port/src/n64stubs.c:134`). So bare
`-level_09` today uses default pools; hence the manual
`-ml0 -me0 -mgfx100 -mvtx50 -mt700 -ma150` in the HANDOFF repro line.
Documented TODO: `PCPortResearch.md` §H (D95 area) and `docs/HANDOFF.md`
"Next task".
**D88.4 is a cross-level blocker.** Every level's `Usetup*Z` feeds the
`propDefs` polymorphic record stream, which `tools_pc/d88_emit.py`
deliberately leaves un-byteswapped. BUNKER1 crashes in `setupDoor`
(`prop.c:971`) on a garbage BE `door->obj`. Any level hits the same class
when its prop set is walked. Groundwork: `tools_pc/d88_propdefs.py`,
`tools_pc/d88_layoutprobe.c` (machine `SZ`/`PTR` lines),
`tools_pc/d88_propdef_scan.py` (per-level type histogram).
Placeholder sizes still to resolve in `d88_propdefs.py`:
`VEHICLE`/`AIRCRAFT`/`TANK`/`AMMO`/`DEPOSIT_IN_ROOM`.
**Tooling:** `GE_PCDUMP="a-b:step"` → `./ppm/` (`port/src/video.c:181`);
`tools_pc/pixcount.py` (non-black pixel count / content check),
`tools_pc/framediff.py` (structural regression vs `tools_pc/golden/` —
the port is NOT frame-deterministic, D117), `tools_pc/ppm2bmp.py`.
Crash log `ge007.crash.log`; symbolicate
`addr2line -e build-pc/ge007.x86_64.exe -f -C <pc>` (image base
0x140000000). Objectives: `propDefs` `PROPDEF_OBJECTIVE_*`
(`bondconstants.h:4332`), status `src/game/objective_status.c`
(`get_status_of_objective()` `:161`) — needed only for WS6.
## Workstreams
WS1 is a prerequisite for all. WS2 (`front.c`/`title.c`/`lv.c`/
`ramromreplay.c`/`initmenus.c`/`port/`) and WS3 (`tools_pc/d88*` +
narrow `prop.c`) are disjoint → parallelisable. WS4 depends on WS3.
WS5 iterative. WS6 is the user's.
### WS1 — Frictionless per-level boot (small, first)
Goal: `./build-pc/ge007.x86_64.exe -level_XX` with no other args boots
any level with its correct pools.
Change: in `src/boss.c` under `#ifdef PORT`, also set
`g_DebugAndUpdateStageFlag = 1` when `-level_` **is** present (mirror of
`boss.c:199-202`). The existing loop `boss.c:369-414` then supplies the
right `memallocstringtable` row per stage; the generic preload at
`boss.c:207-210` is harmless (per-loop `tokenSetString` at `:413`
overrides). Debug-harness / ABI-class `#ifdef PORT` edit (same character
as the `-level_` ASCII parse already in the file). Document as a new Dxx
in `PCPortResearch.md` §F. Alt if boss.c is judged out of bounds:
synthesize the string in the port token layer (`port/src/system.c`
`sysGetTokenString`). Boss.c is simpler and preferred.
Also update `docs/HANDOFF.md` repro line; drop the manual `-m*` list once
verified.
Verify: `-level_09` alone == today's full-arg behaviour (same frame,
`framediff.py` green vs `tools_pc/golden/`).
### WS2 — Front-end walkthrough & crash repair (human playtest loop)
Goal: a human can go boot → (watch or skip intro) → file/folder select →
mode select → mission select → difficulty → briefing → level launches,
and after a level, mission-complete → next briefing, with **no crash /
hang / dead-end**. Cosmetic wrongness acceptable and logged.
Method: same loop as the M-10 input playtest — overseer builds, human
drives, defects come back as a list, overseer root-causes each (expect
the pointer-width / BE-rodata / N64-Gfx-sizing classes in
`PORT-LEARNINGS.md`), narrow `#ifdef PORT` fix, re-test.
Files in scope: `src/game/front.c`, `src/game/lv.c`
(`interface_menu05_fileselect` is here), `src/game/title.c`,
`src/game/initmenus.c`, `src/game/ramromreplay.c`, `port/src/*`. NOT
`prop.c` / `tools_pc/d88*` (WS3).
Human checklist:
1. Boot → legal → Nintendo → Rareware → gun-barrel → GE logo → cast roll
→ (idle) attract demo. Note any crash/hang; cosmetic noted separately.
2. Press a key during intro → confirm short-circuit to file select.
3. File/folder select: create a folder, enter it; folder-delete sub-UI;
30 s idle → back to legal screen.
4. Mode select → Mission select: page through all 20 mission entries,
each briefing text/photo loads without fault.
5. Pick mission + Agent difficulty + briefing → Start → level loads.
6. In-level: die / abort → mission-failed → menu. Complete a short level
(or cheat) → mission-complete → auto-advance to next briefing.
7. MP chain (lower priority): MP options → scenario/stage/char/handicap/
teams → MP match loads.
Do NOT fix here (log/confirm in `GRAPHICS-BACKLOG.md`): D75, D76, D77,
D114/D116.
Deliverable: front-end crash list + root causes + fixes;
`GRAPHICS-BACKLOG.md` updated with confirmed cosmetic state.
### WS3 — D88.4 propDefs converter (cross-level blocker)
Goal: `Usetup*Z` `propDefs` converts correctly offline for all 21 levels
so `setupDoor` / prop instantiation stops reading BE garbage.
Method: subagent brief, same shape as D69 / D88.1-3 (offline sidecar
converter). Byte-spec every `PROPDEF_*` record type (~40, enum
`bondconstants.h`), per-type field byteswap + pointer-width audit,
extend `tools_pc/d88_emit.py`. Use `d88_propdefs.py` /
`d88_layoutprobe.c` / `d88_propdef_scan.py`. Resolve the placeholder
sizes (`VEHICLE`/`AIRCRAFT`/`TANK`/`AMMO`/`DEPOSIT_IN_ROOM`) via
`d88_layoutprobe.c`.
Files: `tools_pc/d88_emit.py`, `tools_pc/d88_propdefs.py`,
`tools_pc/d88_layoutprobe.c`, + narrow `#ifdef PORT` in `src/game/prop.c`
if a struct-width mismatch surfaces (D79/D88 class).
Budget: structural — generous. On expiry: write up spec progress in
§F/D88 with confidence rating.
Verify: `-level_09` walks past `setupDoor` to a rendered BUNKER1 frame
with props; spot-check 2-3 other levels' prop walk.
### WS4 — Per-level load + render sweep
Goal: status matrix for all 21 solo levels — (a) `lvlStageLoad`
completes without fault, (b) ≥1 non-degenerate frame renders, (c)
survives ~300 frames / ~15 s with no input (immediate-crash check only).
Prereq: WS1 + WS3 landed.
Method: agent-driven, one level at a time (`-level_XX`). Each: launch
`GE_PCDUMP="1-300:30"`, capture `ge007.crash.log`, run `pixcount.py`.
Record PASS / CRASH@<site> / RENDERS-EMPTY / HANG. Symbolicate every
crash (`addr2line`). Group failures by root-cause class.
Deliverable: `docs/LEVEL-STATUS.md` — table (level, number, load, render,
crash site, notes), refreshed as WS5 closes items.
### WS5 — Per-level crash triage (iterative)
Goal: drive the WS4 matrix to "all 21 load + render".
Method: one focused investigation per distinct failure class (subagent
brief per `AGENT-WORKFLOW.md` template; read `PORT-LEARNINGS.md` first).
Expected classes: pointer-width struct growth in per-level assets,
BE-rodata not converted, N64-Gfx/Vtx sizing, per-level pool exhaustion
(`memallocstringtable` PC bumps, cf. D95). Fix = narrow `#ifdef PORT` or
converter extension; document each in §F.
Verify per fix: target level's WS4 check → PASS *and* `-level_09`
framediff stays green.
### WS6 — Human completion-validation pass (user-run, after WS4 green)
Goal: confirm each level is playable start to finish and matches the
original — the part needing real input.
Deliverable produced for the user: `docs/LEVEL-PLAYTEST.md` — per-level
checklist template: spawn point; each difficulty-gated objective
(`objective_status.c`) reachable and registering COMPLETE; guards / props
/ doors behave; level exit / mission-complete fires clean; timing +
layout match retail. User fills it per level; findings feed a WS5-style
triage round.
## Verification (overall)
- Per merged patch: `export PATH="/c/msys64/mingw64/bin:$PATH" &&
./build-pc.sh ntsc-final` green, `/linkcheck` clean, single-frame
`GE_PCDUMP` diff vs `tools_pc/golden/` via `framediff.py`.
- WS1: `-level_09` alone == old manual-arg behaviour.
- WS3: BUNKER1 renders props past `setupDoor`.
- WS4/WS5: `docs/LEVEL-STATUS.md` all 21 rows = load+render PASS.
- Regenerate sidecars after any converter change: `python
tools_pc/d43_emit.py ntsc-final && python tools_pc/d69_emit.py
ntsc-final && python tools_pc/d88_emit.py ntsc-final --regen`.
## Out of scope (tracked elsewhere — do not start here)
- D75 / D76 / D114 / D116 / D77 and all `docs/GRAPHICS-BACKLOG.md` items.
- D118a/b input polish, rebinding, gamepad hotplug, `ge007.ini`.
- Weapon-model-doesn't-draw (AUDIT-M6 #5), rest of the `struct player`
offset audit.
- Audio (Phase 3), saves/EEPROM (Phase 4), `docs/BACKLOG.md` (PC
graphics options, co-op, netplay).
+21
View File
@@ -53,6 +53,15 @@ through a converter or a runtime bswap fixup reads scrambled.
(StandTile id/room, header mid/tail).
- **Rule:** prefer an offline sidecar converter (D43, D69, D88 pattern,
`tools_pc/d*_emit.py`) over a runtime fixup for a whole format.
- A one-shot "sync patched values from the ROM copy into a compiled
shadow array" pass must key its slot-detection on a field that is still
intact *at sync time*. D124: `gimgSyncCompiledGlobalDLs` looked for the
`0xABCDxxxx` IMAGESEG marker in the ROM copy — but `texLoad()` had
already overwritten every one of those with a real pointer, so the sync
silently copied nothing and the compiled `globalDL_0xNNN` explosion
DLs kept their link-time markers (latent on *all* levels; only tripped
when an explosion/smoke/particle DL is first drawn). Detect the slot
from the *destination* (compiled) array, which still holds the marker.
- A polymorphic-record converter with a per-type handler table + a
"generic" fallback silently corrupts any type the table forgot: the
fallback's word-granular bswap is wrong for sub-word fields. D122 —
@@ -63,6 +72,18 @@ through a converter or a runtime bswap fixup reads scrambled.
*every* `type` byte a level can emit and assert the handler set is
total; `[u16|u16]` / `[s16|s16]` packed words need a half-swap
(`_hh_word`), never a 32-bit swap.
- **D123 corollary:** a pointer-width tail slot is **not always
runtime-populated**. Some hold a small *integer id* in the ROM image
that game code reads via that same field before overwriting it
(`VehichleRecord/AircraftRecord.ailist`: `prop.c:1764/1786` does
`x->ailist = ailistFindById(x->ailist)`). A converter that lumps every
pointer-width tail slot into one "widen 4→8B, emit zeroes" bucket
destroys the id → `ailistFindById(0)` silently returns global list 0
(`GAILIST_AIM_AT_BOND`) → `ai()` runs a CHR aim list with a NULL chr →
NULL deref in `chrIsNotDeadOrShot`. Before zeroing a widened slot,
confirm the setup code writes it unconditionally; if it reads-then-writes,
emit the value (`_bswap32` into the low 4 bytes, LE). Decide per field,
not per type.
## C2. Port-layer / SDL shims
+8 -3
View File
@@ -112,9 +112,14 @@ void gimgSyncCompiledGlobalDLs(u8 *base)
while (p + 8 <= end && p[0] != (u8)G_ENDDL)
{
/* Post-fixup marker position: LE encoding of 0xABCDxxxx puts
* CD AB at byte offsets 6..7. */
if (p[6] == 0xCD && p[7] == 0xAB)
/* D68/C2: texLoadFromDisplayList() has already replaced every
* IMAGESEG w1 word in the ROM copy with a real texture pointer,
* so the 0xABCDxxxx marker is gone from `p`. Identify the slots
* that need syncing from the COMPILED array instead: a G_SETTIMG
* command whose w1 is still an unresolved IMAGESEG marker
* (0xABCDxxxx). Copy the ROM copy's resolved word into it. */
if ((u8)(dst[j].words.w0 >> 24) == (u8)G_SETTIMG &&
(dst[j].words.w1 >> 16) == 0xABCDu)
{
dst[j].words.w1 = *(u32 *)(p + 4);
}
+25 -5
View File
@@ -86,13 +86,15 @@ OBJ_PC_BYTES = 144
# D122: ObjectRecord-derived types whose tail (N64 words >= 32) the converter
# must lay out with pointer widening. {type: (ptr_word_set, hh_word_set)}.
# 47 TintedGlassRecord tail: TintDist,CullDist,calculatedopacity,portalnum,unk90
# 39 VehichleRecord ailist*@w32, [u16 aioffset|s16 aireturnlist]@w33, path*@w41, Sound*@w43
# 40 AircraftRecord ailist*@w32, [u16|s16]@w33, path*@w43, Sound*@w44
# 39 VehichleRecord ailist(id)@w32, [u16 aioffset|s16 aireturnlist]@w33, path*@w41, Sound*@w43
# 40 AircraftRecord ailist(id)@w32, [u16|s16]@w33, path*@w43, Sound*@w44
# NB w32 (ailist) is a pointer-width slot holding an INT id read by
# prop.c before overwrite -> OBJ_ID_WORDS, not ptr_word_set (D123).
# 45 TankRecord collision*@w32 (rest scalar; struct locs unconfirmed)
OBJ_TAIL_DESC = {
47: (frozenset(), frozenset()),
39: (frozenset((32, 41, 43)), frozenset((33,))),
40: (frozenset((32, 43, 44)), frozenset((33,))),
39: (frozenset((41, 43)), frozenset((33,))),
40: (frozenset((43, 44)), frozenset((33,))),
45: (frozenset((32,)), frozenset()),
# 13 AutogunRecord: unkC4*@w49 unkC8*@w50 beam*@w51 (is_active@w52 unkD4@w53)
13: (frozenset((49, 50, 51)), frozenset()),
@@ -100,6 +102,19 @@ OBJ_TAIL_DESC = {
20: (frozenset(), frozenset(range(32, 45))),
}
# D123: ObjectRecord-derived tail slots that are pointer-width on PC but hold a
# pre-populated *integer AI-list id* in the ROM image which game code reads
# before overwriting (prop.c:1764 `pdef_veh->ailist = ailistFindById(pdef_veh->ailist)`,
# prop.c:1786 for aircraft). Unlike path*/Sound* (genuinely runtime-zeroed by
# prop.c) these must NOT be zeroed by the converter — the id must survive into
# the low 32 bits of the widened 8-byte slot. Was in ptr_word_set -> emitted as
# 8 zero bytes -> ailistFindById(0) -> GAILIST_AIM_AT_BOND -> ai() ran a CHR aim
# command with ChrEntityp==NULL -> NULL->actiontype crash in chrIsNotDeadOrShot.
OBJ_ID_WORDS = {
39: frozenset((32,)), # VehichleRecord.ailist @ N64 0x80
40: frozenset((32,)), # AircraftRecord.ailist @ N64 0x80
}
# Per-type PC struct size (bytes) the converter emits and sizepropdef must
# return / 4. For ObjectRecord-derived types this is 144 + tail growth.
PROPDEF_PC_BYTES = {
@@ -302,10 +317,15 @@ def convert_record(src, so, type_byte):
# tail with pointer members widened 4->8B and 8-aligned.
_emit_object_prefix(out, src, so)
ptr_words, hh_words = OBJ_TAIL_DESC[type_byte]
id_words = OBJ_ID_WORDS.get(type_byte, frozenset())
pc = OBJ_PC_BYTES
for i in range(OBJ_N64_WORDS, n64w):
w = src[so + 4 * i: so + 4 * i + 4]
if i in ptr_words:
if i in id_words: # D123: widened slot carrying an int id
pc = (pc + 7) & ~7
out[pc:pc + 4] = _bswap32(w) # id -> low 32 bits LE; high 32 = 0
pc += 8
elif i in ptr_words:
pc = (pc + 7) & ~7
pc += 8 # 8 zero bytes already present
elif i in hh_words:
+33
View File
@@ -0,0 +1,33 @@
#!/bin/bash
cd "$(dirname "$0")/.."
LV="Dam:33 Facility:34 Runway:35 Surface1:36 Bunker1:09 Silo:20 Frigate:26 Surface2:43 Bunker2:27 Statue:22 Archives:24 Streets:29 Depot:30 Train:25 Jungle:37 Control:23 Caverns:39 Cradle:41 Aztec:28 Egypt:32 Cuba:54"
OUT=/tmp/sweep_results
mkdir -p /tmp/sweeplogs
: > $OUT
rm -f /tmp/sweeplogs/*.crash.log
for entry in $LV; do
name=${entry%%:*}; num=${entry##*:}
rm -f ge007.crash.log; rm -rf ppm
export PATH="/c/msys64/mingw64/bin:$PATH"; GE_PCDUMP="80-260:40" ./build-pc/ge007.x86_64.exe -level_$num > /tmp/sweeplogs/$name.log 2>&1 &
pid=$!
sleep 24
taskkill //F //IM ge007.x86_64.exe > /dev/null 2>&1
sleep 2
status="UNKNOWN"
if [ -f ge007.crash.log ]; then
pc=$(grep -oE '0x[0-9a-fA-F]+' ge007.crash.log | head -1)
sym=$(addr2line -e build-pc/ge007.x86_64.exe -f -C "$pc" 2>/dev/null | head -1)
status="CRASH @ $sym ($pc)"
cp ge007.crash.log /tmp/sweeplogs/$name.crash.log
else
last=$(ls ppm/*.ppm 2>/dev/null | tail -1)
if [ -n "$last" ]; then
pcnt=$(python tools_pc/pixcount.py "$last" 2>/dev/null | grep -oE 'non-clear [0-9]+ \([0-9.]+%\)')
status="PASS $pcnt"
else
status="NO-FRAMES"
fi
fi
echo "$name ($num): $status" | tee -a $OUT
done
echo "SWEEP DONE"