Fix Master Hand's moves breaking and the crash on defeat#50
Merged
Conversation
port_aobj_register_halfswapped_range was called every time a fighter figatree file was loaded (`reloc_animations/FT*` / `reloc_submotions/FT*` match in lbreloc_bridge.cpp). The walker behind it tracks three sets of "already processed" verdicts keyed on raw heap pointer: - sUnhalfswappedVisited: per-block one-shot fixup tracker for spline data (interp.c). - sUnswappedHeads: event32 stream entry points the walker successfully un-halfswapped. - sRejectedHeads: stream entry points the walker decided not to touch (didn't look like halfswap-corrupted event32). The register call evicted only the first cache when a new range was registered; the other two retained stale entries from a previous animation's load at the same heap addresses. Fighter figatree heaps are bump-reset between status changes, so cache entries from a prior load alias bytes that have since been overwritten — the walker short-circuits at the top of walk(), the freshly-loaded halfswap- corrupted bytes never get fixed up, and gcParseDObjAnimJoint reads a command word with an out-of-range opcode and bails with "UNHANDLED opcode=64 — ending anim". Surfaced as Master Hand's okutsubushi (palm slam), okupunch (rocket punch), and drill animations rendering wrong: TopN.translate.z was pinned at the -15000.0F set by ftBossOkutsubushiSetStatus and never integrated forward, because TransN.translate stayed at (0,0,0) every frame after the parser bailed. The X-chase logic in the boss state ProcPhysics still ran (vel_air.x = -40 / +40), so the boss moved across but never forward. Fix factored into a small lambda evict() applied to all three caches. Same shape as the existing project_fixup_idempotency_heap_reuse pattern: cache keyed on pointer, heap reuses addresses, fresh bytes skip fixup unless eviction fires at the load site. Systemic audit confirmed there are no other at-risk caches in port/. Seven pointer-keyed caches total across the port: - 3 in port/port_aobj_fixup.cpp — now all evicted via this register call - 4 in port/bridge/lbreloc_byteswap.cpp — already evicted via portEvictStructFixupsInRange called from lbRelocLoadAndRelocFile. The architecture is sound now: one eviction call site per cache bucket, every cache wired through it. Going forward, a new pointer-keyed cache in either file must be added to the existing eviction site in the same change. Initial wrong-hypothesis detour: a ftPhysicsGetRootMotionJoint (TransN→XRotN→YRotN) resolver was written to "fix" the (false) belief that fp->joints[TransN] was NULL for the boss. Per-frame telemetry showed TransN was non-NULL and the right joint — the bytes were just wrong. That whole change set is discarded. Also enriches the gcParseDObjAnimJoint UNHANDLED log line with the raw u32 word value, so a similar corruption will be diagnosable from log alone in the future. See docs/bugs/boss_event32_cache_invalidation_2026-05-01.md for full write-up including the systemic-audit findings. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
scmanager.c gets a #define PORT_START_BOSS that hard-defaults the boot scene to nSCKind1PGame at nSC1PGameStageBoss with fkind=Mario, plus an SSB64_SPGAME_FKIND env-var override alongside the existing SSB64_START_SCENE / SSB64_SPGAME_STAGE. Off by default; uncomment the #define to enable. Useful for reproducing boss-only bugs without manually advancing through menus on every iteration — load-bearing for the okutsubushi-fix verification cycle. docs/boss_endgame_segfault_handoff_2026-05-01.md captures what's known about the separate SIGSEGV-on-boss-defeat issue the user mentioned next on the queue, and points to the saved log (logs/boss-okutsubushi-FIXED-2026-05-01.log) so the next session investigating the death-status transition has a starting point. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Defeating Master Hand crashed the audio thread ~1s after the death
animation started, inside the FGM bytecode interpreter
(func_80027460_28060) dereferencing a NULL/garbage ucode pointer.
Root cause is an N64-only struct type-pun that no longer holds on LP64.
D_8009EDD0_406D0 is defined in n_env.c as ALWhatever8009EDD0 (with
`u8 **fgm_table_data`, `u16 fgm_ucode_count`) but declared in sc1pgame.c
as `extern alSoundEffect` (with `u16 sfx_max`). On N64 the two struct
layouts coincidentally aligned at byte offset 0x28: alSoundEffect's
sfx_max (u16) overlaid ALWhatever8009EDD0's fgm_ucode_count (u16). The
boss-defeat sequence's
sSC1PGameBossDefeatSoundTerminateTemp = D_8009EDD0_406D0.sfx_max;
D_8009EDD0_406D0.sfx_max = 0;
/* … cinematic plays … */
D_8009EDD0_406D0.sfx_max = sSC1PGameBossDefeatSoundTerminateTemp;
was an N64-only type-pun for "save/zero/restore the FGM playback gate" —
zeroing the count made func_800269C0_275C0 short-circuit on `id >= count`
and refuse to queue any more FGMs while the cinematic ran.
On LP64 the layouts diverge. In ALWhatever8009EDD0, ALPlayer grew 16→32
bytes and three `**` fields each grew 4→8 bytes, shifting fgm_ucode_count
to offset 0x40. In alSoundEffect, four `void *` fields each grew 4→8
bytes, shifting sfx_max to offset 0x38. At LP64 offset 0x38 in
ALWhatever8009EDD0 lives the LOW 16 BITS of fgm_table_data — a u8**
pointer. The boss-defeat write zeroed those 16 bits, leaving the high
48 intact: the array pointer became a wild address. Every subsequent
`D_8009EDD0_406D0.fgm_table_data[id]` read garbage; one of those values
landed in an FGM node's `unk20` ucode pointer; the audio thread's next
bytecode-parse iteration faulted on `*ucode++`.
Diagnostic fingerprint: fgm_table_data observed transitioning from
0x100b251b8 → 0x100b20000. Low 16 bits cleared, high 48 unchanged —
exactly the shape of a u16 write of 0 at the field's address, which
pinpointed the writer to alSoundEffect's sfx_max in sc1pgame.c.
Fix: two `#ifdef PORT` accessors in n_env.c that operate on
fgm_ucode_count directly (the field that actually carried the FGM-gate
semantic on N64) — `portAudioSaveAndBlockFGMs(out)` and
`portAudioRestoreFGMs(saved)`. sc1pgame.c routes through them under
`#ifdef PORT`. The non-PORT branch keeps the original alSoundEffect-
typed accesses so IDO byte-matching on the decomp source isn't
disturbed.
Class: LUS-vs-decomp typename shadow. Same family as
oscontpad_lus_sizeof_overrun_2026-04-24 (libultraship's larger OSContPad
overrunning the decomp's 24-byte stack buffer). General rule: any global
declared with two different struct types across translation units is OK
on N64 if the layouts coincide there, but LP64 widening of pointers/`**`
fields can shift the layouts independently in each view, breaking the
alignment. Look for `extern X Y;` in one TU and `Z Y;` in another with
`X != Z` — those need explicit per-platform accessors or a single shared
type.
Detailed write-up at docs/bugs/boss_defeat_sfx_max_typepun_2026-05-01.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The handoff doc speculated the post-defeat crash was a cutscene/credits loading issue. Root cause turned out to be the sfx_max type-pun trap, fixed in 2d4020f. The bug write-up at docs/bugs/boss_defeat_sfx_max_typepun_2026-05-01.md supersedes this stub. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This branch fixes two distinct Master Hand boss-fight bugs:
Master Hand's moves breaking — okutsubushi (palm slap), okupunch, drill leaving him stuck behind the stage. Root cause:
port_aobj_register_halfswapped_rangeonly evicted one of three Event32 walker caches on figatree heap reload, so re-entry into the stage walked stale halfswapped streams. Fix: evictsUnswappedHeadsandsRejectedHeadsalongsidesUnhalfswappedVisitedwhenever a heap range is re-registered.See
docs/bugs/boss_event32_cache_invalidation_2026-05-01.md.Audio-thread SIGSEGV on defeat — crash ~1s into the death animation in the FGM bytecode interpreter. Root cause:
D_8009EDD0_406D0is defined asALWhatever8009EDD0inn_env.cbutextern'd asalSoundEffectinsc1pgame.c. On N64 the two views aligned at offset 0x28, making the boss-defeatsfx_max = 0an intentional FGM-gate type-pun. On LP64 pointer widening shifts the layouts independently —sfx_maxnow lands inside the low 16 bits offgm_table_data(au8 **), so the write zeros half of a live pointer and the audio thread later faults dereferencing it. Fix: explicit#ifdef PORTaccessorsportAudioSaveAndBlockFGMs/portAudioRestoreFGMsthat targetfgm_ucode_countdirectly.See
docs/bugs/boss_defeat_sfx_max_typepun_2026-05-01.md. Same family as the OSContPad shadowing bug.Both fixes preserve the non-PORT decomp paths verbatim so byte-matching builds are undisturbed.
Commits
2b25c69Evict event32-walker caches on figatree heap reload68f3b6aPORT_START_BOSS dev shortcut (off by default; useful for future boss-stage debugging)2d4020fFix audio-thread SIGSEGV at boss defeat — sfx_max type-pun trap0c6a9e9Remove obsolete endgame-SIGSEGV handoff stubTest plan
🤖 Generated with Claude Code