PC port: Phase 1.5 — root-cause D32 Part B (D33): ROM big-endian

structured fields + x86-64 loop-stride bug; fix designed, handoff updated

- Finding D33 in docs/PCPortResearch.md: the .z64 stores multi-byte
  structured fields in big-endian byte order (bit-packed streams raw);
  per-field load-time transform rule (u32 bswap32 / u16 bswap16 / u8
  identity) validated over all 173 animation entries; full blob layout
  mapped; exact reproduction of the gdb-observed garbage values.
- Second root cause: expand_ani_table_entries iterates s32** (+8 bytes/iter
  on x86-64 vs +4 on N64), rebasing only even-indexed entries (verified in
  compiled binary). Fix design: romdataFixupAnimationData() in port/, one
  call between romCopy and expand, s32* loop stride fix.
- docs/HANDOFF.md rewritten with the implementation task; AGENTS.md phase
  status updated. tools_pc/disasm.py (BE ROM research tool) added.
This commit is contained in:
James Dansereau committed 2026-08-21 20:18:38 -04:00
1 parent ab61e4be4a
commit 8482ba3ed1
4 files changed
+317 -69

No files matched your search

+2 -2
View File
@@ -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_<r>.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.
+71 -33
View File
@@ -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 <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; 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 <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; **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=<fn>` / `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
+114 -34
View File
@@ -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
+130
View File
@@ -0,0 +1,130 @@
#!/usr/bin/env python3
"""Minimal MIPS disassembler for the BE-encoded GE ROM.
Usage: disasm.py <ram_addr_hex> [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()