diff --git a/AGENTS.md b/AGENTS.md index fc591ea5..f5fd70b1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ - `n64decomp/007`: WIP decompilation of GoldenEye 007 (N64), byte-matches US/EU/JP ROMs. - Active work: **PC port** modelled on the Perfect Dark PC port (same Rare "Indy" engine family). -- **Read `docs/PCPortResearch.md` first** — architecture, GE-specific RSP deltas, phased plan, and review findings (A1–D32 in §11; current handoff + plan in §H). +- **Read `docs/PCPortResearch.md` first** — architecture, GE-specific RSP deltas, phased plan, and review findings (A1–D33 in §11; current handoff + plan in §H). - **Starting a fresh session?** Read `docs/HANDOFF.md` — the paste-ready brief with the immediate task, standing procedure, and environment gotchas. ## Non-negotiables @@ -47,7 +47,7 @@ Run `/linkcheck` for this sweep. Record new findings in `docs/PCPortResearch.md` - **Phase 0 (scaffolding):** done, committed. - **Compile+link milestone:** all ~235 TUs compile and `ge007.x86_64.exe` links clean (`ninja ge007 -k 0`, 236/236). Findings D7–D17 in `docs/PCPortResearch.md` §11; asset stubs in `port/src/assetstubs.c`, fast3d no-ops in `port/src/gfxstub.c` (delete when real fast3d lands). - **Phase 1 (boot to window):** done — ROM loads/validates/maps at cart base 0x10000000 (`romdata.c`), SDL2 window + GL clear loop (`video.c`), absolute asset symbols from `scripts/gen_romassets.py` → `port/src/romassets_.s` (findings D18–D23). -- **Phase 1.5 (boot to first frame):** IN PROGRESS — mainThread runs through `bossInitMainthreadData()`; D31 (langInit file-load SIGSEGV) fixed via real-zlib decompression (`port/src/rzdecomp.c`, replaces the excluded `src/game/decompress.c`+`zlib.c`). Current blocker is **D32** (ROM-struct pointer-width): Part A (u32 layout in `ModelAnimation`) done, Part B (load-time rebase in `expand_ani_table_entries`) open. Recurring sub-task: ABI/layout fixes per the D32 procedure (§H) as more asset loading is reached. +- **Phase 1.5 (boot to first frame):** IN PROGRESS — mainThread runs through `bossInitMainthreadData()`; D31 (langInit file-load SIGSEGV) fixed via real-zlib decompression (`port/src/rzdecomp.c`, replaces the excluded `src/game/decompress.c`+`zlib.c`). Current blocker is **D32** (ROM-struct pointer-width): Part A (u32 layout in `ModelAnimation`) done; Part B **root-caused (D33)** — the ROM stores structured fields big-endian (per-field load-time fixup needed) + an x86-64 loop-stride bug in `expand_ani_table_entries`; fix designed, implementation next. Recurring sub-task: ABI/layout fixes per the D32 procedure (§H) as more asset loading is reached. - **Phase 2 (rendering):** bring in PD fast3d (replaces `port/src/gfxstub.c`), verify custom CC/RM modes against `gmain.s`, port the scheduler; replace asset stubs with ROM-backed data (D16). Assumes ABI-clean data loading from Phase 1.5. - **Phase 3 (audio + input):** libaudio → SDL; decide the ASP strategy (C2: `aspMain*` dummies in `ucode.c`). - **Phase 4 (saves + polish):** file-backed EEPROM; PFS/motor already no-op stubs. diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index c8366762..5d5bbaba 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -1,7 +1,7 @@ # Handoff brief — GoldenEye 007 PC port (Phase 1.5: boot to first frame) _Paste-ready briefing for the next agent session. The authoritative detail lives in -`AGENTS.md` and `docs/PCPortResearch.md` (§H handoff, §F/D31–D32 findings); this is +`AGENTS.md` and `docs/PCPortResearch.md` (§H handoff, §F/D31–D33 findings); this is the summary + the immediate task._ ## Your job @@ -13,61 +13,99 @@ when the user is back: a booting game. ## Read first (authoritative, in order) 1. `AGENTS.md` — non-negotiables, critical files, build, verification ritual, phase status. -2. `docs/PCPortResearch.md` §H (handoff + plan), then §F/D31–D32 (findings). +2. `docs/PCPortResearch.md` §H (handoff + plan), then **§F/D33** (root cause of the + current blocker + complete fix design), D31–D32 for background. ## 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), each documented as a D3x finding. No behavior changes. + semantics-preserving ABI/layout fixes forced by the 32→64-bit transition, each documented + as a D3x finding. No behavior changes. - Region macros in `CMakeLists.txt` must mirror the N64 Makefile exactly. -## Current state (verified) +## Current state (verified this session) - **D31 FIXED:** langInit file-load crash gone (real-zlib `decompressdata` in - `port/src/rzdecomp.c`; excluded `src/game/decompress.c`+`zlib.c`). mainThread boots past - all 7 language files into `bossInitMainthreadData()`. -- **D32 Part A DONE:** `ModelAnimation` pointer fields → `u32` (N64 layout, sizeof 64), - casts in `model.c`. -- **D32 Part B = the current blocker.** Boot dies at `boss.c:233` - `init_weapon_animation_groups_maybe()` → … → `modelAnimReadRootMotionValue` - (`model.c:914`) reading `desc->bitCount`, because animation entries' `bitDescriptors` / - `bitStream` are not valid DRAM pointers after load. + `port/src/rzdecomp.c`). mainThread boots into `bossInitMainthreadData()`. +- **D32 Part A DONE:** `ModelAnimation` pointer fields → `u32` (N64 layout, sizeof 64). +- **D32 Part B = ROOT-CAUSED (finding D33). Two independent bugs, both confirmed:** + 1. **ROM endianness.** The `.z64` file stores structured multi-byte fields in + **big-endian byte order** (bit-packed streams are raw). The animation blob's + 20-byte record headers and descriptor arrays must be converted per-field at load + time: u32 → bswap32, u16 → bswap16, u8 → identity. No uniform word transform exists. + Validated in Python over all 173 ROM entries: 0 bad ranges, 0 non-sequential + descriptor arrays; simulated root-motion frames show smooth per-frame deltas. + The exact garbage seen under gdb (0xC82BD8C0) is reproduced by LE-reading the BE + file bytes + rebase. Evidence: GE-specific ROM header, `tools/utils.h` "v64→z64" + convention, build pipeline with no byte-swap step, BE toolchain defaults. + 2. **x86-64 stride bug.** `expand_ani_table_entries()` iterates with `s32** var_v0; + var_v0++` — +4 bytes/iter on N64, but +8 on x86-64 (verified: `add $0x8,%rdx` in the + compiled binary). Only even-indexed entries get rebased; odd ones keep raw small + offsets (fire_standing is index 1 → would stay 0x144 → SIGSEGV on use). The earlier + "zeros at odd indices" gdb observation was a display artifact — the compiled `.data` + array is dense (objdump-verified). This pattern occurs nowhere else in the set. -## Immediate task — D32 Part B -Make `expand_ani_table_entries()` (`src/game/initanitable.c:233`) rebase **every** entry's -`bitDescriptors` (u32 @0x08) and `bitStream` (u32 @0x10) to a valid V1 address (< 0x80000000). +## Immediate task — implement the D33 fix (design complete, ~3 small edits) +1. **`port/src/romdata.c` + `port/include/romdata.h`:** add + `void romdataFixupAnimationData(u8 *blob, u32 blobSize, const s32 *tableA, const s32 *tableB)`. + For each non-null (≠0 and ≠1 — sentinel is **1**) offset in both tables: + - record at `blob+off` (20 bytes): bswap32 fields at +0x00 (address), +0x08 + (bitDescriptors), +0x10 (bitStream); bswap16 fields at +0x04 (frame count), + +0x0C (bitsPerFrame root motion), +0x0E (frame size bits); bytes +0x06/+0x07 + (u8 angle width / loop flag) untouched. + - descriptor array at `[blob+bd, blob+bs)` step 6 (`ModelAnimBitField`): bswap16 + words at +0 and +4 (bitOffset, valueOffset); bytes +2/+3 (bitCount, pad) untouched. + Guards: skip if bd==0 && bs==0; require `bd < bs <= blobSize` and `(bs-bd)%6==0`. + Full field semantics + layout rules in D33 (blob interleave: entry i's payload sits + at [PTR_ANIM_{i-1}+0x14, PTR_ANIM_i); all regions disjoint). +2. **`src/game/initanitable.c`, `alloc_load_expand_ani_table()` (~line 265):** call the + fixup **between** `romCopy(...)` and `expand_ani_table_entries(...)` — it must run + *before* expand (expand reads/writes those fields as LE after fixup). Add + `#include "romdata.h"` (`port/include` is already on the include path). One-line + mechanical ABI edit — document with a D32/D33 comment. +3. **`src/game/initanitable.c`, `expand_ani_table_entries()` (line 233):** change the loop + pointer from `s32** var_v0` to `s32 *var_v0` (cast at assignment: `var_v0 = (s32 *)arg0;`) + so every entry is visited on x86-64. Semantics-preserving on N64. Keep the rest of the + function body identical (`*var_v0 += base`, `((struct anim_entry *)(s32)*var_v0)->unk08/unk10`, + and loop 2's `*(s32 *)(s32)*var_v0 += entries_base`). -Gdb method (launch mode only — attach fails with error 87): +**Do NOT transform the entries segment** (per-frame joint angles, file 0x124AC0, +0x169EC0 bytes) — it is identity-encoded bit-packed data, not consumed at boot; verify +visually in Phase 2 when Bond first animates. + +## Verify (gdb, launch mode only — attach fails with error 87) - Break at `boss.c:233` (right after `alloc_load_expand_ani_table` returns). base = `&ptr_animation_table->data`. -- For each `animation_table_ptrs1[i]` / `animation_table_ptrs2[i]`, check - `*(u32*)(entry+8)` and `*(u32*)(entry+16)`. -- Two anomalies to resolve first: (a) in-memory `animation_table_ptrs1[]` reads - `[ptr, 0, ptr, 0, …]` though the source is a dense `PTR_ANIM_*` list — reconcile that - against the `while (*var_v0 != 0)` fixup loop; (b) entry[0]'s `bitStream` high byte is - 0x88, not 0x70. Likely a 32/64-bit or write-overlap issue in the two fixup loops. -- Prefer understanding *why it works on N64* and matching that. If a game-code edit is - needed it's an ABI/layout change (allowed per non-neg #2) — document it as D3x. +- For a sample of `animation_table_ptrs1/2[i]` entries (include odd indices!): + `*(u32*)(entry+8)` and `*(u32*)(entry+16)` must be in [0x70xxxxxx] (base + blob offset), + and the record's address field (+0x00) must be a cart address 0x10xxxxxx. +- Confirm boot advances past `init_weapon_animation_groups_maybe()` into the next init + step of `bossInitMainthreadData`. Expect more D32-class faults further along (models, + textures, other tables) — apply the standing procedure below each time. ## 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 (models, textures, other tables). The D32 fix procedure (doc §H): at the -fault run `ptype /o `; if a pointer field's offset/size diverges from its N64 -offset comment, change it to `u32` + cast at the use sites; verify the load-time rebase -yields valid V1 addresses; rebuild; confirm the boot advances past this struct. Log each as -D3x in §F and note it in AGENTS.md phase status. +asset loading. The D32 fix procedure (doc §H): at the fault run `ptype /o `; if a +pointer field's offset/size diverges from its N64 offset comment, change it to `u32` + cast +at the use sites; verify the load-time rebase yields valid V1 addresses; **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**; rebuild; confirm boot advances. Log each as D3x in +§F and note it in AGENTS.md phase status. ## 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: `./build-pc.sh ntsc-final` (incremental; full rebuild only when `port/shim/` - changes). ROM is at `data/ge007.ntsc-final.z64`. + changes). ROM is at `data/ge007.ntsc-final.z64` (= `baserom.u.z64`, byte-identical). - gdb: **launch** mode only (`gdb -batch -ex "handle SIGSEGV stop" -ex run …`). Symbolicate offline with `addr2line -e build-pc/ge007.x86_64.exe -f -C <0x140000000+rel>`. Image base is `0x140000000` (re-verify with `info address` after a rebuild). +- `objdump -d --disassemble=` / `objdump -s -j .data` on build-pc/ge007.x86_64.exe is a + fast way to check what the compiler actually emitted (used this session to confirm the + stride bug and the dense table array). - Many init functions use a fake RBP — compute stack offsets from the **entry RSP**, not RBP. -- The D30 crash handler did **not** write `ge007.crash.log` for the game-thread fault this - session; attribute crashes via gdb. (Worth a separate look later.) +- The D30 crash handler did **not** write `ge007.crash.log` for the game-thread fault; + attribute crashes via gdb. (Worth a separate look later.) +- Python on this box: no f-strings with nested quotes (< 3.12) — use `%` formatting. ## Definition of done (this milestone) mainThread reaches `bossMainloop()` and a frame is actually drawn on the GL surface (not just diff --git a/docs/PCPortResearch.md b/docs/PCPortResearch.md index 17d96b26..0ae2698a 100644 --- a/docs/PCPortResearch.md +++ b/docs/PCPortResearch.md @@ -962,26 +962,96 @@ gameInit — and currently dies inside `langInit()` (D31). matches. `expand_ani_table_entries()` writes s32 to 0x08/0x10 via its own `struct anim_entry`, which now aligns exactly (no change needed there). - **Part B — pointer-rebase correctness: OPEN (next target).** After Part A the - *values* are still invalid for the crashing weapon-anim entry (gdb, break at - boss.c:233 after `alloc_load_expand_ani_table`): anim = 0x702adad4 = base - 0x702ad8c0 + 0x214; `bitDescriptors` = 0xf714b067, `bitStream` = 0x000c9100 — - neither a V1 address. Two concrete anomalies to debug in - `expand_ani_table_entries()` (initanitable.c:233): - * `animation_table_ptrs1[]` is a **dense** array of `PTR_ANIM_*` offsets in - source, but in memory reads `[0x702ad8dc, 0, 0x702adad4, 0, …]` (zeros at - odd indices). The fixup loop `while (*var_v0 != 0)` would stop at the first - zero — yet even-index entries show rebased values. Reconcile the array's true - post-fixup shape and confirm every entry's unk08/unk10 is actually rebased - (the crashing anim is `animation_table_ptrs1[2]`). - * Even entry[0]'s `bitStream` comes out wrong (0x882ad8c0 — low 24 bits match - base, high byte 0x88≠0x70) while its `bitDescriptors` is correct - (0x702ad8c0). Inspect the second fixup loop (`**var_v0 += - &_animation_entriesSegmentRomStart`) for a 32/64-bit or overlap bug. + **Part B — pointer-rebase correctness: ROOT-CAUSED (see D33), fix designed, + implementation pending.** The garbage values are *not* a bug in + `expand_ani_table_entries`'s arithmetic — the rebase is correct N64 code fed + wrong bytes, plus one genuine x86-64 ABI stride bug. Both root causes and the + full fix design are in **D33** below. - The crash site is unchanged (still model.c:914) — Part A is a necessary - prerequisite, not a regression. Next session: gdb `expand_ani_table_entries` - step-by-step until every entry's bitDescriptors/bitStream is a valid V1 address. +* **D33 — ROM file stores structured fields big-endian; animation load needs a per-field endianness fixup + an x86-64 loop-stride fix (root cause of D32 Part B).** + + **Discovery.** The `.z64` ROM file is not a plain image: multi-byte + *structured* fields (record headers, tables) are stored in **big-endian byte + order**, while bit-packed data streams are raw. Evidence chain: + 1. `baserom.u.z64` is byte-for-byte identical to `data/ge007.ntsc-final.z64`; + the ROM header is GE-specific (magic `80 37 12 40` from `src/rom_header.s`, + BE u32 CRC at 0x10/0x14 written by `tools/n64cksum.c`) — not the standard + N64 header layout. + 2. Build pipeline: `$(LD)` → ELF → `objcopy -O binary --gap-fill=0xff` → + RareZip cdata compression (`data_compress.sh`) → `n64cksum`. No byte-swap + step; the mips64-elf-gcc toolchain (SGI-style BE default) emits BE bytes. + 3. `tools/utils.h` documents the convention: `swap_bytes()` = "convert from + v64 to z64 ordering", `reverse_endian()` = "convert from n64 to z64 + ordering" — the project's ".z64" is a word-swapped cart image (community + ".n64" format). Real cart = bswap32(file). + 4. Animation-blob analysis: reading headers as BE u32/u16 yields self- + consistent values (frame counts, bit widths 0–31, offsets that tile the + blob exactly); LE reads yield garbage. + + **Per-field transform rule** (applied at load time to the `animation_data` + segment): u32 fields → bswap32; u16 fields → bswap16; u8 fields → identity; + bit-packed streams → identity. No single uniform word transform exists (mixed + field widths). + + **Blob layout** (fully mapped; all 173 entries verified): each C array + (`animation_table_ptrs1/2` in `src/game/initanitable.c`) is a sequence of + 20-byte records at the `PTR_ANIM_*` offsets. Record fields: +0x00 address + (entries-segment offset, u32), +0x04 frame count (u16), +0x06 angle bit width + (u8, used by `modelAnimReadBitsAsU16Angle`), +0x07 loop flag (u8), +0x08 + bitDescriptors blob offset (u32), +0x0C bitsPerFrame root motion (u16, used by + `sub_GAME_7F06D2E4`: `scaled = unk0C * frame`), +0x0E frame size in bits + (u16, used by `loadAnimationFrame`: `frameSize = unk0E >> 3`), +0x10 + bitStream blob offset (u32). Tail (+0x14..+0x3C) is unused by any code — the + effective record size is 20 bytes (= `struct anim_entry`, 5×s32). Interleave + rule: entry *i*'s payload [descriptors][stream] sits at + [PTR_ANIM_{i-1}+0x14, PTR_ANIM_i); all regions are disjoint; stream size = + ceil(frameSizeBits × frames / 8) — exact fit on every entry (fire_standing: + 544 bits × 106 frames → 252 bytes). Descriptor array = `ModelAnimBitField` + {u16 bitOffset, u8 bitCount, u8 pad, u16 valueOffset} ×4 (6 bytes each); after + the transform all 692 descriptor arrays pack bitOffsets sequentially from 0. + Null entries use sentinel **1** (not 0) — `expand_ani_table_entries` skips + `*var_v0 == 1`. + + **Validation.** Simulated the full transform in Python over all 173 entries: + 0 bad descriptor ranges, 0 non-sequential arrays. Root-motion simulation for + fire_standing frames 0–3 (widths 6/7/6/0, bitsPerFrame=19) gives smooth + per-frame deltas (x = 7, 10, 12, 11; y ≈ 1086–1088) — proving the stream + bytes are identity-correct (no transform). The previously observed garbage is + reproduced exactly: file bytes `[00 00 01 58]` at fire_standing_fast's bd → + LE read 0x58010000 + base 0x702ad8c0 = **0xC82BD8C0**, the exact value seen + under gdb. The "zeros at odd indices" observation was a gdb display artifact — + the compiled `.data` array is dense (verified by objdump of + build-pc/ge007.x86_64.exe). + + **Second root cause: x86-64 stride bug in `expand_ani_table_entries`.** The + loops iterate with `s32** var_v0; var_v0++` — +4 bytes/iter on N64, but + `s32*` is 8 bytes on x86-64. Verified in the compiled binary (`add $0x8,%rdx`) + : only even-indexed entries are rebased; odd entries keep raw small offsets + (e.g. fire_standing, index 1, would stay 0x144 → instant SIGSEGV on use). + This pattern occurs nowhere else in the compiled set. + + **Fix design (implementation pending — this is the immediate task):** + 1. New port function `romdataFixupAnimationData(u8 *blob, u32 blobSize, + const s32 *tableA, const s32 *tableB)` in `port/src/romdata.c` (+ decl in + `port/include/romdata.h`): for each non-null (≠0, ≠1) offset in both + tables — bswap the 20-byte record header per-field (u32: +0x00/+0x08/ + +0x10; u16: +0x04/+0x0C/+0x0E; u8: +0x06/+0x07 untouched); then transform + the descriptor array at [bd, bs) step 6 (bswap16 on words 0 and 4 only). + Guards: skip if bd==0 && bs==0; require bd < bs ≤ blobSize and + (bs−bd) % 6 == 0. + 2. Call it in `alloc_load_expand_ani_table()` (initanitable.c ~line 265) + **between** the `romCopy` and `expand_ani_table_entries` — one line + + `#include "romdata.h"` (`port/include` is already on the include path). + Mechanical ABI edit per non-negotiable #2; must run *before* expand + (expand reads/writes the fields as LE after fixup). + 3. Fix the stride in `expand_ani_table_entries()` (initanitable.c:233): + iterate with `s32 *` instead of `s32**` — mechanical ABI fix, semantics- + preserving on N64 where it was already correct. + + **Open item.** The entries segment (per-frame joint angles; file offset + 0x124AC0, 0x169EC0 bytes) is left as identity for Phase 1.5 — not consumed at + boot (only during rendering). Verify visually in Phase 2 when Bond first + animates. ### G. Phase 2 status (current) @@ -1028,9 +1098,14 @@ shim expansion in compiled code. **State.** D31 (langInit file-load SIGSEGV) is fixed & verified — mainThread boots past all language-file loading into `bossInitMainthreadData()`. The boot -now dies in `init_weapon_animation_groups_maybe()` on the **D32** ROM-struct -pointer-width issue. D32 Part A (struct layout) is done; Part B (load-time -rebase correctness) is the next target. No frames rendered yet. +dies in `init_weapon_animation_groups_maybe()` on the **D32** ROM-struct issue. +D32 Part A (struct layout) is done. **D32 Part B is now fully root-caused +(D33):** (1) the ROM file stores the animation segment's structured fields in +big-endian byte order — a per-field endianness fixup is required at load time; +(2) `expand_ani_table_entries` has an x86-64 loop-stride bug (`s32**` advances +8 bytes/iter, rebasing only even-indexed entries). The fix design is complete +and validated against all 173 ROM entries; **implementation is the immediate +task** (three small edits — see D33 "Fix design"). No frames rendered yet. **D32 repeatable fix procedure** (apply to any ROM-serialized struct that faults on a pointer-field read): @@ -1043,18 +1118,23 @@ on a pointer-field read): into those u32 fields; if not, debug the fixup (see Part B below). 4. Rebuild and confirm the boot advances past this struct to the next init step. -**Next concrete target — D32 Part B.** Make `expand_ani_table_entries()` -(initanitable.c:233) rebase *every* entry's `bitDescriptors`/`bitStream` to a -valid V1 address. Gdb plan: break at boss.c:233 (after `alloc_load_expand_ani_table`), -step through the two fixup loops, and for each `animation_table_ptrs1[i]` / -`animation_table_ptrs2[i]` entry verify `*(u32*)(entry+8)` and `*(u32*)(entry+16)` -are in [0x70xxxxxx]. Resolve these two anomalies first: -* In-memory `animation_table_ptrs1[]` reads `[ptr, 0, ptr, 0, …]` (zeros at odd - indices) though the source is a dense `PTR_ANIM_*` list — reconcile the array's - true shape vs the `while (*var_v0 != 0)` loop. -* entry[0]'s `bitStream` = 0x882ad8c0 (high byte 0x88≠0x70) while its - `bitDescriptors` = 0x702ad8c0 is correct — inspect the second fixup loop - (`**var_v0 += &_animation_entriesSegmentRomStart`) for a 32/64-bit or overlap bug. +**Next concrete target — implement the D33 fix.** Three edits (full design in +D33): +1. `port/src/romdata.c` + `port/include/romdata.h`: add + `romdataFixupAnimationData(u8 *blob, u32 blobSize, const s32 *tableA, + const s32 *tableB)` — per-field BE→LE transform of the 20-byte records and + the [bd, bs) descriptor arrays (u32 → bswap32, u16 → bswap16, u8 identity). +2. `src/game/initanitable.c` `alloc_load_expand_ani_table()`: call the fixup + between `romCopy` and `expand_ani_table_entries` (+ `#include "romdata.h"`). +3. `src/game/initanitable.c` `expand_ani_table_entries()`: change the loop + pointer from `s32**` to `s32 *` so every entry is visited on x86-64. + +Then: build (`./build-pc.sh ntsc-final`), verify under gdb (launch mode): +break at boss.c:233 after `alloc_load_expand_ani_table`; for a sample of +`animation_table_ptrs1/2[i]` entries confirm `*(u32*)(entry+8)` and +`*(u32*)(entry+16)` are in [0x70xxxxxx] (blob base + offset) and the record's +address field is a cart address (0x10xxxxxx); confirm boot advances past +`init_weapon_animation_groups_maybe`. Commit at the milestone. **Non-negotiable #2 refinement (applied to AGENTS.md).** The original "game code compiles unmodified / fix belongs in port/" is too absolute: pointer-width layout diff --git a/tools_pc/disasm.py b/tools_pc/disasm.py new file mode 100644 index 00000000..f9e5e307 --- /dev/null +++ b/tools_pc/disasm.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +"""Minimal MIPS disassembler for the BE-encoded GE ROM. + +Usage: disasm.py [n_instructions] +Maps game-segment RAM (base 0x7F000000) to file offset 0x34B28. +Also handles lib segment (RAM base = _startSegmentEnd - 0x10000000, ROM 0x1050). +""" +import struct, sys + +ROM = open('data/ge007.ntsc-final.z64', 'rb').read() +GAME_RAM_BASE = 0x7F000000 +GAME_ROM_OFF = 0x34B28 +LIB_ROM_OFF = 0x1050 +LIB_RAM_BASE = 0x70000450 # from boot.s comment: code at ROM 0x1050 -> RAM 0x70000450 + +REGS = ["$zero","$at","$v0","$v1","$a0","$a1","$a2","$a3", + "$t0","$t1","$t2","$t3","$t4","$t5","$t6","$t7", + "$s0","$s1","$s2","$s3","$s4","$s5","$s6","$s7", + "$t8","$t9","$t10","$t11","$t12","$t13","$gp","$sp", + "$fp","$ra"] + +RTYPE = {0x00:"sll",0x02:"srl",0x04:"sra",0x06:"sllv",0x08:"srav",0x0A:"sralv", + 0x0C:"and",0x0E:"or",0x0F:"xor",0x10:"nor",0x12:"slt",0x14:"sltu", + 0x18:"add",0x1A:"addu",0x1C:"sub",0x1E:"subu", + 0x20:"mult",0x21:"multu",0x22:"div",0x23:"divu", + 0x24:"madd",0x25:"maddu",0x26:"msub",0x27:"msubu"} + +COP0 = {0x00:"mfc0",0x01:"mfc1",0x02:"mfc2",0x10:"mtc0",0x11:"mtc1",0x12:"mtc2", + 0x40:"bc0t",0x44:"bc0f"} + +def disasm_word(w): + op = w >> 26 + rs = (w >> 21) & 31 + rt = (w >> 16) & 31 + rd = (w >> 11) & 31 + sa = (w >> 6) & 31 + fn = w & 63 + imm = w & 0xFFFF + def s16(v): return v - 0x10000 if v >= 0x8000 else v + + if op == 0x00: + if fn in RTYPE: + if fn in (0x00,0x02,0x04,0x06,0x08,0x0A): + return "%s %s,%s,%d" % (RTYPE[fn], REGS[rd], REGS[rt], sa) + if fn in (0x20,0x21,0x22,0x23,0x24,0x25,0x26,0x27): + return "%s %s,%s" % (RTYPE[fn], REGS[rs], REGS[rt]) + return "%s %s,%s,%s" % (RTYPE[fn], REGS[rd], REGS[rs], REGS[rt]) + if fn == 0x19: # tgei etc rare + return "tgeiu? %s,%s,0x%04x" % (REGS[rd], REGS[rs], imm) + return ".word 0x%08x" % w + if op == 0x01: + c = (w >> 21) & 3 + if c == 0: + if fn in COP0: + if fn in (0x00,0x10): + return "%s %s,0x%02x" % (COP0[fn], REGS[rt], rs) + return "%s %s,0x%02x" % (COP0[fn], REGS[rs], rt) + if fn == 0x00: return "mfc0? %s" % REGS[rt] + return ".word 0x%08x" % w + if c == 1: # cop1 + if fn in (0x00,0x01,0x02,0x03): + names = ["mfc1","cfc1","mtc1","ctc1"] + return "%s %s,%s" % (names[fn], REGS[rt], REGS[rs]) + if fn == 0x46: return "bc1t +0x%04x" % s16(imm) + if fn == 0x47: return "bc1f +0x%04x" % s16(imm) + if fn == 0x4E: return "bc1t? (cond)" + if fn == 0x4F: return "bc1f? (cond)" + # fp ops + fops = {0x10:"abs.s",0x11:"add.s",0x12:"div.s",0x13:"cmp.s",0x14:"cvt.s.w", + 0x15:"cvt.w.s",0x16:"max.s",0x17:"min.s",0x18:"mov.s",0x19:"neg.s", + 0x1A:"recip.s",0x1B:"rsq.s",0x1C:"sqrt.s",0x20:"abs.d",0x21:"add.d", + 0x22:"div.d",0x23:"cmp.d",0x26:"max.d",0x27:"min.d",0x28:"mov.d", + 0x29:"neg.d",0x41:"and.b",0x42:"or.b",0x45:"cvt.s.w"} + if fn in fops: + return "%s %f%d,%f%d,%f%d" % (fops[fn], rd, rs, rt, 0) + return ".word 0x%08x" % w + if c == 2: + return "cop2? .word 0x%08x" % w + return ".word 0x%08x" % w + if op == 0x02: return "jalr %s%s" % (REGS[rs], ", "+REGS[rt] if rt else "") + if op == 0x03: return "syscall" + if op == 0x04: return "beq %s,%s,+0x%04x" % (REGS[rs], REGS[rt], s16(imm)) + if op == 0x05: return "bne %s,%s,+0x%04x" % (REGS[rs], REGS[rt], s16(imm)) + if op == 0x06: return "blez %s,+0x%04x" % (REGS[rs], s16(imm)) + if op == 0x07: return "bgtz %s,+0x%04x" % (REGS[rs], s16(imm)) + if op == 0x08: return "j 0x%08x" % ((w & 0x3FFFFFF) << 2) + if op == 0x09: return "jal 0x%08x" % ((w & 0x3FFFFFF) << 2) + if op == 0x0A: return "addi %s,%s,0x%04x" % (REGS[rt], REGS[rs], s16(imm)) + if op == 0x0B: return "addiu %s,%s,0x%04x" % (REGS[rt], REGS[rs], s16(imm)) + if op == 0x0C: return "andi %s,%s,0x%04x" % (REGS[rt], REGS[rs], imm) + if op == 0x0D: return "slti %s,%s,0x%04x" % (REGS[rt], REGS[rs], s16(imm)) + if op == 0x0E: return "sltiu %s,%s,0x%04x" % (REGS[rt], REGS[rs], imm) + if op == 0x0F: return "ori %s,%s,0x%04x" % (REGS[rt], REGS[rs], imm) + if op == 0x10: return "xori %s,%s,0x%04x" % (REGS[rt], REGS[rs], imm) + if op == 0x11: return "lui %s,0x%04x" % (REGS[rt], imm) + if op in range(0x20, 0x3C): + name = {0x20:"lb",0x21:"lhu",0x22:"lw",0x23:"lbu",0x24:"lwl",0x25:"lwr", + 0x28:"sb",0x29:"sh",0x2A:"sw",0x2B:"swl",0x2D:"swr",0x2F:"ld"}.get(op, ".word") + if op in (0x28,0x29,0x2A): + return "%s 0x%04x(%s)" % (name, s16(imm), REGS[rs]) + if op == 0x2F: + return "ld %s,0x%04x(%s)" % (REGS[rt], s16(imm), REGS[rs]) + return "%s %s,0x%04x(%s)" % (name, REGS[rt], s16(imm), REGS[rs]) + if op == 0x3C: return "lui %s,0x%04x" % (REGS[rt], imm) + if op == 0x3D: return "ld %s,0x%04x(%s)" % (REGS[rt], s16(imm), REGS[rs]) + return ".word 0x%08x" % w + +def ram_to_file(ram): + if GAME_RAM_BASE <= ram < GAME_RAM_BASE + 0x200000: + return GAME_ROM_OFF + (ram - GAME_RAM_BASE) + if LIB_RAM_BASE <= ram < LIB_RAM_BASE + 0x40000: + return LIB_ROM_OFF + (ram - LIB_RAM_BASE) + # bss/data in lib segment: RAM 0x8002xxxx etc — not in ROM; skip + return None + +def main(): + if len(sys.argv) < 2: + print(__doc__); sys.exit(1) + ram = int(sys.argv[1], 16) + n = int(sys.argv[2]) if len(sys.argv) > 2 else 40 + off = ram_to_file(ram) + if off is None or off < 0 or off + 4*n > len(ROM): + print("cannot map RAM %x" % ram); sys.exit(1) + for i in range(n): + w = struct.unpack_from('>I', ROM, off + i*4)[0] + addr = ram + i*4 + print(" %08x: %08x %s" % (addr, w, disasm_word(w))) + +if __name__ == '__main__': + main()