Files
goldeneye-pc-port/docs/BRIEF-D122-per-level-modelload.md
T
James Dansereau a8b174a829 D122: fix propDef obj half-swap for ObjectRecord-derived types
tools_pc/d88_propdefs.py had a per-type handler table plus a generic
fallback. Six `inherits ObjectRecord` propDef types (47 TINTED_GLASS,
39 VEHICHLE, 40 AIRCRAFT, 45 TANK, then 13 AUTOGUN, 20 AMMO/MultiAmmoCrate)
had no handler and fell through to the generic arm, which bswap32's N64
word 1. For an ObjectRecord that word is [s16 obj][s16 pad] and needs a
half-swap, not a word-swap - so the model id `obj` landed in the wrong
16 bits, giving a garbage/OOB index into PitemZ_entries[] and a crash in
modelLoad (loadobjectmodel.c:393) / modelInitRwData (model.c:6249).

BUNKER1/Silo were unaffected because neither emits any of the six types.

Fix (converter + matching walk stride, no game logic):
- d88_propdefs.py: OBJ_TAIL_DESC handler - real 144B PC ObjectRecord
  prefix + typed tail (pointers widened 4->8B/8-aligned, [u16|u16] words
  half-swapped). PROPDEF_PC_BYTES 39/40 176/180->208, 45 224->248.
- loadobjectmodel.c sizepropdef() #ifdef PORT arm: VEHICHLE/AIRCRAFT
  44/45->52, TANK 56->62 words (N64 values in the #else sizeof switch).

Verified: -level_33/34/35 clear the model-load chain (they now crash
further in, in unrelated chr/fast3d code - separate blockers);
-level_09 BUNKER1 and -level_20 Silo unregressed. Sidecars regenerated
(d88_emit.py --regen, 21/21). Full write-up: PCPortResearch.md D122
(uncommitted, rides with the in-progress docs restructure).
2026-08-29 15:26:22 -04:00

8.1 KiB
Raw Blame History

BRIEF D122 — per-level prop/item model-load crash (blocks the level sweep)

Repo: REPO_ROOT (git, branch master, Windows; PowerShell + Bash tool available).

READ FIRST

  • docs/HANDOFF.md (top) — current state, environment, repro line.
  • docs/PORT-LEARNINGS.md — recurring bug classes. A / C are the prime suspects here (pointer-width struct growth; BE rodata on LE).
  • docs/PCPortResearch.md §F index (top) + entries D43, D53.2, D56, D57, D67, D68, D39, D108–D112, D120 — model/asset conversion + pointer-width history. Jump to each via the index; do NOT linear-read.
  • AGENTS.md non-negotiables (esp. #2 game logic unmodified — narrow #ifdef PORT ABI/layout/format edits only, each logged in §F).

CONTEXT (established this session, M-12)

  • WS1 landed (commit 02c12068, D121): a bare ./build-pc/ge007.x86_64.exe -level_XX now auto-injects that stage's memallocstringtable[] pool row. Every solo level boots with correct pools now — no manual -m* needed.

  • D88.4 (propDefs converter) is effectively done for the 21 solo levels. tools_pc/d88_emit.py + d88_propdefs.py are committed (ff563812, 4ccc0d3c); python tools_pc/d88_emit.py ntsc-final --regen reports 21/21 converted … ALL CHECKS PASSED. The placeholder PROPDEF_PC_BYTES for VEHICLE/AIRCRAFT/TANK/AMMO are not exercised by any solo level (tools_pc/d88_propdef_scan.py histogram — no VEHICLE/AIRCRAFT/TANK in any of the 21). So D88.4 is NOT the blocker.

  • The actual blocker: prop/item model loading crashes on most levels. Smoke test (each -level_XX, GE_PCDUMP="60-240:60", ~25 s):

    level # result
    Bunker1 09 renders, playable (known good)
    Silo 20 renders ~91% frame content, no crash
    Dam 33 CRASH modelInitRwData (src/game/model.c:6249) — node->Opcode deref, bad node in the ModelNode walk
    Facility 34 CRASH modelLoad (src/game/loadobjectmodel.c:393) — PitemZ_entries[modelid].header->RootNode, bad header (or OOB modelid)
    Runway 35 CRASH same as Facility (loadobjectmodel.c:393)

    Crashes are early and deterministic (before frame 1 renders), same PC every run — distinct from BUNKER1's rare intermittent loadobjectmodel.c:393 (D117 nondeterminism). EXCEPTION 0xc0000005, FAULT ADDR: ffffffffffffffff. Rax at the Facility crash holds ASCII bytes ("…o ng nol…") — looks like a model/texture name string landed in a register, consistent with a mis-strided table read.

TASK

One question: why do prop/item models load fine for BUNKER1/Silo but crash for Dam/Facility/Runway, and what is the narrow fix?

Almost certainly one of:

  1. PitemZ_entries[341] (struct ItemModelFileRecord { ModelFileHeader *header; char *filename; float scale; }, src/bondtypes.h:1466) — table stride or the header/filename pointer values wrong on PC. Find where this array is defined (it is extern everywhere I looked — src/game/prop.c:40, chrobjdata.h:20; the definition is likely a generated .s/rodata blob or an asset — grep harder, check port/src/romassets_*.s, scripts/gen_romassets.py, scripts/gen_*). If the header pointers come from BE ROM rodata they need rebasing/byteswap (D39 Globalimagetable / D68 class).
  2. modelid passed to modelLoad is out of range / unswapped — it comes from a propDef ObjectRecord-family modelid field. If a record type used by Dam/Facility props (but not BUNKER1) has its modelid at a field offset the d88_propdefs.py converter byteswaps wrongly (or not at all), modelid is garbage. Cross-check the per-type field maps in tools_pc/d88_propdefs.py against the record structs.
  3. The pcmodels sidecar (tools_pc/d43_emit.py, loaded at 0x10C00000, "512 sidecars") is missing/mis-converting a model feature (ModelNode opcode, PointUsage[] chain — cf. D120, d43_emit.py opcode-0x18 gap) that Dam/Facility prop models use. model.c:6249 already has a GE_D86 env probe for the node walk — use it.

Determine which, root-cause, and apply the narrowest fix (converter extension preferred over runtime fixup for a whole format — D43/D88 pattern; or a narrow #ifdef PORT ABI edit with the N64 line kept under #else).

FILES YOU MAY TOUCH

  • src/game/model.c, src/game/loadobjectmodel.c, src/game/prop.c (narrow #ifdef PORT only).
  • tools_pc/d43_emit.py (+ tools_pc/d43_*.py helpers, d43_layoutprobe.c).
  • tools_pc/d88_propdefs.py / tools_pc/d88_emit.py (only if the cause is a propDef modelid field).
  • port/src/ model/asset shims if the table is patched at runtime like pcmodelsPatchTable (grep pcmodelsPatchTable, port/src/romdata*.c).
  • New probe files: tools_pc/d122_*.py / env-gated GE_D122 blocks.
  • docs/PORT-LEARNINGS.md (append any new generalisable quirk), docs/PCPortResearch.md §F (write up as D122, add to the index).

Nothing else is running — no file collisions to worry about.

KNOWN-GOOD / RULED OUT — do not re-investigate

  • WS1 per-level pool injection (D121) — works, committed.
  • D88.4 propDefs stream conversion for solo levels — works (21/21).
  • BUNKER1 + Silo prop/model loading — works.
  • The intermittent BUNKER1 loadobjectmodel.c:393 crash — that is D117-class nondeterminism, separate; do not chase it, use the deterministic Dam/Facility/Runway repro.
  • d88_propdef_scan.py "STOP … unmapped type NOTHING" — that is the scan tool's limitation, not a converter bug (d88_emit.py completes).

PRE-FLIGHT (done — results above)

  • ./build-pc.sh ntsc-final green; binary at build-pc/ge007.x86_64.exe.
  • Sidecars regenerated this session (d43_emit.py, d69_emit.py, d88_emit.py --regen) — all pass.
  • Smoke-test table above.

ENVIRONMENT

export PATH="/c/msys64/mingw64/bin:$PATH"
./build-pc.sh ntsc-final                       # ~5 s, from repo root
./build-pc/ge007.x86_64.exe -level_34           # Facility repro (bare — D121)
  • timeout does NOT reliably kill the game on Windows. Launch with … &, sleep N, then kill $PID; pkill -f ge007.
  • Crash log: ge007.crash.log (repo root). Symbolicate: addr2line -e build-pc/ge007.x86_64.exe -f -C 0x<PC> (image base 0x140000000; PC in the log is already absolute).
  • Frame capture: GE_PCDUMP="a-b:step" → ./ppm/; content check python tools_pc/pixcount.py ppm/frame_XXXX.ppm.
  • gdb: attach mode is fast (gdb -batch -x cmds.txt -p <winpid>, winpid = 4th col of ps -p <bashpid>; game must already be running). Launch mode is too slow. A hardware watchpoint on PitemZ_entries or the bad node catches the corrupting write fast.
  • Standalone probe compiles: -std=c11.

BUDGET

Structural bug → generous: ~12 build→run→inspect cycles or ~90 min. On expiry: revert all temp probes (leave only committed env-gated ones), write up the spec progress + best hypothesis in docs/PCPortResearch.md §F as D122 with an explicit confidence rating, and list exactly which files you changed. 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 kept verbatim under #else, each documented in §F/D122.
  • Prefer an offline sidecar-converter extension over a runtime fixup for a whole asset format.
  • Revert your own temp probes before reporting.
  • 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.

REPORT

  1. Root cause — file:line evidence, which of the 3 hypotheses (or other), why Dam/Facility/Runway differ from BUNKER1/Silo.
  2. Fix — the diff, or why-not if unresolved.
  3. Verification — -level_33, -level_34, -level_35 each boot to ≥1 non-degenerate frame (pixcount.py > a few %), AND -level_09 (BUNKER1) still renders (no regression — tools_pc/framediff.py).
  4. Probes left in the tree (env-gated, capped).
  5. Confidence rating.
  6. Append any generalisable quirk to docs/PORT-LEARNINGS.md.