From 8f631462f30e894c1b8358af6ab124b23126fcbf Mon Sep 17 00:00:00 2001 From: James Dansereau <106631954+jkdansereau@users.noreply.github.com> Date: Mon, 24 Aug 2026 03:27:29 -0400 Subject: [PATCH] =?UTF-8?q?PC=20port:=20phase=202=20=E2=80=94=20D59-D68=20?= =?UTF-8?q?milestone:=20full=20intro=20renders;=20blocker=20is=20D69=20(BG?= =?UTF-8?q?-file=20BE=20headers)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit D59/D64: the 'external GL DLL' crash was an unbounded blood-RLE write (sentinel die_blood_image_end lost its N64 section adjacency on PC) clobbering the gun-barrel sub-DL at ptr_logo_and_walletbond_DL+0x200. D60: osPiStartDma DMA-target validation (VirtualQuery-based) in libultra.c. D61/D63: TEMP env-gated ROM-read / VTX-pool / menu probes (strip list in HANDOFF Task 3). D62: OSMesgQueue/OSScMsg PC layout verification. D65/D65b: enum HEADS/BODIES signed sentinels under PORT (PC GCC picks unsigned underlying types; N64 semantics were signed). D66: romCopyAligned + ramrom replay 64-bit pointer width (s32 truncation of .bss targets > 4 GiB). D67: struct image_entry N64 layout reconstruction (dataoffset:24 first, all-u32 bitfields, sizeof==8). D68: Globalimagetable BE->LE fixup (port/src/gimgfixup.c): bswap the IMAGESEG-marked Gfx w1 words in the 17 ROM-copied DLs + sImageTableEntry. index in the 32 tables after texReset's romCopy; marker scan moved to bytes 6..7 under PORT; compiled globalDL shadows synced post-texLoad for explosion.c's g_ExplosionDisplayLists. Result: game boots, plays intro music, and renders the entire ~3.5-minute intro (logo -> gun barrel with Brosnan -> cast screen) at ~59 fps. New blocker D69: load_bg_file faults on first stage load - BG-file headers are N64 big-endian segment-0x0F offsets read LE on PC. Full analysis + strategy in PCPortResearch.md §F/D69; HANDOFF rewritten for the D69 session. --- .gitignore | 5 + AGENTS.md | 2 +- docs/HANDOFF.md | 450 ++++++++---------------- docs/PCPortResearch.md | 581 +++++++++++++++++++++++++++---- port/fast3d/gfx_opengl.h | 10 + port/fast3d/gfx_pc.cpp | 103 +++++- port/include/gimgfixup.h | 36 ++ port/include/pcmodels.h | 3 + port/include/romdata.h | 7 + port/shim/R4300.h | 20 ++ port/src/crash.c | 20 ++ port/src/dram.c | 2 + port/src/dram_syms.s | 8 + port/src/gimgfixup.c | 125 +++++++ port/src/libultra.c | 337 +++++++++++++++++- port/src/n64stubs.c | 1 + port/src/pcmodels.c | 5 + port/src/romdata.c | 43 ++- src/bondconstants.h | 24 ++ src/bondtypes.h | 14 + src/game/bg.c | 34 ++ src/game/blood_animation.c | 43 +++ src/game/bondview2.c | 21 ++ src/game/dyn.c | 43 +++ src/game/front.c | 98 +++++- src/game/image.c | 26 ++ src/game/image.h | 25 ++ src/game/image_bank.c | 17 + src/game/initanitable.c | 8 + src/game/initunk_005520.c | 14 + src/game/language.c | 14 + src/game/model.c | 127 +++++++ src/game/model.h | 6 + src/game/ob.c | 23 ++ src/game/objecthandler.h | 47 +++ src/game/propobj.c | 10 + src/game/ramromreplay.c | 14 + src/game/rsp.c | 24 ++ src/game/title.c | 66 ++++ src/libultra/audio/csplayer.c | 15 + src/libultra/audio/load.c | 18 + src/libultra/audio/seqplayer.c | 15 + src/libultra/audio/synthesizer.c | 16 + src/memp.c | 20 ++ src/music.c | 13 + src/ramrom.c | 22 ++ src/ramrom.h | 5 + tools_pc/d51_gdb.py | 136 ++++++++ tools_pc/pixcount.py | 120 +++++++ tools_pc/ppm2bmp.py | 66 ++++ 50 files changed, 2526 insertions(+), 376 deletions(-) create mode 100644 port/include/gimgfixup.h create mode 100644 port/shim/R4300.h create mode 100644 port/src/gimgfixup.c create mode 100644 tools_pc/d51_gdb.py create mode 100644 tools_pc/pixcount.py create mode 100644 tools_pc/ppm2bmp.py diff --git a/.gitignore b/.gitignore index a42e656e..0ce5d5d8 100644 --- a/.gitignore +++ b/.gitignore @@ -124,3 +124,8 @@ _run.log ge007.crash.log .pi/ run_out.log + +# PC port run/scratch logs +run_*.log +d6*.log +ge007.crash.log diff --git a/AGENTS.md b/AGENTS.md index e007d03f..e0d99116 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 + real frames render. **Plan B EXECUTED (D50, committed):** all 512 NTSC model files converted offline by `tools_pc/d43_emit.py` → `data/pcmodels-ntsc-final/pcmodels.bin` + manifest.csv (single concatenated RZ image; regenerate per region with the script — sidecars are gitignored), served through the existing load path via a port-layer table patch (`port/src/pcmodels.c`, romdata cart-extension, one-shot `pcmodelsPatchTable()` + poolRemaining reset hook in `load_object_fill_header`); runtime C fixups for language-bank BE offset tables + font re-layout (D50.3/D50.4); legal-screen UB seed (D50.5); **D50.6 fixed the first model-render crash** — `texCopyGdls` copied only w0 of each 16-byte Gfx slot on PC (the union's trailing `long long` is 8 bytes), leaving stale mempool w1s in the compacted GDLs; byte-proven against PlegalpageZ, fixed with a full-slot copy under #ifdef PORT. Model GDLs now execute to completion (PlegalpageZ sub-DLs). **Current blocker D51:** SIGSEGV in `import_texture_i8` via a main-DL G_TEXRECT lazy tile-0 upload (frame 5) — GE opcode space decoded (G_TEXRECT=0xE4/0xE5, non-F3DEX numbering; see §F/D51); investigation recipe in HANDOFF Task 1. Next: D51 → pixel-assert soak (PPM dump + pixcount still unbuilt) → next asset type (Tbg_*_stanZ backgrounds, same offline pattern; PD `preprocess/filebg.c` reference). 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; prefer the offline pattern for format work — D48; full-struct Gfx copies on PC — D50.6). +- **Phase 2 (rendering):** IN PROGRESS — fast3d integrated + real frames render. **Plan B EXECUTED (D50, committed):** all 512 NTSC model files converted offline by `tools_pc/d43_emit.py` → `data/pcmodels-ntsc-final/pcmodels.bin` + manifest.csv (single concatenated RZ image; regenerate per region with the script — sidecars are gitignored), served through the existing load path via a port-layer table patch (`port/src/pcmodels.c`, romdata cart-extension, one-shot `pcmodelsPatchTable()` + poolRemaining reset hook in `load_object_fill_header`); runtime C fixups for language-bank BE offset tables + font re-layout (D50.3/D50.4); legal-screen UB seed (D50.5); **D50.6 fixed the first model-render crash** — `texCopyGdls` copied only w0 of each 16-byte Gfx slot on PC (the union's trailing `long long` is 8 bytes), leaving stale mempool w1s in the compacted GDLs; byte-proven against PlegalpageZ, fixed with a full-slot copy under #ifdef PORT. Model GDLs now execute to completion (PlegalpageZ sub-DLs). **D51 RESOLVED** (font `pixeldata` fixup wrote the pointer at the wrong offsets — frame-5 `import_texture_i8` crash gone; GE opcode space decoded along the way, G_TEXRECT=0xE4/0xE5 non-F3DEX numbering), **D52 RESOLVED** (`osGetCount` now advances at the N64 RSP counter rate ≈ 46.5525 ticks/µs — fixed the non-deterministic post-frame-2 hang), **D53 RESOLVED** (model RW-data pool addressing: D53.1 `Model.datas` word-stride → `u32 *` under PORT + casts; D53.2 ModelSlot/AnimModelSlot re-laid on top of the PC struct Model — in-use@0x10 / pool@0x20, slot size = sizeof(Model) = 0xE8 — plus PORT-guarded heap-fallback bumps in model.c), **D54 RESOLVED** (cseq ALCMidiHdr BE header: port-layer `romdataFixupCseq` bswap32 of the first 17 u32s, called from music.c after each decompress), **D54b RESOLVED** (synth param slots sized for ALStartParamAlt = 0x28 in synthesizer.c alSynInit under PORT), **D55 RESOLVED** (RLE folder-menu background BE w/h header: bswap32 of the first word at `(u32 *)&unknown2` at cart-map time in romdataInit, guarded). Build GREEN; game boots, plays intro music, renders through logo/gun-barrel, instantiates Brosnan, and executes the gun-barrel hole DL clean to ENDDL. **D56 RESOLVED** (watch Model/RW-pool raw offsets: field-name Model access + static 0xC8-byte pool under PORT in bondview2.c), **D57 RESOLVED** (pointer-grown rwdata records overflowed N64-sized spare pools — Brosnan PC numRecords 153 > 140: capacities grown in initunk_005520.c + dynamic slot+pool fallback in model.c), **D58 RESOLVED** (gun-barrel DL: vertex-pointer K0 idiom rebuilt via `OS_K0_TO_PHYSICAL(…) | 0x80000000` in title.c + DL reserve 0x100→0x200 because `sizeof(Gfx)` is 16 on PC). **D59–D68 RESOLVED (committed):** the "GL DLL" crash was D64's unbounded blood-RLE write (sentinel section-adjacency lost on PC) clobbering the gun-barrel sub-DL; then D60–D62 (DMA target validation, OSMesgQueue layout), D65/D65b (HEADS/BODIES enum signed sentinels under PORT), D66 (romCopyAligned + ramrom replay 64-bit pointer width), D67 (struct image_entry N64 layout), **D68 (Globalimagetable BE→LE fixup** — `port/src/gimgfixup.c` bswaps IMAGESEG Gfx w1 words + sImageTableEntry.index after texReset's romCopy; compiled globalDL shadows synced for explosion.c). The game now renders the **entire ~3.5-minute intro** (logo → gun barrel w/ Brosnan → cast) at ~59 fps. **Current blocker D69:** `load_bg_file` (bg.c:830) faults on first stage load — BG-file headers/records are N64 big-endian segment-0x0F offsets, read LE on PC; the bg `.seg` + `Tbg_*_stanZ` format work is next (decode from bg.c/stan.c → offline sidecar conversion per the D43 pattern or runtime port-layer fixup; PD `preprocess/filebg.c` is approach-reference only — its BG format differs). Details in PCPortResearch.md §F/D59–D69. 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; prefer the offline pattern for format work — D48; full-struct Gfx copies on PC — D50.6; audit struct type-puns after pointer-width changes — D53.2; raw byte offsets into pointer-containing structs shift on PC — D56; N64-sized reservations must be re-checked against 16-byte Gfx/Vtx widths — D58). - **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 025620f5..2288d145 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -1,321 +1,167 @@ -# Handoff brief — GoldenEye 007 PC port (Phase 2: Plan B executed (D50) — next session diagnoses D51, the main-DL texture-upload crash) +# Handoff brief — GoldenEye 007 PC port (Phase 2: Session H — D69 BG/stan stage loading) _Paste-ready briefing. Authoritative detail lives in `AGENTS.md` and -`docs/PCPortResearch.md` (§F/D43–D51 findings); this is the summary + the -immediate tasks._ +`docs/PCPortResearch.md` (§F/D59–D69 findings, §G status, §H procedure); this +is the summary + the immediate tasks._ ## Your job -- **Session C (next): diagnose + fix D51** — SIGSEGV in `import_texture_i8` - via a main-DL G_TEXRECT lazy tile-0 upload (frame 5). Recipe in Task 1. - Then build the pixel assert (Task 2), run a soak, commit at each milestone - (`PC port: phase 2 — `). The goal: get past frame 5 with actual model - geometry on screen, provable by pixel count. +- **Session H:** make the first stage (BUNKER1) load and render. The game now + plays the entire ~3.5-minute intro at ~59 fps and crashes in + `load_bg_file` because BG-file data is N64 big-endian (D69). This is the + "next asset type" milestone: reverse-engineer GE's bg `.seg` + + `Tbg_*_stanZ` formats from the decompiled consumers, then convert/fix them + for PC. Work agentically: decode → implement → build (~5 s) → verify → + commit at each sub-milestone (document findings as D6x in §F). -Work agentically — fix → build (~5 s) → verify → commit at each milestone. +## Current state (verified this session) + +- **Build GREEN.** `export PATH="/c/msys64/mingw64/bin:$PATH" && ./build-pc.sh ntsc-final`. +- **D59–D68 all resolved** (details in PCPortResearch.md §F). Highlights: + - **D59/D64** — the old "GL DLL" crash was an unbounded blood-RLE write + (sentinel `die_blood_image_end` lost its N64 section adjacency on PC) + clobbering the gun-barrel sub-DL. Sentinel now defined as one-past-the- + end of the array under PORT. + - **D65/D65b** — `enum HEADS`/`BODIES` sentinels: N64 semantics were signed + (`HEAD_FIXED == -1`); PC GCC 16 picks unsigned underlying types, deleting + the guard branches. Negative literals under PORT restore the bit pattern + with signed semantics. + - **D66** — `romCopyAligned` + ramrom replay did s32 pointer math; targets + in `.bss` > 4 GiB truncated to wild addresses. 64-bit version under PORT, + returns `void *`. + - **D67** — `struct image_entry` re-laid (all-u32 bitfields, `dataoffset:24` + first) so texLoad's raw word read + chrprop.c's 8-byte stride agree; + sizeof == 8 on both targets. + - **D68** — Globalimagetable BE→LE fixup (`port/src/gimgfixup.c`): bswap the + IMAGESEG-marked Gfx w1 words (17 DLs) + `sImageTableEntry.index` (32 + tables, counts from D39 layout) after texReset's romCopy; marker scan in + `texLoadFromDisplayList` moved to bytes 6..7 under PORT; compiled + globalDL shadows synced after texLoad (`gimgSyncCompiledGlobalDLs`). +- **Runtime:** boots → intro music → full intro (logo, gun barrel w/ Brosnan, + cast screen) at ~59 fps → **crash in `load_bg_file` (bg.c:830)** on first + stage load. EXCEPTION 0xc0000005, FAULT ADDR ≈ header buffer + 0x500000F. +- **Committed at D68** (see git log). TEMP diagnostics from the D51–D69 + sessions are still in the tree — strip list in Task 3. + +## The blocker, precisely (D69) + +`load_bg_file` (src/game/bg.c:800+) for level BUNKER1: +1. `obLoadBGFileBytesAtOffset("bg/bg_sev_all_p.seg", header, 0, 0x40)` — + **works**. `&fileentry->hw_address[offset]` == `hw_address + offset`; + compiled asset symbols are absolute cart addresses (e.g. + `bg_sev_all_p_seg` @ 0x10438660) into the ROM mapped at CART_BASE, and the + PI shim memcpys from there. GE_D69 probe confirms: idx=1, rom_size=0x10DF0, + hw=0x10438660. +2. `((s32 *)ptr_bg_data)[1]` — **the bug**. ROM bytes at +4 are + `0F 00 00 14` (BE value 0x0F000014). N64: BG_SEG_TO_PTR folds + `off + 0xF1000000` → file offset 0x14. PC reads LE 0x1400000F → pointer + ~5 MB past the stack buffer → fault reading `[1].pPointTableBin`. +3. Same class applies to: every bg_room_data record (more 0x0Fxxxxxx offsets, + pPointTableBin at record+0x28 per the crash disasm), the rest of the .seg + header/tables, and the whole `Tbg_sev_all_p_stanZ` geometry file + (stanDetermineEOF/stanLoadFile in stan.c). + +**Strategy options** (decide after decoding; AGENTS.md prefers offline): +- **(a) Offline sidecar conversion** (the D43/Plan-B pattern): a tools_pc + script converts all bg/*.seg + Tbg_*_stanZ per region into one image + + manifest, served through the existing load path via a port-layer table + patch (cf. `port/src/pcmodels.c`). Pros: matches established pattern, no + runtime cost, regenerable per region. Cons: must fully decode both formats. +- **(b) Runtime port-layer fixup** after each load (hook where the bytes land). + Same format knowledge; smaller initial step if only a few fields matter for + first-frame render — but stage geometry is deep; likely most fields end up + needing conversion anyway. + +**Caveat:** PD's `port/src/preprocess/filebg.c` describes a *different* BG +format (zipped multi-section: primary/section1/2/3 headers). GE's .seg files +are raw with segment-0x0F internal offsets. Same engine family ≠ identical +format — use PD only as an approach reference; validate every field against +GE's own consumers (bg.c, stan.c, and the N64 disasm where in doubt). ## Read first (authoritative, in order) + 1. `AGENTS.md` — non-negotiables, critical files, build, verification ritual. -2. `docs/PCPortResearch.md` **§F/D50** (what the last session did, incl. the - texCopyGdls byte proof), **D51** (current blocker + GE opcode facts), then - D49/D48/D47 (sidecar contract — still ground truth for regeneration). -3. The crash-chain code: `port/fast3d/gfx_pc.cpp` (`gfx_run_dl` G_TEXRECT case - ~line 2457, `import_texture*` ~600–950, `seg_addr` ~2282, - `gfx_sp_moveword` ~2290s), `src/game/tex.c` (texCopyGdls + texLoadFromGdl). +2. `docs/PCPortResearch.md` **§F/D69** (blocker + strategy), then D68 + (Globalimagetable fixup — the template for BE data in this segment family), + §G status, and the environment reminders at the end of §H. +3. The consumers: `src/game/bg.c` (`load_bg_file` ~line 795+, levelinfotable + line 183, BG_SEG_TO_PTR in bg.h:23), `src/game/stan.c` (stanLoadFile / + stanDetermineEOF), `src/game/ob.c` (obLoadBGFileBytesAtOffset — note the + TEMP D69 probe), `assets/obseg/file_resource_table.inc.c` (name→symbol map). +4. Reference for the offline pattern: `tools_pc/d43_emit.py` + + `port/src/pcmodels.c` + romdata cart-extension plumbing. -## Non-negotiables (the ones that bite) -- N64 build files (`Makefile`, `tools/`, `rsp/`, `ld/`) — never touch. -- **Game logic is unmodified.** Hardware deps go in `port/`. Narrow exception: - mechanical, semantics-preserving ABI/layout fixes forced by the 32→64-bit - transition (embedded pointers → u32 + cast at use; PC-guarded pool sizing / - idiom branches; UB-exposure seeds), each documented as a D3x/D50 finding. - No behavior changes. The N64 path stays verbatim under `#else`. -- Region macros in `CMakeLists.txt` must mirror the N64 Makefile exactly. +## Task 1 — decode the formats -## Current state -- **Plan B is EXECUTED (D50, committed):** all 512 NTSC model files converted - offline by `tools_pc/d43_emit.py` → `data/pcmodels-ntsc-final/pcmodels.bin` - + `manifest.csv` (single concatenated RZ image, 16-aligned offsets, decimal - manifest); port plumbing in `port/src/pcmodels.c` (+ `port/include/pcmodels.h`, - romdata.c/h cart-extension); one-shot `pcmodelsPatchTable()` + poolRemaining=0 - reset hook in `load_object_fill_header` (objecthandler_2.c, #ifdef PORT). - Boot log: `[INFO] pcmodels: table patched (512 model entries)`. -- **Runtime C fixups landed (D50.3/D50.4):** language-bank BE offset tables - (`romdataFixupLangBank`, called from language.c after each load + lazily in - the langGetJpnCharPixels paths) and font re-layout - (`romdataFontPcSize`/`romdataFixupFont` in textrelated.c). These are - load-time C fixups, NOT offline sidecars — small blobs on different load - paths; fine to keep runtime. -- **Legal-screen UB seed (D50.5):** front.c `constructor_menu00_legalscreen` - read an uninitialized pointer (N64 survived by luck); PORT branch seeds it - with `legalpage_text_array`. Identical lookat. -- **First model-render crash fixed (D50.6):** `texCopyGdls` copied only w0 of - each 16-byte Gfx slot on PC (the union's trailing `long long` is 8 bytes), - so the compacted GDLs kept stale mempool w1s → garbage G_SETTIMG → OOB in - `import_texture_rgba16`. Byte-proven against PlegalpageZ (sidecar w1 - 0x050012C8 vs RAM garbage 0xACAAB819 at file offset 0x2488). Fixed with a - full-slot copy under #ifdef PORT. **Model GDLs now execute to completion.** -- **Current blocker — D51:** SIGSEGV on frame 5 in `import_texture_i8` - (gfx_pc.cpp:782) via `G_TEXRECT` (main DL offset +0x260) → lazy upload of - RDP tile 0 with an invalid/stale source. See Task 1. Frames 1–4 render - successfully in every run (deterministic). +1. Dump BUNKER1's .seg header + first records from the ROM (cart 0x10438660, + size 0x10DF0) and map each word to its consumer in bg.c: + - word0 = 0; word1 = room-fileposition-list offset (0x14); word2/word3 = + more table offsets (0x334 / 0x32C). + - Follow `ptr_bgdata_room_fileposition_list[1].pPointTableBin` etc. into + bg_room_data (bg.h) and note every offset field + its base + (BG_SEG_TO_PTR = base + off + 0xF1000000, i.e. stored as 0x0Fxxxxxx). +2. Do the same for `Tbg_sev_all_p_stanZ` (cart 0x1087C3F0, size 0x3DD0): + StanPrefixRecord + whatever stanLoadFile walks. Cross-check struct field + offsets against the N64 disasm of the consumer functions where the decomp + is ambiguous. +3. Write the format notes into PCPortResearch.md §F/D69 (or a new §I) as you + go — they are the converter spec. -## Task 1 — D51 diagnosis (the immediate task) +## Task 2 — implement + verify -The failing blit: 32×48 px screen rect (236,92)–(268,140), tile 0, i8 format. -G_TEXRECT carries no source address — it uses RDP texture-memory tiles; the -fault is in `import_texture(tile=0)` reading `rdp.textures[0]`'s stored source. -Work the suspects in order: +1. Implement the chosen conversion/fixup; wire it in (sidecar table patch or + load hook). +2. Verify: stage loads without fault, then renders frames (watch for the next + BE field class surfacing at render time — vertex tables, DL pointers, + texture refs inside stan data; expect a few iterations, D43 had ~10). +3. Once a stage renders: pixel-assert soak (PPM dump + tools_pc/pixcount.py), + then move on (next stages use the same files pattern — make sure all 20+ + levels' bg/stan files are covered, not just BUNKER1). -1. **Dump live (post-mortem dumps FAIL — 0x70052e80 reads "Cannot access - memory" after the crash).** gdb launch mode: - ``` - gdb -batch \ - -ex "handle SIGSEGV stop" \ - -ex "break import_texture_i8" \ - -ex run \ - -ex "print rdp.textures[0]" \ - -ex "up 4" \ - -ex "print cmd" -ex "print dListStart" \ - -ex "x/256wx " \ - ./build-pc/ge007.x86_64.exe - ``` - (Adjust `up` depth to land in `gfx_run_dl`; the DL start may not be - 0x70052e80 every run — it's a mempool bump allocation. Alternatively break - at `osSpTaskStartGo` and read `t->t.data_ptr` / `data_size`.) -2. **Find the G_SETTIMG (w0 top byte 0xFD) slots in the dumped main DL** and - decode each w1 through the seg_addr rules (raw 0x05xxxxxx → seg5+low24; - marked LSB → segmentPointers[seg]+addr; unmarked <0x10000000 → - segmentPointers[seg]+low24; <0x800000 → +0x80000000; else passthrough). - Also dump `segmentPointers[]` at the breakpoint. If a SETTIMG w1 resolves - to an OOB/invalid pointer → that's the bug (converter remap miss, or a new - address class seg_addr doesn't handle). -3. **If no main-DL SETTIMG sets tile 0 before the G_TEXRECT:** the tile is - stale/uninitialized — check whether the game expects a tile set by an - earlier frame's DL (fast3d `rdp` persistence across frames) or by a sub-DL - that never ran. -4. **If the source pointer is valid but points at wrong-format/wrong-size - data:** the game may point it at a ROM/ramrom image needing a port fixup - (D50.3/D50.4 class) — check what asset lives there and how N64 PD handles - the analogue. -5. **Find who emits the G_TEXRECT** in game code: grep `src/game/` for - `gSPTileRect\|G_TEXRECT\|gSPTextureRectangle` (likely image.c / front.c / - title.c — legal-screen or logo 2D blit). Reading the emitter tells you - which texture it means and where its source should come from. +## Task 3 — strip TEMP diagnostics before committing -Record findings as **D51.x** in §F; fix in `port/` where possible (seg_addr / -tile setup), PORT-guarded game-code only if ABI/layout-forced. Rebuild, -confirm the crash moves past frame 5, then Task 2. +Env-gated probes to remove (grep `GE_D` + `TEMP D`): +- ob.c: D69 probe (both obLoadBGFileBytesAtOffset versions) + its stdio/stdlib + includes. +- image.c / image_bank.c: D66 texLoad probe if still present. +- gfx_pc.cpp: D59 seg probes, D63 dram-branch/modelRenderNodeGundl/rspGfxTaskStart. +- blood_animation.c: D63 g_GfxMemPos probe. +- front.c: D63 menu probes (d63MenuProbe/After + call sites). +- rsp.c, model.c, initanitable.c, language.c, title.c, ramromreplay.c, + n64stubs.c, libultra.c, romdata.c: leftover TEMP blocks from D51–D66 + (D51 vi-post logging, D54 music, D56 watch, D57 pool, D60/D61 DMA logging — + decide per item whether the guard stays permanent; D60's target validation + is a permanent safety net and should stay). +- Scratch files at repo root: `run_*.log`, `d62mesg.log`, `all.txt`, `b3.c`, + `btest*.c`, `buildtest.txt`, `err.txt`, `gcout.txt`, `preproc.txt`, + `vsize.c/.exe`, `scratch-logs/` — delete (add to .gitignore if they recur). -## Task 2 — pixel assert + soak (build it now — dev-loop item #1) +## Standing procedure (per AGENTS.md) -Env-gated framebuffer PPM dump every N frames in `port/video.c` -(e.g. `GE_PCDUMP=1` → `build-pc/frame_%05d.ppm`) + `tools_pc/pixcount.py` -(counts non-clear-color pixels; diffs two captures). Milestone: pixel count -rises above a small threshold and stays up through a 30 s+ soak. Screenshot -diffs against Mupen64Plus (verified No-Intro dump) are the ground truth for -"looks right". Commit: `PC port: phase 2 — D51 …` then `… pixel assert + -soak`. +- N64 build untouched; game **logic** unmodified — narrow exception for + pointer-width/ABI-only edits under #ifdef PORT, each documented in + PCPortResearch.md §F/D3x style. +- Verification ritual after any build-affecting change: undefined symbols, + duplicates, syntax (`./build-pc.sh` is the final word). +- **Run the game from the repo root** (`cd REPO_ROOT && + ./build-pc/ge007.x86_64.exe`) — running from build-pc/ picks up the wrong + ROM (baserom.u.z64) and misses the sidecar. +- `Gfx.words.w0/w1` are both uintptr_t (16-byte Gfx on PC); ROM-copied N64 + Gfx DLs stay 8 bytes in memory — walk them at 8-byte stride when scanning. +- N64 CPU/RDP data in ROM is big-endian; any raw `*(u32*)` read of ROM-copied + buffers needs a bswap under PORT (D68 pattern) or offline conversion. -## Task 3 — next asset type (after D51 + soak) +## Environment reminders -Tbg_*_stanZ stage backgrounds (different format/path than models). SAME -offline pattern (D48): pin the format with Python over ROM ground truth, emit -+ validate offline, serve through the existing load path. PD's -`port/src/preprocess/filebg.c` is the per-field reference (§2.4) — but same -family ≠ identical format; validate every field against GE headers + ROM. - -## Verified facts (sidecar spec — ground truth; do NOT re-derive) - -**Sidecar serving (D50):** -- Sidecars live in `data/pcmodels-/` = **one concatenated - `pcmodels.bin` + `manifest.csv` (`name,offset,size`, DECIMAL — C parser - uses strtol base 10; rows in file_resource_table.inc.c order)**. NOT - per-file .bin. Regenerate: `python tools_pc/d43_emit.py [ntsc-final| - pal-final|jpn-final|--check-only]` (needs the region ROM in data/). -- RZ format = `[0x11 0x72][raw deflate]`. Python decompress for inspection: - `zlib.decompress(raw[2:], -15)` after checking the 2-byte header. -- pcmodels.c copies the whole blob to `[CART_BASE+romSize, …)` (cart base - 0x10000000, romdataInit reserves romSize+sidecarTotal; - romdataCartAddrValid accepts the extension) and patches - `file_resource_table[i].hw_address = cartBase+romSize+off` + - `resource_lookup_data_array[i].rom_size = C_pc` per manifest row — one-shot, - from `load_object_fill_header` (after obInit has run). The existing - romCopy/decompressdata path then serves PC bytes unmodified and sets - poolRemaining = D_PC. -- **Inspecting a loaded file offline:** find the name in manifest.csv → - decompress that slice → it is exactly what lands at the file base in RAM - (verified byte-for-byte for PlegalpageZ). GDLs are at the tail of the image - in visit order; `x/Nwx ` in gdb compares directly. - -**Format / layout:** -- All 512 C*/G*/P*Z model files 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`). -- PC file layout: `[switches NS×8B][texconfigs NT×12B][root node @ 8NS+12NT]…`; - all embedded pointers are vma `0x05xxxxxx` (mask 0xFFFFFF). N64 was - `[NS×4][NT×12][root @ 4NS+12NT]`. -- **Tiling invariant verified, 0 failures in all 512 files** - (`tools_pc/d43_invariants.py`): the image is exactly tiled by {switches, - textures, nodes(48B PC), records(PC_REC per opcode), vertex arrays(nv×16B), - collision arrays(nc×16B), PointUsage (numVertices s16 entries), 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". -- **ModelNode = 24B N64 / 48B PC** (u16 op + 2 pad + 5 ptrs: Data@4/8, - Parent, Next, Prev, Child). 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 by `tools_pc/d43_layoutprobe.c`). Opcodes present: - 1,2,4,8,9,10,12,13,15,18,21,22,23,24. -- **Endianness:** f32 fields BE in ROM → bswap; every u16/s16 record scalar - BE → bswap16; vertex-array s16s BE → bswap16 (x,y,z,index,s,t); TextureID - u32 → bswap32. Padding/reserved zeroed. -- **Vertex arrays stay 16B-stride** (GDL gSPVertex hardcodes it). Collision - vertices carry LinkedTo@8 = vma of a ModelNode — remapped. -- **GDLs:** N64 8B BE word pairs → PC 16B LE slots: `w0=bswap32(rom_w0); - w1=bswap32(rom_w1)`; **no LSB set** (fast3d's seg_addr handles unmarked + - raw-0x05). Opcode-aware remap: only {G_VTX=0x04, G_SETTIMG=0xFD, - G_LOADBLOCK=0xF3} carry addresses in w1; seg-5 low24 remapped via the region - map. **0xB1 (G_TRI4) w1 is packed 8-bit vertex indices — NEVER an address.** - G_VTX w0 is GBI1-style (`04<<24 | dst<<16 | 16·n`) — bswap only. -- **Marker format:** top byte of w0 = 0xC0; type = `w0 & 7`; texnum = - `w1 & 0xfff`. texLoadFromGdl expands markers into RDP commands (that's the - compaction growth); default case copies the full 16B slot as-is — **the - sidecar must contain final correct w1 values** (no runtime remap in - texLoadFromGdl). -- **PROMOTE contract:** promoted pointers emitted as zero-extended u64 with - low32 = VMA `0x05xxxxxx`; `PROMOTE` (`(u32)var + diff`, vma 0x5000000) works - unmodified. GDL Primary/Secondary NOT promoted (raw VMAs); BaseAddr=0 (game - overwrites with fileramaddr). -- **texCopyGdls contract (D50.6):** on PC it must copy the FULL 16-byte slot - (`*arg1 = *arg0` under #ifdef PORT) — the N64 `long long` idiom copies only - w0 on x86-64 and poisons the compaction scratch. texLoadFromGdl then reads - the scratch and writes back at the original GDL offsets; final fileSetSize - shrinks the bank chunk. - -**GE opcode space (include/PR/gbi.h, non-F3DEX numbering — verified D51):** -- DMA: G_SPNOOP=0, G_MTX=1, G_MOVEMEM=3, G_VTX=4, G_DL=6, G_SPRITE2D_BASE=9. -- IMM (G_IMMFIRST=−65): TRI1=0xBF, CULLDL=0xBE, POPMTX=0xBD, MOVEWORD=0xBC, - TEXTURE=0xBB, SETOTHERMODE_H=0xBA, SETOTHERMODE_L=0xB9, ENDDL=0xB8, - SETGEOMETRYMODE=0xB7, CLEARGEOMETRYMODE=0xB6, LINE3D=0xB5, RDPHALF_1=0xB4, - RDPHALF_2=0xB3, TRI2=0xB2, **G_TRI4(GE)=0xB1** (gbi_extension.h). -- RDP pass-through: G_SETTIMG=0xFD, G_LOADTXC=0xFE, G_SETCIMG=0xFF, - **G_TEXRECT=0xE4 / G_TEXRECTFLIP=0xE5** (GE-specific values — NOT libultra's - 0x46/0x45), plus VI2D=0xF5 etc. -- `gSPSegment(pkt,seg,base)` = `gMoveWd(pkt,G_MW_SEGMENT,seg*4,base)` → - w0=(0xBC<<24)|(seg*4<<8)|6, w1=data. fast3d gfx_sp_moveword stores data as-is - when ≥0x800000 else +0x80000000. -- Main DL is game-built per frame with gSP* macros (16B slots) — NOT from a - sidecar. Segment table: seg0=0, seg1=0x7029F800-ish, seg3=render_pos, - seg4=runtime vtx buffer, seg5=file base (set per object). - -**Load path / compaction (unchanged by D50 — see D47/D46/D48 for full detail):** -- ALL model loads go through `load_object_fill_header()`; dst≠0 → - `_fileNameLoadToAddr` (caller's buffer), else `_fileNameLoadToBank` (fresh - bank when poolRemaining==0 — the hook resets it every time). -- Compaction: texCopyGdls mirrors [G,D) to tail scratch [B−D+G,B); - texLoadFromGdl rewrites each GDL in place with marker expansion; final size - = D + 16·Σ(K_t−1) worst case (P_est model — the emit pass's cross-checks - encode it for every dst!=0 buffer and chain). - -## Helper scripts (tools_pc/, tracked; run from repo root) -- `d43_emit.py` — **THE emit pass (D50.1):** converts all 512 files, validates - round-trip + tiling + buffer fits, writes pcmodels.bin + manifest.csv. - `--check-only` runs validation without writing. First stop for any - "sidecar looks wrong" question. -- `d43_layoutprobe.c` — compiler probe that re-verifies every PC record/node/ - header size (run with the exact CMake flags; compare against d43_emit.py's - tables after any bondtypes.h change). -- `d43_convert.py` — reference layout + validator (D47); foundation of the - emit pass. -- `d43_invariants.py` / `d43_gdlorder.py` / `d43_sizes*.py` / `d43_chainbound.py` - / `d43_cover.py` / `d43_lutscan.py` / `d43_seg5*.py` / `d43_gdl*.py` — the - investigation suite (all pass on NTSC). -- Gotcha: MODELFILEHEADER's name is field a[0] of the macro args (NOT the - filename); nsnt lookup keys by header symbol (`armourguard`), not file base - name (`CarmourguardZ`). - -## Dev-loop speedups (priority order) -1. **Frame capture + pixel assert** — NOW (Task 2): env-gated PPM dump every N - frames in `port/video.c` + `tools_pc/pixcount.py`. Turns "the scene contains - actual model geometry" into an assert and makes the 30 s soak scriptable. -2. **`tools_pc/sym.sh`** — tiny wrapper: PC or rel offset → add image base - 0x140000000 → `addr2line -f -C`. Re-verify the image base with `info - address` after any link-layout change. -3. Sidecar inspection is now trivial offline (manifest + zlib, see Verified - facts) — don't gdb the game for "geometry looks wrong" in a model file. -4. D44 automatic backtraces — DONE; `ge007.crash.log` is the first stop. - -## PD port as standing reference (always available) -`PD_PORT_CHECKOUT` is the Perfect Dark PC port — same Rare -engine family, the **standing reference** for this project (noted in -AGENTS.md). Consult it whenever a remaining work item has a PD analogue: -- **Phase 2 asset conversion:** `port/src/preprocess/` (~4,100 lines) — - per-ROM-segment N64→PC layout converters hooked from their `romdata.c`. - `filemodel.c` = D43 near-analogue (PD converts at LOAD time — we convert - OFFLINE for speed, same field rules apply); `filebg.c` (expected follow-on), - `segaudio.c`, `filelang.c`, `filetiles.c`, `filepads.c`, `filesetup.c`, - `gbi.c`, `segfonts.c`. -- **Phase 3/4 (copy-and-adapt; our stubs were scaffolded for this):** - `port/src/mixer.c` (722 lines vs our 31), `input.c` (1,551 vs 48 — - SDL_GameController backend; remap buttons to GE's scheme), `fs.c` (294 vs 81 - — file-backed save dir). -- **Caveats:** copy port-layer files only (their game code was modified for - PC); same family ≠ identical format — validate every conversion per-field - against GE headers + ROM ground truth. Full audit: §2.4. - -## Standing procedure (you will hit more of these) -Every ROM-serialized struct with a pointer field faults the same way once you -reach more asset loading. The D32 fix procedure (doc §H): at the fault, -`ptype /o `; if a pointer field's offset/size diverges from its N64 -offset comment, change it to `u32` + cast at the use sites; verify the -load-time rebase yields valid V1 addresses (< 0x80000000); **also check the -ROM bytes' endianness per-field (D33 rule: u32 bswap32 / u16 bswap16 / u8 -identity) and any fixup loop's pointer stride on x86-64**; for packed struct -trees use the D37 re-layout pattern; for address-arithmetic idioms over exe -symbols (D39/D42 class) guard a PC branch that reproduces the N64 32-bit value -exactly. Rebuild; confirm boot advances. Log each as D5x in §F and note it in -AGENTS.md phase status. **For asset FORMAT work, prefer the offline pattern -(D48): pin the format with Python over the ROM ground truth, emit + validate -offline, serve through the existing load path — runtime C conversion only as -fallback.** Also: any code that copies Gfx slots must use full-struct copies on -PC (D50.6) — never the `long long` union member. - -## Environment (do not rediscover these) -- MSYS2/MinGW tools are in `/c/msys64/mingw64/bin/` — **not** on PATH. Prefix - every shell: `export PATH="/c/msys64/mingw64/bin:$PATH"` (build-pc.sh needs - it for cmake too). -- Build: `./build-pc.sh ntsc-final` (incremental; full rebuild ~5 s on the - 16-core dev box). ROM at `data/ge007.ntsc-final.z64` (= `baserom.u.z64`, - byte-identical to the No-Intro good dump, MD5 - `70c525880240c1e838b8b1be35666c3b`). -- **Sidecars are gitignored (data/).** If `data/pcmodels-ntsc-final/` is - missing on a fresh checkout, regenerate: `python tools_pc/d43_emit.py - ntsc-final`. The game boots without them (warn + ROM-only mode) but model - loads fail. -- The ROM is **mapped at cart base 0x10000000** on PC; sidecar image follows - at [CART_BASE+romSize, …). All game RAM lives in the V1 view at 0x70xxxxxx - (< 4 GiB) — s32 truncation of game-RAM pointers is lossless. -- gdb: **launch** mode only (`gdb -batch -ex "handle SIGSEGV stop" -ex run …` - — attach fails with error 87). Symbolicate offline with `addr2line -e - build-pc/ge007.x86_64.exe -f -C <0x140000000+rel>`. Image base - 0x140000000. **Post-mortem memory dumps of game RAM can fail** ("Cannot - access memory") — dump at a live breakpoint instead (hit this with the main - DL in D51). -- Many init functions use a fake RBP — compute stack offsets from the **entry - RSP**, not RBP. -- Python on this box: no f-strings with nested quotes (< 3.12) — use `%` - formatting. -- Untracked scratch at repo root (all.txt, b3.c, btest*.c, buildtest.txt, - err.txt, gcout.txt, preproc.txt, vsize.*) + `tools_pc/__pycache__/` are - session scratch — do NOT commit them; use explicit pathspecs with git add. - -## Definition of done -- **Session C:** D51 diagnosed + fixed (finding in §F, crash moves past frame - 5), pixel assert built, 30 s+ soak clean with real geometry on screen. - Committed + pushed (`PC port: phase 2 — `). After that: Phase 2 makes - it look right (fast3d CC/RM correctness vs `rsp/graphics/gmain.s` — - GE-specific ground truth, no PD analogue), next asset type (Tbg_*_stanZ - backgrounds), and Phase 3 adds sound + input (copy-and-adapt from the PD - port). +- MSYS2 tools: `export PATH="/c/msys64/mingw64/bin:$PATH"`. Build ~5 s. +- Crash log `ge007.crash.log` now includes FAULT ADDR, STACK@RSP window, and + module list — first stop for any fault. Symbolicate offline: + `addr2line -e build-pc/ge007.x86_64.exe -f -C `. +- gdb is launch-mode only and far too slow for timing-dependent crashes — + prefer env-gated probes + the crash log. +- Image base 0x140000000; DRAM V1 @ 0x70000000, V2 @ 0x80000000 (8 MB each); + CART_BASE = 0x10000000; model sidecar extends the cart region at + 0x10C00000. `osGetCount` advances at ~46.5525 ticks/µs (D52). +- ROM: data/ge007.ntsc-final.z64 (byte-identical to the No-Intro good dump); + sidecar: data/pcmodels-ntsc-final/ (gitignored, regenerate with + tools_pc/d43_emit.py per region). diff --git a/docs/PCPortResearch.md b/docs/PCPortResearch.md index b456fe29..2a8a9e97 100644 --- a/docs/PCPortResearch.md +++ b/docs/PCPortResearch.md @@ -1745,84 +1745,510 @@ Fix: `*arg1 = *arg0;` under `#ifdef PORT` (tex.c). Audit: tex.c is the only local 8-byte union); every other Gfx copy is full-struct. Post-fix: PlegalpageZ's sub-DLs execute to completion; crash moves on (D51). -**D51 (OPEN — current blocker: `import_texture_i8` SIGSEGV via main-DL G_TEXRECT lazy tile upload)** +**D51 (RESOLVED — font `pixeldata` fixup wrote the pointer at the wrong offsets; frame-5 `import_texture_i8` SIGSEGV)** -Post-D50 run: frames 1–4 log, then SIGSEGV on frame 5 in -`import_texture_i8` (gfx_pc.cpp:782, `addr[i]`) ← `import_texture(i=0, -tile=0)` ← `gfx_sp_tri1(128,129,131,is_rect=true)` ← -`gfx_draw_rectangle(236,92,268,140)` ← `gfx_dp_texture_rectangle` ← -`G_TEXRECT` case (gfx_run_dl ~line 2472), main DL start 0x70052e80, -failing opcode slot at +0x260 (0x700530e0). G_TEXRECT carries no source -address — it blits from RDP texture-memory tiles; the fault is the **lazy -upload of tile 0** whose stored source pointer is invalid (or the tile was -never set up this frame → stale `rdp.textures[0]`). Suspects: (a) a -main-DL G_SETTIMG (0xFD) earlier than +0x260 whose w1 seg_addr -mis-resolves; (b) tile referenced before setup / across frames; (c) the -game points the tile source at a ROM/ramrom image that needs a port fixup -(D50.3/D50.4 class). **Investigation not started.** Note: after the crash, -0x70052e80 is unreadable from gdb ("Cannot access memory") — dump live at a -breakpoint, not post-mortem. GE opcode facts verified this session -(include/PR/gbi.h, non-F3DEX numbering): G_IMMFIRST=−65; DMA G_MTX=1 / -G_MOVEMEM=3 / G_VTX=4 / G_DL=6; IMM TRI1=0xBF … ENDDL=0xB8; -GE extension G_TRI4=0xB1 (gbi_extension.h, 8-bit packed indices — NOT an -address carrier); RDP pass-throughs include G_SETTIMG=0xFD, -G_SETCIMG=0xFF, **G_TEXRECT=0xE4 / G_TEXRECTFLIP=0xE5** (GE-specific -values, not libultra's 0x46/0x45). The main DL is game-built per frame with -gSP* macros (16B slots); the segment table is set via +The G_TEXRECT tile-0 upload's source was a **font glyph** (I8, loaded via +`gDPLoadTextureBlock`). Root cause in `romdataFixupFont` +(port/src/romdata.c): it wrote each char's pixel pointer at blob offsets +d+20/d+24, but on PC `struct fontchar.pixeldata` is a **u64 at char offset ++24** — the low word landed in padding and only half the pointer was +written, so glyph sources resolved to stale/invalid addresses → OOB read in +`import_texture_i8`. Fix: write the blob-relative offset into d+24 (low) +and zero-extend d+28 (high). Verified: glyphs load, SIGSEGV gone. The old +D51 hypothesis list (seg_addr / tile staleness) was wrong; its GE opcode +facts remain valid reference material (G_IMMFIRST=−65; DMA G_MTX=1 / +G_MOVEMEM=3 / G_VTX=4 / G_DL=6; IMM TRI1=0xBF … ENDDL=0xB8; GE extension +G_TRI4=0xB1, 8-bit packed indices — NOT an address carrier; RDP +pass-throughs G_SETTIMG=0xFD, G_SETCIMG=0xFF, **G_TEXRECT=0xE4 / +G_TEXRECTFLIP=0xE5** — GE-specific values, not libultra's 0x46/0x45; +width-1 stored in the 12-bit field, fmt I=4, G_TX_LOADTILE=7; main DL is +game-built per frame with gSP* macros; segment table via `gMoveWd(G_MW_SEGMENT, seg*4, base)` → w0=(0xBC<<24)|(seg*4<<8)|6, w1=data; fast3d's gfx_sp_moveword stores data as-is when ≥ 0x800000 else -+0x80000000. ++0x80000000). -Environment notes for Task 2: standalone gcc needs +**D52 (RESOLVED — `osGetCount` tick-rate mismatch → non-deterministic post-frame-2 hang)** + +Plain runs sometimes hung after frame 2 (kernel watchdog: "no frame rendered +for 3006180 ms") instead of crashing at frame 5. Root cause: +port/src/libultra.c `osGetCount()` returned **microseconds** (1M/s), but GE's +pacing assumes the N64 RSP counter rate ≈ **46.5525 ticks/µs**: +`MAIN_LOOP_TICK_INTERVAL` = 387,937 ticks (boss.c: NTSC +`INTERVAL_INTER_MATH - 2688U`; PAL +`frameDelay*(CYCLES_PER_FRAME-6450)-(INTERVAL_INTER_MATH-3225)`), and +`waitForNextFrame()` (frametiming.c) waits for +`(elapsed+interval)/775875` ticks (NTSC frame = 775,875; PAL 931,050). At +1M/s the steady-state 16,667 µs between retraces < 387,937 → bossMainloop's +gate never passes → no DL built → hang. Frames 1–2 rendered only because +stage loading took >388 ms of real time. Fix: `osGetCount()` returns +`(u32)(((uint64_t)sysGetMicroseconds()*465525ull)/10000ull)` (rate derived +from GE's own constants: 775875/16666.67µs = 931050/20000µs); wraps every +~92 s like the HW counter. `osGetTime()` still returns µs (osSetTimer/ +OSTime). PD's port has the same µs implementation but lacks GE's cycle-based +pacing gate — do not copy it blindly. + +**D53 (RESOLVED: model RW-data pool addressing on PC; frame-5 SIGSEGV in `modelInitRwData`)** + +Post-D51/D52, the frame-5 SIGSEGV moves to `modelInitRwData` +(model.c ~6131): first BSP node → `movl $0x0,(%rax)` right after +`call modelGetNodeRwData` — writing `visible=FALSE` through NULL. Two +compounding PC layout bugs, both in the D32 class: + +- **D53.1 (applied; necessary but not sufficient): `Model.datas` word + stride.** `RwDataIndex` values are **4-byte word offsets** into the + RW-data pool (`modelCalculateRwDataIndexes` accumulates + `len += sizeof(record)/4`; pool = round16(numRecords×4) bytes, allocated + in modelmgrInstantiateModel(WithAnim)). On N64 `&data[index]` with + `union ModelRwData **data` strides 4 B; on PC it strides 8 B → every + non-zero index addresses the wrong record. Fix (3 files, all #ifdef PORT): + `Model.datas` → `u32 *datas` (bondtypes.h — layout unchanged: single + pointer field); casts in model.c (`modelGetNodeRwData` local + return + `(union ModelRwData *)&data[index]`, parent-walk + `data=(u32*)tmp->RwDatas`, `modelAttachPart`) and propobj.c:7301/14343. +- **D53.2 (root-caused; fix designed, NOT applied): `ModelSlot` / + `AnimModelSlot` ↔ `Model` type-pun breaks on PC.** The game puns the slot + structs and Model in both directions: `slot.unk08@8` ↔ `Model.obj@8` (the + **in-use flag** — `modelInit`'s `objinst->obj = header` marks a slot), + `slot.unk10@0x10` ↔ `Model.datas@0x10` (RW pool), `slot.unk02@2` ↔ + `rwdatalen@2`. On N64 all pointers are 4 B → offsets agree. On PC there is + no pack pragma — natural alignment (probe-verified with the exact CMake + flags, see vsize.c): **Model = 0xE8 B**: chr@8, **obj@0x10**, + render_pos@0x18, **datas@0x20**, scale@0x28, attachedto@0x30, anim@0x40; + the slot structs still have unk08@8 / unk10@0x10. (An earlier draft of + this section said obj@0xC/datas@0x1C — that assumed packed layout and is + wrong; the log evidence below only fits the natural-alignment offsets.) + Consequences: (a) `modelInit` writes obj@0x10 — the free check reads + unk08@8 (low word of Model.chr, never written) → slots are never marked in + use → every model reuses slot 0; (b) the legal-screen logo model takes + slot 0, then `update_menu00_legalscreen()` (front.c:1430) → + `clear_model_obj(logoinst)` writes obj=NULL at @0x10..0x17 — exactly where + `slot.unk10@0x10` lives; the next instantiation passes + `modelmgrCanSlotFitRwdata` on stale Model.datas@0x20 (non-NULL) + real + unk02=20 ≥ 17, then `rwdata = g_ModelSlots[i].unk10` = NULL → + `modelInit(model, header, NULL)` → fault. **Log evidence** (TEMP D51 trace + in model.c → d52rw.log): + ``` + INST model=0x7020ac48 header=0x140141c40 numRecords=0 rwdata=0x7020b0f8 rwdatalen=20 lvreset=0 + INST model=0x7020ac48 header=0x140142700 numRecords=17 rwdata=NULL rwdatalen=20 lvreset=0 + GND obj=0x7020ac48 datas=NULL idx=0 rwdatalen=-1 op=2 data=NULL res=NULL + ``` + (same Model address both times = slot reuse; 20 = MODEL_SPARE_RWDATALEN + spare-slot pool.) +- **D53.2 fix APPLIED and verified.** objecthandler.h under #ifdef PORT: + both slot structs re-laid out on top of the PC struct Model — `unk02`@2, + in-use marker `unk08`@**0x10** (low word of Model.obj), pool pointer + `unk10`@**0x20** (Model.datas), then `char pad28[sizeof(struct Model)-0x28]` + so each slot is exactly sizeof(Model) = 0xE8 (a full Model, including + animInit's writes up to PC offset 0xE3, fits). Member names kept; N64 + layout verbatim under #else. model.c heap fallbacks bumped PORT-guarded: + 0x20 → sizeof(struct Model) (non-animated), 0xC0 → sizeof(struct + AnimModelSlot) (animated). Verified with the d52rw.log probe: both INSTs + get valid pools (rwdata=0x7020c778, non-NULL), all 17 records initialize, + frame-5 modelInitRwData crash gone; game runs to ~frame 102 and past the + Nintendo-logo transition. Note: the bump allocator's `pos` drifts with the + new sizes → some slots land on 4-byte boundaries; misaligned 8-byte field + stores are functionally fine on x86-64 (individual field accesses, no + faults) — accepted without a PORT alignment bump in memp.c. + +Size/offset probe: `vsize.c` at repo root (untracked scratch — delete before +commit) prints sizeof + field offsets of Model / ModelSlot / AnimModelSlot / +struct player; compile with the CMake include order (port/shim first, then ., +include, include/PR, src, src/game, src/libultra, port/include) plus +`-DPORT=1 -DAVOID_UB=1 -std=c11`. Re-run after any slot-struct edit. + +Environment notes: standalone gcc needs `/c/msys64/mingw64/bin:/c/msys64/usr/bin` on PATH (cc1 fails SILENTLY without it — exit 1, zero diagnostics), and `-std=c11` is required (the CMake flag; under the default gnu23 `typedef s32 bool` in bondtypes.h breaks). Also: `include/stddef.h`'s body is `#if 0`'d — offsetof/size_t are unavailable in -game TUs. +game TUs (use pointer-difference arithmetic in probes). + +**D54 (RESOLVED: cseq ALCMidiHdr endianness; audio-thread SIGSEGV in +`__getTrackByte`)** + +After D53, the first music load (M_INTROSWOOSH, seq 44) crashed the audio +thread in `__getTrackByte`. Root cause: a decompressed compact-sequence file +starts with `struct ALCMidiHdr` — 16 **big-endian** u32 trackOffset values + +a BE u32 division. N64 reads them natively; on an LE host alCSeqNew() does +not swap them (track-0 offset 0x44 becomes 0x44000000), builds "valid" +curLoc pointers ~1 GB past the buffer, and the first track-byte read faults. +The rest of the stream is byte-oriented (varlens, MIDI bytes, BE loop offsets +assembled byte-by-byte) so only the 17 header words need fixing. Fix: port- +layer `romdataFixupCseq(u8 *blob)` (port/src/romdata.c — bswap32 of the first +17 u32s; declared in port/include/romdata.h), called under #ifdef PORT after +each of the 3 `decompressdata` calls in musicTrack1Play/2/3 (src/music.c). +Verified: __getTrackByte crash gone; full 60 s run (exit=124 timeout) with no +audio fault, past the Nintendo-logo transition. + +**D54b (RESOLVED: synthesizer param-slot sizing; audio-thread SIGSEGV in +`alLoadParam`)** + +Next audio crash: `alLoadParam` dereferenced a corrupted free-list slot. Root +cause: alSynInit's "build the parameter update list" allocates +`c->maxUpdates` slots of `sizeof(ALParam)` and the game type-puns those slots +as several AL*Param structs. On N64 all of them are 0x1C bytes (one slot +each); on x86-64 **ALStartParamAlt is 0x28** (two 8-byte pointers: next + +wave) vs ALParam = 0x20 — every start-voice update wrote `wave` into the +neighbouring slot, corrupting the free list. Fix in synthesizer.c alSynInit +under #ifdef PORT: allocate `maxUpdates * sizeof(ALStartParamAlt)` and stride +the init loop by that size (cast each slot to ALParam* for the next-pointer +link); N64 verbatim under #else. Cost +8 B/slot × 0x80 slots = +1 KB vs +MUSIC_ALLOCATION_BYTES — no alHeapAlloc failure observed. Verified: +alLoadParam crash gone; full 60 s run clean. + +**D55 (RESOLVED: RLE folder-menu background header endianness; SIGSEGV in +`rle_expand_8bit`)** + +After the audio fixes, the game ran to the gun-barrel intro (~frame 654, +~23 s) and crashed in `rle_expand_8bit` (src/game/rle.c:30, the `*dst++` +store). Caller: title.c `sub_GAME_7F008DE4` (the initializeGunBarrelIntro path) +romCopies the asset at +`unknown2` (romassets_.s, NTSC cart 0x102A4D50, size 0x1A580) and RLE- +decodes it into a 0x40400-byte buffer. Root cause: the asset is the title +folder-menu background; its raw ROM header is **big-endian** `01 B8 01 2B` +(w=440, h=299) + 6 pad bytes + a valid RLE stream (decodes to exactly +440×299 = 131560 bytes). rle_expand_8bit reads w/h as **LE** u16s (byte- +matched N64 code): the raw header gives w=47105, h=11009 → remaining ≈ +518 MB written into a 256 KB buffer → SIGSEGV. The N64 build embeds this +asset into .data via `assets/romfiles2.s` (`.incbin +"assets/ge007.u.2A4D50.usedby7F008DE4.bin"`) from an extracted .bin whose +header is byte-swapped — title2.c hardcodes 440-wide I8 rows × 299, and this +is the only decodable 440×299 RLE stream in the ROM (a full-file 16-bit swap +was ruled out: it zeroes the first RLE count at +0xC), so only the 4-byte +header differs between raw ROM and the N64 .bin. Fix (port-layer, romdata.c +romdataInit, after the cart-base mapping): bswap32 the first word at +`(u32 *)&unknown2` in place — the image is a writable VirtualAlloc at CART_ +BASE — guarded by "only swap if the LE-read w or h > 512" so an already-LE +region copy is a no-op. rle.c/title.c untouched (D37/D54 pattern). Verified: +RLE crash gone; game renders 600+ frames past the gun-barrel background into +the watch intro, where it hits D56. + +**D56 (RESOLVED: watch-intro embedded Model/RW-pool raw offsets into struct +player; SIGSEGV in `modelSetScale`)** + +Post-D55, the game renders 600+ frames (~10 s into the gun-barrel/watch +intro) then SIGSEGVs in `modelSetScale` (src/game/model.c:778, +`objinst->scale = scale`) with a garbage Model* (crash-log return frame was +corrupted; caller inferred — see below). Prime suspect, and the only code +passing a **raw N64 offset into struct player** as a Model*: `sub_GAME_7F07E7CC` +(bondview2.c:3102-3116, called from bondview2.c:3400 whenever the pause/watch +transition completes — every watch-menu open, and during the gun-barrel +intro): +``` +animInit((Model *)((u8 *)g_CurrentPlayer + 0x230), itemheader, (u32 *)((u8 *)g_CurrentPlayer + 0x2ec)); +modelSetScale((Model *)((u8 *)g_CurrentPlayer + 0x230), c_item_entries[41].scale * 0.1f); +modelSetAnimation((Model *)((u8 *)g_CurrentPlayer + 0x230), …ANIM_DATA_bond_watch…); +*(s32 *)((u8 *)g_CurrentPlayer + 0x220) = 0; // = step_in_view_watch_animation +``` +Layout facts (probe-verified where noted): +- struct Model (bondtypes.h:1482): 8 pointer fields (chr, obj, render_pos, + datas [u32* under PORT, D53.1 — note the in-code comment there still says + "D52", flagged for rename in HANDOFF Task 3], attachedto, + attachedto_objinst, anim, anim2). + sizeof_N64 = **0xBC**; sizeof_PC = **0xE8** (vsize probe: chr@8, obj@0x10, + render_pos@0x18, datas@0x20, scale@0x28, attachedto@0x30, anim@0x40 — note + the 8-byte pointer alignment padding after the two leading s16s). +- N64 struct player: the **watch Model is embedded at +0x230** (its first + word is the anonymous s32 `something_with_watch_object_instance`), size + 0xBC, so it ends exactly at +0x2EC where the **RW-data pool** begins. The + pool region runs to `buttons_pressed`@0x3B4 = **0xC8 bytes** of capacity; + every field in [0x2EC, 0x3B4) is an anonymous s32 (no pointers → no extra + PC shift inside the region). +- On PC the embedded Model sits at X = offsetof(struct player, + something_with_watch_object_instance) (≥ 0x230 + 5×4: cameratile@0x34, + prop@0xA8, bodyModel@0xD4, autoaim_target_y@0x130, autoaim_target_x@0x140 + are the pointer fields before it) and spans [X, X+0xE8). Remaining capacity + before buttons_pressed is only 0x184−0xE8 = **0x9C < 0xC8** → the pool does + NOT fit embedded on PC. +- modelInit stores the pool pointer in `Model.datas` and ALL rwdata access + goes through `modelGetNodeRwData(model, node)` via `model->datas` — so + redirecting the pool to separate storage is safe; only sub_GAME_7F07E7CC + references +0x2EC directly (whole-tree grep). +- The N64 capacity 0xC8 bounds the watch model's real pool size (the game + works on N64), so a fixed static buffer of that size is safe. +**Applied as designed + verified:** bondview2.c sub_GAME_7F07E7CC under +#ifdef PORT takes the Model by field name — +`Model *watch = (Model *)&g_CurrentPlayer->something_with_watch_object_instance;` +(probe-verified at +0x24C on PC — not 8-aligned; unaligned pointer stores are +fine on x86-64 per D53.2) and hosts the pool in `static u8 watchRwPool[0xC8]` +(N64 capacity; N64 embeds it at player+0x2EC); animInit/modelSetScale/ +modelSetAnimation on `watch`; the +0x220 store becomes +`g_CurrentPlayer->step_in_view_watch_animation = 0;`. N64 raw-offset path kept +verbatim under #else. Verified: the watch path no longer crashes — but the +same `modelSetScale` SIGSEGV remained, and an env-gated probe (GE_D56, +logging `__builtin_return_address(0)` in modelSetScale) proved the real +caller was **not** the watch path: `initializeGunBarrelIntro` (title.c) +calling `modelSetScale(NULL, 0.18779343f)` because `setup_chr_instance()` → +`modelmgrInstantiateModelWithAnim()` returned NULL for BODY_Brosnan_Tuxedo — +see D57. + +**D57 (RESOLVED: pointer-grown rwdata records overflow the N64-sized spare +pools; `modelmgrInstantiateModelWithAnim` returns NULL → SIGSEGV in +`modelSetScale(NULL, …)`)** + +The Brosnan tuxedo's computed PC `numRecords` is **153** words vs the N64- +sized anim spare-pool capacity of **140** (`ANIM_MODEL_SPARE_RWDATALEN = +0x8C`). Cause: two rwdata record structs contain pointer fields and grow 8 → +16 bytes on x86-64 — `ModelRwData_HeadPlaceholderRecord` (ModelFileHeader* + +void*) and `ModelRwData_DisplayList_CollisionRecord` (Vertex* + Gfx*). Since +`modelCalculateRwDataLen()` accumulates sizeof(record)/4 per node, every +HEAD/DLCOLLISION node adds +2 words vs N64. Fix (two parts, both #ifdef PORT): +(1) initunk_005520.c: spare capacities grown with headroom — +`MODEL_SPARE_RWDATALEN 0x14→0x38`, `ANIM_MODEL_SPARE_RWDATALEN 0x8C→0xA8` +(N64 values kept under #else); (2) model.c: in the non-LvResetting branches +of both `modelmgrInstantiateModel()` and `modelmgrInstantiateModelWithAnim()`, +a dynamic slot+pool fallback mirroring the existing LvResetting path +(`mempAllocBytesInBank(sizeof(struct ModelSlot/AnimModelSlot))` + 16-aligned +pool of numRecords words) — the slot is untracked (never reused), acceptable +because with the grown capacities it should not trigger. A u32-field approach +for the two pointer records was rejected: `ModelFileHeader` pointers there are +exe-resident globals (>0x80000000 on PC) and would truncate. Verified: +Brosnan gets a dynamic slot, `modelSetScale` succeeds, game proceeds to +rendering — where it hits D58. + +**D58 (RESOLVED: gun-barrel DL — K0 vertex-pointer idiom + 16-byte Gfx +overflow of the N64-sized reservation; SIGSEGV in `gfx_sp_vertex`, then FATAL +"Unknown GBI opcode 0x00")** + +Two distinct PC-layout breaks in the same buffer (initializeGunBarrelIntro, +title.c), both found via env-gated probes (GE_D57: per-command VTX/CALL/JMP +log + entry-time hexdump of the barrel DL): +- **Part A (vertex pointer):** title.c passed `barrelDisplayListPtr + + 0x80000000` to sub_GAME_7F01BFF8, which embeds it verbatim in each G_VTX + w1 (GE's gDma1p writes `(uintptr_t)(v)` — no LSB). On N64 the mempool + pointer was physical, so +0x80000000 gave the RSP-visible KSEG0 address; + on PC it is a V1 pointer (0x70xxxxxx, dram.c) and +0x80000000 lands at + 0xF0xxxxxx — unresolvable by fast3d's seg_addr() → SIGSEGV reading the + vertex array. Fix: rebuild the exact N64 value — + `(Vtx *)(OS_K0_TO_PHYSICAL((void *)barrelDisplayListPtr) | 0x80000000u)` + (→ 0x80xxxxxx; seg_addr passes it through to the KSEG0 mirror; segments + 7/8 are never registered, so the unmarked-segment path is skipped). +- **Part B (DL reservation):** on PC `sizeof(Gfx) == 16` — the union's + trailing `long long` (gbi.h documents it: "except on 64-bit, where it is + exactly 128 bit"), same class as D50.6. Both writers (`gdl++`) and fast3d + (`++cmd`) advance by 16, so all game-written DLs are 16-byte-wide — but + the barrel-DL reservation `bufferSize -= 0x100` was sized for N64's 8-byte + Gfx. sub_GAME_7F01BFF8 emits 31 Gfx (2×VTX + 28×TRI + ENDDL) = 496 B, so + slots 16–30 (second TRI batch + ENDDL) overflowed into the RLE region at + +0x300, and sub_GAME_7F008DE4's expand then clobbered them with image data. + At render the RSP executed VTX/TRI×14/VTX fine, then hit slot 16 = RLE + pixels (w0=0x00000001 → opcode 0x00; fast3d has no G_SPNOOP case) → FATAL. + Fix: reserve 0x200 under PORT. The 0x200 vertex reserve still fits + (30 Vtx × 16 B = 0x1E0). NOTE for future asset work: any other N64-sized + reservation for game-written DLs/vertex arrays must be re-checked against + the 16-byte Gfx / 16-byte Vtx widths (recurring class, cf. D50.6/D53.2). +Verified: barrel DL executes to ENDDL; game proceeds past the gun-barrel +hole into model rasterization — where it hits D59. + +**D59 (OPEN — current blocker: SIGSEGV inside an external GL DLL during the +first real model rasterization after the gun-barrel hole)** + +Post-D58, the barrel DL runs clean (probe-verified: VTX@+0, TRI×14, +VTX@slot15(+0xF0 in 16-byte form), … ENDDL) and the game crashes shortly +after with EXCEPTION 0xc0000005 at PC 0x7ff8d42a44d3 — inside a DLL loaded +at 0x7ff8d4230000 (offset +0x744d3; almost certainly the OpenGL driver, +not yet confirmed). The crash-log backtrace frame #1 (main+0x227cc0) is a +BSS symbol (`memoryMesgMB`) — garbage stack, no usable caller. Draw path: +gfx_flush() (gfx_pc.cpp:299) → `gfx_rapi->draw_triangles(buf_vbo, +buf_vbo_len, buf_vbo_num_tris)` → gfx_opengl.cpp:813-816 +`glBufferData(GL_ARRAY_BUFFER, sizeof(float)*buf_vbo_len, buf_vbo, +GL_STREAM_DRAW); glDrawArrays(GL_TRIANGLES, 0, 3*buf_vbo_num_tris)`, where +`buf_vbo` is the static `float buf_vbo[MAX_BUFFERED*(32*3)]`. Hypotheses: +(a) buf_vbo overflow while accumulating transformed vertices for the first +real model (Brosnan) — check the vertex-append site in gfx_pc.cpp (the +transform loop after ~line 1118) for a missing bounds check against +MAX_BUFFERED; (b) garbage buf_vbo_len/num_tris; (c) bad texture/shader state +on first model draw. Next: env-gated probe logging buf_vbo_len/ +buf_vbo_num_tris at every gfx_flush + identify the DLL (PowerShell module +list during a run, or the GL vendor string in the log). + +**D59 RESOLVED (sessions G–I).** The "external GL DLL" crash was not a +driver bug: it was an msvcrt.dll `memcpy` faulting on a wild source — the +gun-barrel sub-DL region at `ptr_logo_and_walletbond_DL + 0x200` had been +clobbered by the unbounded RLE write of D64 (below). With D64 fixed, the +barrel renders and the game advances; no fast3d/vertex-buffer change was +needed. The crash handler gained permanent improvements along the way: +FAULT ADDR (ExceptionInformation[1]), a 16-qword STACK@RSP window, and a +module list in `ge007.crash.log` (crash.c + psapi). + +**D60 RESOLVED — DMA target validation (port layer).** +`osPiStartDma` (libultra.c) now validates ROM-read targets: the N64 PI can +DMA to any KSEG address, but on PC an unmapped target is a wild memcpy. +`dramHostAddrValid()` accepts DRAM V1/V2 and any host-committed region +(VirtualQuery), so legitimate `.bss`/`.data` targets (e.g. +`ramrom_data_target`) pass while s32-truncated wild addresses are rejected +with a logged FATAL instead of a silent crash. Also added the GE_D60 +sidecar-read tracer and GE_D61 per-ROM-read log (`d61dma.log`). + +**D62 RESOLVED — OSMesgQueue/OSScMsg layout (port layer).** The shim's +message-queue bookkeeping had to match the PC struct widths: OSMesgQueue is +40 bytes on PC (two OSThread* + 3×s32 + OSMesg*), OSScMsg stays 32 bytes. +Scheduler-thread message flow (retrace/pre-NMI/interrupt/cmd queues in +`os_scheduler`, g_AudioManager frame/reply queues) verified against those +layouts. + +**D63 — TEMP diagnostics (to strip).** GE_D63-gated probes in gfx_pc.cpp / +blood_animation.c / front.c / rsp.c tracing the VTX-pool bump pointer +(`g_GfxMemPos`), the gun-barrel sub-DL slot word, dram-branch targets and +rspGfxTaskStart hand-off. Used to prove D64's clobber path and to rule out +VTX-pool overflow; no permanent change. + +**D64 RESOLVED — blood RLE sentinel (src/game/blood_animation.c).** +The N64 build places `die_blood_image_end` in the same section directly +after `die_blood_image_1[]`; the RLE decoder's guard `bloodImgNxt < +&die_blood_image_end` relies on that adjacency (only the address is used). +On PC a zero-init symbol lands in `.bss` ~1 MB away, so the guard never +fires and the decoder writes unbounded past the array — it clobbered the +gun-barrel sub-DL at `ptr_logo_and_walletbond_DL + 0x200`, which is what +surfaced as the D59 "GL DLL" crash. Fixed under #ifdef PORT by defining +`die_blood_image_end` as one-past-the-end of the array. + +**D65 RESOLVED — `enum HEADS` signed sentinels (src/bondconstants.h).** +The N64 toolchain gave this enum a signed underlying type, so +`HEAD_FIXED == -1` and `head >= 0` guards were real branches. PC GCC 16 +picks `unsigned int` for enums whose enumerators are all non-negative +(0xFFFFFFFF > INT_MAX), making every `head >= 0` always true and turning +`c_item_entries[HEAD_FIXED]` into a wild 64-bit OOB read (SIGSEGV in +init_menu18_displaycast). Under PORT the sentinels are now negative +literals (`HEAD_FIXED = -1`, `HEAD_RANDOM = -97`) — identical bit pattern, +signed semantics restored. + +**D65b RESOLVED — `enum BODIES` signed sentinel (src/bondconstants.h).** +Same class as D65: ROM tables store 0xFFFFFFFF in `body` fields and the +cast-end check compares `intro_char_table[f].body < 0`; PC's unsigned +underlying type deleted the reset branch, so the cast screen rendered the +terminator entry and `langGet(0)` dereferenced a NULL bank. Added +`BODY_FIXED = -1` under PORT (forces signed underlying type; no existing +value changes). + +**D66 RESOLVED — romCopyAligned pointer width (src/ramrom.c/.h) + +ramrom replay truncations (src/game/ramromreplay.c).** The N64 build did +all of `romCopyAligned` in s32; on PC targets live in `.bss` above 4 GiB, +so `(s32)target` truncated (0x1401C6F00 → 0x401C6F00) and the DMA went to a +wild address. PORT version uses uintptr_t throughout and returns `void *` +callers assign straight to pointers. The ramrom replay path had the same +class of `(s32)` truncation on `ramrom_data_target`. + +**D67 RESOLVED — struct image_entry layout (src/game/image.h).** The +decompiled field order cannot be right: texLoad() reads +`*(s32*)&entry & 0xFFFFFF` as the data offset (dataoffset must occupy bits +0-23 of word 0) while chrprop.c indexes entries with an 8-byte stride +(sizeof == 8). Under PORT the struct is re-declared with all-u32 bitfields +and `dataoffset : 24` first, so GCC packs it to exactly two words on both +targets and the raw word read is satisfied. The IMAGE() macro initializer +order in image.c is adjusted to match under PORT. + +**D68 RESOLVED — Globalimagetable endianness (port/src/gimgfixup.c + +src/game/image_bank.c + src/game/image.c).** The ROM-copied Globalimagetable +segment (texReset) is N64 big-endian, but PC code reads its CPU-interpreted +u32 fields natively: the IMAGESEG-marked G_SETTIMG w1 words +(`IMAGESEG(id) = 0xABCD0000 | id`) and the `sImageTableEntry.index` field of +all 32 table arrays. Unfixed, texLoad computed texnum from byte-swapped ids +(e.g. 52651 for IMAGE_SMOKE_11 = 2106) → out-of-range offsets → a 925 KB +ROM read into the 4000-byte stack compbuffer (FATAL at boot). Fix: +`gimgFixupGlobalimagetable()` bswaps exactly those u32s in place after the +romCopy (17 Gfx DLs walked op-by-op for the AB CD marker; table entry +counts fall out of the D39 symbol layout, 12-byte stride); everything else +in the segment is byte-level (opcodes, single-byte fields, raw pixel blocks +referenced via 0x02xxxxxx segmented addresses) and untouched. Two +consequences handled under PORT: (1) texLoadFromDisplayList's marker scan +now checks bytes 6..7 (CD AB — the LE encoding of 0xABCDxxxx) instead of +4..5; (2) explosion.c executes the *compiled* globalDL_0xNNN shadows via +g_ExplosionDisplayLists[], so `gimgSyncCompiledGlobalDLs()` copies the +texLoad()-patched IMAGESEG w1 values from the ROM copy into those arrays +(command j of the 8-byte ROM DL maps to Gfx slot j of the 16-byte compiled +array; D39 verified them byte-identical). Verified: 137 texLoads with valid +in-range ids (2106, 2084, …), real offsets/sizes from g_Textures, and the +game runs the full ~3.5-minute intro (logo → gun barrel → cast) at ~59 fps +to the first stage load. + +**D69 (OPEN — current blocker: BG-file big-endian headers at stage load)** + +Post-D68 the game plays through the entire intro and crashes in +`load_bg_file` (src/game/bg.c:830) when loading the first stage (BUNKER1, +"bg/bg_sev_all_p.seg", cart 0x10438660). The header IS loaded correctly — +`obLoadBGFileBytesAtOffset` works: `&fileentry->hw_address[offset]` +evaluates to `hw_address + offset` (a valid cart address; the compiled +absolute asset symbols point into the ROM mapped at the cart base, and the +PI shim memcpys from there). The bug is interpretation: BG-file offsets are +N64 big-endian u32s in segment-0x0F form. Header word 1 in the ROM is +`0F 00 00 14` (BE value 0x0F000014 → file offset 0x14 after +BG_SEG_TO_PTR's `+ 0xF1000000` fold); PC reads it LE as 0x1400000F, so +`ptr_bgdata_room_fileposition_list = header + 0x1400000F - 0xF000000` +lands ~0x5 MB past the stack buffer and `...[1].pPointTableBin` faults +(EXCEPTION 0xc0000005, FAULT ADDR ≈ header + 0x500000F). The whole +stage-load path (bg .seg headers/room tables + Tbg_*_stanZ geometry files) +is riddled with BE u32 fields — the same class as D68 but a far larger +format surface. This is the "next asset type" milestone anticipated in +AGENTS.md. Strategy options: (a) offline per-region conversion of all bg/*.seg ++ Tbg_*_stanZ files into sidecars (the D43/Plan-B pattern; requires fully +decoding GE's BG/stan formats from bg.c/stan.c — note PD's +preprocess/filebg.c describes a *different*, zipped multi-section format; +same family ≠ identical, validate per field); (b) runtime port-layer fixup +after each load (same format knowledge, placed in port/). Either way the +first task is reverse-engineering the formats: header words 0..3 are +pointers (rooms/portals/bgcmds/lights-style tables per the D69 probe: +word1=0x14 room-fileposition list), bg_room_data records carry more +0x0Fxxxxxx offsets (pPointTableBin at record+0x28, see crash disasm), and +stanZ files go through stanDetermineEOF/stanLoadFile. TEMP D69 probe in +ob.c (GE_D69) logs name/index/rom_size/hw_address per BG load. ### G. Phase 2 status (current) -Done through **D50**: PD fast3d integrated (`port/fast3d/`); GE's real +Done through **D68**: PD fast3d integrated (`port/fast3d/`); GE's real `src/sched.c` + pthread kernel; dual-mapped DRAM; ROM mapped at cart base; SDL2 window; full boot chain (D31–D42); **Plan B executed (D50)** — all 512 -NTSC model files offline-converted to PC-layout RZ sidecars -(`tools_pc/d43_emit.py` → `data/pcmodels-ntsc-final/pcmodels.bin` + -manifest.csv), served through the existing load path via a port-layer table -patch (`port/src/pcmodels.c`, romdata cart-extension, one-shot -`pcmodelsPatchTable()` + poolRemaining reset hook in -`load_object_fill_header`); runtime C fixups for language-bank BE offset -tables (D50.3) and font re-layout (D50.4); legal-screen UB seed (D50.5); -`texCopyGdls` w1 partial-copy bug fixed and byte-proven (D50.6). Frames 1–4 -render; PlegalpageZ model GDLs execute to completion. **Current blocker: -D51** — SIGSEGV in `import_texture_i8` via a main-DL G_TEXRECT lazy tile-0 -upload (frame 5); investigation not started, recipe in docs/HANDOFF.md. -After D51: pixel-assert soak (env-gated PPM dump + pixcount still unbuilt), -then the next asset type (Tbg_*_stanZ backgrounds) with the same offline -pattern. +NTSC model files offline-converted to PC-layout RZ sidecars, served through +the existing load path via a port-layer table patch; runtime C fixups for +language-bank BE offset tables (D50.3) and font re-layout (D50.4); +legal-screen UB seed (D50.5); `texCopyGdls` w1 partial-copy bug fixed and +byte-proven (D50.6). **D51–D58 resolved** (font pixeldata fixup, osGetCount +tick rate, model RW-data pools, cseq BE header, synth param slots, RLE +folder-menu background, watch-intro raw offsets, spare-pool capacities, +gun-barrel DL idiom + reservation). **D59 resolved** (the "GL DLL" crash +was D64's unbounded blood-RLE write clobbering the barrel sub-DL), +**D60–D62 resolved** (DMA target validation, OSMesgQueue layout), +**D63** TEMP diagnostics, **D64 resolved** (blood RLE sentinel adjacency), +**D65/D65b resolved** (HEADS/BODIES enum signed sentinels under PORT), +**D66 resolved** (romCopyAligned + ramrom replay 64-bit pointer width), +**D67 resolved** (struct image_entry N64 layout reconstruction), +**D68 resolved** (Globalimagetable BE→LE fixup for IMAGESEG Gfx words + +sImageTableEntry.index; compiled globalDL shadows synced after texLoad). +The game now boots, plays the intro music, and renders the **entire ~3.5- +minute intro** (Nintendo logo → gun barrel with Brosnan → cast screen) at +~59 fps, then crashes in `load_bg_file` on the first stage load — +**D69, the current blocker**: BG-file headers are N64 big-endian +(segment-0x0F offsets) and PC reads them LE. Details in §F/D59–D69. + +**Committed at D68** (this milestone): D51–D68 fixes + TEMP diagnostics +still present (strip list in HANDOFF Task 3); build is GREEN. ### H. Handoff & plan (current session) -Full paste-ready brief: **docs/HANDOFF.md** (rewritten this session). Summary: +Full paste-ready brief: **docs/HANDOFF.md** (rewritten for the D69 session — +BG/stan format work). Immediate task: reverse-engineer GE's bg .seg + +Tbg_*_stanZ formats from the decompiled consumers (bg.c, stan.c), then +choose offline sidecar conversion (Plan-B pattern, D43) vs runtime port-layer +fixup; PD's preprocess/filebg.c is a reference for the *approach* only — its +BG format is different (zipped multi-section). +Summary: -**State.** Plan B is EXECUTED (D50): offline sidecars for all 512 NTSC model -files + port-layer plumbing + lang/font runtime fixups + legal-screen UB seed -+ the texCopyGdls w1 fix (D50.6, byte-proven against PlegalpageZ). Boot runs -to the main loop; frames 1–4 render; model GDLs execute (PlegalpageZ -sub-DLs complete). +**State.** D50–D68 all resolved and verified; committed at the D68 +milestone. The game boots, plays intro music, and renders the entire +~3.5-minute intro (logo → gun barrel with Brosnan → cast screen) at ~59 +fps. TEMP diagnostics from the D51–D69 sessions are still in the tree +(strip list in HANDOFF Task 3). Build is GREEN. -**Current blocker — D51.** SIGSEGV on frame 5 in `import_texture_i8` via a -main-DL G_TEXRECT lazy tile-0 upload (gfx_pc.cpp:782; failing opcode slot at -DL offset +0x260). G_TEXRECT blits from RDP texture-memory tiles, so the fault -is an invalid/stale tile source — either a mis-resolved main-DL G_SETTIMG, -a tile used before setup, or a ROM/ramrom image needing a port fixup. -Investigation not started; the gdb recipe (break live at -`import_texture_i8`, dump `rdp.textures[0]` + the full main DL — post-mortem -dumps of 0x70052e80 fail with "Cannot access memory") is in HANDOFF Task 1. +**Current blocker — D69.** `load_bg_file` (src/game/bg.c:830) faults on +the first stage load (BUNKER1): BG-file header words are N64 big-endian +segment-0x0F offsets; PC reads them LE, so the room-fileposition-list +pointer lands ~5 MB past the stack header buffer. Full analysis + +strategy options in §F/D69. -**Next after D51:** build the pixel assert (env-gated PPM dump every N frames -in port/video.c + `tools_pc/pixcount.py`) and run a 30 s+ soak; then move to -the next asset type (Tbg_*_stanZ backgrounds) with the same offline pattern — -PD's `port/src/preprocess/filebg.c` is the per-field reference (§2.4). +**After D69:** get a stage to load + render (bg .seg + Tbg_*_stanZ format +work — see §H immediate task), then continue the diagnose→fix→verify loop +through gameplay; strip TEMP diagnostics at each milestone; pixel-assert +soak (PPM dump + tools_pc/pixcount.py) once a stage is stable. **D32 repeatable fix procedure** (apply to any ROM-serialized struct that faults on a pointer-field read): @@ -1844,6 +2270,24 @@ on a pointer-field read): guard a PC branch that reproduces the N64 32-bit value exactly (e.g. keep the 0x02000000 base) or replace the idiom with an explicit equivalent; N64 line stays verbatim under `#else`. +7. If two structs are **type-punned** (cast back and forth, e.g. ModelSlot ↔ + Model), verify their layouts still agree on PC — pointer-width changes break + puns silently: fields the game reaches "through" one struct land at wrong + offsets in the other (D53.2). Also check any fixed-size allocation that must + contain the grown struct (the 0x20-byte model heap fallback, D53). +8. If a hang (not a crash) appears with no thread making progress, suspect a + **timing/pacing gate**: GE's loop gates on `osGetCount()` deltas in N64 RSP + counter ticks (~46.5525/µs), not µs (D52). +9. If the fault is a store through a pointer built from a **raw byte offset + into a struct that contains pointers** (`(u8 *)ptr + 0xNNN`, D56): every + pointer field before 0xNNN shifts +4 on PC (plus 8-byte alignment padding), + so the offset no longer lands on the intended object. Fix under #ifdef PORT + via the named field (`&s->field`); if the embedded sub-object has grown + (sizeof_PC > sizeof_N64) and its trailing companion storage (e.g. a model + RW pool packed right after it in the struct) no longer fits before the next + live field, relocate that companion to a static buffer of the N64 capacity — + safe when all access goes through a pointer stored in the sub-object at init + (modelInit → Model.datas) and only the one site references the raw offset. **Non-negotiable #2 refinement (applied to AGENTS.md).** The original "game code compiles unmodified / fix belongs in port/" is too absolute: pointer-width layout @@ -1856,9 +2300,16 @@ following PD ground truth. No logic/behavior changes; each such edit is logged i **Environment reminders.** MSYS2 tools in `/c/msys64/mingw64/bin/` (not on PATH — prefix `export PATH=…`). Build: `./build-pc.sh ntsc-final`. gdb **launch** mode -only (attach fails, error 87); symbolicate offline with `addr2line -e -build-pc/ge007.x86_64.exe -f -C <0x140000000+rel>`. Image base 0x140000000. -`load_resource`/many init fns use a fake RBP — compute stack offsets from entry -RSP. The D30 crash handler writes `ge007.crash.log` with a working Phase-2 -backtrace (**D44** fixed) — first stop for any fault; frames past the true chain -may be stale, sanity-check they fall inside `.text`. +only (attach fails, error 87) and it is **far too slow for timing-dependent +crashes** (a D56-class crash ~10 s in took >300 s under gdb to reach 2 frames — +DBGHELP symbol loading + the D51 stall-heartbeat thread dumps; don't wait on +it): prefer env-gated TEMP probes + the built-in crash log, and symbolicate +offline with `addr2line -e build-pc/ge007.x86_64.exe -f -C <0x140000000+rel>`. +Image base 0x140000000. `load_resource`/many init fns use a fake RBP — compute +stack offsets from entry RSP. The D30 crash handler writes `ge007.crash.log` +with a working Phase-2 backtrace (**D44** fixed) — first stop for any fault; +frames past the true chain may be stale (a corrupted return address outside the +module, as in D56, means unwind depth is limited — confirm callers by code- +path analysis + behavior). Standalone probe compiles need `-std=c11` (without +it `typedef s32 bool` in bondtypes.h breaks under gnu23) and pointer-difference +arithmetic instead of offsetof (include/stddef.h is #if 0'd). diff --git a/port/fast3d/gfx_opengl.h b/port/fast3d/gfx_opengl.h index 95be897a..21f9969e 100644 --- a/port/fast3d/gfx_opengl.h +++ b/port/fast3d/gfx_opengl.h @@ -5,4 +5,14 @@ extern struct GfxRenderingAPI gfx_opengl_api; +#ifdef __cplusplus +extern "C" { +#endif +/* Frame capture (dev tool, env GE_PCDUMP=1) — see gfx_opengl.cpp. */ +bool gfx_opengl_pcdump_enabled(void); +bool gfx_opengl_dump_bound_fbo(uint32_t width, uint32_t height, const char* path); +#ifdef __cplusplus +} +#endif + #endif diff --git a/port/fast3d/gfx_pc.cpp b/port/fast3d/gfx_pc.cpp index abef6b58..e125aa32 100644 --- a/port/fast3d/gfx_pc.cpp +++ b/port/fast3d/gfx_pc.cpp @@ -2320,6 +2320,13 @@ static inline void *seg_addr(uintptr_t w1) { uintptr_t clearMtx; +/* TEMP D63 (strip before commit) */ +static int s_d63taskcount = 0; +static int s_d63ctxdump = 0; +extern "C" void d63RecordBranch(const Gfx *from, const Gfx *to); +extern "C" void d63DumpTrail(void); +extern "C" void d63LogLastBranchReread(void); + static void gfx_run_dl(Gfx* cmd) { // puts("dl"); int dummy = 0; @@ -2357,7 +2364,30 @@ static void gfx_run_dl(Gfx* cmd) { case G_VTX: gfx_sp_vertex(C0(0, 16) / sizeof(Vtx), C0(16, 4), (const Vtx*)seg_addr(cmd->words.w1)); break; - case G_DL: + case G_DL: { + /* TEMP D63: record the branch trail for post-mortem */ + { + const Gfx *tgt = (const Gfx *)seg_addr(cmd->words.w1); + uintptr_t ta = (uintptr_t)tgt; + if (ta >= 0x70000000 && ta < 0xA0000000) + { + sysLogPrintf(LOG_NOTE, "D63 dram-branch t=%d from=%p to=%p first=(%08x,%08x)", + s_d63taskcount++, (const void *)cmd, (const void *)tgt, + tgt->words.w0, tgt->words.w1); + /* Context: 4 commands before the parent G_DL */ + if (ta == 0x8012EC38 && s_d63ctxdump < 3) + { + for (int k = 4; k >= 0; --k) + { + const Gfx *c = cmd - k; + sysLogPrintf(LOG_NOTE, "D63 ctx[%d]=%p (%08x,%08x)", + k, (const void *)c, c->words.w0, c->words.w1); + } + ++s_d63ctxdump; + } + } + } + d63RecordBranch(cmd, (const Gfx *)seg_addr(cmd->words.w1)); if (C0(16, 1) == 0) { // Push return address Gfx* subGFX = (Gfx*)seg_addr(cmd->words.w1); @@ -2370,6 +2400,7 @@ static void gfx_run_dl(Gfx* cmd) { --cmd; // increase after break } break; + } case (uint8_t)G_ENDDL: return; case (uint8_t)G_SETGEOMETRYMODE: @@ -2576,9 +2607,28 @@ static void gfx_run_dl(Gfx* cmd) { case G_RDPLOADSYNC: case G_RDPTILESYNC: break; - default: + default: { + /* TEMP D63: dump context around the garbage command */ + uint32_t *w = (uint32_t *)cmd; + sysLogPrintf(LOG_ERROR, + "D63 unknown opcode 0x%02x at %p (dlstart=%p); words[-2..+2]:", + opcode, (void *)cmd, (void *)dListStart); + for (int i = -2; i <= 2; ++i) + sysLogPrintf(LOG_ERROR, "D63 [%d] %p = %08x %08x", + i, (void *)&w[i], w[i * 2], w[i * 2 + 1]); + d63DumpTrail(); + d63LogLastBranchReread(); + /* TEMP D63: dump the sub-DL region to see the 0xFF extent. */ + if (getenv("GE_D63")) { + uint32_t *r = (uint32_t *)dListStart; + for (int i = 0; i < 128; i += 4) { + sysLogPrintf(LOG_ERROR, "D63 region +%03x: %08x %08x %08x %08x", + i * 4, r[i], r[i + 1], r[i + 2], r[i + 3]); + } + } sysFatalError("Unknown GBI opcode 0x%02x at %p.\nw0 %08x\nw1 %08x", opcode, cmd, cmd->words.w0, cmd->words.w1); break; + } } ++cmd; } @@ -2848,3 +2898,52 @@ extern "C" void gfx_reset_framebuffer(void) { gfx_rapi->start_draw_to_framebuffer(0, (float)gfx_current_dimensions.height / SCREEN_HEIGHT); active_fb = framebuffers.end(); } + +/* TEMP D63: G_DL branch trail for post-mortem (strip before commit) */ +#include +#define D63_TRAIL_SIZE 4096 +static struct { const Gfx *from; const Gfx *to; uintptr_t w1; } s_d63trail[D63_TRAIL_SIZE]; +static int s_d63count = 0; + +extern "C" void d63RecordBranch(const Gfx *from, const Gfx *to) { + int idx = s_d63count % D63_TRAIL_SIZE; + s_d63trail[idx].from = from; + s_d63trail[idx].to = to; + s_d63trail[idx].w1 = from->words.w1; + ++s_d63count; +} + +extern "C" void d63LogLastBranchReread(void) { + if (s_d63count > 0) { + int li = (s_d63count - 1) % D63_TRAIL_SIZE; + const Gfx *lf = s_d63trail[li].from; + sysLogPrintf(LOG_ERROR, "D63 last-branch re-read: from=%p now w0=%08x w1=%llx (w1@rec was %llx) to=%p", + (const void *)lf, lf->words.w0, + (unsigned long long)lf->words.w1, + (unsigned long long)s_d63trail[li].w1, + (const void *)s_d63trail[li].to); + } +} + +extern "C" void d63DumpTrail(void) { + int n = 24; + if (s_d63count < n) n = s_d63count; + sysLogPrintf(LOG_ERROR, "D63 branch trail (last %d of %d):", n, s_d63count); + for (int i = s_d63count - n; i < s_d63count; ++i) + sysLogPrintf(LOG_ERROR, "D63 [%d] from=%p w1@rec=%llx to=%p", + i % D63_TRAIL_SIZE, (const void *)s_d63trail[i % D63_TRAIL_SIZE].from, + (unsigned long long)s_d63trail[i % D63_TRAIL_SIZE].w1, + (const void *)s_d63trail[i % D63_TRAIL_SIZE].to); + /* Also: every branch whose target is in the DRAM views, to reconstruct + * the sub-DL chain that led to the fault. */ + int span = s_d63count < D63_TRAIL_SIZE ? s_d63count : D63_TRAIL_SIZE; + sysLogPrintf(LOG_ERROR, "D63 DRAM-targeted branches (last %d):", span); + for (int i = s_d63count - span; i < s_d63count; ++i) + { + int idx = i % D63_TRAIL_SIZE; + uintptr_t to = (uintptr_t)s_d63trail[idx].to; + if (to >= 0x70000000 && to < 0xA0000000) + sysLogPrintf(LOG_ERROR, "D63 [%d] from=%p to=%p", + idx, (const void *)s_d63trail[idx].from, (const void *)to); + } +} diff --git a/port/include/gimgfixup.h b/port/include/gimgfixup.h new file mode 100644 index 00000000..7faccc5f --- /dev/null +++ b/port/include/gimgfixup.h @@ -0,0 +1,36 @@ +#ifndef GIMG_FIXUP_H +#define GIMG_FIXUP_H + +#include + +/* D68 (docs/PCPortResearch.md): endianness fixup for the Globalimagetable + * segment after texReset()'s romCopy(). The N64 CPU is big-endian, so every + * u32 in the ROM copy is BE-encoded; PC consumers read them natively (LE). + * + * Two kinds of CPU-interpreted u32 live in the segment: + * - Gfx w1 words of IMAGESEG-marked G_SETTIMG commands (IMAGESEG(id) = + * 0xABCD0000 | id, see bondconstants.h). texLoadFromDisplayList() scans + * for the AB CD marker bytes and texLoad() reads the word back as + * `*updateword & 0xffff` to get the image id. + * - sImageTableEntry.index (first u32 of each 12-byte entry) in the 32 + * table arrays; read both as a raw word by texLoad() and as a struct + * field by texSelect()/explosion.c. + * + * Everything else in the segment is byte-level (Gfx opcodes, single-byte + * table fields, raw pixel blocks referenced via 0x02xxxxxx segmented + * addresses) and needs no transform. + * + * After the fixup the marker bytes sit at offsets 6..7 of each Gfx (LE + * encoding of 0xABCDxxxx), so texLoadFromDisplayList() checks them there + * under PORT. + */ +void gimgFixupGlobalimagetable(u8 *base); + +/* Copy the IMAGESEG w1 words patched by texLoad() in the ROM copy back into + * the compiled globalDL_0xNNN arrays (oddtextures.o), which explosion.c + * executes via g_ExplosionDisplayLists[] on PC. The ROM copy and the + * compiled arrays are byte-identical per D39, so command j of the ROM DL + * maps to Gfx slot j of the compiled array (16-byte stride on PC). */ +void gimgSyncCompiledGlobalDLs(u8 *base); + +#endif diff --git a/port/include/pcmodels.h b/port/include/pcmodels.h index 58eb96e4..16fbd04e 100644 --- a/port/include/pcmodels.h +++ b/port/include/pcmodels.h @@ -39,6 +39,9 @@ void pcmodelsPatchTable(void); uint32_t pcmodelsTotalSize(void); +/* Cart address where the sidecar image lives (0 if absent). */ +uintptr_t pcmodelsSidecarBase(void); + #ifdef __cplusplus } #endif diff --git a/port/include/romdata.h b/port/include/romdata.h index ec729e94..e969d93f 100644 --- a/port/include/romdata.h +++ b/port/include/romdata.h @@ -61,6 +61,13 @@ void romdataFixupAnimationData(u8 *blob, u32 blobSize, */ void romdataFixupMusicSeqTable(u8 *blob, u32 blobSize); +/* + * D54 (docs/PCPortResearch.md): decode a decompressed compact-sequence file's + * big-endian ALCMidiHdr in place (16 x u32 trackOffset + u32 division; + * see romdata.c). Call after decompressdata(), before alCSeqNew(). + */ +void romdataFixupCseq(u8 *blob); + /* * D50 (docs/PCPortResearch.md): decode a language bank's big-endian offset * table in place ([u32 offsets x N][text]; see romdata.c). blobSize is the diff --git a/port/shim/R4300.h b/port/shim/R4300.h new file mode 100644 index 00000000..15cbc246 --- /dev/null +++ b/port/shim/R4300.h @@ -0,0 +1,20 @@ +/* + * PC port shim for bare includes (some game files include it + * without the PR/ prefix, which resolves to include/PR/R4300.h and would + * bypass the PHYS_TO_K0 redefinition in port/shim/PR/R4300.h). Same content + * as that shim; keep the two in sync. + */ +#ifndef _PORT_SHIM_R4300_BARE_H_ +#define _PORT_SHIM_R4300_BARE_H_ + +#if defined(PORT) +# include "include/PR/R4300.h" + +# undef PHYS_TO_K0 +# define PHYS_TO_K0(x) ((u32)(x)) /* identity: V1 view is already virtual */ + +#else +# include +#endif + +#endif /* _PORT_SHIM_R4300_BARE_H_ */ diff --git a/port/src/crash.c b/port/src/crash.c index 891347bd..c064ddf4 100644 --- a/port/src/crash.c +++ b/port/src/crash.c @@ -64,6 +64,26 @@ static void crashStackTraceRaw(char *msg, PEXCEPTION_POINTERS exinfo) CRASH_MSG("PC: %p (Rip=%p Rsp=%p Rbp=%p)\n", exinfo->ExceptionRecord->ExceptionAddress, (void *)context.Rip, (void *)context.Rsp, (void *)context.Rbp); + CRASH_MSG("REGS: Rax=%p Rcx=%p Rdx=%p Rsi=%p Rdi=%p R8=%p R9=%p R10=%p R11=%p\n", + (void *)context.Rax, (void *)context.Rcx, (void *)context.Rdx, + (void *)context.Rsi, (void *)context.Rdi, (void *)context.R8, + (void *)context.R9, (void *)context.R10, (void *)context.R11); + if (exinfo->ExceptionRecord->NumberParameters > 1) { + CRASH_MSG("FAULT ADDR: %p\n", + (void *)exinfo->ExceptionRecord->ExceptionInformation[1]); + } + /* Raw stack window around RSP: survives even when the EBP chain is + * broken (tail calls, corrupted frames). 16 qwords up from RSP. */ + { + const uint64_t *sp = (const uint64_t *)context.Rsp; + CRASH_MSG("STACK@RSP:"); + for (int i = 0; i < 16; i++) { + char cell[24]; + snprintf(cell, sizeof(cell), " %p", (void *)sp[i]); + CRASH_MSG("%s", cell); + } + CRASH_MSG("\n"); + } /* FPU state: for STATUS_FLOATING_POINT_* we want to know whether the * exception masks were actually cleared (MxCsr bits 7..10 / x87 CW * mask bits) or this is a stale status bit. */ diff --git a/port/src/dram.c b/port/src/dram.c index 9d2b7efb..fef2b6cd 100644 --- a/port/src/dram.c +++ b/port/src/dram.c @@ -41,6 +41,8 @@ * +0x050000 _bssSegmentEnd -> mempool start (boss.c:217) * ... game heap (mempools / dyn / audio buffers) * +0x2F4400 tlb block -> mempool end (tlbmanageGetTlbAllocatedBlock) + * ... free + * -0x0002D0 animations_frame_buffer (0x2D0, D59; dram_syms.s absolute sym) * +0x800000 end of region (8 MB, like an 8-MB-RAM N64) */ diff --git a/port/src/dram_syms.s b/port/src/dram_syms.s index b9253113..5765ad7f 100644 --- a/port/src/dram_syms.s +++ b/port/src/dram_syms.s @@ -16,3 +16,11 @@ .global _bssSegmentEnd .set _bssSegmentEnd, 0x70050000 + +/* D59: animation-frame scratch buffer (src/game/initanitable.c). The game + * addresses it through s32 fields (Model.unk34/38/64/68) and loadAnimationFrame + * computes its address with a (u32) cast, so on PC it must live below 4 GiB; + * the BSS copy at 0x140xxxxxx truncates to wild 0x40xxxxxx. Parked at the top + * of DRAM V1 (above the mempool sentinel at +0x2F4400). */ +.global animations_frame_buffer +.set animations_frame_buffer, 0x707FFD30 diff --git a/port/src/gimgfixup.c b/port/src/gimgfixup.c new file mode 100644 index 00000000..51a6b55e --- /dev/null +++ b/port/src/gimgfixup.c @@ -0,0 +1,125 @@ +/* + * gimgfixup.c - D68 (docs/PCPortResearch.md): Globalimagetable endianness + * fixup for the PC port. See port/include/gimgfixup.h for the rationale. + * + * The offset tables below must stay in lockstep with the g_pc_gimg_off_* + * values in src/game/image_bank.c (D39) and with assets/oddtextures.c, whose + * linked layout defines the segment: 17 Gfx display lists followed by 32 + * sImageTableEntry arrays, total size 0x13F8. + */ + +#include +#include +#include "gimgfixup.h" + +#define GIMG_REGION_SIZE 0x13F8u + +/* Segment offsets of the 17 global display lists (globalDL_0xNNN). */ +static const u32 s_dl_offs[] = { + 0x0, 0x78, 0x120, 0x1C8, 0x270, 0x318, 0x3C0, 0x468, + 0x510, 0x5B8, 0x660, 0x708, 0x7B0, 0x858, 0x900, 0x9A8, 0xA50 +}; + +/* Segment offsets of the 32 sImageTableEntry arrays (12-byte entries). */ +static const u32 s_tbl_offs[] = { + 0xAC8, 0xAD4, 0xBC4, 0xC0C, 0xC48, 0xC54, 0xC60, 0xC6C, + 0xC78, 0xC84, 0xC90, 0xC9C, 0xCA8, 0xCB4, 0xCC0, 0xCCC, + 0xCD8, 0xCE4, 0xCF0, 0xCFC, 0xD08, 0xD14, 0xD20, 0xD2C, + 0xD38, 0xD44, 0xD5C, 0xFB4, 0xFD8, 0x1020, 0x102C, 0x132C +}; + +#define N_DLS ((int)(sizeof(s_dl_offs) / sizeof(s_dl_offs[0]))) +#define N_TBLS ((int)(sizeof(s_tbl_offs) / sizeof(s_tbl_offs[0]))) + +/* Compiled shadows of the ROM-copied display lists (assets/oddtextures.c). + * Incomplete-type externs are fine: we only do pointer arithmetic into them. */ +extern Gfx globalDL_0x000; +extern Gfx globalDL_0x078; +extern Gfx globalDL_0x120; +extern Gfx globalDL_0x1c8; +extern Gfx globalDL_0x270; +extern Gfx globalDL_0x318; +extern Gfx globalDL_0x3c0; +extern Gfx globalDL_0x468; +extern Gfx globalDL_0x510; +extern Gfx globalDL_0x5b8; +extern Gfx globalDL_0x660; +extern Gfx globalDL_0x708; +extern Gfx globalDL_0x7b0; +extern Gfx globalDL_0x858; +extern Gfx globalDL_0x900; +extern Gfx globalDL_0x9a8; +extern Gfx globalDL_0xa50; + +static Gfx *const s_dl_syms[] = { + &globalDL_0x000, &globalDL_0x078, &globalDL_0x120, &globalDL_0x1c8, + &globalDL_0x270, &globalDL_0x318, &globalDL_0x3c0, &globalDL_0x468, + &globalDL_0x510, &globalDL_0x5b8, &globalDL_0x660, &globalDL_0x708, + &globalDL_0x7b0, &globalDL_0x858, &globalDL_0x900, &globalDL_0x9a8, + &globalDL_0xa50 +}; + +void gimgFixupGlobalimagetable(u8 *base) +{ + int i, j; + u8 *end = base + GIMG_REGION_SIZE; + + /* 1) Gfx display lists: bswap the w1 word of every IMAGESEG-marked + * G_SETTIMG command. In the raw BE bytes the marker sits at + * offsets 4..5 (AB CD); after the swap it is at 6..7 (CD AB). */ + for (i = 0; i < N_DLS; i++) + { + u8 *p = base + s_dl_offs[i]; + + while (p + 8 <= end && p[0] != (u8)G_ENDDL) + { + if (p[4] == 0xAB && p[5] == 0xCD) + { + u32 *w1 = (u32 *)(p + 4); + *w1 = __builtin_bswap32(*w1); + } + p += 8; + } + } + + /* 2) sImageTableEntry arrays: bswap the index field (word 0 of each + * 12-byte entry). Entry counts fall out of the D39 layout: each + * table runs up to the next symbol. */ + for (i = 0; i < N_TBLS; i++) + { + u32 start = s_tbl_offs[i]; + u32 stop = (i + 1 < N_TBLS) ? s_tbl_offs[i + 1] : GIMG_REGION_SIZE; + u32 count = (stop - start) / 12u; + + for (j = 0; j < (int)count; j++) + { + u32 *idx = (u32 *)(base + start + (u32)j * 12u); + *idx = __builtin_bswap32(*idx); + } + } +} + +void gimgSyncCompiledGlobalDLs(u8 *base) +{ + int i; + u8 *end = base + GIMG_REGION_SIZE; + + for (i = 0; i < N_DLS; i++) + { + u8 *p = base + s_dl_offs[i]; + Gfx *dst = s_dl_syms[i]; + int j = 0; + + while (p + 8 <= end && p[0] != (u8)G_ENDDL) + { + /* Post-fixup marker position: LE encoding of 0xABCDxxxx puts + * CD AB at byte offsets 6..7. */ + if (p[6] == 0xCD && p[7] == 0xAB) + { + dst[j].words.w1 = *(u32 *)(p + 4); + } + p += 8; + j++; + } + } +} diff --git a/port/src/libultra.c b/port/src/libultra.c index e440e7de..e11e96ca 100644 --- a/port/src/libultra.c +++ b/port/src/libultra.c @@ -87,7 +87,16 @@ OSTime osGetTime(void) u32 osGetCount(void) { - return (u32)osGetTime(); + /* The N64 RSP core counter advances at ~OS_CPU_COUNTER (~46.5 MHz), NOT + * microseconds. GE's pacing assumes this rate: waitForNextFrame() in + * frametiming.c waits for 775,875 - 387,937 ticks per NTSC frame (931,050 + * per PAL frame) and bossMainloop gates DL builds on + * MAIN_LOOP_TICK_INTERVAL = 387,937 ticks. Feeding it microseconds made + * waitForNextFrame block ~388 ms/frame and the gate never pass in steady + * state (hang after the first couple of frames). Scale real time to + * 46.5525 ticks/us (= 775875/16666.67us = 931050/20000us, both regions) + * and wrap like the 32-bit hardware counter. */ + return (u32)(((uint64_t)sysGetMicroseconds() * 465525ull) / 10000ull); } /* ------------------------------------------------------------------------ */ @@ -249,6 +258,7 @@ void portKernelInit(void) { memset(g_pt, 0, sizeof(g_pt)); g_pqCount = 0; + d63WatchdogStart(); /* TEMP D63 */ /* Anchor the heartbeat clock so the first check doesn't compare against * epoch 0 (the host QPC has a large absolute offset). */ g_lastFrameUs = sysGetMicroseconds(); @@ -379,10 +389,24 @@ void osSetThreadPri(OSThread *t, OSPri prio) /* Message queues */ /* ------------------------------------------------------------------------ */ +/* D59: the lookup/create below MUST be serialized. Two threads first + * touching the same (or different) OSMesgQueue concurrently used to race on + * g_pqCount and end up with two PortQueues — or one re-initialized slot — + * for a single queue. Sends then signal a cond the receiver never waits on + * (lost wakeup: permanent stall after frame 2), and re-initing a live mutex + * is UB that can corrupt unrelated state (wild-pointer crashes). Boot-time + * osCreateMesgQueue() pre-registers most queues, but first-touches still race + * for any queue created later or touched before its create call. */ +static pthread_mutex_t s_pqLock = PTHREAD_MUTEX_INITIALIZER; + static PortQueue *portQueueGet(OSMesgQueue *mq) { + pthread_mutex_lock(&s_pqLock); for (int i = 0; i < g_pqCount; ++i) { - if (g_pq[i].os == mq) return &g_pq[i]; + if (g_pq[i].os == mq) { + pthread_mutex_unlock(&s_pqLock); + return &g_pq[i]; + } } if (g_pqCount >= PORT_MAX_QUEUES) { sysFatalError("portQueueGet: out of port queue slots"); @@ -392,6 +416,7 @@ static PortQueue *portQueueGet(OSMesgQueue *mq) pthread_mutex_init(&pq->lock, NULL); pthread_cond_init(&pq->cond, NULL); pq->loggedFirstBlock = 0; + pthread_mutex_unlock(&s_pqLock); return pq; } @@ -441,9 +466,162 @@ void osEnqueueMesg(OSMesgQueue *mq, OSMesg msg) pthread_mutex_unlock(&pq->lock); } +/* TEMP D62: full message-flow trace (env GE_D62=1 -> d62mesg.log). */ +static FILE *s_d62log = NULL; +static int s_d62opened = 0; +static pthread_mutex_t s_d62lock = PTHREAD_MUTEX_INITIALIZER; +static void d62log(const char *op, OSMesgQueue *mq, OSMesg msg) +{ + if (!s_d62opened) { + s_d62opened = 1; + if (getenv("GE_D62")) + s_d62log = fopen("d62mesg.log", "w"); + } + if (s_d62log) { + pthread_mutex_lock(&s_d62lock); + fprintf(s_d62log, "%s mq=%p msg=%p valid=%d/%d first=%d\n", + op, (void *)mq, (void *)msg, mq->validCount, + mq->msgCount, mq->first); + fflush(s_d62log); + pthread_mutex_unlock(&s_d62lock); + } +} + +/* TEMP D63: watch the gun-barrel sub-DL slot (V1+0x12EC38) for clobbering. + * Hooked in osSendMesg/osRecvMesg so both game threads are sampled often. */ +void d63SlotLog(const char *tag) +{ + if (!getenv("GE_D63")) return; + u32 cur = *(const u32 *)0x7012EC38; + sysLogPrintf(LOG_NOTE, "D63 slot %s: word@0x7012ec38=%08x", tag, cur); +} + +/* TEMP D63: log only when the slot value changes (first 8 per tag). */ +void d63SlotCheck(const char *tag) +{ + if (!getenv("GE_D63")) return; + static const char *s_tags[16]; + static u32 s_vals[16]; + static int s_n = 0, s_count = 0; + u32 cur = *(const u32 *)0x7012EC38; + int i; + for (i = 0; i < s_n; i++) { + if (s_tags[i] == tag) { + if (cur != s_vals[i] && s_count < 64) { + sysLogPrintf(LOG_NOTE, "D63 check %s: %08x -> %08x", tag, s_vals[i], cur); + s_vals[i] = cur; + ++s_count; + } + return; + } + } + if (s_n < 16) { s_tags[s_n] = tag; s_vals[s_n] = cur; ++s_n; } +} + +/* TEMP D63: timestamped cross-thread activity ring + watchdog thread. */ +typedef struct { uint64_t t; unsigned long tid; const char *tag; } D63Act; +#define D63_ACT_N 2048 +static D63Act s_d63act[D63_ACT_N]; +static volatile long s_d63actIdx = 0; +static int s_d63on = -1; + +int d63On(void) { if (s_d63on < 0) s_d63on = getenv("GE_D63") != 0; return s_d63on; } + +unsigned long d63Tid(void) +{ +#if defined(PLATFORM_WINDOWS) + return GetCurrentThreadId(); +#else + return (unsigned long)pthread_self(); +#endif +} + +void d63Act(const char *tag) +{ + if (!d63On()) return; + long i = s_d63actIdx++; + s_d63act[i & (D63_ACT_N - 1)] = (D63Act){ sysGetMicroseconds(), d63Tid(), tag }; +} + +static void d63DumpActivity(uint64_t now) +{ + long newest = s_d63actIdx - 1; + int k; + for (k = 47; k >= 0; --k) { + long i = newest - k; + if (i < 0) break; + const D63Act *e = &s_d63act[i & (D63_ACT_N - 1)]; + sysLogPrintf(LOG_NOTE, "D63 act t=%llu d=%lld tid=%lu %s", + (unsigned long long)e->t, + (long long)(e->t >= now ? (long long)(e->t - now) : -(long long)(now - e->t)), + e->tid, e->tag); + } +} + +static void d63WatchdogThread(void *arg) +{ + const u32 *slot = (const u32 *)0x7012EC38; + u32 last = *slot; + int changes = 0; + for (;;) { + u32 cur = *slot; + if (cur != last) { + uint64_t now = sysGetMicroseconds(); + if (changes < 8) { + sysLogPrintf(LOG_NOTE, "D63 WATCHDOG t=%llu word@0x7012ec38 %08x -> %08x", + (unsigned long long)now, last, cur); + d63DumpActivity(now); + } + ++changes; + last = cur; + } +#if defined(PLATFORM_WINDOWS) + Sleep(0); /* yield to same-priority threads */ +#else + sched_yield(); +#endif + } +} + +void d63WatchdogStart(void) +{ + if (!d63On()) return; + static pthread_t th; + pthread_create(&th, NULL, + (void *(*)(void *))d63WatchdogThread, NULL); + pthread_detach(th); + sysLogPrintf(LOG_NOTE, "D63 watchdog started"); +} + +static void d63WatchGunbarrelSlot(void) +{ + if (!d63On()) return; + static u32 s_last = 0; + static int s_init = 0; + static int s_changes = 0; + const u32 *slot = (const u32 *)0x7012EC38; + u32 cur = *slot; + if (!s_init) { s_init = 1; s_last = cur; return; } + if (cur != s_last && s_changes < 64) { + sysLogPrintf(LOG_NOTE, "D63 clobber-watch: word@0x7012ec38 %08x -> %08x tid=%lu changes=%d", + s_last, cur, d63Tid(), s_changes); + s_last = cur; + ++s_changes; + } +} + s32 osSendMesg(OSMesgQueue *mq, OSMesg msg, s32 flag) { PortQueue *pq = portQueueGet(mq); + d62log("SEND", mq, msg); /* TEMP D62 */ + d63Act("send"); /* TEMP D63 */ + d63WatchGunbarrelSlot(); /* TEMP D63 */ + /* TEMP D51: watch sends to the 32-slot queues (sched cmdQ + client Qs) */ + if (getenv("GE_D51") && mq->msgCount == 32 && !(g_viRetraceMQ && mq == g_viRetraceMQ)) { + void *ra = __builtin_return_address(0); + sysLogPrintf(LOG_NOTE, "D51 send32 mq=%p from %p msg=%p valid=%d/%d", + (void *)mq, ra, (void *)msg, mq->validCount, mq->msgCount); + } pthread_mutex_lock(&pq->lock); while (mq->validCount >= mq->msgCount) { if (flag != OS_MESG_BLOCK) { @@ -459,9 +637,26 @@ s32 osSendMesg(OSMesgQueue *mq, OSMesg msg, s32 flag) return 0; } +/* TEMP D60: gfxFrameMsgQ receive watch (env GE_D60=1). The main loop only + * acts on msg type 1 (retrace) / 4 (RSP done); anything else means the + * message object's .type field was clobbered. */ +extern OSMesgQueue gfxFrameMsgQ; /* src/init.c */ +static void d60logRecv(OSMesgQueue *mq, OSMesg m) { + if (mq != &gfxFrameMsgQ || !getenv("GE_D60")) return; + static uint64_t n = 0; + const u16 type = *(const u16 *)m; /* OSScMsg.type */ + if (n < 20 || (n % 300) == 0 || (type != 1 && type != 4)) + sysLogPrintf(LOG_NOTE, "D60 recv #%llu mq=%p msg=%p type=%u", + (unsigned long long)n, (void *)mq, (void *)m, type); + ++n; +} + s32 osRecvMesg(OSMesgQueue *mq, OSMesg *msg, s32 flag) { PortQueue *pq = portQueueGet(mq); + d62log("RECV", mq, 0); /* TEMP D62 */ + d63Act("recv"); /* TEMP D63 */ + d63WatchGunbarrelSlot(); /* TEMP D63 */ pthread_mutex_lock(&pq->lock); while (mq->validCount == 0) { if (flag != OS_MESG_BLOCK) { @@ -484,6 +679,7 @@ s32 osRecvMesg(OSMesgQueue *mq, OSMesg *msg, s32 flag) mq->first = (mq->first + 1) % mq->msgCount; --mq->validCount; if (msg) *msg = m; + d60logRecv(mq, m); /* TEMP D60 */ pthread_cond_signal(&pq->cond); pthread_mutex_unlock(&pq->lock); return 0; @@ -531,12 +727,22 @@ void osViSetEvent(OSMesgQueue *mq, OSMesg msg, u32 retraceCount) g_viRetraceMsg = msg; } +/* TEMP D51: instrument the retrace pacemaker */ +static uint64_t g_viPostCount = 0; static void portPostVIEvent(void) { if (g_viRetraceMQ) { - osSendMesg(g_viRetraceMQ, g_viRetraceMsg, OS_MESG_NOBLOCK); + s32 r = osSendMesg(g_viRetraceMQ, g_viRetraceMsg, OS_MESG_NOBLOCK); + if (++g_viPostCount % 60 == 1 || r != 0) { + sysLogPrintf(r != 0 ? LOG_ERROR : LOG_NOTE, + "D51 vi post #%llu mq=%p msg=%d valid=%d/%d ret=%d", + (unsigned long long)g_viPostCount, (void *)g_viRetraceMQ, + (int)g_viRetraceMsg, g_viRetraceMQ->validCount, + g_viRetraceMQ->msgCount, r); + } } } +/* END TEMP D51 */ void osViSetMode(OSViMode *vm) { @@ -604,11 +810,53 @@ void osAiSetConvert(u32 convert) { (void)convert; } * completion; osPiStartDma() below replicates that, because the game blocks * in romReceiveMesg()/osRecvMesg() after every romCopy(). */ +/* TEMP D60: identify sidecar model reads (env GE_D60=1). srcPA-base is the + * manifest offset; cross-reference data/pcmodels-/manifest.csv for + * the file name. */ +extern uintptr_t pcmodelsSidecarBase(void); +extern uint32_t pcmodelsTotalSize(void); +static void d60logSidecarRead(u32 srcPA, void *dstVA, u32 size) { + if (!getenv("GE_D60")) return; + uintptr_t base = pcmodelsSidecarBase(); + if (base && srcPA >= base && srcPA < base + pcmodelsTotalSize()) + sysLogPrintf(LOG_NOTE, "D60 sidecar read off=%llu size=0x%X dst=%p", + (unsigned long long)(srcPA - base), size, dstVA); +} + +/* TEMP D60/D61: a ROM-read DMA target must land in the game DRAM views + * (V1/V2) or the current thread's stack (texLoad's compbuffer). Anything + * else is unmapped host memory on PC. */ +static FILE *s_d61log = NULL; +static int s_d61opened = 0; + +static int dramHostAddrValid(uintptr_t addr, u32 size) +{ + static const uintptr_t bases[2] = { 0x70000000UL, 0x80000000UL }; + for (int i = 0; i < 2; i++) { + if (addr >= bases[i] && addr + size <= bases[i] + 0x00800000UL) + return 1; + } + /* Any other host-committed region is a legitimate DMA target: .bss/.data + * buffers (e.g. ramrom_data_target), stack compbuffers, sidecar images. + * Truncated wild addresses (0x40xxxxxx from s32 pointer math) are not + * committed, so VirtualQuery still catches them. */ + { + MEMORY_BASIC_INFORMATION mbi; + if (VirtualQuery((LPCVOID)addr, &mbi, sizeof(mbi)) == sizeof(mbi) && + mbi.State == MEM_COMMIT && + (uintptr_t)mbi.BaseAddress <= addr && + (uintptr_t)mbi.BaseAddress + mbi.RegionSize >= addr + size) + return 1; + } + return 0; +} + static void piServiceDma(s32 direction, u32 srcPA, void *dstVA, u32 size) { if (size == 0) return; if (direction == OS_READ) { + d60logSidecarRead(srcPA, dstVA, size); /* TEMP D60 */ if (!romdataCartAddrValid(srcPA, size)) { sysLogPrintf(LOG_WARNING, "osPiStartDma: ROM read out of range " @@ -616,6 +864,50 @@ static void piServiceDma(s32 direction, u32 srcPA, void *dstVA, u32 size) srcPA, size); return; } + /* TEMP D60: validate the DMA target too. The N64 PI happily DMAs to + * any KSEG address; on PC an unmapped target is a wild memcpy. + * Log the whole call chain (romCopy <- doRomCopy <- osPiStartDma) so + * the game-side caller can be symbolicated offline. */ + /* TEMP D61: log every ROM read (dst/src/size). The last line before + * a crash identifies the culprit; dst symbolizes offline via nm. */ + if (!s_d61opened && getenv("GE_D61")) { + s_d61log = fopen("d61dma.log", "w"); + s_d61opened = 1; + } + if (s_d61log) { + static int d61count = 0; + fprintf(s_d61log, "D61 %06d dst=%p src=0x%08X size=0x%X\n", + ++d61count, dstVA, srcPA, size); + fflush(s_d61log); + } + if (!dramHostAddrValid((uintptr_t)dstVA, size)) { + /* No __builtin_return_address here: the caller's frame chain is + * not always walkable (it faulted). Log the raw stack window + * instead — return addresses are in it and symbolicate offline. + * The FATAL below re-dumps RSP/registers via the crash handler. */ + const uint64_t *sp = (const uint64_t *)__builtin_frame_address(0); + char win[1200] = ""; + char *wp = win; + for (int i = 0; i < 32; i++) { + wp += snprintf(wp, win + sizeof(win) - (wp - win), + " %p", (void *)sp[i]); + } + { + PVOID tlow = NULL, thigh = NULL; + GetCurrentThreadStackLimits(&tlow, &thigh); + sysLogPrintf(LOG_ERROR, + "D60 thread stack: %p..%p dst-in-stack=%d\n", + (void *)tlow, (void *)thigh, + ((uintptr_t)dstVA >= (uintptr_t)tlow && + (uintptr_t)dstVA < (uintptr_t)thigh)); + } + sysLogPrintf(LOG_ERROR, + "D60 BAD DMA TARGET dst=%p src=0x%08X size=0x%X " + "stack@rbp:%s\n", + dstVA, srcPA, size, win); + sysFatalError("D60: ROM-read target %p + 0x%X not host-mapped " + "(src=0x%08X)", dstVA, size, srcPA); + } memcpy(dstVA, (const void *)(uintptr_t)srcPA, size); } else { /* OS_WRITE: the game never writes the cart (saves go to EEPROM via @@ -804,12 +1096,51 @@ void osSpTaskLoad(OSTask *t) { (void)t; /* nothing to load on the host */ } void osSpTaskStartGo(OSTask *t) { + d63Act(t->t.type == M_AUDTASK ? "audtask" : "spgo"); /* TEMP D63 */ if (t->t.type == M_AUDTASK) { /* Phase 3: execute the audio ucode against t->t.data_ptr (an Acmd * list). For now the task simply completes; libaudio's amMain still * runs its per-frame bookkeeping. */ } else { /* Graphics task: run the software RSP on the display list. */ + /* TEMP D63: log every gfx task's DL pointer (env GE_D63=1) */ + if (getenv("GE_D63")) { + unsigned long tid = 0; +#if defined(PLATFORM_WINDOWS) + tid = GetCurrentThreadId(); +#else + tid = (unsigned long)pthread_self(); +#endif + sysLogPrintf(LOG_NOTE, "D63 gfx task data_ptr=%p data_size=%d tid=%lu slot=%08x", + (void *)t->t.data_ptr, (int)t->t.data_size, tid, + *(const u32 *)0x7012EC38); + } +#if defined(PORT) + /* TEMP D63: find when the fatal G_DL value first appears in the master DL */ + { + static int s_d63seen = 0; + if (getenv("GE_D63") && !s_d63seen) { + const uint32_t *p = (const uint32_t *)t->t.data_ptr; + size_t nwords = t->t.data_size / 4; + static int s_d63scancount = 0; + if ((s_d63scancount++ % 100) == 0) + sysLogPrintf(LOG_NOTE, "D63 scan running: ptr=%p nwords=%zu", (void *)p, nwords); + for (size_t i = 1; i + 1 < nwords; i += 2) { + if (p[i] == 0x0012EC38u) { + s_d63seen = 1; + sysLogPrintf(LOG_NOTE, "D63 fatal-value first seen: frame=%d off=0x%zx words:", + g_framesRendered + 1, i * 4); + for (int k = -3; k <= 3; ++k) { + long j = (long)i + 2 * k; + if (j >= 0 && j + 1 < (long)nwords) + sysLogPrintf(LOG_NOTE, "D63 [%d] %08x %08x", k, p[j], p[j+1]); + } + break; + } + } + } + } +#endif uint64_t t0 = sysGetMicroseconds(); videoStartFrame(); gfx_run((Gfx *)t->t.data_ptr); diff --git a/port/src/n64stubs.c b/port/src/n64stubs.c index 03d9f33b..0fc3ff12 100644 --- a/port/src/n64stubs.c +++ b/port/src/n64stubs.c @@ -135,6 +135,7 @@ void osSyncPrintf(const char *fmt, ...) va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap); + fflush(stderr); /* keep probe lines intact across crashes (block-buffered when redirected) */ } /* --- VI manager (src/libultrare/io/vimgr.c, not built for PC) ----------- */ diff --git a/port/src/pcmodels.c b/port/src/pcmodels.c index 67894136..de58b6df 100644 --- a/port/src/pcmodels.c +++ b/port/src/pcmodels.c @@ -234,3 +234,8 @@ uint32_t pcmodelsTotalSize(void) { return s_loaded ? s_total : 0; } + +uintptr_t pcmodelsSidecarBase(void) +{ + return s_loaded ? (uintptr_t)s_base : 0; +} diff --git a/port/src/romdata.c b/port/src/romdata.c index f5a21370..686d23b4 100644 --- a/port/src/romdata.c +++ b/port/src/romdata.c @@ -102,6 +102,8 @@ static int romHeaderValid(const u8 *h, char *err, size_t errsz) return 1; } +static u32 romdataBswap32(u32 v); /* defined below; used by the D55 fixup */ + int romdataInit(void) { char err[128] = ""; @@ -175,6 +177,23 @@ int romdataInit(void) tok, romSize, CART_BASE, sideTotal ? ", + model sidecars" : ""); pcmodelsLoadSidecars(CART_BASE, romSize); + + /* D55 (docs/PCPortResearch.md): the RLE folder-menu background at + * `unknown2` (romassets_.s) is stored in the ROM with a + * big-endian w/h header (01 B8 01 2B = 440x299), but + * rle_expand_8bit() reads it little-endian. The N64 build embeds + * this asset into .data via assets/romfiles2.s from an extracted + * .bin whose header is byte-swapped (title2.c hardcodes 440x299 + * row rendering, and this is the only decodable 440x299 RLE + * stream in the ROM), so make the cart copy match it: swap the + * w/h word in place. The guard makes the fixup a no-op if a + * region's copy already reads as a plausible LE size. */ + { + extern void *unknown2; /* absolute cart address */ + u32 *hdr = (u32 *)&unknown2; + if ((u16)*hdr > 512 || (u16)(*hdr >> 16) > 512) + *hdr = romdataBswap32(*hdr); + } return 0; } if (at) @@ -372,6 +391,23 @@ void romdataFixupMusicSeqTable(u8 *blob, u32 blobSize) } } +/* + * D54 (docs/PCPortResearch.md): a decompressed compact-sequence (cseq) file + * starts with struct ALCMidiHdr — 16 big-endian u32 trackOffset values plus a + * big-endian u32 division. N64 reads them natively; on a little-endian host + * alCSeqNew() byte-swaps them (track 0 offset 0x44 becomes 0x44000000), + * builds "valid" curLoc pointers ~1 GB past the buffer, and __getTrackByte() + * faults. The rest of the stream is byte-oriented (varlens, MIDI bytes, BE + * loop offsets assembled byte-by-byte), so only these 17 words need fixing. + */ +void romdataFixupCseq(u8 *blob) +{ + u32 *hdr = (u32 *)blob; + for (u32 i = 0; i < 17; i++) { + hdr[i] = romdataBswap32(hdr[i]); + } +} + /* * D50 (docs/PCPortResearch.md): a language bank is [u32 offsets x N][text]. * Offsets are big-endian and point into the text region; langGet() reads them @@ -464,8 +500,11 @@ void romdataFixupFont(u8 *blob, u32 n64Size) o = romdataBswap32(*(const u32 *)(s + 20)); if (o != 0 && pixStart != 0 && o >= pixStart) o = pcPixOff + (o - pixStart); - *(u32 *)(d + 20) = o; /* zero-extended relative offset */ - *(u32 *)(d + 24) = 0; + /* D51: on PC pixeldata is a u64 at d+24 (fontchar needs 8-align, + * sizeof=32) — the N64 u32-at-d+20 layout does not hold. Write the + * offset into the pointer and zero-extend it. */ + *(u32 *)(d + 24) = o; + *(u32 *)(d + 28) = 0; } } diff --git a/src/bondconstants.h b/src/bondconstants.h index 45bd0ecd..7af1c6bc 100644 --- a/src/bondconstants.h +++ b/src/bondconstants.h @@ -3022,6 +3022,19 @@ enum CCRMLUT BODY_Male_Pierce_Bond_Tuxedo, BODY_Natalya_Jungle_Fatigues, BODIES_MAX +#ifdef PORT + /* D65b: same class of bug as D65 (HEADS). ROM tables store 0xFFFFFFFF in + * body fields as a "no body" sentinel and the cast-end check compares + * `intro_char_table[f].body < 0`. The N64 toolchain gave this enum a + * signed underlying type, so the sentinel read as -1. PC GCC 16 picks + * unsigned int for enums whose enumerators are all non-negative, which + * makes `body < 0` always false and the compiler deletes the reset + * branch entirely — the cast screen then renders the terminator entry + * (text1 == 0) and langGet(0) dereferences a NULL bank. A negative + * enumerator forces the signed underlying type without changing any + * existing value or BODIES_MAX. */ + , BODY_FIXED = -1 +#endif } BODIES; typedef enum GENDER @@ -3082,8 +3095,19 @@ enum CCRMLUT HEAD_COUNT = HEAD_END - HEAD_START, // Total number of heads HEAD_MALE_COUNT = HEAD_F_START - HEAD_START, // Total number of usable randon male heads HEAD_FEMALE_COUNT = HEAD_BOND_START - HEAD_F_START, // Total number of usable randon female heads +#ifdef PORT + /* D65: the N64 toolchain treated these sentinels as signed (HEAD_FIXED == -1), + * so `head >= 0` guards compiled to real branches. PC GCC picks unsigned int + * as this enum's underlying type (0xFFFFFFFF > INT_MAX), which makes every + * `head >= 0` always true and turns c_item_entries[HEAD_FIXED] into a wild + * 64-bit OOB read (SIGSEGV in init_menu18_displaycast). Negative literals + * keep the bit pattern identical while restoring the signed semantics. */ + HEAD_FIXED = -1, + HEAD_RANDOM = -97 +#else HEAD_FIXED = 0xFFFFFFFF, HEAD_RANDOM = 0xFFFFFF9F +#endif } HEADS; //Canonical name and style "ai_destroyobj 2 : (def->obj == PROP_ELVIS_SAUCER)\n" diff --git a/src/bondtypes.h b/src/bondtypes.h index b1714ba4..76fdbf12 100644 --- a/src/bondtypes.h +++ b/src/bondtypes.h @@ -1493,7 +1493,21 @@ typedef union * List of length model->obj->numMatrices dynamically allocated. */ RenderPosView *render_pos; /*0x0c*/ +#ifdef PORT + /* PC port (D52): `datas` is the model's RW-data pool itself, not an + * array of pointers. RwDataIndex values are 4-byte WORD offsets: + * modelCalculateRwDataIndexes() accumulates sizeof(record)/4 and + * sizes the pool as round16(numRecords*4) bytes (modelmgrInstantiate* + *ModelWithAnim). On N64, `union ModelRwData **` strides 4B so + * &datas[i] lands on record i; on PC the same type strides 8B and + * every non-zero index addresses the wrong record (crash in + * modelInitRwData at the first BSP node). u32* keeps the N64 byte + * math exactly; use sites cast to the record type. Layout is a + * single pointer either way, so struct Model is unchanged. */ + u32 *datas; +#else union ModelRwData **datas; // array of pointers to modeldata structs /*0x10*/ +#endif f32 scale; /*0x14*/ struct Model *attachedto; /*0x18*/ diff --git a/src/game/bg.c b/src/game/bg.c index bc77b5c3..12aaeba2 100644 --- a/src/game/bg.c +++ b/src/game/bg.c @@ -2337,6 +2337,16 @@ s32 bgLoadRoomPrimaryGdl(s32 roomnum, u8 *dst, s32 allocsize) roominfo->ptr_expanded_mapping_info = dst; roominfo->usize_primary_DL_binary = expanded_size; +#if defined(PORT) + /* TEMP D63: log primary GDL load result (env GE_D63=1) */ + if (getenv("GE_D63")) { + u32 *w = (u32 *)dst; + osSyncPrintf("D63 bgLoadRoomPrimaryGdl room=%d dst=%p expanded=%d final=%d w0..3=(%08x,%08x,%08x,%08x)\n", + roomnum, (void *)dst, (int)expanded_size, (int)size, + w[0], w[1], w[2], w[3]); + } +#endif + // Return the uncompressed data size. return expanded_size; } @@ -2663,8 +2673,12 @@ void bgRoomsTickUnload(void) * Ensures the room's bg data is loaded if budget allows, then appends its display list. * Also resets the age of rendered rooms so bgRoomsTickUnload won't unload it. */ +static int d63bgprimarycount = 0; + Gfx *bgRenderRoomPrimary(Gfx *gdl, s32 room_index) { + if (getenv("GE_D63") && (d63bgprimarycount++ % 500 == 0)) + osSyncPrintf("D63 bgRenderRoomPrimary called count=%d room=%d\n", d63bgprimarycount, room_index); if (room_index >= g_MaxNumRooms) { return gdl; @@ -2689,6 +2703,16 @@ Gfx *bgRenderRoomPrimary(Gfx *gdl, s32 room_index) { gdl = applyRoomMatrixToDisplayList(gdl, room_index); +#if defined(PORT) + /* TEMP D63: log primary GDL at render time (env GE_D63=1) */ + if (getenv("GE_D63")) { + u32 *w = (u32 *)g_BgRoomInfo[room_index].ptr_expanded_mapping_info; + osSyncPrintf("D63 bgRenderRoomPrimary room=%d ptr=%p usize=%d w0..3=(%08x,%08x,%08x,%08x)\n", + room_index, (void *)g_BgRoomInfo[room_index].ptr_expanded_mapping_info, + (int)g_BgRoomInfo[room_index].usize_primary_DL_binary, + w[0], w[1], w[2], w[3]); + } +#endif gSPSegment(gdl++, SPSEGMENT_BG_VTX, OS_K0_TO_PHYSICAL(g_BgRoomInfo[room_index].vertices)); gSPDisplayList(gdl++, OS_K0_TO_PHYSICAL(g_BgRoomInfo[room_index].ptr_expanded_mapping_info)); @@ -2724,6 +2748,16 @@ Gfx *bgRenderRoomSecondary(Gfx *gdl, s32 room_index) { gdl = applyRoomMatrixToDisplayList(gdl, room_index); +#if defined(PORT) + /* TEMP D63: log secondary GDL at render time (env GE_D63=1) */ + if (getenv("GE_D63")) { + u32 *w = (u32 *)g_BgRoomInfo[room_index].ptr_secondary_expanded_mapping_info; + osSyncPrintf("D63 bgRenderRoomSecondary room=%d ptr=%p usize=%d w0..3=(%08x,%08x,%08x,%08x)\n", + room_index, (void *)g_BgRoomInfo[room_index].ptr_secondary_expanded_mapping_info, + (int)g_BgRoomInfo[room_index].usize_secondary_DL_binary, + w[0], w[1], w[2], w[3]); + } +#endif gSPSegment(gdl++, SPSEGMENT_BG_VTX, OS_K0_TO_PHYSICAL(g_BgRoomInfo[room_index].vertices)); gSPDisplayList(gdl++, OS_K0_TO_PHYSICAL(g_BgRoomInfo[room_index].ptr_secondary_expanded_mapping_info)); diff --git a/src/game/blood_animation.c b/src/game/blood_animation.c index 3fc956aa..3214db37 100644 --- a/src/game/blood_animation.c +++ b/src/game/blood_animation.c @@ -193,7 +193,19 @@ u8 die_blood_image_1[] = { 0xE5, 0xE5, 0xE5, 0xE5 }; +#ifdef PORT +/* D64: the N64 build places this sentinel in the same section directly after + * die_blood_image_1[]; the guard `bloodImgNxt < &die_blood_image_end` and the + * completion test rely on that adjacency (the value is never read — only its + * address). On PC a zero-init symbol lands in .bss, a different section ~1 MB + * away, so the guard never fires and the RLE decoder runs past the array into + * adjacent .data, writing unbounded garbage (clobbered the gun-barrel sub-DL + * at +0x200 — see docs/PCPortResearch.md §F/D64). Define it as one-past-the- + * end of the array instead. Only used in this TU. */ +#define die_blood_image_end (*(const u8 *)(die_blood_image_1 + sizeof(die_blood_image_1))) +#else u8 die_blood_image_end = 0; +#endif Gfx *clear_framebuffer_black(Gfx *gdl) { @@ -219,6 +231,12 @@ Gfx *sub_GAME_7F01C1A4(Gfx *gdl) { s32 die_blood_image_routine(s32 arg0) { s8 sp37; u8* temp_v0_2; +#ifdef PORT + /* TEMP D63: catch VTX-pool overflow into the gun-barrel buffer. */ + extern u8 *g_GfxMemPos; + if (getenv("GE_D63")) + osSyncPrintf("D63 blood-routine arg=%d mempos=%p\n", arg0, (void *)g_GfxMemPos); +#endif if (arg0 == 0) { g_CurrentPlayer->bloodImgCur = die_blood_image_1; @@ -228,14 +246,39 @@ s32 die_blood_image_routine(s32 arg0) { } } +#ifdef PORT + extern void d63SlotCheck(const char *tag); /* TEMP D63 */ +#endif g_CurrentPlayer->bloodImgIdx = (1 - g_CurrentPlayer->bloodImgIdx); g_CurrentPlayer->bloodImgBufPtrArray[g_CurrentPlayer->bloodImgIdx] = dynAllocate(BLOOD_IMG_WIDTH * BLOOD_IMG_HEIGHT); temp_v0_2 = dynAllocate(BLOOD_IMG_WIDTH * BLOOD_IMG_HEIGHT); +#ifdef PORT + if (getenv("GE_D63")) + osSyncPrintf("D63 blood: buf=%p temp=%p\n", + (void *)g_CurrentPlayer->bloodImgBufPtrArray[g_CurrentPlayer->bloodImgIdx], + (void *)temp_v0_2); + d63SlotCheck("blood-a-alloc"); +#endif g_CurrentPlayer->bloodImgNxt = decrypt_bleeding_animation_data(g_CurrentPlayer->bloodImgCur, BLOOD_IMG_WIDTH, BLOOD_IMG_HEIGHT, temp_v0_2, &sp37); +#ifdef PORT + d63SlotCheck("blood-b-decrypt"); +#endif bloodImgTranspose(temp_v0_2, BLOOD_IMG_WIDTH, BLOOD_IMG_HEIGHT, g_CurrentPlayer->bloodImgBufPtrArray[g_CurrentPlayer->bloodImgIdx]); +#ifdef PORT + d63SlotCheck("blood-c-transpose"); +#endif sub_GAME_7F01D02C(g_CurrentPlayer->bloodImgBufPtrArray[g_CurrentPlayer->bloodImgIdx], BLOOD_IMG_WIDTH, g_CurrentPlayer->bloodImgBufPtrArray[g_CurrentPlayer->bloodImgIdx]); +#ifdef PORT + d63SlotCheck("blood-d-d02c"); +#endif sub_GAME_7F01CEEC(g_CurrentPlayer->bloodImgBufPtrArray[g_CurrentPlayer->bloodImgIdx], BLOOD_IMG_WIDTH, g_CurrentPlayer->bloodImgBufPtrArray[g_CurrentPlayer->bloodImgIdx]); +#ifdef PORT + d63SlotCheck("blood-e-ceec"); +#endif sub_GAME_7F01CC94(g_CurrentPlayer->bloodImgBufPtrArray[g_CurrentPlayer->bloodImgIdx], BLOOD_IMG_WIDTH * BLOOD_IMG_HEIGHT, g_CurrentPlayer->bloodImgBufPtrArray[g_CurrentPlayer->bloodImgIdx]); +#ifdef PORT + d63SlotCheck("blood-f-cc94"); +#endif return (g_CurrentPlayer->bloodImgNxt >= &die_blood_image_end); } diff --git a/src/game/bondview2.c b/src/game/bondview2.c index 677ea82b..c289bb89 100644 --- a/src/game/bondview2.c +++ b/src/game/bondview2.c @@ -3109,10 +3109,31 @@ void sub_GAME_7F07E7CC(void) { return_null(); } #endif +#ifdef PORT + /* PC port (D56, docs/PCPortResearch.md): the watch Model is embedded in + * struct player at N64 +0x230 and its RW-data pool at +0x2EC. On x86-64 + * the pointer fields before 0x230 shift the region to +0x24C (probe: + * vsize.c, not 8-aligned — unaligned pointer stores are fine on x86-64, + * D53.2), and the grown Model (0xE8 vs 0xBC) leaves only 0x9C of the N64 + * 0xC8-byte pool capacity in-struct — so take the Model by field name and + * host the pool in a static buffer of the N64 capacity. All rwdata access + * goes through Model.datas (set by modelInit), so nothing else references + * the old raw offsets. */ + { + static u8 watchRwPool[0xC8]; /* N64: embedded at player+0x2EC */ + Model *watch = (Model *)&g_CurrentPlayer->something_with_watch_object_instance; + + animInit(watch, itemheader, (u32 *)watchRwPool); + modelSetScale(watch, c_item_entries[41].scale * 0.10000001f); + modelSetAnimation(watch, (ModelAnimation *)&ptr_animation_table->data[(s32)&ANIM_DATA_bond_watch], 0, 0.0f, 0.5f * watch_transition_time, 0.0f); + g_CurrentPlayer->step_in_view_watch_animation = 0; /* N64: *(s32*)(player+0x220) */ + } +#else animInit((Model *)((u8 *)g_CurrentPlayer + 0x230), itemheader, (u32 *)((u8 *)g_CurrentPlayer + 0x2ec)); modelSetScale((Model *)((u8 *)g_CurrentPlayer + 0x230), c_item_entries[41].scale * 0.10000001f); modelSetAnimation((Model *)((u8 *)g_CurrentPlayer + 0x230), (ModelAnimation *)&ptr_animation_table->data[(s32)&ANIM_DATA_bond_watch], 0, 0.0f, 0.5f * watch_transition_time, 0.0f); *(s32 *)((u8 *)g_CurrentPlayer + 0x220) = 0; +#endif } diff --git a/src/game/dyn.c b/src/game/dyn.c index ec59cc8c..d76aa017 100644 --- a/src/game/dyn.c +++ b/src/game/dyn.c @@ -81,6 +81,23 @@ s32 dynGetFreeGfx2(Gfx *gdl) { */ Vtx *dynAllocateVertices(s32 count) { +#ifdef PORT + /* TEMP D63: catch the gun-barrel sub-DL clobber with caller context. */ + if (getenv("GE_D63")) { + static u32 s_last = 0; + static int s_init = 0; + static int s_changes = 0; + const u32 *slot = (const u32 *)0x7012EC38; + u32 cur = *slot; + if (!s_init) { s_init = 1; s_last = cur; } + else if (cur != s_last && s_changes < 64) { + osSyncPrintf("D63 vtx-alloc clobber: word@0x7012ec38 %08x -> %08x caller=%p mempos=%p\n", + s_last, cur, __builtin_return_address(0), (void *)g_GfxMemPos); + s_last = cur; + ++s_changes; + } + } +#endif void *ptr = g_GfxMemPos; g_GfxMemPos += count * sizeof(Vtx); return ptr; @@ -104,6 +121,10 @@ Light *dynAllocateLights(s32 count) } void *dynAllocate(s32 size) { +#ifdef PORT + extern void d63Act(const char *tag); /* TEMP D63 */ + d63Act("dyn"); +#endif void *ptr = g_GfxMemPos; size = ALIGN16_a(size); g_GfxMemPos += size; @@ -111,6 +132,28 @@ void *dynAllocate(s32 size) { } void dynSwapBuffers(void) { +#ifdef PORT + /* TEMP D63: log buffer bounds + bump position each frame */ + if (getenv("GE_D63")) { + static int d63dyncount = 0; + if ((d63dyncount++ % 100) == 0 || g_GfxMemPos > g_VtxBuffers[g_GfxActiveBufferIndex + 1]) { + osSyncPrintf("D63 dynSwap #%d active=%d gfx=[%p..%p) vtx=[%p..%p) mempos=%p\n", + d63dyncount, (int)g_GfxActiveBufferIndex, + (void *)g_GfxBuffers[g_GfxActiveBufferIndex], + (void *)g_GfxBuffers[g_GfxActiveBufferIndex + 1], + (void *)g_VtxBuffers[g_GfxActiveBufferIndex], + (void *)g_VtxBuffers[g_GfxActiveBufferIndex + 1], + (void *)g_GfxMemPos); + extern MemoryPool g_mempPools[]; + for (int b = 0; b < 8; b++) { + if (g_mempPools[b].start || g_mempPools[b].end) + osSyncPrintf("D63 pool[%d] start=%p end=%p pos=%p prevpos=%p\n", + b, (void *)g_mempPools[b].start, (void *)g_mempPools[b].end, + (void *)g_mempPools[b].pos, (void *)g_mempPools[b].prevpos); + } + } + } +#endif g_GfxActiveBufferIndex = (g_GfxActiveBufferIndex ^ 1); g_GfxRequestedDisplayList = FALSE; g_GfxMemPos = g_VtxBuffers[g_GfxActiveBufferIndex]; diff --git a/src/game/front.c b/src/game/front.c index 24d49acf..77abfe86 100644 --- a/src/game/front.c +++ b/src/game/front.c @@ -1402,16 +1402,39 @@ u32 frontCheckCursorOnNextTab(void) //******************************************************************************************************** //LEGAL SCREEN //******************************************************************************************************** +#ifdef PORT +/* TEMP D63: log each intro-menu init that reuses ptr_logo_and_walletbond_DL, + * with the word at base+0x200 (the gun-barrel sub-DL slot) before/after. */ +static void d63MenuProbe(const char *menu) +{ + if (!getenv("GE_D63") || !ptr_logo_and_walletbond_DL) return; + u32 before = *(u32 *)(ptr_logo_and_walletbond_DL + 0x200); + osSyncPrintf("D63 menu init %s: base=%p word@+0x200 before=%08x\n", menu, (void *)ptr_logo_and_walletbond_DL, before); +} +static void d63MenuProbeAfter(const char *menu) +{ + if (!getenv("GE_D63") || !ptr_logo_and_walletbond_DL) return; + u32 after = *(u32 *)(ptr_logo_and_walletbond_DL + 0x200); + osSyncPrintf("D63 menu init %s: word@+0x200 after=%08x\n", menu, after); +} +#endif + void init_menu00_legalscreen(void) { s32 padding; struct coord3d pos; +#ifdef PORT + d63MenuProbe("legalscreen"); +#endif musicTrack1Stop(); maybe_is_in_menu = TRUE; g_MenuTimer = 0; pos = legalpage_pos; load_object_fill_header(PitemZ_entries[PROP_LEGALPAGE].header, PitemZ_entries[PROP_LEGALPAGE].filename, ptr_logo_and_walletbond_DL, 0x3c000, 0); +#ifdef PORT + d63MenuProbeAfter("legalscreen"); +#endif modelCalculateRwDataLen(PitemZ_entries[PROP_LEGALPAGE].header); logoinst = modelmgrInstantiateModel(PitemZ_entries[PROP_LEGALPAGE].header); @@ -1612,7 +1635,13 @@ void init_menu01_nintendo(void) g_MenuTimer = 0; pos = nintendologo_pos; +#ifdef PORT + d63MenuProbe("nintendologo"); +#endif load_object_fill_header(PitemZ_entries[PROP_NINTENDOLOGO].header, PitemZ_entries[PROP_NINTENDOLOGO].filename, ptr_logo_and_walletbond_DL, 0x3c000, 0); +#ifdef PORT + d63MenuProbeAfter("nintendologo"); +#endif modelCalculateRwDataLen(PitemZ_entries[PROP_NINTENDOLOGO].header); logoinst = modelmgrInstantiateModel(PitemZ_entries[PROP_NINTENDOLOGO].header); modelSetScale(logoinst, 1.0f); @@ -1793,7 +1822,13 @@ Gfx *constructor_menu01_nintendo(Gfx *DL) */ void init_menu02_rarelogo(void) { +#ifdef PORT + d63MenuProbe("rarelogo"); +#endif setupRarewareLogoData(ptr_logo_and_walletbond_DL, 0x78000); +#ifdef PORT + d63MenuProbeAfter("rarelogo"); +#endif sndPlaySfx(g_musicSfxBufferPtr, RARELOGO_SFX, 0); } @@ -1839,9 +1874,22 @@ Gfx * constructor_menu02_rareware(Gfx * DL) { //GUNBARREL //******************************************************************************************************** void init_menu03_gunbarrel(void) { +#ifdef PORT + d63MenuProbe("gunbarrel"); + extern void d63SlotLog(const char *tag); /* TEMP D63 */ +#endif initializeGunBarrelIntro(ptr_logo_and_walletbond_DL, 0x78000); +#ifdef PORT + d63SlotLog("gb:after-init"); +#endif musicTrack1Play(M_INTRO); +#ifdef PORT + d63SlotLog("gb:after-music"); +#endif maybe_is_in_menu = TRUE; +#ifdef PORT + d63SlotLog("gb:end"); +#endif } /* @@ -1883,7 +1931,13 @@ void init_menu04_goldeneyelogo(void) g_MenuTimer = 0; pos = goldeneyelogo_pos; +#ifdef PORT + d63MenuProbe("goldeneyelogo"); +#endif load_object_fill_header(PitemZ_entries[PROP_GOLDENEYELOGO].header, PitemZ_entries[PROP_GOLDENEYELOGO].filename, ptr_logo_and_walletbond_DL, 0x3c000, 0); +#ifdef PORT + d63MenuProbeAfter("goldeneyelogo"); +#endif modelCalculateRwDataLen(PitemZ_entries[PROP_GOLDENEYELOGO].header); logoinst = modelmgrInstantiateModel(PitemZ_entries[PROP_GOLDENEYELOGO].header); #ifdef DEBUG @@ -7758,6 +7812,15 @@ void init_menu18_displaycast(void) randomly_selected_intro_animation = randomGetNext() % ((u32) intro_animation_count); body = intro_char_table[intro_character_index].body; head = intro_char_table[intro_character_index].head; +#ifdef PORT + /* TEMP D64: dump the live cast-table entry (suspected .data clobber). */ + if (getenv("GE_D63")) { + const u32 *e = (const u32 *)&intro_char_table[intro_character_index]; + osSyncPrintf("D64 cast-table idx=%d body=%d head=%08x raw=[%08x %08x %08x %08x %08x]\n", + (int)intro_character_index, (int)body, (unsigned)head, + e[0], e[1], e[2], e[3], e[4]); + } +#endif cameraPreset = intro_animation_table[randomly_selected_intro_animation].camera_preset; if (body == BODY_Special_Operations_Uniform) @@ -7824,6 +7887,15 @@ void init_menu18_displaycast(void) bufferRemaining -= allocSize; bufferPtr += allocSize; +#ifdef PORT + /* TEMP D64: dump head/body/index state right before the wild access. */ + if (getenv("GE_D63")) { + extern u32 num_male_heads, num_female_heads; + osSyncPrintf("D64 pre-citem idx=%d body=%d head=%08x nmh=%u nfh=%u\n", + (int)intro_character_index, (int)body, (unsigned)head, + num_male_heads, num_female_heads); + } +#endif if (head >= 0) { headHeader = c_item_entries[head].header; @@ -8009,15 +8081,27 @@ void interface_menu18_displaycast(void) } f = intro_character_index; - +#ifdef PORT + /* TEMP D65: trace the cast-end decision. */ + if (getenv("GE_D63")) + osSyncPrintf("D65 cast-end f=%d body=%d\n", (int)f, (int)intro_char_table[f].body); +#endif if (intro_char_table[f].body < 0) { intro_character_index = 0; f = 0; +#ifdef PORT + if (getenv("GE_D63")) + osSyncPrintf("D65 cast-end RESET idx=0\n"); +#endif } if (intro_character_index > 0) { +#ifdef PORT + if (getenv("GE_D63")) + osSyncPrintf("D65 cast-end frontChangeMenu idx=%d\n", (int)intro_character_index); +#endif frontChangeMenu(MENU_DISPLAY_CAST, 1); } else if (full_actor_intro != 0) @@ -8280,6 +8364,18 @@ Gfx *constructor_menu18_displaycast(Gfx *DL) DL = microcode_constructor(DL); +#ifdef PORT + /* TEMP D65: dump index state at the crashing langGet. */ + if (getenv("GE_D63")) { + int i65 = (int)intro_character_index; + osSyncPrintf("D65 ctor idx=%d full=%d\n", i65, (int)full_actor_intro); + if (i65 >= 0 && i65 <= 40) { + const u32 *e = (const u32 *)&intro_char_table[i65]; + osSyncPrintf("D65 ctor raw=[%08x %08x %08x %08x %08x]\n", + e[0], e[1], e[2], e[3], e[4]); + } + } +#endif if (full_actor_intro == FALSE) { text = langGet( (u16)intro_char_table[intro_character_index].text1); diff --git a/src/game/image.c b/src/game/image.c index a29470fd..1ff9c247 100644 --- a/src/game/image.c +++ b/src/game/image.c @@ -98,14 +98,25 @@ s32 D_800492E4[] = { 0, 0, 0, 0, 0, 0, 0 }; +#ifdef PORT +/* D67: match the PORT struct image_entry field order (dataoffset first) — + * see src/game/image.h. */ +#define IMAGE(NAME, SZ, HS, HT, F3, F4, F5, F6) \ + {SZ, HT, HS, F3, F4, F5, F6 }, +#else #define IMAGE(NAME, SZ, HS, HT, F3, F4, F5, F6) \ {HS, HT, SZ, F3, F4, F5, F6 }, +#endif //D:80049300 //need way to calculate size at compile time from external data struct image_entry g_Textures[] = { #include +#ifdef PORT + {0xFFFF, HIT_DEFAULT, HIT_DEFAULT, 0, 0, 0, 0} +#else {HIT_DEFAULT, HIT_DEFAULT,0xFFFF,0,0,0,0} +#endif }; #undef IMAGE @@ -2321,7 +2332,14 @@ void texLoadFromDisplayList(Gfx *gdl, struct texpool *arg1) while (bytes[0] != (u8)G_ENDDL) { // Look for GBI sequence: fd...... abcd.... +#ifdef PORT + /* D68 (docs/PCPortResearch.md): after gimgFixupGlobalimagetable() the + * 0xABCDxxxx word is stored host-order, so the marker bytes sit at + * offsets 6..7 of the Gfx. */ + if (bytes[0] == G_SETTIMG && bytes[6] == 0xcd && bytes[7] == 0xab) +#else if (bytes[0] == G_SETTIMG && bytes[4] == 0xab && bytes[5] == 0xcd) +#endif { texLoad((u32 *)((s32)bytes + 4), arg1); } @@ -2405,6 +2423,14 @@ void texLoad(s32 *updateword, struct texpool *pool) if (TRUE) { +#ifdef PORT + /* TEMP D66: catch oversized texLoad ROM reads. */ + if (getenv("GE_D63")) + osSyncPrintf("D66 texLoad texnum=%d thisoff=%08x nextoff=%08x size=%08x compbuf=%p\n", + g_TexNumToLoad, thisoffset, nextoffset, + (unsigned)(((u32)(nextoffset - thisoffset) + 0x1f) >> 4 << 4), + (void *)alignedcompbuffer); +#endif // Copy the compressed texture to RAM romCopy(alignedcompbuffer, (u32) &_imagesSegmentRomStart + (thisoffset & 0xfffffff8), diff --git a/src/game/image.h b/src/game/image.h index f9f01738..dbc8972b 100644 --- a/src/game/image.h +++ b/src/game/image.h @@ -52,6 +52,30 @@ struct tex { /*0x0c*/ u32 next : 24; }; +#ifdef PORT +/* D67 (docs/PCPortResearch.md): N64 layout reconstruction for struct + * image_entry. The decompiled field order cannot be right: texLoad() reads + * `*(s32*)&entry & 0xFFFFFF` as the data offset (so dataoffset must occupy + * bits 0-23 of word 0), while chrprop.c indexes entries with an 8-byte + * stride (`(u8*)g_Textures + n*8`, so sizeof == 8). Declaring every field + * u32 makes GCC pack the struct into exactly two words on both targets, and + * putting dataoffset first satisfies the raw word read. The IMAGE() macro + * initializer order is adjusted to match under PORT (see image.c). + * NOTE: chrprop.c/propobj.c also read `((u8*)&entry)[0] & 0xf` as a hit + * type; with this layout that yields the low nibble of dataoffset, which is + * what the N64 binary does too (the .hitSound bitfield is used by the other + * hit-sound paths in gunfire.c). */ +struct image_entry +{ + u32 dataoffset : 24; // runtime: cumulative start offset within the images segment (image_entries_load converts sizes -> offsets) + u32 hitTexture : 4; // HitType-Texture + u32 hitSound : 4; // HitType-Sound (should be enum HIT_TYPE, but it needs to be unsigned) + u32 flag3 : 4; // Detailflag1 used once with value 0x38D2 (S/T offset of detail) + u32 flag4 : 4; + u32 flag5 : 4; // DetailFlag2 difference from detail image, to large image, in terms of 2 to the power of value. It is subtracted from the small one, and absolute value or something. (Zoinkity) + u32 flag6 : 4; +}; +#else struct image_entry { u8 hitSound : 4; // HitType-Sound (should be enum HIT_TYPE, but it needs to be unsigned) @@ -62,6 +86,7 @@ struct image_entry u32 flag5 : 4; // DetailFlag2 difference from detail image, to large image, in terms of 2 to the power of value. It is subtracted from the small one, and absolute value or something. (Zoinkity) u32 flag6 : 4; }; +#endif struct texcacheitem { s16 texturenum; diff --git a/src/game/image_bank.c b/src/game/image_bank.c index ca26e4df..552d9b0b 100644 --- a/src/game/image_bank.c +++ b/src/game/image_bank.c @@ -2,6 +2,9 @@ #include #include #include "image_bank.h" +#ifdef PORT +#include +#endif // bss //8008D0A0 @@ -243,6 +246,13 @@ void texReset(void) romCopy(pGlobalimagetable, &_GlobalimagetableSegmentRomStart, size); +#ifdef PORT + /* D68 (docs/PCPortResearch.md): the ROM copy is N64 big-endian; convert + * the CPU-interpreted u32 fields (IMAGESEG Gfx w1 words and + * sImageTableEntry.index) to host order before any code reads them. */ + gimgFixupGlobalimagetable((u8 *)pGlobalimagetable); +#endif + globalbank_rdram_offset = (u32)pGlobalimagetable + 0xFE000000; genericimage = (void *) (globalbank_rdram_offset + GIMG_OFF(s_genericimage)); impactimages = (void *) (globalbank_rdram_offset + GIMG_OFF(s_impactimages)); @@ -306,4 +316,11 @@ void texReset(void) { texLoad(&scattered_explosions[i], 0); } + +#ifdef PORT + /* D68: explosion.c executes the compiled globalDL_0xNNN shadows via + * g_ExplosionDisplayLists[]; copy the IMAGESEG words that texLoad() + * patched in the ROM copy over into those arrays. */ + gimgSyncCompiledGlobalDLs((u8 *)pGlobalimagetable); +#endif } diff --git a/src/game/initanitable.c b/src/game/initanitable.c index 2187b7e5..edf2fd19 100644 --- a/src/game/initanitable.c +++ b/src/game/initanitable.c @@ -10,7 +10,15 @@ //bss // Where animation frames are saved. Can possibly hold as much as nine, but the game will ever store four at maximum. +#ifdef PORT +/* D59: relocated into DRAM by port/src/dram_syms.s (absolute symbol at + * 0x707FFD30) because loadAnimationFrame() addresses it through s32 fields + * and a (u32) cast — PC BSS (0x140xxxxxx) would truncate to a wild pointer. + * N64 build keeps the original BSS array. */ +extern char animations_frame_buffer[0x2D0]; +#else char animations_frame_buffer[0x2D0]; +#endif // Msg Queue stuff (unused) OSMesgQueue animMsgQ; diff --git a/src/game/initunk_005520.c b/src/game/initunk_005520.c index 4cdf1036..1daad594 100644 --- a/src/game/initunk_005520.c +++ b/src/game/initunk_005520.c @@ -8,8 +8,22 @@ #define MODEL_SPARE_SLOTS 30 #define ANIM_MODEL_SPARE_SLOTS 10 +#ifdef PORT +/* PC port (D57, docs/PCPortResearch.md): two rwdata record structs contain + * pointer fields — ModelRwData_HeadPlaceholderRecord (ModelFileHeader* + + * void*) and ModelRwData_DisplayList_CollisionRecord (Vertex* + Gfx*) — so + * they grow 8 -> 16 bytes on x86-64. modelCalculateRwDataLen() accumulates + * sizeof(record)/4 per node, so every HEAD/DLCOLLISION node adds +2 words + * vs N64, and the largest models (e.g. BODY_Brosnan_Tuxedo: 153 words on PC) + * no longer fit the N64-sized spare pools below. Grow the spare capacities + * with headroom over the measured max; modelmgrInstantiate*() additionally + * falls back to a dynamic slot+pool (D57) if a model still exceeds them. */ +#define MODEL_SPARE_RWDATALEN 0x38 +#define ANIM_MODEL_SPARE_RWDATALEN 0xA8 +#else #define MODEL_SPARE_RWDATALEN 0x14 #define ANIM_MODEL_SPARE_RWDATALEN 0x8c +#endif void modelmgrResetSlotCounts(void) diff --git a/src/game/language.c b/src/game/language.c index 74208bdb..cb2cc25c 100644 --- a/src/game/language.c +++ b/src/game/language.c @@ -404,6 +404,20 @@ void langClearBank(s32 textBank) { u8 * langGet(s32 slotID) { u32 * textbank_ptr = g_LangBanks[slotID >> 10]; /* get the text file bank ID index the text ptr table */ +#ifdef PORT + /* TEMP D65: log NULL-bank derefs (cast screen langGet crash). */ + if (getenv("GE_D63")) { + static int d65first = 1; + if (d65first) { + d65first = 0; + for (int b = 0; b < 45; b++) + if (g_LangBanks[b]) + osSyncPrintf("D65 bank %d = %p\n", b, (void *)g_LangBanks[b]); + } + if (!textbank_ptr) + osSyncPrintf("D65 langGet slotID=0x%08x bank=%d ptr=NULL\n", (unsigned)slotID, slotID >> 10); + } +#endif u32 textslot_offset = textbank_ptr[slotID & 0x03FF]; /* load the textbank ptr table then get the slot's offset */ u32 output_slot = textslot_offset; /* add the text slot offset to the base ptr to get the ptr to text file's slot */ diff --git a/src/game/model.c b/src/game/model.c index ba4dac0d..ce7284d0 100644 --- a/src/game/model.c +++ b/src/game/model.c @@ -1,4 +1,8 @@ #include +#ifdef PORT /* TEMP D51 */ +#include +#include +#endif #include #include "model.h" #include "../rmon.h" /**/ @@ -67,7 +71,14 @@ Model *modelmgrInstantiateModel(ModelFileHeader *header) if (model == NULL) { +#ifdef PORT + /* PC port (D53.2): this buffer is used as a struct Model via the + * same type-pun as g_ModelSlots; on PC that struct is 0xE8 bytes, + * not the N64 0x20 slot size. */ + model = mempAllocBytesInBank(sizeof(struct Model), MEMPOOL_STAGE); +#else model = mempAllocBytesInBank(0x20, MEMPOOL_STAGE); +#endif } if (header->numRecords > 0) @@ -91,9 +102,34 @@ Model *modelmgrInstantiateModel(ModelFileHeader *header) } } } +#ifdef PORT + /* PC port (D57, docs/PCPortResearch.md): pointer-grown rwdata records can + * push a model's word count past the (grown) spare-pool capacity; fall + * back to a dynamic slot+pool exactly like the LvResetting path above. + * The slot is not tracked in g_MaxModelSlots, so it is never reused — + * acceptable: with the D57 capacities this should not trigger. */ + if (model == NULL && header->numRecords > 0) + { + model = mempAllocBytesInBank(sizeof(struct ModelSlot), MEMPOOL_STAGE); + if (model != NULL) + { + rwdata = mempAllocBytesInBank((((header->numRecords * 4) + 0xf) | 0xf) ^ 0xf, MEMPOOL_STAGE); + rwdatalen = header->numRecords; + } + } +#endif if (model != NULL) { +#ifdef PORT /* TEMP D51 */ + if (getenv("GE_D51")) { + static FILE *f = NULL; + if (!f) { f = fopen("d52rw.log", "a"); setvbuf(f, NULL, _IONBF, 0); } + fprintf(f, "INST model=%p header=%p numRecords=%d rwdata=%p rwdatalen=%d lvreset=%d\n", + (void *)model, (void *)header, header->numRecords, + (void *)rwdata, rwdatalen, g_ModelIsLvResetting); + } +#endif modelInit(model, header, rwdata); ((struct ModelSlot *)model)->unk02 = rwdatalen; } @@ -137,7 +173,13 @@ Model *modelmgrInstantiateModelWithAnim(ModelFileHeader *modelFileHeader) if (newModel == NULL) { +#ifdef PORT + /* PC port (D53.2): same pun as g_AnimModelSlots — must hold a full + * struct Model (0xE8 on PC), not the N64 0xC0 anim-slot size. */ + newModel = mempAllocBytesInBank(sizeof(struct AnimModelSlot), MEMPOOL_STAGE); +#else newModel = mempAllocBytesInBank(0xc0, MEMPOOL_STAGE); +#endif } requiredRwdatalen = modelFileHeader->numRecords; @@ -167,10 +209,46 @@ Model *modelmgrInstantiateModelWithAnim(ModelFileHeader *modelFileHeader) break; } } +#ifdef PORT + /* PC port (D57, docs/PCPortResearch.md): same fallback as + * modelmgrInstantiateModel() — a dynamic slot+pool when no spare + * fits the (pointer-grown) rwdata word count. */ + if (newModel == NULL && requiredRwdatalen > 0) + { + newModel = mempAllocBytesInBank(sizeof(struct AnimModelSlot), MEMPOOL_STAGE); + if (newModel != NULL) + { + rwdatas = mempAllocBytesInBank((((requiredRwdatalen * 4) + 0xf) | 0xf) ^ 0xf, MEMPOOL_STAGE); + rwdatalen = requiredRwdatalen; + } + } +#endif +#ifdef PORT /* TEMP D56: log slot-scan outcome incl. failure */ + if (getenv("GE_D56")) { + fprintf(stderr, "[D56] animInstantiate header=%p numRecords=%d maxslots=%d newModel=%p\n", + (void *)modelFileHeader, modelFileHeader->numRecords, + g_MaxAnimModelSlots, (void *)newModel); + if (newModel == NULL) { + for (i2 = 0; i2 < g_MaxAnimModelSlots; i2++) + fprintf(stderr, "[D56] slot%d unk08=%d unk10=%p unk02=%d\n", i2, + g_AnimModelSlots[i2].unk08, g_AnimModelSlots[i2].unk10, + g_AnimModelSlots[i2].unk02); + } + } +#endif } if (newModel != NULL) { +#ifdef PORT /* TEMP D51 */ + if (getenv("GE_D51")) { + static FILE *f = NULL; + if (!f) { f = fopen("d52rw.log", "a"); setvbuf(f, NULL, _IONBF, 0); } + fprintf(f, "INSTA model=%p header=%p numRecords=%d rwdatas=%p rwdatalen=%d lvreset=%d\n", + (void *)newModel, (void *)modelFileHeader, modelFileHeader->numRecords, + (void *)rwdatas, rwdatalen, g_ModelIsLvResetting); + } +#endif animInit(newModel, modelFileHeader, rwdatas); newModel->rwdatalen = rwdatalen; } @@ -218,9 +296,16 @@ void sub_GAME_7F06C418(Vew4s32 *src, Vew4s32 *dst) { } +#ifdef PORT +/* D59: keep the full 64-bit code address (see model.h). */ +void set_vtxallocator(void *param_1) { + vtxallocator = (struct Vertex *(*)(s32 numvertices))param_1; +} +#else void set_vtxallocator(s32 param_1) { vtxallocator = param_1; } +#endif #if defined(LEFTOVERDEBUG) @@ -383,7 +468,12 @@ f32 sub_GAME_7F06C768(Model *objinst) union ModelRwData* modelGetNodeRwData(Model *Objinst, ModelNode *root) { s32 index = 0; +#ifdef PORT + /* D52: word-indexed pool access — see bondtypes.h struct Model.datas. */ + u32 *data = Objinst->datas; +#else union ModelRwData **data = Objinst->datas; +#endif switch (root->Opcode & 0xff) { @@ -440,12 +530,29 @@ union ModelRwData* modelGetNodeRwData(Model *Objinst, ModelNode *root) if ((root->Opcode & 0xFF) == MODELNODE_OPCODE_HEAD) { ModelRwData_HeadPlaceholderRecord *tmp = modelGetNodeRwData(Objinst, root); +#ifdef PORT + data = (u32 *)tmp->RwDatas; +#else data = tmp->RwDatas; +#endif break; } } +#ifdef PORT + /* TEMP D51: trace rwdata pool addressing */ + if (getenv("GE_D51")) { + static FILE *f = NULL; + if (!f) { f = fopen("d52rw.log", "a"); setvbuf(f, NULL, _IONBF, 0); } + fprintf(f, "GND obj=%p datas=%p idx=%d rwdatalen=%d op=%d data=%p res=%p\n", + (void *)Objinst, (void *)data, index, Objinst->rwdatalen, + root->Opcode & 0xff, (void *)data, + (void *)&data[index]); + } + return (union ModelRwData *)&data[index]; +#else return &data[index]; +#endif } @@ -718,6 +825,14 @@ void setsubroty(Model *model, f32 angle) void modelSetScale(Model *objinst, f32 scale) { +#ifdef PORT /* TEMP D56: identify the crashing caller */ + static int d56count = 0; + if (getenv("GE_D56")) { + fprintf(stderr, "[D56] modelSetScale #%d objinst=%p scale=%.4f ret=%p\n", + d56count++, (void *)objinst, scale, + __builtin_return_address(0)); + } +#endif objinst->scale = scale; } @@ -4098,6 +4213,13 @@ void modelRenderNodeGundl(ModelRenderData* renderdata, ModelNode* arg1) modelApplyRenderModeType2(renderdata); } +#if defined(PORT) + /* TEMP D63: log Primary/Secondary before emit (env GE_D63=1) */ + if (getenv("GE_D63") && ((renderdata->flags & 1) && rodata->Primary)) + osSyncPrintf("D63 modelRenderNodeGundl Primary=%p Secondary=%p BaseAddr=%p ModelType=%d\n", + (void *)rodata->Primary, (void *)rodata->Secondary, + (void *)rodata->BaseAddr, (int)rodata->ModelType); +#endif gSPDisplayList(renderdata->gdl++, rodata->Primary); if ((rodata->ModelType == 3) && rodata->Secondary) @@ -6235,7 +6357,12 @@ void modelAttachPart(Model *pmodel, ModelFileHeader *pmodeldef, ModelNode *pnode ModelNode *node; rwdata->ModelFileHeader = cmodeldef; +#ifdef PORT + /* D52: word-indexed pool offset — see bondtypes.h struct Model.datas. */ + rwdata->RwDatas = (void *)&pmodel->datas[pmodeldef->numRecords]; +#else rwdata->RwDatas = &pmodel->datas[pmodeldef->numRecords]; +#endif pnode->Child = cmodeldef->RootNode; diff --git a/src/game/model.h b/src/game/model.h index 5bbde7ca..b9f263c2 100644 --- a/src/game/model.h +++ b/src/game/model.h @@ -18,7 +18,13 @@ void modelAttachHead(Model *, ModelNode*, ModelFileHeader *); void clear_aircraft_model_obj(Model *objinstance); void modelSetDistanceDisabled(s32 param_1); void modelSetDistanceScale(f32 param_1); +#ifdef PORT +/* D59: function pointers must not pass through s32 on PC (the caller passes + * &get_ptr_allocated_block_for_vertices, a 0x140xxxxxx code address). */ +void set_vtxallocator(void *param_1); +#else void set_vtxallocator(s32 param_1); +#endif void modelCalculateScaledRootToOriginDir(Model* model, coord3d* coord); void modelGetScaledRootToOriginDir(Model* model, coord3d* coord); s32 modelFindNodeMtxIndex(ModelNode *node, s32 arg1); diff --git a/src/game/ob.c b/src/game/ob.c index 48ee44d3..17245504 100644 --- a/src/game/ob.c +++ b/src/game/ob.c @@ -1,4 +1,9 @@ #include +#ifdef PORT +/* TEMP D69: probe below uses fprintf/getenv. */ +#include +#include +#endif #include "macro.h" #include "ob.h" #include @@ -142,6 +147,15 @@ void obLoadBGFileBytesAtOffset(u8 *bgname, u8 *target, s32 offset, s32 len) index = fileGetIndex(bgname); fileentry = &file_resource_table[index]; +#ifdef PORT + /* TEMP D69: trace BG-file loads (env GE_D69=1). */ + if (getenv("GE_D69")) + fprintf(stderr, "D69 obLoadBGFile %s idx=%d rom_size=0x%X hw=0x%08X off=0x%X len=0x%X\n", + (const char *)bgname, index, + (unsigned)resource_lookup_data_array[index].rom_size, + (unsigned)(u32)fileentry->hw_address, (unsigned)offset, (unsigned)len); +#endif + if (resource_lookup_data_array[index].rom_size != 0) { //if the size of offset data would exceed file size, loop forever @@ -195,6 +209,15 @@ void obLoadBGFileBytesAtOffset(u8 *bgname, u8 *target, s32 offset, s32 len) index = fileGetIndex(bgname); fileentry = &file_resource_table[index]; +#ifdef PORT + /* TEMP D69: trace BG-file loads (env GE_D69=1). */ + if (getenv("GE_D69")) + fprintf(stderr, "D69 obLoadBGFile %s idx=%d rom_size=0x%X hw=0x%08X off=0x%X len=0x%X\n", + (const char *)bgname, index, + (unsigned)resource_lookup_data_array[index].rom_size, + (unsigned)(u32)fileentry->hw_address, (unsigned)offset, (unsigned)len); +#endif + if (resource_lookup_data_array[index].rom_size != 0) { //if the size of offset data would exceed file size, loop forever diff --git a/src/game/objecthandler.h b/src/game/objecthandler.h index 04e5a5a6..8edc4af1 100644 --- a/src/game/objecthandler.h +++ b/src/game/objecthandler.h @@ -16,6 +16,52 @@ struct bondstruct_unk_op07_related { s32 unk0C; }; +#ifdef PORT +/* PC port (D53.2): the game type-puns these slot structs with struct Model — + * model.c does `(Model *)&g_ModelSlots[i]` / `(Model *)&g_AnimModelSlots[i]` + * and reads/writes Model fields through the slot storage. On N64 the punned + * pairs line up exactly: slot.unk08@0x08 IS the low word of Model.obj (the + * in-use flag — modelInit's `obj = header` marks a slot used, obj=NULL frees + * it), slot.unk10@0x10 IS Model.datas (the RW-data pool pointer), and + * slot.unk02@0x02 IS Model.rwdatalen. + * + * On x86-64 natural alignment moves the Model pointers: obj@0x10, datas@0x20 + * (probe-verified with vsize.c; bondtypes.h has no pack pragma). The old slot + * layout kept N64 offsets, so the pun silently diverged: + * - clear_model_obj() (`model->obj = NULL`, called every frame by + * update_menu00_legalscreen) wrote 8 bytes at @0x10..0x17 — clobbering + * slot.unk10 (the pool pointer lived at @0x10 here); + * - modelmgrCanSlotFitRwdata() read `(Model*)&slot)->datas` at @0x20 + * (garbage: old unk14/unk18) while the assignment read unk10@0x10 — + * check passed on garbage, assignment got NULL; + * - modelInit/animInit write through the whole punned Model (up to PC + * offset 0xE3), overflowing the 0x20/0xC0-byte slots. + * Result: frame-5 SIGSEGV in modelInitRwData (NULL pool deref). + * + * Fix: re-lay the slots out on top of the PC struct Model so every punned + * pair matches again, and pad each slot to sizeof(struct Model) so a full + * Model fits. Member names are kept — only unk02/unk08/unk10 are referenced + * anywhere (whole-tree grep). N64 layout verbatim under #else. */ +struct AnimModelSlot { + s16 unk00; /* 0x00 */ + s16 unk02; /* 0x02 — Model.rwdatalen */ + char pad04[0x0C]; /* 0x04..0x0F: Model.chr@8 (PC) */ + s32 unk08; /* 0x10 — low word of Model.obj = in-use flag */ + char pad14[0x0C]; /* 0x14..0x1F: Model.render_pos@0x18 (PC) */ + void *unk10; /* 0x20 — Model.datas (RW-data pool pointer) */ + char pad28[sizeof(struct Model) - 0x28]; /* rest of the punned Model */ +}; + +struct ModelSlot { + s16 unk00; /* 0x00 */ + s16 unk02; /* 0x02 — Model.rwdatalen */ + char pad04[0x0C]; /* 0x04..0x0F: Model.chr@8 (PC) */ + s32 unk08; /* 0x10 — low word of Model.obj = in-use flag */ + char pad14[0x0C]; /* 0x14..0x1F: Model.render_pos@0x18 (PC) */ + void *unk10; /* 0x20 — Model.datas (RW-data pool pointer) */ + char pad28[sizeof(struct Model) - 0x28]; /* rest of the punned Model */ +}; +#else struct AnimModelSlot { s16 unk00; s16 unk02; @@ -81,6 +127,7 @@ struct ModelSlot { s32 unk18; s32 unk1c; }; +#endif extern struct AnimModelSlot *g_AnimModelSlots; extern struct ModelSlot *g_ModelSlots; diff --git a/src/game/propobj.c b/src/game/propobj.c index c0d41f00..446e0d0a 100644 --- a/src/game/propobj.c +++ b/src/game/propobj.c @@ -7298,7 +7298,12 @@ void sub_GAME_7F04AC20(PropRecord *prop, ModelRenderData *mrData, s32 arg2) if (nodedata != NULL) { +#ifdef PORT + /* D52: word-indexed pool access — see bondtypes.h struct Model.datas. */ + rwdataSlot = (union ModelRwData **)&obj->model->datas[nodedata->DisplayListCollisions.RwDataIndex]; +#else rwdataSlot = &obj->model->datas[nodedata->DisplayListCollisions.RwDataIndex]; +#endif if (nodedata->DisplayListCollisions.Vertices != (Vertex *)*rwdataSlot) { @@ -14340,7 +14345,12 @@ void sub_GAME_7F056690(void) } if (sub_GAME_7F04B590(s1->model->obj, t0) != 0) { +#ifdef PORT + /* D52: word-indexed pool access — see bondtypes.h struct Model.datas. */ + new_var = (union ModelRwData **)&s3->datas[s0->RwDataIndex]; +#else new_var = &s3->datas[s0->RwDataIndex]; +#endif if ((s32)s0->Vertices != (s32)*new_var) { objFreePermanently(s1, 1); diff --git a/src/game/ramromreplay.c b/src/game/ramromreplay.c index 45031175..a3ff7b58 100644 --- a/src/game/ramromreplay.c +++ b/src/game/ramromreplay.c @@ -195,7 +195,12 @@ void load_ramrom_from_devtool(void) // Address 0x7F0BFE5C NTSC void record_player_input_as_packet(struct contsample *arg0, s32 arg1, s32 arg2) { +#ifdef PORT + /* D66: 64-bit pointer width — ramrom_data_target lives above 4 GiB in .bss. */ + uintptr_t temp_t5; +#else s32 temp_t5; +#endif s32 temp_t1; s32 var_a0; s32 var_a2; @@ -206,7 +211,11 @@ void record_player_input_as_packet(struct contsample *arg0, s32 arg1, s32 arg2) u8 t1; s32 others0; +#ifdef PORT + temp_t5 = ALIGN16_a((uintptr_t)&ramrom_data_target[0x1f8]); +#else temp_t5 = ALIGN16_a((s32)&ramrom_data_target[0x1f8]); +#endif temp_t1 = ptr_active_demofile->size_cmds; var_t2 = 0; @@ -432,7 +441,12 @@ void test_if_recording_demos_this_stage_load(enum LEVELID arg0, enum DIFFICULTY { if (g_ramromRecordFlag != 0) { +#ifdef PORT + /* D66: 64-bit pointer width — ramrom_data_target lives above 4 GiB in .bss. */ + ptr_active_demofile = (ramromfilestructure *) ALIGN16_a((uintptr_t)ramrom_data_target); +#else ptr_active_demofile = (ramromfilestructure *) ALIGN16_a((s32)ramrom_data_target); +#endif dword_CODE_bss_8008C5F8 = 0; ptr_active_demofile->stagenum = arg0; ptr_active_demofile->difficulty = arg1; diff --git a/src/game/rsp.c b/src/game/rsp.c index 4eab881a..79309441 100644 --- a/src/game/rsp.c +++ b/src/game/rsp.c @@ -229,6 +229,24 @@ void rspGfxTaskStart(Gfx *firstGdl, Gfx *gdl, s32 arg2, OSMesg rspReplyMsg) OSScTask *sctask; OSTask *task; +#ifdef PORT + extern void d63Act(const char *tag); /* TEMP D63 */ + d63Act("rspstart"); + /* TEMP D63: log task submission (main thread) with tid */ + if (getenv("GE_D63")) { + unsigned long tid = 0; +#if defined(PLATFORM_WINDOWS) + tid = GetCurrentThreadId(); +#else + tid = (unsigned long)pthread_self(); +#endif + osSyncPrintf("D63 rspGfxTaskStart first=%p last=%p size=%d tid=%lu slot=%08x\n", + (void *)firstGdl, (void *)gdl, + (int)((gdl - firstGdl) * sizeof(Gfx)), tid, + *(const u32 *)0x7012EC38); + } +#endif + sctask = &((struct GfxInfo_s *)g_gfxTaskSettingsList)->task; task = &sctask->list; @@ -265,6 +283,12 @@ void rspGfxTaskStart(Gfx *firstGdl, Gfx *gdl, s32 arg2, OSMesg rspReplyMsg) sctask->framebuffer = (void *) ((struct GfxInfo_s *)g_gfxTaskSettingsList)->cfb; +#if defined(PORT) + /* TEMP D63: log the DL range before task submit (env GE_D63=1) */ + if (getenv("GE_D63")) + osSyncPrintf("D63 rspGfxTaskStart firstGdl=%p gdl=%p\n", (void *)firstGdl, (void *)gdl); +#endif + osWritebackDCacheAll(); /* start graphic task */ diff --git a/src/game/title.c b/src/game/title.c index 8d9d8f7e..df89fa52 100644 --- a/src/game/title.c +++ b/src/game/title.c @@ -470,10 +470,37 @@ void initializeGunBarrelIntro(u8 *gfxBuffer, s32 bufferSize) createGunbarrelRenderHole(barrelDisplayListPtr, 0x1E); gunbarrelgfxListPointer = (Gfx*)gfxBuffer; +#ifdef PORT + /* PC port (D58, docs/PCPortResearch.md): sub_GAME_7F01BFF8 emits 31 Gfx + * (2x VTX + 28 TRI + ENDDL). On N64 sizeof(Gfx)==8 so the 0x100 reserve + * covers it (248 B); on PC the Gfx union's trailing `long long` makes + * sizeof(Gfx)==16, so the same DL is 496 B and overflowed the reserve, + * with sub_GAME_7F008DE4's RLE expand then clobbering slots 16-30 (the + * second TRI batch + ENDDL) — the RSP ran into RLE pixel data (opcode + * 0x00, fatal). Reserve 0x200 so the whole DL stays clear of the RLE + * region. (The 0x200 vertex reserve above still fits: 30 Vtx x 16 B = + * 0x1E0.) */ + bufferSize -= 0x200; + gfxBuffer += 0x200; +#else bufferSize -= 0x100; gfxBuffer += 0x100; +#endif +#ifdef PORT + /* PC port (D58, docs/PCPortResearch.md): on N64 barrelDisplayListPtr is a + * physical DRAM address, so +0x80000000 yields the KSEG0 (RSP-visible) + * address embedded in each G_VTX w1 by sub_GAME_7F01BFF8. On PC the same + * buffer is a V1 pointer (0x70xxxxxx, s32-safe per dram.c), and + * +0x80000000 lands at 0xF0xxxxxx — unresolvable by fast3d's seg_addr() + * (SIGSEGV in gfx_sp_vertex reading the vertex array). Rebuild the exact + * N64 K0 value from the physical offset; seg_addr() passes it through to + * the KSEG0 mirror at 0x80000000 (segments 7/8 are never registered, so + * the unmarked-segment path is skipped). */ + sub_GAME_7F01BFF8(gunbarrelgfxListPointer, (Vtx *)(OS_K0_TO_PHYSICAL((void *)barrelDisplayListPtr) | 0x80000000u), 0x1E); +#else sub_GAME_7F01BFF8(gunbarrelgfxListPointer, barrelDisplayListPtr + 0x80000000, 0x1E); +#endif sub_GAME_7F008DE4((u8 **)&gfxBuffer, &bufferSize); // struct copy @@ -575,7 +602,19 @@ void clearChrGunModelInstances(void) /* * Address: 0x7F009254 */ +#ifdef PORT +extern void d63SlotCheck(const char *tag); /* TEMP D63 */ +#endif Gfx *renderGunbarrelEyeIntroSequence (Gfx *gdl) { +#ifdef PORT + d63SlotCheck("gb-render-entry"); + if (getenv("GE_D63")) { + static int n = 0; + if ((++n % 200) == 1) + osSyncPrintf("D63 gb-render call #%d mode=%d slot=%08x\n", + n, (int)gunbarrel_mode, *(const u32 *)0x7012EC38); + } +#endif D_8002A7D0 = (1 - D_8002A7D0); switch (gunbarrel_mode - 2) { @@ -608,10 +647,19 @@ Gfx *renderGunbarrelEyeIntroSequence (Gfx *gdl) { gdl = clear_framebuffer_black(gdl++); #endif gdl = insert_sniper_sight_eye_intro(gdl++); +#ifdef PORT + d63SlotCheck("gb1-after-sight"); +#endif gdl = insert_sight_backdrop_eye_intro(gdl++); +#ifdef PORT + d63SlotCheck("gb1-after-backdrop"); +#endif if (g_TitleX < 600.0f) { gdl = insert_bond_eye_intro(gdl); +#ifdef PORT + d63SlotCheck("gb1-after-bond"); +#endif } g_TitleX -= XDEC3; if (g_TitleX <= -80.0f) { @@ -627,7 +675,13 @@ Gfx *renderGunbarrelEyeIntroSequence (Gfx *gdl) { intro_eye_counter--; if (intro_eye_counter < 0) { gunbarrel_mode++; +#ifdef PORT + d63SlotCheck("gb2-before-blood0"); +#endif die_blood_image_routine(0); +#ifdef PORT + d63SlotCheck("gb2-after-blood0"); +#endif intro_state_blood_animation = 0; intro_eye_counter = 1; } @@ -636,13 +690,25 @@ Gfx *renderGunbarrelEyeIntroSequence (Gfx *gdl) { case 3: intro_eye_counter--; if (intro_eye_counter == 0) { +#ifdef PORT + d63SlotCheck("gb3-before-blood1"); +#endif intro_state_blood_animation = die_blood_image_routine(1); +#ifdef PORT + d63SlotCheck("gb3-after-blood1"); +#endif intro_eye_counter = 2; } gdl = insert_sniper_sight_eye_intro(gdl); gdl = insert_sight_backdrop_eye_intro(gdl); gdl = insert_bond_eye_intro(gdl); +#ifdef PORT + d63SlotCheck("gb3-before-bloodDL"); +#endif gdl = gunbarrelBloodOverlayDL(gdl); +#ifdef PORT + d63SlotCheck("gb3-after-bloodDL"); +#endif if (intro_state_blood_animation != 0) { gunbarrel_mode++; word_CODE_bss_80069584 = 0; diff --git a/src/libultra/audio/csplayer.c b/src/libultra/audio/csplayer.c index 353bb362..3f34f9bb 100644 --- a/src/libultra/audio/csplayer.c +++ b/src/libultra/audio/csplayer.c @@ -43,6 +43,11 @@ #include "seqp.h" #include "cseqp.h" #include "cseq.h" +#ifdef PORT +/* TEMP D54 probe — delete before commit. */ +#include +#include +#endif static ALMicroTime __CSPVoiceHandler(void *node); @@ -442,6 +447,16 @@ static void __CSPHandleMIDIMsg(ALCSPlayer *seqp, ALEvent *event) break; sound = __lookupSoundQuick((ALSeqPlayer*)seqp, key, vel, chan); +#ifdef PORT + /* TEMP D54 probe — delete before commit. */ + if (getenv("GE_D54")) { + fprintf(stderr, "D54 NOTEON seqp=%p chan=%d key=%d vel=%d sound=%p wt=%p inst=%p\n", + (void *)seqp, chan, key, vel, (void *)sound, + sound ? (void *)sound->wavetable : 0, + (void *)seqp->chanState[chan].instrument); + fflush(stderr); + } +#endif ALFlagFailIf(!sound, seqp->debugFlags & NO_SOUND_ERR_MASK, ERR_ALSEQP_NO_SOUND); diff --git a/src/libultra/audio/load.c b/src/libultra/audio/load.c index 8e53c11b..e54fa46d 100644 --- a/src/libultra/audio/load.c +++ b/src/libultra/audio/load.c @@ -23,6 +23,11 @@ #include "synthInternals.h" #include #include +#ifdef PORT +/* TEMP D54 probe — delete before commit. */ +#include +#include +#endif #ifndef MIN # define MIN(a,b) (((a)<(b))?(a):(b)) @@ -359,6 +364,19 @@ alLoadParam(void *filter, s32 paramID, void *param) switch (paramID) { case (AL_FILTER_SET_WAVETABLE): +#ifdef PORT + /* TEMP D54 probe — delete before commit. */ + if (getenv("GE_D54")) { + ALWaveTable *wt = (ALWaveTable *) param; + fprintf(stderr, "D54 SET_WAVETABLE wt=%p base=%p len=%d type=%d book=%p\n", + (void *)wt, + wt ? (void *)wt->base : 0, + wt ? wt->len : -1, + wt ? wt->type : -1, + (wt && wt->type == AL_ADPCM_WAVE) ? (void *)wt->waveInfo.adpcmWave.book : 0); + fflush(stderr); + } +#endif a->table = (ALWaveTable *) param; a->memin = (s32) a->table->base; a->sample = 0; diff --git a/src/libultra/audio/seqplayer.c b/src/libultra/audio/seqplayer.c index f9f57932..a84dbfca 100644 --- a/src/libultra/audio/seqplayer.c +++ b/src/libultra/audio/seqplayer.c @@ -42,6 +42,11 @@ #include "include/assert.h" #include "seqp.h" #include "seq.h" +#ifdef PORT +/* TEMP D54 probe — delete before commit. */ +#include +#include +#endif static ALMicroTime __seqpVoiceHandler(void *node); @@ -458,6 +463,16 @@ void __handleMIDIMsg(ALSeqPlayer *seqp, ALEvent *event) break; sound = __lookupSoundQuick(seqp, key, vel, chan); +#ifdef PORT + /* TEMP D54 probe — delete before commit. */ + if (getenv("GE_D54")) { + fprintf(stderr, "D54 SFXNOTEON seqp=%p chan=%d key=%d vel=%d sound=%p wt=%p inst=%p\n", + (void *)seqp, chan, key, vel, (void *)sound, + sound ? (void *)sound->wavetable : 0, + (void *)seqp->chanState[chan].instrument); + fflush(stderr); + } +#endif ALFlagFailIf(!sound, seqp->debugFlags & NO_SOUND_ERR_MASK, ERR_ALSEQP_NO_SOUND); diff --git a/src/libultra/audio/synthesizer.c b/src/libultra/audio/synthesizer.c index ca8d6f75..7d28d38f 100644 --- a/src/libultra/audio/synthesizer.c +++ b/src/libultra/audio/synthesizer.c @@ -124,6 +124,21 @@ void alSynNew(ALSynth *drvr, ALSynConfig *c) /* * build the parameter update list */ +#ifdef PORT + /* PC port (D54): the N64 code type-puns these slots as various AL*Param + * structs. On x86-64, ALStartParamAlt is 0x28 bytes (two 64-bit pointers: + * next + wave), larger than the slot type ALParam (0x20), so every + * start-voice update wrote `wave` into the neighbouring slot and + * corrupted the free list. Size each slot for the largest param struct, + * preserving the N64 invariant that every param type fits one slot. */ + params = alHeapAlloc(hp, c->maxUpdates, sizeof(ALStartParamAlt)); + drvr->paramList = 0; + for (i = 0; i < c->maxUpdates; i++) { + paramPtr = (ALParam *)((u8 *)params + i * sizeof(ALStartParamAlt)); + paramPtr->next = drvr->paramList; + drvr->paramList = paramPtr; + } +#else params = alHeapAlloc(hp, c->maxUpdates, sizeof(ALParam)); drvr->paramList = 0; for (i = 0; i < c->maxUpdates; i++) { @@ -131,6 +146,7 @@ void alSynNew(ALSynth *drvr, ALSynConfig *c) paramPtr->next = drvr->paramList; drvr->paramList = paramPtr; } +#endif drvr->heap = hp; } diff --git a/src/memp.c b/src/memp.c index 40a177c9..d0a17ee0 100644 --- a/src/memp.c +++ b/src/memp.c @@ -209,6 +209,26 @@ void *mempAllocBytesInBank(u32 bytes, u8 poolnum) if (needmemallocation); +#ifdef PORT + /* TEMP D63: map allocations around the gun-barrel buffer base 0x7012EA38. */ + if (getenv("GE_D63")) { + u8 *a = allocation; + if ((a >= (u8 *)0x70129000 && a < (u8 *)0x7012EA38) || + (a >= (u8 *)0x7012D000 && a < (u8 *)0x7012F500)) { + static u32 s_seen[64]; + static u32 s_sizes[64]; + int i, n = 0; + for (i = 0; i < 64 && s_seen[i]; i++) { + if (s_seen[i] == (u32)a && s_sizes[i] == bytes) return allocation; + } + for (n = 0; n < 64 && s_seen[n]; n++); + if (n < 64) { s_seen[n] = (u32)a; s_sizes[n] = bytes; } + osSyncPrintf("D63 alloc %p size=%x ra=%p\n", (void *)a, bytes, + __builtin_return_address(0)); + } + } +#endif + return allocation; } diff --git a/src/music.c b/src/music.c index dd3756e7..3638cea6 100644 --- a/src/music.c +++ b/src/music.c @@ -880,6 +880,11 @@ void musicTrack1Play(s32 track) romCopy(temp_a0, romAddress, trackSizeBytes); decompressdata(temp_a0, thing.seqData, &hlist); +#ifdef PORT + /* PC port (D54): the decompressed ALCMidiHdr is big-endian; fix it up + * before alCSeqNew reads trackOffset[]/division natively. */ + romdataFixupCseq(thing.seqData); +#endif alCSeqNew(&g_musicXTrack1Seq, g_musicXTrack1SeqData); alCSPSetSeq(g_musicXTrack1SeqPlayer, &g_musicXTrack1Seq); @@ -1069,6 +1074,10 @@ void musicTrack2Play(s32 track) romCopy(temp_a0, romAddress, trackSizeBytes); decompressdata(temp_a0, thing.seqData, &hlist); +#ifdef PORT + /* PC port (D54): see track 1 — fix up the big-endian ALCMidiHdr. */ + romdataFixupCseq(thing.seqData); +#endif alCSeqNew(&g_musicXTrack2Seq, g_musicXTrack2SeqData); alCSPSetSeq(g_musicXTrack2SeqPlayer, &g_musicXTrack2Seq); @@ -1258,6 +1267,10 @@ void musicTrack3Play(s32 track) romCopy(temp_a0, romAddress, trackSizeBytes); decompressdata(temp_a0, thing.seqData, &hlist); +#ifdef PORT + /* PC port (D54): see track 1 — fix up the big-endian ALCMidiHdr. */ + romdataFixupCseq(thing.seqData); +#endif alCSeqNew(&g_musicXTrack3Seq, g_musicXTrack3SeqData); alCSPSetSeq(g_musicXTrack3SeqPlayer, &g_musicXTrack3Seq); diff --git a/src/ramrom.c b/src/ramrom.c index 42b7bdbb..f55590a3 100644 --- a/src/ramrom.c +++ b/src/ramrom.c @@ -62,6 +62,27 @@ void romCopy(void *target, void *source, u32 size) * romCopyAligned * aligns data, does a romCopy(), then returns aligned pointer to target */ +#ifdef PORT +/* D66: the N64 build did all of this in s32 (every address fit). On PC the + * targets live in .bss above 4 GiB, so `(s32)target` truncates (e.g. + * 0x1401C6F00 -> 0x401C6F00) and romCopy DMA's into a wild address. Same + * class as D53/D56: pointer-width reconciliation only — the arithmetic and + * the returned (aligned + offset) value are unchanged, just 64-bit wide. + * Callers assign the result straight to pointers, so it returns void* here + * instead of s32. */ +void *romCopyAligned(void *target, void *source, s32 length) +{ + uintptr_t target_u = (uintptr_t)target; + uintptr_t source_u = (uintptr_t)source; + uintptr_t source_aligned = align_addr_even(source_u); + uintptr_t source_offset = source_u - source_aligned; + uintptr_t target_aligned = ALIGN16_a(target_u); + + romCopy((void *)target_aligned, (void *)source_aligned, + (s32)ALIGN16_a(source_offset + length)); + return (void *)(target_aligned + source_offset); +} +#else s32 romCopyAligned(void *target, void *source, s32 length) { s32 target_offset; @@ -76,6 +97,7 @@ s32 romCopyAligned(void *target, void *source, s32 length) romCopy(target_aligned, source_aligned, ALIGN16_a((s32)source_offset + length)); return ((s32)target_aligned + target_offset); } +#endif /** * 68A8 70005CA8 diff --git a/src/ramrom.h b/src/ramrom.h index ea79684e..3d73a2e1 100644 --- a/src/ramrom.h +++ b/src/ramrom.h @@ -4,7 +4,12 @@ void romCreateMesgQueue(void); void romCopy(void *target, void *source, u32 size); +#ifdef PORT +/* D66: 64-bit pointer width on PC (see ramrom.c). */ +void *romCopyAligned(void *target, void *source, s32 length); +#else s32 romCopyAligned(void *target, void *source, s32 length); +#endif void romWrite(void *source, void *target, u32 size); #endif diff --git a/tools_pc/d51_gdb.py b/tools_pc/d51_gdb.py new file mode 100644 index 00000000..3adfe93e --- /dev/null +++ b/tools_pc/d51_gdb.py @@ -0,0 +1,136 @@ +# D51 live capture: log every import_texture_i8 hit; on SIGSEGV dump the +# RDP tile state, segment table, and the main DL around the faulting G_TEXRECT. +import gdb +import struct + +def log_hit(frame): + try: + tile = int(frame.read_var("tile")) + lt = frame.read_var("loaded_texture") + addr = lt["addr"] + size = int(lt["size_bytes"]) + line = int(lt["line_size_bytes"]) + fline = int(lt["full_image_line_size_bytes"]) + t2l = gdb.parse_and_eval("rdp.texture_to_load.addr") + print("I8HIT tile=%d addr=%s size=%u line=%u full_line=%u to_load=%s" % ( + tile, addr, size, line, fline, t2l)) + except Exception as e: + print("I8HIT log error: %s" % e) + +def dump_crash(): + print("=== CRASH STATE ===") + frame = gdb.selected_frame() + frames = [] + while frame is not None: + frames.append(frame) + frame = frame.older() + for i, f in enumerate(frames[:12]): + fn = f.symtab.filename if f.symtab else "?" + print("FRAME %d: %s at %s:%d" % (i, f.name(), fn, f.line())) + + try: + f0 = frames[0] + lt = f0.read_var("loaded_texture") + print("f0: tile=%d size_bytes=%u line=%u full_line=%u addr=%s" % ( + int(f0.read_var("tile")), int(lt["size_bytes"]), + int(lt["line_size_bytes"]), int(lt["full_image_line_size_bytes"]), lt["addr"])) + except Exception as e: + print("f0 vars: %s" % e) + + try: + tt = gdb.parse_and_eval("rdp.texture_tile[0]") + print("tile0: fmt=%d siz=%d line_size=%u tmem=%d palette=%d uls=%d ult=%d lrs=%d lrt=%d w=%d h=%d" % ( + int(tt["fmt"]), int(tt["siz"]), int(tt["line_size_bytes"]), int(tt["tmem"]), + int(tt["palette"]), int(tt["uls"]), int(tt["ult"]), int(tt["lrs"]), int(tt["lrt"]), + int(tt["width"]), int(tt["height"]))) + t2l = gdb.parse_and_eval("rdp.texture_to_load") + print("to_load: addr=%s siz=%d width=%u" % (t2l["addr"], int(t2l["siz"]), int(t2l["width"]))) + except Exception as e: + print("rdp state: %s" % e) + + # TEMP D51: font char tables + for name in ("ptrFontBankGothicChars", "ptrFontZurichBoldChars"): + try: + ch = gdb.parse_and_eval(name) + bad = 0 + for i in range(94): + v = int(ch[i]["pixeldata"]) + if v >= 0x100000000: + print("%s[%d].pixeldata=0x%x (HIGH BYTES SET)" % (name, i, v)) + bad += 1 + print("%s: %d/94 entries with high bytes set" % (name, bad)) + except Exception as e: + print(name + ": %s" % e) + try: + sp = gdb.parse_and_eval("segmentPointers") + for i in range(16): + v = int(sp[i]) + if v: + print("seg[%d]=0x%x" % (i, v)) + except Exception as e: + print("segs: %s" % e) + + inf = gdb.selected_inferior() + for i, f in enumerate(frames): + if f.name() == "gfx_run_dl": + try: + cmd = int(f.read_var("cmd")) + dls = int(f.read_var("dListStart")) + print("gfx_run_dl frame=%d cmd=0x%x dListStart=0x%x" % (i, cmd, dls)) + # 8 slots around cmd + base = cmd - 64 + b = bytes(inf.read_memory(base, 128)) + ws = struct.unpack("<32I", b) + for j in range(0, 32, 2): + w0, w1 = ws[j], ws[j + 1] + op = (w0 >> 24) & 0xff + tag = "" + if op == 0xe4: tag = " <-- G_TEXRECT" + elif op == 0xe5: tag = " <-- G_TEXRECTFLIP" + elif op == 0xfd: tag = " [SETTIMG]" + elif op == 0xf5: tag = " [SETTILE]" + elif op == 0xf2: tag = " [SETTILESIZE]" + elif op == 0xf3: tag = " [LOADBLOCK]" + elif op == 0xf4: tag = " [LOADSYNC]" + elif op == 0xe7: tag = " [PIPESYNC]" + elif op == 0xbc: tag = " [MOVEWORD]" + elif op == 0x06: tag = " [DL]" + elif op == 0xb8: tag = " [ENDDL]" + mark = " *" if (base + j * 4) == cmd else "" + print(" dl[0x%x] w0=0x%08x w1=0x%08x%s%s" % (base + j * 4, w0, w1, tag, mark)) + # walk the main DL from its start; log SETTIMGs and find the G_TEXRECT + n = 0 + off = 0 + while n < 600: + b2 = bytes(inf.read_memory(dls + off, 8)) + w0, w1 = struct.unpack("> 24) & 0xff + if op == 0xe4 or op == 0xe5: + print("MAIN-DL G_TEXRECT at dls+0x%x (slot %d) w0=0x%08x w1=0x%08x" % (off, n, w0, w1)) + # dump the 10 slots before it + for k in range(max(0, n - 10), n): + o2 = k * 16 + a, b3 = struct.unpack(" (width, height, bytes).""" + with open(path, "rb") as f: + data = f.read() + if data[:2] != b"P6": + raise ValueError("%s: not a binary P6 PPM" % path) + pos = 2 + fields = [] + while len(fields) < 3: + # skip whitespace and '#' comments + while pos < len(data) and data[pos:pos + 1].isspace(): + pos += 1 + if data[pos:pos + 1] == b"#": + while pos < len(data) and data[pos] != 0x0A: + pos += 1 + continue + start = pos + while pos < len(data) and not data[pos:pos + 1].isspace(): + pos += 1 + fields.append(int(data[start:pos])) + pos += 1 # single whitespace after maxval + w, h, maxval = fields + if maxval != 255: + raise ValueError("%s: unsupported maxval %d" % (path, maxval)) + px = data[pos:pos + w * h * 3] + if len(px) < w * h * 3: + raise ValueError("%s: truncated pixel data (%d of %d bytes)" + % (path, len(px), w * h * 3)) + return w, h, px + + +def _bbox(indices, w): + if not indices: + return None + minx = min(i % w for i in indices) + maxx = max(i % w for i in indices) + miny = min(i // w for i in indices) + maxy = max(i // w for i in indices) + return "(%d,%d)-(%d,%d)" % (minx, miny, maxx, maxy) + + +def stats(path): + w, h, px = load_ppm(path) + total = w * h + # pixel indices where any channel byte is non-zero (C-speed regex scan) + idx = set() + for off in (0, 1, 2): + ch = px[off::3] + for m in _NZ.finditer(ch): + idx.add(m.start()) + pct = 100.0 * len(idx) / total + print("%s: %dx%d non-clear %d (%.2f%%) bbox %s" + % (path, w, h, len(idx), pct, _bbox(idx, w) or "-")) + return len(idx) + + +def diff(path_a, path_b): + wa, ha, pa = load_ppm(path_a) + wb, hb, pb = load_ppm(path_b) + if (wa, ha) != (wb, hb): + print("sizes differ: %dx%d vs %dx%d" % (wa, ha, wb, hb)) + w, h = min(wa, wb), min(ha, hb) + n = w * h * 3 + pa, pb = pa[:n], pb[:n] + # XOR as big ints, then scan the result for non-zero bytes (C-speed) + x = (int.from_bytes(pa, "big") ^ int.from_bytes(pb, "big")).to_bytes(n, "big") + idx = {m.start() // 3 for m in _NZ.finditer(x)} + total = w * h + print("diff %s vs %s: %d differing pixels (%.2f%% of %dx%d) bbox %s" + % (path_a, path_b, len(idx), 100.0 * len(idx) / total, w, h, + _bbox(idx, w) or "-")) + return len(idx) + + +def main(argv): + if len(argv) < 2: + print(__doc__) + return 2 + do_diff = "--diff" in argv + files = [a for a in argv[1:] if a != "--diff"] + try: + if do_diff: + if len(files) != 2: + print("--diff needs exactly two files") + return 2 + return 0 if diff(files[0], files[1]) else 1 + ok = True + for f in files: + if not stats(f): + ok = False + return 0 if ok else 1 + except (IOError, ValueError) as e: + print("error: %s" % e) + return 2 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/tools_pc/ppm2bmp.py b/tools_pc/ppm2bmp.py new file mode 100644 index 00000000..81674d33 --- /dev/null +++ b/tools_pc/ppm2bmp.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +"""ppm2bmp.py — convert binary P6 PPM to 24-bit BMP (no dependencies). + +Usage: ppm2bmp.py in.ppm out.bmp [scale] (scale = integer, default 1) +Useful for eyeballing GE_PCDUMP frames without an image library. +""" +import struct +import sys + + +def load_ppm(path): + with open(path, "rb") as f: + data = f.read() + if data[:2] != b"P6": + raise ValueError("%s: not a binary P6 PPM" % path) + pos = 2 + fields = [] + while len(fields) < 3: + while pos < len(data) and data[pos:pos + 1].isspace(): + pos += 1 + if data[pos:pos + 1] == b"#": + while pos < len(data) and data[pos] != 0x0A: + pos += 1 + continue + start = pos + while pos < len(data) and not data[pos:pos + 1].isspace(): + pos += 1 + fields.append(int(data[start:pos])) + w, h, maxval = fields + if maxval != 255: + raise ValueError("unsupported maxval %d" % maxval) + px = data[pos + 1:pos + 1 + w * h * 3] + if len(px) < w * h * 3: + raise ValueError("truncated pixel data") + return w, h, px + + +def main(argv): + if len(argv) < 3: + print(__doc__) + return 2 + scale = int(argv[3]) if len(argv) > 3 else 1 + w, h, px = load_ppm(argv[1]) + w *= scale + h *= scale + row_size = (w * 3 + 3) & ~3 + out = bytearray() + out += struct.pack("<2sIHHI", b"BM", 54 + row_size * h, 0, 0, 54) + out += struct.pack(" %s (%dx%d)" % (argv[1], argv[2], w, h)) + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv))