From 181f6db07dc26e3493d7f7009884d9a160ef0e9f Mon Sep 17 00:00:00 2001 From: James Dansereau <106631954+jkdansereau@users.noreply.github.com> Date: Sat, 22 Aug 2026 18:24:31 -0400 Subject: [PATCH] =?UTF-8?q?PC=20port:=20phase=202=20=E2=80=94=20D43=20mech?= =?UTF-8?q?anical=20ABI=20edits=20+=20D46=20buffer=20sizing=20+=20fast3d?= =?UTF-8?q?=20seg-5=20case;=20converter=20spec=20validated=20(D47)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- AGENTS.md | 2 +- docs/HANDOFF.md | 169 +++++++++++++++++++++++++-------- docs/PCPortResearch.md | 189 +++++++++++++++++++++++++++++++++++++ port/fast3d/gfx_pc.cpp | 20 ++++ src/bondtypes.h | 12 +++ src/game/chr.c | 7 ++ src/game/front.c | 18 ++++ src/game/gun.c | 25 +++++ src/game/initmenus.c | 7 ++ src/game/model.c | 12 +++ src/game/objecthandler_2.c | 7 +- src/game/tex.c | 21 +++++ 12 files changed, 450 insertions(+), 39 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 609b16ac..ef2e2744 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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_.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. diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index 3f7dbe5f..d60b3439 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -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: — `. ## 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: — `. - 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 `. Frames past the diff --git a/docs/PCPortResearch.md b/docs/PCPortResearch.md index 414eb375..cd4fcaae 100644 --- a/docs/PCPortResearch.md +++ b/docs/PCPortResearch.md @@ -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); diff --git a/port/fast3d/gfx_pc.cpp b/port/fast3d/gfx_pc.cpp index 8580f7f0..abef6b58 100644 --- a/port/fast3d/gfx_pc.cpp +++ b/port/fast3d/gfx_pc.cpp @@ -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. diff --git a/src/bondtypes.h b/src/bondtypes.h index b6300422..b1714ba4 100644 --- a/src/bondtypes.h +++ b/src/bondtypes.h @@ -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 diff --git a/src/game/chr.c b/src/game/chr.c index 50544ece..ed981fc9 100644 --- a/src/game/chr.c +++ b/src/game/chr.c @@ -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); diff --git a/src/game/front.c b/src/game/front.c index c7ef19d9..d51971d9 100644 --- a/src/game/front.c +++ b/src/game/front.c @@ -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); diff --git a/src/game/gun.c b/src/game/gun.c index 79e69c8a..ff7086d9 100644 --- a/src/game/gun.c +++ b/src/game/gun.c @@ -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 { diff --git a/src/game/initmenus.c b/src/game/initmenus.c index 4e419f3d..e2f90a71 100644 --- a/src/game/initmenus.c +++ b/src/game/initmenus.c @@ -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); diff --git a/src/game/model.c b/src/game/model.c index 6cff5829..ba4dac0d 100644 --- a/src/game/model.c +++ b/src/game/model.c @@ -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; diff --git a/src/game/objecthandler_2.c b/src/game/objecthandler_2.c index e033dfe7..e567e132 100644 --- a/src/game/objecthandler_2.c +++ b/src/game/objecthandler_2.c @@ -100,9 +100,14 @@ 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]; diff --git a/src/game/tex.c b/src/game/tex.c index 12c3ec11..ea5d16ae 100644 --- a/src/game/tex.c +++ b/src/game/tex.c @@ -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);