PC port: phase 2 — D43 mechanical ABI edits + D46 buffer sizing + fast3d seg-5 case; converter spec validated (D47)

- bondtypes.h/model.c/chr.c/objecthandler_2.c/tex.c: PORT-guarded pointer-width
  and Gfx-size fixes (D43 step 2, uncommitted since prior session)
- front.c cast-screen bufferRemaining 0x1C000->0x25000, initmenus.c logo
  buffer 0x7C000->0x85000 (D46 strict bound)
- gfx_pc.cpp seg_addr: resolve raw 0x05xxxxxx via segmentPointers[5] + low24
  (unmarked segmented addresses, ROM convention)
- docs: D47 finding — converter contract finalized (compaction delta=R-P,
  accessors at 0x80090000, G_VTX GBI1 encoding, opcode-aware w1 remap rule,
  vertex formats, no embedded blobs, zero-vtx-array rule, staging contract,
  reload-hazard poolRemaining reset); HANDOFF rewritten around the remaining
  C implementation
- build-pc/d43_convert.py (gitignored, local): reference converter + validator —
  full N64->PC conversion spec run on all 512 ROM files, ALL CLEAN (max D_PC=0xB7E0)
This commit is contained in:
James Dansereau committed 2026-08-22 18:24:31 -04:00
1 parent f298dc6703
commit 181f6db07d
12 files changed
+449 -38

No files matched your search

+1 -1
View File
@@ -48,6 +48,6 @@ 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 + first frames render; current blocker is **D43** (model-file loading ABI mismatch: N64-layout ROM model files read as 8-byte-pointer PC structs → D37-style re-layout in `port/src/romdata.c`; a Rare-validated near-analogue exists in the PD port's `preprocess/filemodel.c` — §2.4). **D43 investigation complete:** all 512 model files validated (opcodes, tiling invariants, GDL visit order), converter design + full implementation checklist in `docs/HANDOFF.md`; helper scripts in `build-pc/d43_*.py`. Recurring sub-task: ABI/layout fixes per the D32 procedure (§H) as more asset types are first touched (PD's `preprocess/` module already solves most of them).
- **Phase 2 (rendering):** IN PROGRESS — fast3d integrated + first frames render; current blocker is **D43** (model-file loading ABI mismatch: N64-layout ROM model files read as 8-byte-pointer PC structs → D37-style re-layout in `port/src/romdata.c`; a Rare-validated near-analogue exists in the PD port's `preprocess/filemodel.c` — §2.4). **D43 status:** all 512 model files validated; buffer sizing finalized by the D46 overlap-safety analysis (strict bound from N64 ground truth); step-2 mechanical game-code edits APPLIED (uncommitted); **D47 closed every open converter question** — compaction state values, G_VTX encoding, opcode-aware remap rule, vertex formats, staging contract — and the full conversion spec is implemented + validated 512/512 by `build-pc/d43_convert.py` (max D_PC = 0xB7E0, ratio ≤ 1.31). Done this session: D46 buffer corrections (front.c 0x25000 / initmenus.c 0x85000) + fast3d seg_addr seg-5 raw-VMA case. Remaining: C implementation of `romdataFixupModelFile` in `port/src/romdata.c` (port d43_convert.py 1:1) + hook in load_object_fill_header (+ poolRemaining=0 reset), build + soak — full checklist in `docs/HANDOFF.md`; findings D47 in §F. Recurring sub-task: ABI/layout fixes per the D32 procedure (§H) as more asset types are first touched (PD's `preprocess/` module already solves most of them).
- **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 4 (saves + polish):** file-backed EEPROM — build on PD's `fs.c` (file-backed save dir); PFS/motor already no-op stubs.
+132 -37
View File
@@ -1,32 +1,32 @@
# Handoff brief — GoldenEye 007 PC port (Phase 1.5 done, Phase 2: first frames render)
_Paste-ready briefing for the next agent session. The authoritative detail lives in
`AGENTS.md` and `docs/PCPortResearch.md` (§H handoff, §F/D31–D44 findings); this is
`AGENTS.md` and `docs/PCPortResearch.md` (§H handoff, §F/D31–D46 findings); this is
the summary + the immediate tasks._
## Your job
Two tasks, in order:
1. ~~**D44 (quick win, ~15 min):** one-line fix to the crash handler~~ — **DONE,
committed.** Every crash now auto-logs a `BACKTRACE:` section to stdout and
`ge007.crash.log`; the D43 chain (#00 model.c:5688 ← #01
load_object_fill_header) was confirmed without gdb. The debug loop is now
"read ge007.crash.log".
One task left on D43 (everything else is done and validated):
1. ~~**D44:** one-line fix to the crash handler~~ — **DONE, committed.** Every
crash now auto-logs a `BACKTRACE:` section to stdout and `ge007.crash.log`.
2. **D43 (main task):** model-file loading ABI mismatch — stage object load faults
in `modelPromoteNodeOffsetsToPointers()` (src/game/model.c:5688) because ROM model
files are serialized with N64 layout (4-byte pointer fields) but read on PC as
8-byte-pointer structs. **Investigation is now COMPLETE** (all format details,
invariants, and the exact implementation plan are below + in §F/D43). What remains
is pure implementation: a two-pass re-layout converter in `port/src/romdata.c`,
~6 one-line mechanical game-code edits, one fast3d seg_addr case, one hook line.
Then keep pushing rendering forward (more asset types will fault the same way).
8-byte-pointer structs. **Investigation COMPLETE; the converter spec is fully
pinned down and validated 512/512 by `build-pc/d43_convert.py`** (see §F/D47).
What remains: **the C implementation of `romdataFixupModelFile()` in
`port/src/romdata.c`, the hook in `load_object_fill_header`, build + soak.**
After that keep pushing rendering forward (more asset types will fault the
same way — Tbg_*_stanZ backgrounds next).
Work agentically — fix → build (~5 s) → verify → commit at each working milestone.
Commit message style: `PC port: <phase> — <what>`.
## Read first (authoritative, in order)
1. `AGENTS.md` — non-negotiables, critical files, build, verification ritual, phase status.
2. `docs/PCPortResearch.md` §H (handoff + plan), then **§F/D44** (the quick win),
**D43** (the open model-file task), **§2.4** (PD-port copy-candidate audit —
2. `docs/PCPortResearch.md` §H (handoff + plan), then **§F/D47** (converter
contract finalized — read before writing the C converter), **D46** (overlap
safety + final buffer sizing), **D45** (buffer constants), **D43** (the open
model-file task), **§2.4** (PD-port copy-candidate audit —
read before starting D43 or Phase 3/4), **D37** (the libaudio bank-tree
re-layout pattern), D39–D42 for the first-render fixes, D31–D38 for background.
@@ -39,10 +39,28 @@ Commit message style: `PC port: <phase> — <what>`.
- Region macros in `CMakeLists.txt` must mirror the N64 Makefile exactly.
## Current state
- **D43 investigation finished (latest session):** all 512 model files decompress and
walk cleanly; layout invariants verified with zero failures; the converter design,
every mechanical edit, and the compaction contract are fully pinned down — see the
task section below before writing any code. No D43 code has been written yet.
- **D43 status (latest session, §F/D47):**
- DONE + APPLIED (uncommitted): all step-2 mechanical game-code edits
(bondtypes.h Vertex u32 union; model.c PROMOTE32; chr.c casts;
objecthandler_2.c Textures offset; tex.c Gfx-size/dispatch/byte-check/
copy-count fixes; gun.c/front.c/initmenus.c D45 buffer constants).
- DONE (this session): **D46 buffer corrections applied** (front.c
bufferRemaining 0x1C000→0x25000, initmenus.c logo 0x7C000→0x85000) and the
**fast3d seg_addr seg-5 raw-VMA case** (port/fast3d/gfx_pc.cpp).
- DONE (this session): every open converter question resolved — compaction
state values (delta = R−P; accessors at 0x80090000), G_VTX w0 encoding
(GBI1 style; fast3d already correct), G_TRI4 false-positive eliminated
(opcode-aware remap rule), vertex formats verified, no embedded blobs,
zero-vtx-array rule, reload hazard + poolRemaining-reset decision.
- **VALIDATED:** `build-pc/d43_convert.py` implements the full conversion spec
and runs it on all 512 ROM files: ALL CLEAN (every pointer remap resolves;
max D_PC = 0xB7E0; max ratio D_PC/D_N64 = 1.31).
- **REMAINING:** port that validated logic to C as
`romdataFixupModelFile()` in port/src/romdata.c (two-pass: walk+layout,
then emit PC image staged at the tail of the reserved block and memmove to
F), hook it into load_object_fill_header (+ poolRemaining=0 reset before
_fileNameLoadToBank, per D47.4), set poolRemaining = D_PC after conversion,
build + run + 30 s soak.
- (Earlier session, verified live 2026-08-22)
- **Re-ran the committed build:** frames 1–4 render (`[NOTE] frame N rendered in … us`,
frame 1 ~21 ms), then FATAL `0xc0000005` at PC `0x14007a31a` =
@@ -83,23 +101,28 @@ build-pc/ge007.x86_64.exe -f -C 0x14007a31a`).
**Format / layout:**
- All 512 C*/G*/P*Z model files (Tbg_*_stanZ backgrounds are a different format/path — later) decompress fine; each file starts directly with the switches array (no in-file header — NS/NT come from the exe `ModelFileHeader` record via `objheader->numSwitches/numtextures`).
- File layout: `[switches NS×4B][textures NT×12B][root node @ 4NS+12NT]…`; all embedded pointers are vma `0x05xxxxxx` (mask 0xFFFFFF).
- **Tiling invariant verified, 0 failures in all 512 files** (`build-pc/d43_invariants.py`): `[0,D)` is exactly tiled by {switches, textures, nodes(20B), records(RSZ per opcode), vertex arrays(nv×16B), collision arrays(nc×16B), PointUsage (extends to the next owned object — size varies 8..3248B, NOT derivable from counts), GDLs (each extends to the next GDL start or D)}. Records INTERLEAVE with vertex arrays in character bodies — do not assume "all records before bulk".
- **Opcodes present: only 1,2,4,8,9,10,18,21,23,24** (HEADER, GROUP, DL, LOD, BSP, BOX, SWITCH, GROUPSIMPLE, HEADPH, DLCOLLISION). Converter handles exactly these; fatal on anything else.
- N64 record sizes: OP01=0x10, OP02=0x1C, OP04=0x14, OP08=0x10, OP09=0x24, OP10=0x1C, OP18=0x08, OP21=0x14, OP23=0x02, OP24=0x20. Field layouts per opcode are in `src/bondtypes.h` (ModelRoData_*Record) — PC record sizes differ due to pointer padding (e.g. OP04 20B→0x28). Since `port/src/romdata.c` is PC-only, read N64 fields from packed offsets and emit via the real PC structs (or explicit placement) — don't hand-compute PC offsets.
- **Tiling invariant verified, 0 failures in all 512 files** (`build-pc/d43_invariants.py`): `[0,D)` is exactly tiled by {switches, textures, nodes(24B), records(RSZ per opcode), vertex arrays(nv×16B), collision arrays(nc×16B), PointUsage (exactly numVertices s16 entries — +2 cases are pure alignment padding), GDLs (each extends to the next GDL start or D; 8-byte aligned, inter-GDL gaps copied as NOOP)}. Records INTERLEAVE with vertex arrays in character bodies — do not assume "all records before bulk".
- **ModelNode = 24B N64** (u16 op + 2 pad + 5×4B ptrs: Data@4, Parent@8, Next@0xC, Prev@0x10, Child@0x14) / **48B PC**. Older "20B/40B" notes are wrong — verified against raw hex stride 0x18.
- **Opcodes present: 1,2,4,8,9,10,12,13,15,18,21,22,23,24** (HEADER, GROUP, DL, LOD, BSP, BOX, GUNFIRE, SHADOW, INTERLINK, SWITCH, GROUPSIMPLE, DLPRIMARY, HEADPH, DLCOLLISION). op22 N64: numVertices s32@0, Vertices@4, Primary@8, BaseAddr@0xC (size 0x10); only 5 files have it.
- Record sizes N64/PC: OP01 0x10/0x18, OP02 0x1C/0x28, OP04 0x14/0x28, OP08 0x10/0x18, OP09 0x24/0x30, OP10 0x1C/0x1C, OP12 0x28/0x30, OP13 0x20/0x30, OP15 0x1C/0x1C, OP17 0x20/0x28, OP18 0x08/0x10, OP21 0x14/0x14, OP22 0x10/0x20, OP23 0x02/0x02, OP24 0x20/0x40 (compiler-verified). Field layouts in `src/bondtypes.h` (ModelRoData_*Record); read N64 fields from packed offsets, emit via real PC structs — don't hand-compute PC offsets.
- **f32 fields are big-endian in ROM** — bswap all f32 (record origins, BoundingVolumeRadius, Scale, Min/MaxDistance) and every s16 in vertex arrays; also bswap32 the TextureID u32 in the texture table (texLoad reads `*updateword & 0xffff` as the texture number).
- **Vertex arrays must stay 16B-stride** (GDL gSPVertex hardcodes 16B/vertex; propobj.c copies with sizeof(Vertex); model.c:4360 allocates numVertices×4 words). bswap every s16 field BE→LE. Main Vertices array is pure data; **CollisionVertices carry LinkedTo@8 = vma of a ModelNode** — remap to the new node offset (PROMOTE promotes them after rebase).
- **GDLs:** 8B BE word pairs → 16B LE slots: `w0=bswap32(rom_w0); w1=bswap32(rom_w1)`. Set LSB of w1 on segmented commands (PD's gbi.c trick — fast3d's seg_addr requires w1&1 to take the segment path): GE values G_MTX=0x01, G_MOVEMEM=0x03, G_VTX=0x04, G_DL=0x06 + the RDP setimg family (check exact opcodes in include/PR/gbi.h before coding; PD's CMD_IS_SEGMENTED = MOVEMEM/MTX/VTX/COL/DL/SETTIMG/SETCIMG/SETZIMG). Do NOT set LSB on non-segmented commands.
- **GDLs:** 8B BE word pairs → 16B LE slots: `w0=bswap32(rom_w0); w1=bswap32(rom_w1)` — **leave w1 raw, NO LSB set** (ROM convention; fast3d's extended seg_addr case handles unmarked segmented addresses — D47.13). GE uses STANDARD libultra GBI numbering (include/PR/gbi.h); opcodes live in the TOP byte of w0. **Opcode-aware remap: only {G_VTX=0x04, G_SETTIMG=0xFD, G_LOADBLOCK=0xF3} carry addresses in w1** (seg-5 → remap low24 via region map). All other opcodes get bswap-only — G_TRI4 (GE extension 0xB1) w1 is 4-bit vertex-index data, NOT an address (D47.6); same for TRI1/TEXTURE/SET*MODE/CLEARGEO/SETGEO/syncs. **G_VTX w0 is GBI1-style** (`04<<24 | dst<<16 | 16·n`, dst=((n−1)<<4)|v0; all 2826 have v0=0, n≤16) — bswap only, no field remap (D47.5). ROM scan: all 2805 seg-5 VTX refs have even offsets → lossless; no nested G_DL in model files.
- **Marker format:** top byte of w0 = 0xC0; type = `w0 & 7` (only 0/2/3/4 exist); texnum = `w1 & 0xfff`; min = top byte of w1. texLoadFromGdl dispatches on the top byte — PC fix: `(u8)(in->words.w0 >> 24)`. Type-0/1 markers with a valid tex are SKIPPED by the game (no expansion); the rest expand per D45's K table, which D46 verified as a true worst-case bound (no LUT textures exist; 99% of textures have maxlod=0 → TileLods emits nothing; state dedup resets per GDL via sub_GAME_7F0CC4C8).
- **seg-5 VTX offsets must be remapped** — all 2805 point into op4/op22 vertex arrays that move during re-layout (PexplosionbitZ: op4 with numVertices=0 but GDL loads 16 verts from offset 0x98 — the GDL's VTX count is authoritative, not the record field).
- PROMOTE contract: emit every promoted pointer as zero-extended u32 (low word of the 8B slot, high word 0); `PROMOTE` (`(u32)var + diff`, vma 0x5000000) then works unmodified. Fields promoted per opcode (model.c switch): all nodes Data/Parent/Next/Prev/Child; OP01 FirstGroup; OP02 ChildGroup; OP04 Vertices; OP08 Affects; OP09 left/rightChild; OP18 Controls; OP24 Vertices/CollisionVertices/PointUsage/CollisionVertices[i].LinkedTo. **NOT promoted:** GDL Primary/Secondary (stay raw 0x05xxxxxx), BaseAddr (game overwrites with fileramaddr).
**Load path (single hook point):**
- ALL model loads go through `load_object_fill_header()` (objecthandler_2.c:97): dst≠0 → `_fileNameLoadToAddr(name,0,dst,size)` (region = caller's bytes; body/head use 0x14820 per hand — PC image P must fit); else `_fileNameLoadToBank(name,0,0x100,4)` which allocates the ENTIRE remaining MEMPOOL_ME, so expansion D→P always fits and no memp bookkeeping is needed.
- `fileIndexLoadToAddr` sets `rom_remaining = bytes` (region size) BEFORE load_resource sets `poolRemaining = D`. The compaction function reads both: **the fixup must set ONLY poolRemaining (=P), never rom_remaining.** Do NOT use fileSetSize for this — it clobbers rom_remaining too. Set `resource_lookup_data_array[fileGetIndex(name)].poolRemaining = P` directly (check where the array is declared, likely ob.h).
- **Hook location:** inside load_object_fill_header, immediately after the filedata assignment and BEFORE Switches/Textures/RootNode setup + sub_GAME_7F075A90 — PROMOTE walks with PC ModelNode stride (40B), so the file must already be re-laid-out. Signature: `u32 romdataFixupModelFile(u8 *blob, const char *name, s16 NS, s16 NT)` returning P; one `#if defined(PORT)` call site.
- ALL model loads go through `load_object_fill_header()` (objecthandler_2.c:97): dst≠0 → `_fileNameLoadToAddr(name,0,dst,size)` (region = caller's bytes — see D45 for the grown per-region sizes); else `_fileNameLoadToBank(name,0,0x100,4)`. **Fresh-load semantics (D47.2-4):** `fileIndexLoadToBank` takes ALL remaining MEMPOOL_STAGE space (`S_bank`) only when poolRemaining==0; then F := alloc(S_bank), R := S_bank, load_resource decompresses D into F and sets P := D_N64. At compaction: **delta = R − P** (positive on fresh loads — the GDL mirror moves FORWARD). `fileSetSize(reallocate=1)` → `mempAddEntryOfSizeToBank` rewinds the bank cursor to the post-compaction size, giving the tail back.
- **Reload hazard + PC fix (D47.4):** fileSetSize leaves P=R=post-compaction size, so a same-stage reload would alloc(P_old) which can be < D_N64. PORT-guarded reset of poolRemaining=0 before _fileNameLoadToBank forces fresh-bank semantics every load (staging space = whole remaining bank; steady-state usage identical).
- **The fixup must set ONLY poolRemaining (= D_PC exactly — D46/D47), never rom_remaining.** Do NOT use fileSetSize for this — it clobbers rom_remaining too. Set `resource_lookup_data_array[fileGetIndex(name)].poolRemaining` directly (array defined in ob.c:14, not extern'd — declare `extern` in romdata.c; note the accessors actually index **0x80090000 + idx*20** — D47.2).
- **Hook location:** inside load_object_fill_header — (a) poolRemaining=0 reset immediately BEFORE `_fileNameLoadToBank`; (b) `u32 romdataFixupModelFile(u8 *blob, const char *name, s16 NS, s16 NT)` (returns D_PC, 0 on failure; reads entry.rom_remaining itself for the staging-space guard) called immediately after the filedata assignment and BEFORE Switches/Textures/RootNode setup + sub_GAME_7F075A90 — PROMOTE walks with PC ModelNode stride (48B), so the file must already be re-laid-out. One `#if defined(PORT)` call site.
**Compaction contract (sub_GAME_7F0762E0, objecthandler_2.c:24-83) — the tightest constraint:**
- It mirrors the GDL region [first_gdl_off, P) into the caller-region's tail headroom, then rewrites each GDL in place via texLoadFromGdl (expands G_SETTEX/G_NOOP into RDP texture commands), writing back starting at the original first-GDL offset; final fileSetSize = end of last rewritten GDL.
- It mirrors the GDL region [first_gdl_off, P_conv) into the caller-region's tail headroom (texCopyGdls to [A+R−P_conv+g, A+R)), then rewrites each GDL in place via texLoadFromGdl (expands texture markers into RDP commands), writing back starting at the original first-GDL offset; final fileSetSize = end of last rewritten GDL (= P_conv + 16·M_actual). **D46: fit and no-clobber are the SAME condition — R ≥ P_conv + 16×Σ(K_t−1) worst case — and all buffers satisfy it (see §F/D46 for the strict bound + the two cast-screen corrections).**
- Per-GDL `count` = byte distance between CONSECUTIVELY VISITED gdls → **GDLs must be packed contiguously at the tail of the PC image, in modelIterateDisplayLists visit order.**
- `modelIterateDisplayLists` (model.c:6254) **MUTATES the tree while walking**: LOD → `node->Child = LOD.Affects`; SWITCH → `node->Child = Switch.Controls`; BSP → `modelApplyReorderRelationsByArg(node,TRUE)` splices siblings (leftChild group before rightChild). Visit order per DL node: Primary, then Secondary (if non-null and ≠ previous), then DFS Child/Next/Parent.
- `build-pc/d43_gdlorder.py` implements this exact simulation to verify visit-order adjacency — **has a known bug**: the initial node-collection pass follows only Child links, but the mutable walk reaches nodes via Next → crashes `KeyError: 400`. Fix: also push `n.next` during collection. Run it after fixing; expect all 512 files to pass (consecutive visited GDLs offset-adjacent).
- `build-pc/d43_gdlorder.py` implements this exact simulation to verify visit-order adjacency — **bug fixed + verified**: node collection now pushes child+next plus LOD.Affects/SWITCH.Controls targets; all 512 files pass (total GDLs 2301).
- Primary/Secondary are NOT promoted (raw 0x05xxxxxx), so modelNodeReplaceGdl's `Primary == find` comparisons work unchanged; after compaction Primary = replacementgdl (raw value, arbitrary parity — see next item).
**Render-time GDL resolution:**
@@ -107,24 +130,93 @@ build-pc/ge007.x86_64.exe -f -C 0x14007a31a`).
- Segments 3/4 (MTX table / vertex buffer) are set by the game per-render via gSPSegment ✓. BG segments (13/14/15) belong to the background path — later task.
### Implementation checklist (in order)
1. Fix + run `build-pc/d43_gdlorder.py` → confirm visit-order adjacency for all 512 files.
2. **Mechanical game-code edits (D32 class — document in §F, each semantics-identical on N64):**
0. ~~**D46 buffer corrections (2 lines)**~~ — **DONE this session:** front.c
`bufferRemaining = 0x25000`, initmenus.c logo buffer `0x85000` (comments cite D46).
1. ~~Fix + run `build-pc/d43_gdlorder.py`~~ — DONE: all 512 files pass, op22 added.
2. ~~**Mechanical game-code edits (D32 class)**~~ — **APPLIED (uncommitted):**
bondtypes.h Vertex LinkedTo/CollisionRelatedNode → u32 under `#ifdef PORT`;
model.c PROMOTE32 macro + use at line ~5735; chr.c:3257-3259 cast + !=0 check;
objecthandler_2.c Textures offset via `sizeof(struct ModelNode*) * numSwitches`;
tex.c (4 fixes: texCopyGdls count, dispatch `(u8)(in->words.w0>>24)`,
0xba byte check `(s8)((w0>>8)&0xff)`, texLoadFromGdl count `srcsize/sizeof(Gfx)`);
gun.c size_item_buffer {0x23000,0x23000}, D_80032464 {0xF000,0xF000}, SUIT R
0x18000 (pool = size−0x18000), TRIGGER/WATCHLASER R 0x17000; front.c wallet R
0x17000 + cast bufferRemaining (now corrected to 0x25000 per item 0);
initmenus.c logo buffer (now 0x85000 per item 0). All PORT-guarded with D45/D46
comments.
- `src/bondtypes.h` Vertex: union members `struct Vertex *LinkedTo; void *CollisionRelatedNode;` → `u32 LinkedTo; u32 CollisionRelatedNode;` (sizeof(Vertex) stays 16 both platforms; game heap <4GiB so lossless on PC).
- `src/game/model.c`: add `#define PROMOTE32(var) if (var) var = (u32)((u32)var + diff)` beside PROMOTE (~line 5677); line 5735 `PROMOTE(...LinkedTo)` → `PROMOTE32(...)`.
- `src/game/chr.c:3257`: cast the now-u32 field `(ModelNode *)(uintptr_t)…`; line 3259 `!= NULL` → `!= 0`. (These are the only 3 use sites of the union.)
- `src/game/objecthandler_2.c:103`: `&((s32*)filedata)[objheader->numSwitches]` → `((u8*)filedata + sizeof(ModelNode*) * objheader->numSwitches)` (N64: 4B stride, identical).
- `src/game/tex.c`: texCopyGdls `arg2 = (arg2 >> 3)` → `arg2 / sizeof(Gfx)`; texLoadFromGdl `count = srcsize >> 3` → `srcsize / sizeof(Gfx)` and dispatch `switch (*(u8*)in)` → `switch ((u8)(in->words.w0 >> 24))` (N64-identical; those are the ONLY byte-level accesses in that function — everything else already uses words.w0/w1, and the default case copies whole slots).
3. **fast3d seg_addr** — add the 0x05xxxxxx → segmentPointers[5] case (port layer).
4. **`romdataFixupModelFile` in `port/src/romdata.c`** (PC-only): two-pass walk — collect all objects + sizes, then emit the PC image in place. Emission order: switches(NS×8B) → textures(NT×12B; bswap TextureID only) → nodes+records in DFS preorder (PC layout; rewrite every promoted pointer field to 0x05xxxxxx|new_offset) → bulk arrays (vertex/collision: bswap s16s, remap LinkedTo; PointUsage: bswap s16s, size = distance to next owned object in the source file) → **GDLs LAST, contiguous, in visit order** (16B LE slots, LSB-set on segmented commands).
5. **Hook** into load_object_fill_header (`#if defined(PORT)` one-liner + poolRemaining update as above).
6. **Build, run, verify:** no SIGSEGV in modelPromoteNodeOffsetsToPointers; scene shows actual geometry (not clear color); 30 s+ soak stable. Check PC image sizes vs the ToAddr region (body = 0x14820): extend `build-pc/d43_sizes.py` with a PC-size estimate (nodes 40B, records PC-sized, switches 8B, GDLs ×2) and confirm max fits; if not, D40-class pool growth.
3. ~~**fast3d seg_addr**~~ — **DONE this session:** port/fast3d/gfx_pc.cpp now
resolves raw 0x05xxxxxx (no LSB) via segmentPointers[5] + low24 BEFORE the LSB
path; also handles unmarked segmented addresses generally (nibble 24-27 = seg,
w1 < 0x10000000). Segment 5 is set per-render by the game's gSPSegment
(BaseAddr = live host file base).
4. **`romdataFixupModelFile` in `port/src/romdata.c`** (PC-only; bswap helpers +
extern `resource_lookup_data_array` from ob.c are already there). **Port the
validated logic of `build-pc/d43_convert.py` 1:1 — it IS the spec** (512/512
clean; D47.12). Two-pass: (a) DFS walk collecting nodes/records/GDLs + build
the region map (old_off, old_size → new_off); (b) emit the PC image into a
STAGING area at `blob + avail − D_PC` (avail = entry.rom_remaining; guard:
fail with an error if D_PC > avail), then memmove to blob. Emission order:
switches(NS×8B LE) → texconfigs(NT×12B; bswap TextureID only — no seg-5 TIDs
exist in any model file, D47.9) → nodes+records in DFS preorder (PC layout via
struct assignment; every promoted pointer field = zero-extended u64 of
0x05|new_off — full per-opcode field map in the facts above + D47.13;
Primary/Secondary GDL ptrs remapped but NOT promoted; BaseAddr emitted 0) →
vertex arrays immediately after their record (normal: bswap s16 x,y,z,index,s,t,
bytes @C-F raw; collision: bswap s16 x,y,z,index + remap LinkedTo u32@8 +
bswap CollisionRelatedIndex s16@C/reserved@E; **nv==0 with non-null ptr → size
up to next object offset**, D47.10) → PointUsage (2×numVertices s16, after
op24 CollisionVerts) → **GDLs LAST, contiguous, visit order, 16B LE slots**:
w0'=bswap32(w0); w1' = bswap32(raw) EXCEPT for seg-5 of {G_VTX=0x04,
G_SETTIMG=0xFD, G_LOADBLOCK=0xF3} → remap low24 via the region map (sorted
regions + binary search); **no LSB set** (ROM convention; D47.6/13). Emit each
GDL up to and including its ENDDL (drop trailing junk — fast3d returns at
ENDDL, D47.11). Set `resource_lookup_data_array[fileGetIndex(name)].poolRemaining`
= D_PC EXACTLY after conversion (load_resource left it at D_N64). NEVER touch
rom_remaining. Assert `D_PC + 16×Σ_markers(K_t−1) ≤ avail` (K_BOUND
{0:38,1:46,2:36,3:18,4:15}) with a clear error.
5. **Hook** into load_object_fill_header (`#if defined(PORT)`): (a) reset
`resource_lookup_data_array[fileGetIndex(name)].poolRemaining = 0` immediately
BEFORE `_fileNameLoadToBank` (D47.4 — forces fresh-bank allocation so staging
space = whole remaining bank; avoids the reload-under-alloc hazard);
(b) call romdataFixupModelFile after the filedata assignment and BEFORE
Switches/Textures/RootNode setup + sub_GAME_7F075A90 (PROMOTE walks PC
ModelNode stride so the image must be re-laid-out first).
6. **Build, run, verify:** no SIGSEGV in modelPromoteNodeOffsetsToPointers; scene
shows actual geometry (not clear color); 30 s+ soak stable. Sizing is already
proven by D46's strict bound + D47.12's max-ratio 1.31 — if anything still
overflows, the converter has a bug, not the buffers.
### Helper scripts (build-pc/, written this session)
### Helper scripts (build-pc/, written across D43 sessions)
- `d43_decode.py` — decompress + decode one model's node tree + GDL command stream.
- `d43_sizes.py` — decompress all 512, report sizes (total 0x323100; largest body ~34KB).
- `d43_walk.py` — walks all trees using exe NS/NT (parses all 512 `assets/obseg/**/modelFileHeader.inc.c`); opcode histogram. Note: header names lack the C/G/P prefix and trailing Z (e.g. file `CcamguardZ` → header `camguard`).
- `d43_sizes.py` / `d43_sizes2.py` — decompress all 512, report N64/PC sizes +
per-file worst-case P_final (B_pc + 2E + 16×Σ(K_t−1)).
- `d43_walk.py` — walks all trees using exe NS/NT; opcode histogram. Note: header
names lack the C/G/P prefix and trailing Z (file `CcamguardZ` → header `camguard`).
- `d43_invariants.py` — full ownership tiling check (0 failures / 512).
- `d43_gdlorder.py` — exact visit-order simulation (**collection bug, see above**).
- `d43_gdlorder.py` — exact visit-order simulation; **bug fixed, all 512 pass**.
- `d43_gdldump.py` / `d43_gdlhist.py` / `d43_gdlseq.py` / `d43_seg5.py` — GDL
command dumps/histograms/sequences; seg-5 VTX reference analysis (all 2805 point
into op4/op22 vertex arrays → must remap).
- `d43_cover.py` — verifies every seg-5 VTX + SETTIMG target falls inside a modeled
object (2804/2805 covered; the one exception is PexplosionbitZ's orphan array at
offset 0x98 — handled by the converter's implicit-object rule). SETTIMG targets in
logo files are vestigial (in gaps, never dereferenced).
- `d43_lutscan.py` — scans all model texture tables → image headers (via
imagelist.u.csv, order = assets/images.def): proves NO LUT textures (formats 9-12)
and maxlod distribution {0:1063, 6:7, 7:1} across all 512 files (D46).
- `d43_convert.py` — **THE reference converter + validator (D47):** implements
the full N64→PC conversion spec (DFS walk with LOD/SWITCH rewiring, region map,
per-opcode record/vertex/GDL conversion, remap checks) and runs it on all 512
ROM files — ALL CLEAN. This IS the spec to port to C in romdata.c.
- `d43_chainbound.py` — computes the D46 strict bound B_pc + 2×(R_share_N64 − B_n64)
for every buffer/chain and compares against the PC region sizes.
- Gotcha: MODELFILEHEADER's name is field a[0] of the macro args (NOT the filename —
all header files are literally named `modelFileHeader.inc.c` in per-model dirs).
### Follow-on
Expect later faults in other asset types (BG/stan/propobj tables) — same D32/D37 procedure; PD's `preprocess/` module already solves most of them (`filebg.c`, `filetiles.c`, `filepads.c`, `filesetup.c`, …) — see the PD reference section below.
@@ -195,6 +287,9 @@ Log each as D3x in §F and note it in AGENTS.md phase status.
offline with `addr2line -e build-pc/ge007.x86_64.exe -f -C <0x140000000+rel>`. Image base
is `0x140000000` (re-verify with `info address` after a rebuild). Debug scripts live in
`build-pc/` (`d37_tree.py`, `d39_*.gdb`, `d41_probe.gdb`, …).
- **Capstone gotcha (D47.1):** the ROM is BIG-ENDIAN MIPS — always disassemble with
`CS_MODE_MIPS32 | CS_MODE_BIG_ENDIAN`. Default (LE) mode produces garbage that looks
plausible; an earlier session's disassembly was all LE and had to be redone.
- Crash log: `ge007.crash.log` (repo root) now holds Phase 1 + a `BACKTRACE:`
section (**D44** fixed). First stop for any fault; symbolicate with
`addr2line -e build-pc/ge007.x86_64.exe -f -C <abs-addr>`. Frames past the
+189
View File
@@ -1305,6 +1305,195 @@ one such frame observed at #02). addr2line takes the full absolute address
directly (`addr2line -e build-pc/ge007.x86_64.exe -f -C 0x14007a31a`); image
base is still 0x140000000 (re-verified post-rebuild via nm).
**D45 (OPEN — sizing for D43)** Model-file buffers must grow for PC: the
GDL region doubles (8 B Gfx slots → 16 B) and texture-marker expansion in
texLoadFromGdl() emits full RDP setup sequences whose size is data-driven.
Per-file worst-case final size P_final = B_pc + 2×(D_n64−g1) + 16×Σ_markers
(K_t−1), where K_t is the exact worst-case command count per marker type t
(tex.c helpers, maxlod≤8, all texTry* state guards emitting, valid=FALSE —
the realistic branch since D_800483C4 is a gunfire texture index):
type0/LOD 37, type1/DETAIL 46, type2/MIPMAP 36, type3/TILE 18,
type4/TILE_PRESWAP 15 (preamble = PipeSync + gSPTexture ≤ 2; water check ≤ 1).
Marker type = w0&7 of the 0xC0-top-byte slot. Verified over all 512 model
files (build-pc/d43_sizes.py). Texture *pixel* data is unaffected by Gfx
width, so texpool sizes can stay at their N64 values — only the model-file
regions grow. Required buffer edits (all D40-class ABI-forced size constants,
semantics unchanged): gun.c:106 size_item_buffer 0x14820→**0x23000**
(bondview body+head+held-prop chain worst 0x1DB9A; suit pool 0xA0B0 + R
0x18000 = 0x220B0); gun.c:109 D_80032464 0x7530→**0xF000** (GautoshotZ
0xE788); gun.c ITEM_SUIT_LF_HAND R 0xBD70→**0x18000** (Csuit_lf_handZ
0x16F9C) with pool expr size−0x18000; gun.c ITEM_TRIGGER/ITEM_WATCHLASER R
0xAFD0→**0x17000** (GtriggerZ 0x16030); front.c load_walletbond R 0xA000→
**0x17000** (PwalletbondZ 0x1664C; stays below the +0x28000 DL region);
front.c cast screen bufferRemaining 0x18160→**0x1C000** (cast chain worst
0x19CA0 — zbuf at 0x19000+region must not be clobbered); initmenus.c:34 logo
buffer 0x78000→**0x7C000** (texpool 0x19000 + region 0x1C000 + zbuf
440×330×2=0x46B80). Unchanged: title/gunbarrel chain (0x16DF0 ≤ 0x23A00),
front.c logos (≤0x9BCC ≤ 0x3C000), dst=0 fileLoad path (allocates ALL
remaining STAGE space and gives the tail back via
mempAddEntryOfSizeToBank — stage props/NPC bodies are bounded only by live
STAGE headroom, ~2.3 MB). The converter will also assert P_final ≤ R at load
time with a clear error instead of overflowing.
**D46 CORRECTION:** the cast-screen values above are superseded —
front.c bufferRemaining is **0x25000** (not 0x1C000) and the initmenus.c
logo buffer is **0x85000** (not 0x7C000); see D46.
**D46 (RESOLVED — overlap safety + final buffer sizing for D43)**
The GDL expansion in sub_GAME_7F0762E0 is in-place: output starts at the
first-GDL offset and grows rightward; input is read from the mirror copy
texCopyGdls() made at the region tail ([A+R−P_conv+g, A+R)). The initial
gap is R−P_conv and the cumulative output-minus-input excess is ≤
16·Σ(K_t−1) over markers processed so far (monotone; 0xba "texture
already set" skips only reduce it). Since Gfx is 16 B for BOTH input and
output on PC, the no-clobber condition **R ≥ P_conv + 16·M_actual** is
identical to the fit condition — one constraint, not two. (The N64 game
satisfies the same identity with 8-B slots.) The converter therefore sets
`poolRemaining = P_conv = B_pc + 2×(D−g1)` EXACTLY (the pre-expansion
image; markers are expanded at runtime, consuming [P_conv, R)).
**Strict bound from N64 ground truth.** The N64 game works, so per file
8·M_a ≤ R_share_N64 − B_n64 − E. PC slots are 2× wide with the same M_a
(tex.c logic + texture data identical), hence
**P_final_actual(PC) ≤ B_pc + 2×(R_share_N64 − B_n64)** (E cancels).
N64 R_share values (game code): suit 0xBD70, trigger/watchlaser 0xAFD0,
wallet 0xA000, weapons 0x7530, bondview chain 0x14820
(size_item_buffer), cast chain 0x18160 (front.c bufferRemaining).
For chains ΣR_share = bufferRemaining EXACTLY: each subsequent load's R
is the previous file's post-load poolRemaining via
`get_pc_buffer_remaining_value()` (front.c:7809-7825), so the shares
tile the N64 budget with no margin assumption beyond "N64 works".
**tex.c expansion state machine** (for reference / any future
simulation): `sub_GAME_7F0CC4C8()` resets g_TexTileStates[8],
g_TexTileSizes[8] and g_TexLutMode at the START OF EVERY
texLoadFromGdl call → texTry*/texSetLutMode dedup only works WITHIN one
GDL. Per-marker slot counts (lutmodeindex=0): type0/LOD ≤ 2 + 6
(LoadToTmemAddr: SETTIMG+SETTILE≤1+LoadSync+LoadBlock+PipeSync) + 4
(TileFromDef: PrimColor+LUT≤1+SETTILE≤1+SETTILESIZE≤1) + 3×min(maxlod,7)
(TileLods basetile=1) + 3 (CycleType/TxLOD/Detail); type2/MIPMAP ≤ 2 + 6
+ 3×(maxlod + [maxlod==1]); type1 ≤ 2 + 6 (Zero) + 1 (TileSync) + 6 + 4
+ 3×min(maxlod,7) + 3; type3 ≤ 2+6+4+4; type4 ≤ 2+6+4. With LUT formats
the LoadToTmem* helpers double to ≤12 — see next item.
**Texture scan (build-pc/d43_lutscan.py), all 512 files:** 1071 valid
texture-table refs (TID < MAX_TEXTURES=3001; the rest are skipped by
texLoadFromModelFileHeader, and markers whose texnum was never loaded get
tex==NULL → PipeSync only). Image headers parsed via imagelist.u.csv
(order = assets/images.def order): **NO LUT textures** (formats 9-12)
exist in any model file — only format 0 (×1069) and 8 (×2) — so
lutmodeindex=0 universally and the ≤6-slot LoadToTmem bound holds.
maxlod distribution {0: 1063, 6: 7, 7: 1} → texWriteTileLods emits
NOTHING for 99% of markers. The D45 K_BOUND table {0:38, 1:46, 2:36,
3:18, 4:15} is verified a true worst-case bound under these conditions.
**Strict-bound results (build-pc/d43_chainbound.py):** suit_lf_hand
0x1002C ≤ 0x18000 ✓; trigger/watchlaser 0xED20 ≤ 0x17000 ✓;
walletbond 0x105BC ≤ 0x17000 ✓; worst weapon GmapZ 0xE91C ≤ 0xF000 ✓;
bondview chain (worst body spicebond + headbrosnan + autoshot) 0x1CE5C
≤ 0x23000 ✓; **cast chain: rifle 0x240DC / pistol 0x23F24 > 0x1C000 —
SHORT by ~0x8000** (worst: body spicebond B_pc=0x8A14/B_n64=0x7D60 +
head headbrosnan 0x1D90/0x1D50 + rifle autoshot 0x4828/0x3E28 / pistol
wppksil 0x45B0/0x3DC8). The D45 worst-K estimate (0x19CA0) was below the
strict bound because it assumed Rare's N64 buffer had zero margin.
**Resolution (two one-line constant changes, both already PORT-guarded):**
front.c init_menu18_displaycast bufferRemaining 0x1C000 → **0x25000**
(covers 0x240DC); initmenus.c logo buffer 0x7C000 → **0x85000**
(texpool 0x19000 + region 0x25000 + zbuf @ALIGN64(0x3E000) size
440×330×2=0x46B80 → 0x84B80, rounded). MEMPOOL_STAGE headroom is ample
(total stage usage at menu init ≈ 0xD0040 vs ~2.3 MB pool). The converter
should additionally assert `P_conv + 16×Σ_markers(K_t−1) ≤ R` at load
with a clear error (K_BOUND table; provably ≥ actual expansion).
**D47 (RESOLVED — D43 converter contract finalized; capstone endianness fix)**
Session that closed every open question on the model-file converter.
1. **Capstone endianness bug (environment gotcha).** All earlier-session
disassembly of the ROM used capstone's default LITTLE-endian mode against a
big-endian MIPS image → garbage register flow. MUST use
`CS_MODE_MIPS32 | CS_MODE_BIG_ENDIAN`. Compaction + accessors were re-disassembled
cleanly; where BE disassembly is still ambiguous, the byte-matched C in `src/`
is ground truth.
2. **Compaction state values (BE disasm of 0x7F0762E0 + byte-matched C).**
P = entry.poolRemaining, R = entry.rom_remaining, **delta = R − P** (not
P−R); `texCopyGdls(F+G, F+R−P+G, P−G bytes)`; per-GDL count = off_{i+1}−off_i
bytes (or P−off_last); final fileSetSize = ((rep&0xFFFFFF)+0xf)&~0xf.
Both accessors (0x7F0BD11C / 0x7F0BD100) compute `0x80090000 + idx*20` →
**resource_lookup_data_array actually lives at 0x80090000**; the ob.c comment
(0x800888B0) is stale.
3. **memp allocator (src/memp.c).** Bump allocator; `mempAddEntryOfSizeToBank`
rewinds `pool->pos` for the most recent allocation →
`fileSetSize(reallocate=1)` returns the post-compaction tail to the bank.
`load_resource` decompresses INTO ptrdata (=F), reading the compressed source
from the block's TAIL. Fresh dst==0 load: P := S_bank (all remaining stage
bank) → F := alloc(S_bank) → R := S_bank → load_resource sets P := D_N64.
4. **Reload hazard + PC decision.** `fileIndexLoadToBank` takes S_bank only
when poolRemaining==0; fileSetSize leaves P=R=post-compaction size, so a
reload within one stage would alloc(P_old) which can be < D_N64 (source
pointer before the block; decompress past it — latent on N64 too). PC fix:
PORT-guarded reset of `poolRemaining = 0` immediately before
`_fileNameLoadToBank` in load_object_fill_header → every load gets fresh-
bank semantics: staging space = whole remaining bank, steady-state bank
usage identical (fileSetSize rewinds to the same place).
5. **G_VTX w0 encoding (ROM-data proven).** GBI1/gDma1p style:
w0 = 04<<24 | dst<<16 | (16·n), dst = ((n−1)<<4)|v0. All 2826 model-file
instances have v0=0, n≤16 (batches of ≤16 verts — matches G_TRI4's 4-bit
indices). fast3d's `gfx_sp_vertex(C0(0,16)/sizeof(Vtx), C0(16,4), …)` is
correct AS-IS; the converter bswaps w0 only (no field remap). The
F3DEX_GBI_2 `gsSPVertex` block in GE's gbi.h is a red herring — the asset
toolchain emits GBI1 style.
6. **G_TRI4 = standard 4-bit indices.** The "out-of-range seg-5 w1" values
found by scanning are G_TRI4 index data, not addresses (w1 bits 20-31 hold
the last two 4-bit indices). **Opcode-aware remap rule: only {G_VTX=0x04,
G_SETTIMG=0xFD, G_LOADBLOCK=0xF3} carry addresses in w1.** Never remap
TRI4/TRI1/TEXTURE/SETOTHERMODE/CLEARGEO/SETGEO/syncs.
7. **Render-time segment bindings (model.c).** seg5 (COL1) = BaseAddr = F on
every model path; seg4 (VTX) = the record's Vertices base (or a runtime
buffer in dorottex). Hence G_VTX seg4 w1 = displacement from that array →
NO remap needed (array order preserved); G_VTX seg5 w1 = absolute file
offset — 2804/2805 verified to land inside a vertex array at vo+d with
d%16==0 → remap via the unified region map.
8. **Vertex format (all 512 files).** Normal: bswap s16 x,y,z,index,s,t;
bytes @C-F raw (rgba/normal are single bytes). Collision (34,580 verts):
bswap s16 x,y,z,index; LinkedTo u32@8 is ALWAYS 0 or 0x05xxxxxx (node vma);
CollisionRelatedIndex s16@C (range −1..113) + reserved s16@E bswapped.
9. **No embedded texture blobs in any model file** — texconfig TextureID
seg-5 count = 0 across all 512 (the earlier "3 title files" note was the
vestigial logo SETTIMG refs already covered by d43_cover.py). No blob
handling needed in the model converter.
10. **Zero-count vertex arrays.** PexplosionbitZ is the ONLY file with
nv=0 and non-null Vertices (0x98); its GDL uploads 16 verts from there via
absolute seg-5 ref. Rule: emit the array sized up to the next object offset
(→ [0x98,0x198) = exactly 256B here).
11. **GDL tight packing is safe.** fast3d `case G_ENDDL: return` — trailing
junk after ENDDL is never executed at render time. Spans: 2064 tight,
238 with trailing bytes (1904B total). Converter emits up to and including
ENDDL; each PC span = exactly 16·slots → compaction counts are exact.
12. **Reference validator: `build-pc/d43_convert.py`** implements the full
conversion spec (DFS walk with LOD/SWITCH rewiring, region map, per-opcode
record conversion, remap checks) and runs it on all 512 files:
**ALL CLEAN** — every pointer remap resolves, layout invariants hold,
max D_PC = 0xB7E0, max D_PC/D_N64 ratio = 1.31 → staging headroom is
trivially satisfied vs the ~2.3 MB stage bank. This script IS the spec for
the C implementation.
13. **Final converter contract.** Emit `[switches NS×8B LE][texconfigs NT×12B
(bswap TextureID; 0x05xxxxxx would remap — never occurs)]` → nodes+records in
DFS preorder (PC layout via struct assignment; every promoted pointer field
emitted as zero-extended u64 of 0x05|new_off; Primary/Secondary GDL ptrs
remapped but NOT "promoted"; BaseAddr emitted 0) → vertex arrays immediately
after their record (PointUsage = 2×numVertices s16 after op24 CollisionVerts)
→ **GDLs LAST, contiguous, visit order, 16B LE slots**: w0'=bswap32(w0);
w1' = bswap32(raw) EXCEPT for seg-5 of {0x04,0xFD,0xF3} → remap low24 via
the region map; **no LSB set** (ROM convention; fast3d's extended seg_addr
case handles unmarked segmented addresses). Stage at F+R−D_PC, memmove to
F. Set `poolRemaining = D_PC` exactly (never touch rom_remaining).
14. **N64 record field offsets** come from the bondtypes.h comments (which
preserve N64 offsets); all 14 opcodes present in ROM files — 1,2,4,8,9,10,
12,13,15,18,21,22,23,24 — verified against RSZ sizes. PC layout: assign
through the real structs (compiler packs); don't hand-compute.
15. **PointUsage** = 2×numVertices s16 entries, indexed by MAIN-vertex index,
chain terminated by −1 (chr.c:3309-3331).
### G. Phase 2 status (current)
Done: PD fast3d integrated (`port/fast3d/`, replaces the deleted gfxstub.c);
+20
View File
@@ -2280,6 +2280,14 @@ static void gfx_dp_set_other_mode(uint32_t h, uint32_t l) {
}
static inline void *seg_addr(uintptr_t w1) {
// GE model files reference GDLs (gSPDisplayList) and vertex arrays by raw
// VMA 0x05xxxxxx WITHOUT the LSB set; segment 5 is set per-render to the
// live host file base by the game's gSPSegment. Resolve it explicitly
// before the segmented-address path below (a converter-remapped seg-5 w1
// already carries the LSB and takes that path).
if ((w1 & 0xFF000000) == 0x05000000 && segmentPointers[5]) {
return (void *)(segmentPointers[5] + (w1 & 0x00FFFFFF));
}
// all segmented addresses have the least significant bit set
if (w1 & 1) {
// seg 0 is reserved and doesn't count here
@@ -2289,6 +2297,18 @@ static inline void *seg_addr(uintptr_t w1) {
return (void *)(segmentPointers[seg] + addr);
}
}
// GE's ROM GDLs also carry UNMARKED segmented refs with the LSB clear:
// G_MTX w1=0x03xxxxxx (seg 3 = render_pos), G_VTX w1=0x04/0x05xxxxxx
// (seg 4 = runtime vtx buffer, seg 5 = file base), G_SETTIMG w1=0x05xxxxxx
// (embedded image blob). Convention: segment in bits 24-27, 24-bit offset.
// Runtime pointers never land here: DRAM lives at >= 0x70000000 and
// K0-physical values are < 0x800000 (nibble 24 == 0).
if (w1 < 0x10000000 && ((w1 >> 24) & 0xf)) {
const uintptr_t seg = (w1 >> 24) & 0xf;
if (segmentPointers[seg]) {
return (void *)(segmentPointers[seg] + (w1 & 0x00FFFFFF));
}
}
// GE passes OS_K0_TO_PHYSICAL(ptr) == ptr - 0x80000000 for RAM that lives
// in the reserved N64-DRAM region (port/src/dram.c); map it back. The
// region is 8 MB, so any offset below 0x800000 came from there.
+12
View File
@@ -687,10 +687,22 @@ typedef union
s16 t; /* 0xa */
};
#ifdef PORT
/* PC port (D43/D45): stored as a raw vma (0x05xxxxxx) instead of a real
* pointer so sizeof(Vertex) stays 16 bytes — the gSPVertex stride is
* hardcoded to 16B and propobj.c copies vertices with sizeof(Vertex).
* modelPromoteNodeOffsetsToPointers rebases it via PROMOTE32; chr.c casts
* back to ModelNode* at the use site. N64 layout unchanged (4B either way). */
u32 LinkedTo;
/* Collision vertices only: points to related model node (raw vma). */
u32 CollisionRelatedNode; /* 0x8 */
#else
struct Vertex *LinkedTo;
/* Collision vertices only: points to related model node. */
void *CollisionRelatedNode; /* 0x8 */
#endif
};
union
+7
View File
@@ -3254,9 +3254,16 @@ after_opcode:
paintval += 50;
}
#ifdef PORT
/* PC port (D43/D45): CollisionRelatedNode is a raw vma (u32), not a pointer. */
relatednode = (ModelNode *)(uintptr_t)((ModelRoData_DisplayList_CollisionRecord *) node)->CollisionVertices[bestindex].CollisionRelatedNode;
if (((ModelRoData_DisplayList_CollisionRecord *) node)->CollisionVertices[bestindex].CollisionRelatedNode != 0)
#else
relatednode = (ModelNode *) ((ModelRoData_DisplayList_CollisionRecord *) node)->CollisionVertices[bestindex].CollisionRelatedNode;
if (((ModelRoData_DisplayList_CollisionRecord *) node)->CollisionVertices[bestindex].CollisionRelatedNode != NULL)
#endif
{
relatedrodata = &relatednode->Data->DisplayListCollisions;
relatedrwdata = (ModelRwData_DisplayList_CollisionRecord *) modelGetNodeRwData(model, relatednode);
+18
View File
@@ -2067,12 +2067,23 @@ void load_walletbond(void)
if (walletinst[0] == NULL)
{
#if defined(PORT)
/* PC port (D45): PwalletbondZ expands to 0x1664C; the region ends well
* below the +0x28000 DL area of the same buffer. */
load_object_fill_header(
PitemZ_entries[PROP_WALLETBOND].header,
(s8*)PitemZ_entries[PROP_WALLETBOND].filename,
(u8*)ptr_logo_and_walletbond_DL,
0x17000,
0);
#else
load_object_fill_header(
PitemZ_entries[PROP_WALLETBOND].header,
(s8*)PitemZ_entries[PROP_WALLETBOND].filename,
(u8*)ptr_logo_and_walletbond_DL,
0xA000,
0);
#endif
modelCalculateRwDataLen(PitemZ_entries[PROP_WALLETBOND].header);
@@ -7777,7 +7788,14 @@ void init_menu18_displaycast(void)
}
allocSize = 0x19000;
#if defined(PORT)
/* PC port (D45/D46): cast-screen chain strict bound is 0x240DC (rifle);
* D46 overlap-safety analysis: R >= P_conv + 16*M_actual, so the zbuf
* below must not be clobbered by marker expansion into [P_conv, R). */
bufferRemaining = 0x25000;
#else
bufferRemaining = 0x18160;
#endif
if (1);
+25
View File
@@ -103,10 +103,23 @@ Lights1 g_WeaponEnvmapLight = gdSPDefLights1(
u32 D_80032458 = 0;
//D:8003245C
#if defined(PORT)
/* PC port (D45): Gfx slots are 16B on x86-64, so model-file GDL regions double
* and texture-marker expansion adds data-driven RDP commands. bondview's
* body+head+held-prop chain worst case is 0x1DB9A; the suit path needs pool
* 0xA0B0 + region 0x18000 = 0x220B0. See docs/PCPortResearch.md D45. */
u32 size_item_buffer[] = {0x23000, 0x23000};
#else
u32 size_item_buffer[] = {0x14820, 0x14820};
#endif
//D:80032464
#if defined(PORT)
/* PC port (D45): largest weapon model file is GautoshotZ at 0xE788. */
u32 D_80032464[] ={0xF000, 0xF000};
#else
u32 D_80032464[] ={0x7530, 0x7530};
#endif
@@ -912,13 +925,25 @@ void used_to_load_1st_person_model_on_demand(GUNHAND hand)
if (item == ITEM_SUIT_LF_HAND)
{
#if defined(PORT)
/* PC port (D45): Csuit_lf_handZ expands to 0x16F9C. */
texInitPool(&g_CurrentPlayer->item_related[hand], buffer_weapon + 0x18000, size_buffer_weapon - 0x18000);
load_object_fill_header(&g_CurrentPlayer->copy_of_body_obj_header[hand], (u8 *)ptr_item_text, buffer_weapon, 0x18000, &g_CurrentPlayer->item_related[hand]);
#else
texInitPool(&g_CurrentPlayer->item_related[hand], buffer_weapon + 0xBD70, size_buffer_weapon + 0xFFFF4290);
load_object_fill_header(&g_CurrentPlayer->copy_of_body_obj_header[hand], (u8 *)ptr_item_text, buffer_weapon, 0xBD70, &g_CurrentPlayer->item_related[hand]);
#endif
}
else if ((item == ITEM_TRIGGER) || (item == ITEM_WATCHLASER))
{
#if defined(PORT)
/* PC port (D45): GtriggerZ/GwatchlaserZ expand to 0x16030. */
texInitPool(&g_CurrentPlayer->item_related[hand], buffer_weapon + 0x17000, size_buffer_weapon - 0x17000);
load_object_fill_header(&g_CurrentPlayer->copy_of_body_obj_header[hand], (u8 *)ptr_item_text, buffer_weapon, 0x17000, &g_CurrentPlayer->item_related[hand]);
#else
texInitPool(&g_CurrentPlayer->item_related[hand], buffer_weapon + 0xAFD0, size_buffer_weapon + 0xFFFF5030);
load_object_fill_header(&g_CurrentPlayer->copy_of_body_obj_header[hand], (u8 *)ptr_item_text, buffer_weapon, 0xAFD0, &g_CurrentPlayer->item_related[hand]);
#endif
}
else
{
+7
View File
@@ -31,7 +31,14 @@ void init_menus_or_reset(void)
menu_update = MENU_FILE_SELECT;
}
#if defined(PORT)
/* PC port (D45/D46): texpool 0x19000 + model region 0x25000 (D46) +
* zbuf @ALIGN64(0x3E000) size 440x330x2=0x46B80 -> 0x84B80, rounded to
* 0x85000; logo/gunbarrel/wallet usage is unchanged. */
ptr_logo_and_walletbond_DL = (u8 *)mempAllocBytesInBank(0x85000, MEMPOOL_STAGE); // allocation in bytes so cast to u8 from gfx
#else
ptr_logo_and_walletbond_DL = (u8 *)mempAllocBytesInBank(0x78000, MEMPOOL_STAGE); // allocation in bytes so cast to u8 from gfx
#endif
#if defined(VERSION_EU)
ptr_menu_videobuffer = mempAllocBytesInBank(0x55040, MEMPOOL_STAGE);
+12
View File
@@ -5678,6 +5678,14 @@ void modelResetAnimationsScratchBuffer(void)
if (var) \
var = (void *)((u32)var + diff)
#ifdef PORT
/* PC port (D43/D45): Vertex.LinkedTo is a raw vma (u32), not a pointer —
* rebases the same way, keeps sizeof(Vertex) at 16 bytes. */
#define PROMOTE32(var) \
if (var) \
var = (u32)((u32)var + diff)
#endif
void modelPromoteNodeOffsetsToPointers(ModelNode *node, u32 vma, u32 fileramaddr)
{
s32 diff = fileramaddr - vma;
@@ -5732,7 +5740,11 @@ void modelPromoteNodeOffsetsToPointers(ModelNode *node, u32 vma, u32 fileramaddr
PROMOTE(rodata->PointUsage);
for (i = 0; i < rodata->numCollisionVertices; i++)
{
#ifdef PORT
PROMOTE32(rodata->CollisionVertices[i].LinkedTo);
#else
PROMOTE(rodata->CollisionVertices[i].LinkedTo);
#endif
}
rodata->BaseAddr = (void *)fileramaddr;
break;
+5
View File
@@ -101,8 +101,13 @@ void load_object_fill_header(struct ModelFileHeader *objheader, u8 *name, u8* ds
objheader->Switches = (struct ModelNode **)filedata;
#ifdef PORT
/* PC port (D43/D45): the switches array is NS x 8B pointer slots, not NS x 4B words. */
objheader->Textures = (struct ModelFileTextures *)((u8 *)filedata + sizeof(struct ModelNode *) * objheader->numSwitches);
#else
// hmmmmmmmmmmmm
objheader->Textures = (struct ModelFileTextures *)&((s32*)filedata)[objheader->numSwitches];
#endif
objheader->RootNode = (struct ModelNode *)&objheader->Textures[objheader->numtextures];
+21
View File
@@ -797,7 +797,12 @@ s32 texLoadFromGdl(Gfx *src, s32 srcsize, Gfx *dst, void *texpool)
Gfx *in = src;
Gfx *out = dst;
#ifdef PORT
/* PC port (D43): Gfx slots are 16 bytes on x86-64, not 8. */
count = srcsize / sizeof(Gfx);
#else
count = srcsize >> 3;
#endif
sub_GAME_7F0CC4C8();
@@ -808,7 +813,13 @@ s32 texLoadFromGdl(Gfx *src, s32 srcsize, Gfx *dst, void *texpool)
while (count > 0)
{
#ifdef PORT
/* PC port (D43): the opcode is the TOP byte of w0 (N64 read the first
* memory byte, which big-endian puts there). */
switch ((u8)(in->words.w0 >> 24))
#else
switch (*(u8 *)in)
#endif
{
case G_NOOP:
if (!syncEmitted)
@@ -1008,7 +1019,12 @@ s32 texLoadFromGdl(Gfx *src, s32 srcsize, Gfx *dst, void *texpool)
case 0xba:
if (valid)
{
#ifdef PORT
/* PC port (D43): N64 byte offset 2 of the slot = bits 8..15 of w0. */
if (((s8)((in->words.w0 >> 8) & 0xff) == 17) || ((s8)((in->words.w0 >> 8) & 0xff) == 20) || ((s8)((in->words.w0 >> 8) & 0xff) == 16))
#else
if ((((*(((s8 *)in) + 2)) == 17) || ((*(((s8 *)in) + 2)) == 20)) || ((*(((s8 *)in) + 2)) == 16))
#endif
{
in++;
}
@@ -1045,7 +1061,12 @@ s32 texLoadFromGdl(Gfx *src, s32 srcsize, Gfx *dst, void *texpool)
*/
void texCopyGdls(Gfx *arg0, Gfx *arg1, s32 arg2)
{
#ifdef PORT
/* PC port (D43): Gfx slots are 16 bytes on x86-64, not 8. */
arg2 = (arg2 / sizeof(Gfx));
#else
arg2 = (arg2 >> 3);
#endif
arg0 = arg0 + (arg2 - 1);
arg1 = arg1 + (arg2 - 1);