PC port: phase 2 — D59-D68 milestone: full intro renders; blocker is D69 (BG-file BE headers)

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.
This commit is contained in:
James Dansereau committed 2026-08-24 03:27:29 -04:00
1 parent 2c0c0515dd
commit 8f631462f3
50 files changed
+2526 -376

No files matched your search

+5
View File
@@ -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
+1 -1
View File
@@ -48,6 +48,6 @@ Run `/linkcheck` for this sweep. Record new findings in `docs/PCPortResearch.md`
- **Compile+link milestone:** all ~235 TUs compile and `ge007.x86_64.exe` links clean (`ninja ge007 -k 0`, 236/236). Findings D7–D17 in `docs/PCPortResearch.md` §11; asset stubs in `port/src/assetstubs.c`, fast3d no-ops in `port/src/gfxstub.c` (delete when real fast3d lands).
- **Phase 1 (boot to window):** done — ROM loads/validates/maps at cart base 0x10000000 (`romdata.c`), SDL2 window + GL clear loop (`video.c`), absolute asset symbols from `scripts/gen_romassets.py` → `port/src/romassets_<r>.s` (findings D18–D23).
- **Phase 1.5 (boot to first frame):** DONE — boot chain complete AND real frames render (`[NOTE] frame N rendered in … us`). D34–D42 closed the blockers on top of D31–D33: ANIM_DATA lvalues, music seq-table ABI+endianness, music-heap sizing, libaudio bank-tree re-layout, implicit-declaration prototype shim (D38), Globalimagetable rebasing + true segment size (D39), ModelHitEntry pool sizing (D40), cross-thread GL context release (D41), rsp task-settings toggle (D42). ROM dump verified against the No-Intro good dump (byte-identical, header CRCs re-checked — `tools_pc/romverify.c`).
- **Phase 2 (rendering):** IN PROGRESS — fast3d integrated + 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.
+148 -302
View File
@@ -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 — <what>`). 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 <dListStart>" \
./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-<region>/` = **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 <filebase+off>` 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 <Struct>`; 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 — <what>`). 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 <pc>`.
- 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).
+516 -65
View File
@@ -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_<r>.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).
+10
View File
@@ -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
+101 -2
View File
@@ -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 <cstdio>
#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);
}
}
+36
View File
@@ -0,0 +1,36 @@
#ifndef GIMG_FIXUP_H
#define GIMG_FIXUP_H
#include <PR/ultratypes.h>
/* 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
+3
View File
@@ -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
+7
View File
@@ -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
+20
View File
@@ -0,0 +1,20 @@
/*
* PC port shim for bare <R4300.h> 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 <PR/R4300.h>
#endif
#endif /* _PORT_SHIM_R4300_BARE_H_ */
+20
View File
@@ -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. */
+2
View File
@@ -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)
*/
+8
View File
@@ -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
+125
View File
@@ -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 <PR/ultratypes.h>
#include <PR/gbi.h>
#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++;
}
}
}
+334 -3
View File
@@ -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-<region>/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);
+1
View File
@@ -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) ----------- */
+5
View File
@@ -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;
}
+41 -2
View File
@@ -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_<r>.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;
}
}
+24
View File
@@ -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"
+14
View File
@@ -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*/
+34
View File
@@ -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));
+43
View File
@@ -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);
}
+21
View File
@@ -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
}
+43
View File
@@ -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];
+97 -1
View File
@@ -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);
+26
View File
@@ -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 <assets/images.def>
#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),
+25
View File
@@ -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;
+17
View File
@@ -2,6 +2,9 @@
#include <ramrom.h>
#include <memp.h>
#include "image_bank.h"
#ifdef PORT
#include <gimgfixup.h>
#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
}
+8
View File
@@ -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;
+14
View File
@@ -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)
+14
View File
@@ -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 */
+127
View File
@@ -1,4 +1,8 @@
#include <ultra64.h>
#ifdef PORT /* TEMP D51 */
#include <stdio.h>
#include <stdlib.h>
#endif
#include <memp.h>
#include "model.h"
#include "../rmon.h" /*<PR/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;
+6
View File
@@ -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);
+23
View File
@@ -1,4 +1,9 @@
#include <ultra64.h>
#ifdef PORT
/* TEMP D69: probe below uses fprintf/getenv. */
#include <stdio.h>
#include <stdlib.h>
#endif
#include "macro.h"
#include "ob.h"
#include <deb.h>
@@ -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
+47
View File
@@ -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;
+10
View File
@@ -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);
+14
View File
@@ -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;
+24
View File
@@ -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 */
+66
View File
@@ -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;
+15
View File
@@ -43,6 +43,11 @@
#include "seqp.h"
#include "cseqp.h"
#include "cseq.h"
#ifdef PORT
/* TEMP D54 probe — delete before commit. */
#include <stdio.h>
#include <stdlib.h>
#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);
+18
View File
@@ -23,6 +23,11 @@
#include "synthInternals.h"
#include <os.h>
#include <R4300.h>
#ifdef PORT
/* TEMP D54 probe — delete before commit. */
#include <stdio.h>
#include <stdlib.h>
#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;
+15
View File
@@ -42,6 +42,11 @@
#include "include/assert.h"
#include "seqp.h"
#include "seq.h"
#ifdef PORT
/* TEMP D54 probe — delete before commit. */
#include <stdio.h>
#include <stdlib.h>
#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);
+16
View File
@@ -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;
}
+20
View File
@@ -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;
}
+13
View File
@@ -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);
+22
View File
@@ -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
+5
View File
@@ -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
+136
View File
@@ -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("<II", b2)
op = (w0 >> 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("<II", bytes(inf.read_memory(dls + o2, 8)))
print(" pre[%d] +0x%x w0=0x%08x w1=0x%08x" % (k, o2, a, b3))
break
if op == 0xb8:
print("MAIN-DL ENDDL at dls+0x%x (slot %d), no G_TEXRECT seen" % (off, n))
break
n += 1
off += 16
except Exception as e:
print("gfx_run_dl dump: %s" % e)
break
def on_stop(signal_name, stop_reason):
if stop_reason == "breakpoint-hit":
f = gdb.selected_frame()
if f.name() == "import_texture_i8":
log_hit(f)
gdb.execute("continue")
elif signal_name == "SIGSEGV":
dump_crash()
gdb.Breakpoint("import_texture_i8")
gdb.events.stop.connect(on_stop)
print("D51 capture armed")
+120
View File
@@ -0,0 +1,120 @@
#!/usr/bin/env python3
"""pixcount.py — count non-clear-color pixels in PPM frame dumps (GE_PCDUMP).
The game clears to black (gfx_opengl_clear_framebuffer: glClearColor 0,0,0),
so "non-clear" == any pixel that is not exactly (0,0,0). This turns "the
scene contains actual model geometry / glyphs" into a number: a blank frame
counts ~0, a rendered legal page counts thousands.
Usage (run from anywhere; paths relative to CWD):
pixcount.py frame.ppm [more.ppm ...] per-file stats (count + bbox)
pixcount.py a.ppm b.ppm --diff pixel diff between two captures
Exit status: 0 if every listed file has at least one non-clear pixel
(--diff: 0 if the two files differ at all), 1 otherwise, 2 on usage/IO error.
"""
import re
import sys
# any non-zero byte (channels are stored R,G,B interleaved)
_NZ = re.compile(rb"[^\x00]")
def load_ppm(path):
"""Parse a binary P6 PPM -> (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))
+66
View File
@@ -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("<IiiHHIIiiII", 40, w, h, 1, 24, 0, w * h * 3, 2835, 2835, 0, 0)
for y in range(h - 1, -1, -1): # BMP is bottom-up
src_y = (y // scale) * (w // scale) * 3
row = bytearray(row_size)
for x in range(w):
i = src_y + (x // scale) * 3
row[x * 3] = px[i + 2] # B
row[x * 3 + 1] = px[i + 1] # G
row[x * 3 + 2] = px[i] # R
out += row
with open(argv[2], "wb") as f:
f.write(out)
print("%s -> %s (%dx%d)" % (argv[1], argv[2], w, h))
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))