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).
8.1 KiB
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.mdnon-negotiables (esp. #2 game logic unmodified — narrow#ifdef PORTABI/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_XXnow auto-injects that stage'smemallocstringtable[]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.pyare committed (ff563812,4ccc0d3c);python tools_pc/d88_emit.py ntsc-final --regenreports21/21 converted … ALL CHECKS PASSED. The placeholderPROPDEF_PC_BYTESfor VEHICLE/AIRCRAFT/TANK/AMMO are not exercised by any solo level (tools_pc/d88_propdef_scan.pyhistogram — 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->Opcodederef, badnodein the ModelNode walkFacility 34 CRASH modelLoad(src/game/loadobjectmodel.c:393) —PitemZ_entries[modelid].header->RootNode, badheader(or OOBmodelid)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.Raxat 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:
PitemZ_entries[341](struct ItemModelFileRecord { ModelFileHeader *header; char *filename; float scale; },src/bondtypes.h:1466) — table stride or theheader/filenamepointer values wrong on PC. Find where this array is defined (it isexterneverywhere I looked —src/game/prop.c:40,chrobjdata.h:20; the definition is likely a generated.s/rodata blob or an asset —grepharder, checkport/src/romassets_*.s,scripts/gen_romassets.py,scripts/gen_*). If theheaderpointers come from BE ROM rodata they need rebasing/byteswap (D39 Globalimagetable / D68 class).modelidpassed tomodelLoadis out of range / unswapped — it comes from a propDefObjectRecord-familymodelidfield. If a record type used by Dam/Facility props (but not BUNKER1) has itsmodelidat a field offset thed88_propdefs.pyconverter byteswaps wrongly (or not at all),modelidis garbage. Cross-check the per-type field maps intools_pc/d88_propdefs.pyagainst the record structs.- The
pcmodelssidecar (tools_pc/d43_emit.py, loaded at0x10C00000, "512 sidecars") is missing/mis-converting a model feature (ModelNode opcode,PointUsage[]chain — cf. D120,d43_emit.pyopcode-0x18 gap) that Dam/Facility prop models use.model.c:6249already has aGE_D86env 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 PORTonly).tools_pc/d43_emit.py(+tools_pc/d43_*.pyhelpers,d43_layoutprobe.c).tools_pc/d88_propdefs.py/tools_pc/d88_emit.py(only if the cause is a propDefmodelidfield).port/src/model/asset shims if the table is patched at runtime likepcmodelsPatchTable(grep pcmodelsPatchTable,port/src/romdata*.c).- New probe files:
tools_pc/d122_*.py/ env-gatedGE_D122blocks. 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:393crash — 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.pycompletes).
PRE-FLIGHT (done — results above)
./build-pc.sh ntsc-finalgreen; binary atbuild-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)
timeoutdoes NOT reliably kill the game on Windows. Launch with… &,sleep N, thenkill $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 base0x140000000; PC in the log is already absolute). - Frame capture:
GE_PCDUMP="a-b:step"→./ppm/; content checkpython tools_pc/pixcount.py ppm/frame_XXXX.ppm. - gdb: attach mode is fast (
gdb -batch -x cmds.txt -p <winpid>, winpid = 4th col ofps -p <bashpid>; game must already be running). Launch mode is too slow. A hardware watchpoint onPitemZ_entriesor the badnodecatches 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 PORTwith 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
- Root cause —
file:lineevidence, which of the 3 hypotheses (or other), why Dam/Facility/Runway differ from BUNKER1/Silo. - Fix — the diff, or why-not if unresolved.
- Verification —
-level_33,-level_34,-level_35each boot to ≥1 non-degenerate frame (pixcount.py> a few %), AND-level_09(BUNKER1) still renders (no regression —tools_pc/framediff.py). - Probes left in the tree (env-gated, capped).
- Confidence rating.
- Append any generalisable quirk to
docs/PORT-LEARNINGS.md.