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).
10 KiB
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,
docs/HANDOFF.md, docs/PCPortResearch.md §F/D69 (+D68), §G, §H.
Mission
Get the first stage (BUNKER1) to load and render after the intro, without
faulting. Then extend the same conversion to all levels' bg/*.seg +
Tbg_*_stanZ files (all three regions). Success bar: loads without fault +
plausible pixels — not pixel-perfect (3D pipeline correctness is a separate
milestone, D75; do not let visual wrongness displace this one).
Non-negotiables (from AGENTS.md)
- N64 build untouched:
Makefile,tools/,rsp/,ld/. - Game logic unmodified. Only narrow
#ifdef PORTABI/layout exceptions, each documented as its own finding in §F. The intended solution for this task requires ZERO game-code edits — pure offline data conversion + port-layer table patch. If you believe a game-file edit is needed, stop and document why as a finding before making it. - Offline sidecar conversion preferred over runtime fixup (AGENTS.md; the
D43/Plan-B precedent in
tools_pc/d43_emit.py+port/src/pcmodels.c). - The PD port (
PD_PORT_CHECKOUT) is an approach reference only — its BG format differs (zipped multi-section). Validate every field against GE's own consumers (bg.c,stan.c, N64 disasm).
Current state (verified)
- Build GREEN:
export PATH="/c/msys64/mingw64/bin:$PATH" && ./build-pc.sh ntsc-final(~5 s). - Game boots → full intro (Rareware logo, gun barrel w/ Brosnan, cast; ~frame
2100) → crashes in
load_bg_file. - Crash: EXCEPTION 0xc0000005; symbolicated PC =
load_bg_file, src/game/bg.c:865; the crash stack contains BG header words0x0F000334/0x0F00032C— confirms the BE-offset misread below.
The bug, precisely
- BG-file internal offsets are N64 big-endian u32s in segment-0x0F form
(
0x0Fxxxxxx). BG_SEG_TO_PTR(bg.h:23):((void*)(((u32)(base)) + (((u32)(off)) + 0xF1000000)))— on N64,off = 0x0Fxxxxxxand+ 0xF1000000wraps in 32-bit arithmetic to the plain file offset0x00xxxxxx.- On PC,
((s32 *)ptr_bg_data)[1]reads LE: ROM bytes0F 00 00 14become0x1400000F; the fold yieldsbase + 0x0500000F≈ 5 MB past the header buffer → fault dereferencing[1].pPointTableBin. - Scope: every bg
.segheader word and record (room file-position list, portals, etc.) and the wholeTbg_*_stanZgeometry file (StanPrefixRecord+ everythingstanDetermineEOF/stanLoadFilewalk). Same class as D68 (image table) but a far larger format surface.
Known data points (BUNKER1)
- Per-level filenames:
levelinfotable(bg.c ~line 183) →bg_seg_filename/bg_stan_filename. Name→symbol map:assets/obseg/file_resource_table.inc.c(e.g. line 6"bg/bg_sev_all_p.seg"→&bg_sev_all_p_seg; line 586"Tbg_sev_all_p_stanZ"→&Tbg_sev_all_p_stanZ). - BUNKER1 files:
bg/bg_sev_all_p.seg@ cart 0x10438660, size 0x10DF0;Tbg_sev_all_p_stanZ@ cart 0x1087C3F0, size 0x3DD0. - Header words (BE values): word0 =
0x00000000; word1 =0x0F000014(room file-position list @ file offset 0x14); word2 =0x0F000334; word3 =0x0F00032C. bg_room_datarecords:pPointTableBinat record+0x28 (per crash disasm), more0x0Fxxxxxxoffsets per record.- Load path:
obLoadBGFileBytesAtOffset(name, buf, offset, size)does partial loads via memcpy from&fileentry->hw_address[offset]; stan files load via_fileNameLoadToBank(..., bank 2). Both already work on PC — the bug is interpretation only, so redirectinghw_addressat converted bytes is sufficient.
Task 1 — decode the formats (write notes as you go)
- Dump BUNKER1's
.segheader + first records from the ROM; map every word to its consumer inbg.c(load_bg_file~line 795+ and the table walks it performs: rooms/portals/bgcmds/lights). Note which words are0x0Fxxxxxxoffsets vs plain data. - Same for
Tbg_sev_all_p_stanZ:StanPrefixRecord+ whateverstanDetermineEOF/stanLoadFilewalk (src/game/stan.c). Identify vertex batches, bounds (cf.RoomVtxBatchBoundsin bg.h), Gfx/DL references, and texture refs inside stan data. - Classify every field: u32 numeric (bswap32 for PC) / s16 numeric (bswap16) / byte stream (leave alone — compressed payloads etc.). Where the decomp is ambiguous, cross-check struct field offsets against the N64 disasm of the consumer function.
- Write the format notes into
PCPortResearch.md§F as you go (labels D78+) — they are the converter spec and must be complete enough to regenerate the converter from them.
Task 2 — implement offline conversion (Plan-B pattern)
Key design constraint: in-place, size-preserving conversion. If every
converted file is byte-identical in size to its ROM original, all internal
0x0Fxxxxxx offsets remain valid after BG_SEG_TO_PTR folding, and the game
code needs zero changes — bswapped words read back as the original BE
values. (bswap32/bswap16 are size-preserving by construction; byte streams
pass through untouched.)
- New converter
tools_pc/d69_emit.py(model ond43_emit.py): per region, for everybg/*.seg+Tbg_*_stanZreferenced bylevelinfotable: extract from ROM → convert per the Task 1 spec → concatenate intodata/pccg-<region>/pccg.bin+manifest.csv(name,offset,size, decimal — same format as pcmodels). - Port layer: clone the
port/src/pcmodels.cpattern (it is the template — read it first): sidecar image lives in the cart reservation extension at[CART_BASE + romSize, ...); a one-shot table patch matchesfile_resource_table[]by filename and rewriteshw_address+rom_size. Either extendpcmodels.cor addport/src/pccg.c. Wiring points (all small):port/src/romdata.c~line 165: include the pccg size in theVirtualAlloc(CART_BASE, romSize + sideTotal)reservation; load the sidecar next topcmodelsLoadSidecars.port/src/romdata.c~line 253 (romdataCartAddrValid) andport/src/libultra.c~line 821 (DMA source validation, D60 safety net): extend the bounds check to cover pccg bytes.- One-shot patch call site: same pattern as
load_object_fill_header → pcmodelsPatchTable()(or hook the BG load path's first use — your choice, document it).
- Regenerate per region (
ntsc-final/pal-final/jpn-final); adddata/pccg-*/to.gitignorelikepcmodels-*.
Fallback: if the decode shows only a handful of fields matter for first-frame render, a runtime port-layer fixup hook (D68-style bswap after load) is acceptable as an interim step — but stage geometry is deep; expect most fields to need conversion anyway. Document the choice as a finding.
Task 3 — verify
- Clean run from the repo root (
cd <repo> && ./build-pc/ge007.x86_64.exe) — running frombuild-pc/picks up the wrong ROM and misses sidecars (spurious early crash). Expect: intro → BUNKER1 loads without fault; stage geometry visible (plausible pixels, not pixel-perfect). GE_PCDUMP="2100-2400:5"frames +tools_pc/pixcount.py— confirm non-black stage content past the old crash point.- All levels: converter must cover every bg/stan file in
levelinfotable(20+); the table patch logs its hit count — verify it matches the manifest row count exactly. - Verification ritual (AGENTS.md): build green; no new undefined/duplicate
symbols (
/linkcheck); every touched file parses. - Strip TEMP probes before committing (keep the
GE_D69probe inob.cuntil resolved, then strip it per the HANDOFF Task 3 list).
Acceptance criteria (all must hold)
- Clean run reaches BUNKER1 without fault (no new
ge007.crash.logpast ~frame 2100). - Stage renders plausible geometry (PPM-verified, non-black content).
- Converter covers all levels' bg/stan files; all three regions regenerable from the script.
- Zero game-code edits — or any
#ifdef PORTedit individually justified and documented as a D7x finding. - Format spec written in
PCPortResearch.md§F (D78+). - Build green +
/linkcheckclean; TEMP probes stripped.
Environment gotchas
- MSYS2 tools:
export PATH="/c/msys64/mingw64/bin:$PATH". Build ~5 s. - Run the game from the repo root, not
build-pc/. - Crash log
ge007.crash.log(repo root) is the first stop for any fault; symbolicate offline:addr2line -e build-pc/ge007.x86_64.exe -f -C <pc>(image base 0x140000000). - gdb is launch-mode only and far too slow for timing-dependent issues — prefer env-gated probes + the crash log.
- Heavy logging perturbs tick-driven timing (
osGetCountadvances at ~46.5525 ticks/µs) — diagnose stability with clean runs only; use short, targeted probe windows. Gfxis 16 bytes on PC; ROM-copied N64 DLs stay 8 bytes in memory — walk them at 8-byte stride when scanning.- N64 CPU/RDP data in ROM is big-endian; any raw
*(u32*)read of a ROM-copied buffer needs bswap under PORT (D68 pattern) or offline conversion. - Don't "fix" the s32 address idioms. The decomp passes addresses through
s32/u32(e.g.ptr_bg_data = (s32)header); this works because all emulated addresses (cart 0x10000000, DRAM 0x70/0x80xxxxxx) and host stack/heap fit in the low 4 GB, so truncation is lossless. Changing these to 64-bit types breaks theBG_SEG_TO_PTRfold and is out of scope. - Standalone probe compiles need
-std=c11.
Out of scope (do not let these displace the milestone)
- D75 (3D matrix/model bugs), D76 (copyright-screen 2D partial draw), D77 (no audio). The stage may render wrong — that is expected; the bar is loads-without-fault + plausible pixels.
- Pixel-perfect intro-logo check vs N64 footage.
Documentation requirements
- New findings:
PCPortResearch.md§F, labels D78+ (D75–D77 are taken). - Update §G status and
docs/HANDOFF.mdat each sub-milestone. - Commit per sub-milestone: (1) format spec notes, (2) converter + sidecar, (3) port wiring, (4) verification + probe strip.