- Commit CLAUDE.md + AGENTS.md so a fresh clone has the operating manual and
the ABI/no-game-logic rules (trimmed of stale phase status; the personal
pd_port path removed; points at the split docs).
- Sweep every stale doc reference across the tree after the restructure:
PCPortResearch.md -> internals.md / dev/findings.md,
PORT-LEARNINGS.md -> porting-notes.md,
AGENT-WORKFLOW.md -> dev-process.md,
docs/{GRAPHICS-BACKLOG,HANDOFF-ARCHIVE,GE-ENV-PROBES,LEVEL-*,TEXTURE-GLITCH-ANALYSIS,AUDIT-M6-*,AUDIO-PLAN}.md -> docs/dev/.
- Drop the superseded docs/PLAN-linear-level-sweep.md.
- docs/HANDOFF.md + docs/BACKLOG.md stay local-only (rolling working files);
CLAUDE.md now handles their absence in a fresh clone.
- README, LICENSE (MIT for the port layer), NOTICE, CONTRIBUTING.
- Restructure docs: internals.md, porting-notes.md, dev-process.md +
dev/agentic-development.md (the research write-up), dev/ (raw record).
- Replace decomp status-badge CI with a PC-port validate workflow.
- Remove session/agent scaffolding and stale planning docs from the tree.
- Add .gitattributes (normalize line endings to LF); add screenshots.
The pre-2026-08 history is the GoldenEye 007 decompilation by Larry Ficken
(KholdFuzion) and contributors, preserved unchanged. The PC-port work from
2026-08-17 on is mine.
Adds env-gated GE_D75 probe in title.c sub_GAME_7F007F30 (capped 8 lines).
Booted the bare front end through the gun-barrel sequence:
- Bond chr model + gun model entirely absent; both Model instances are
valid (non-NULL obj, numMatrices 21/1) -> 'model not loaded' ruled out.
- chrModelInstance->render_pos is valid and freshly written each frame
(== that frame's dynAllocate mtxlist base, clean double-buffer swap,
o2p returns it unchanged) -> the 'render_pos = stale dynAllocate arena'
hypothesis (D115 #5) is RULED OUT; do NOT land a persistent render_pos
buffer.
- Zero fast3d DL warnings during the sequence -> not the D144/D146
corrupt-sub-DL family.
Failure is downstream in drawjointlist/dotube vtx/node-DL resolution or an
off-screen basemtx. Next step: a drawjointlist-level probe. Full write-up
in PCPortResearch.md 'D75 Bug 2 - RUNTIME PROBE'.
-level_09 nonclear stable (91.66->91.62, D117 pan noise); build green.
Removes inert env-gated (GE_D54/GE_D63/GE_D90) probe blocks left in game
and libaudio TUs after those findings closed. All were #ifdef PORT +
getenv-guarded with no side effects; bg.c also drops a bare non-PORT
static (d63bgprimarycount) + entry log. No behaviour change.
Build green (ntsc-final, 10/10 link); -level_09 + -level_20 render
crash-free (19 frames each, Silo frame 300 @ 88.4% non-clear).
Static audit of every osSetIntMask(OS_IM_NONE) in the compiled audio code
(event.c, csplayer.c, synaddplayer.c, snd.c): all balanced on every exit
path, including alEvtqPostEvent's freeList-empty early return. No unbalanced
same-thread leak exists -- the D152 s_imLock wedge is not a missing restore.
Two real defects fixed (#ifdef PORT, N64 verbatim under #else):
1. snd.c sndSetSfxSlotVolume walked the live ALSoundState list and posted to
the shared ALEventQueue without holding OS_IM_NONE, unlike its twin
sndDeactivateAllSfxByFlag. On PC: unguarded walk racing amMain + one lock
acquire/release per matching sound. During the mission-failed fade-out
(sndSetScalerApplyVolumeAllSfxSlot -> sndApplyVolumeAllSfxSlot -> here,
per frame) that is a per-frame lock-acquire storm. Now holds the mask once
across the walk; sndApplyVolumeAllSfxSlot batches its slot loop under one
recursive hold.
2. port/src/libultra.c: portThreadWrapper now calls imThreadExitRelease() when
a host thread returns from its entry -- if it still owns s_imHeld, release
the orphan (+ LOG_ERROR). Kills the 'transient thread acquired OS_IM_NONE
and exited' leak (the §F dump's own alternate hypothesis) and the re-wedge
when a new thread reuses the dead pthread id and imAcquire mis-detects
recursion. Steal-lock kept as backstop.
Build 240/240 green; -level_09 frame 300 renders 91.7% non-clear crash-free;
-level_20 boots crash-free. Fade-out repro is playtest-gated (not headless-
reproducible) -- user to confirm no black screen + no D152: log lines.
Re-audited the M-28 bgTestRayIntersectionInRoom GBI-parser port (bg.c):
- G_TRI1 (w1>>16/8/0 & 0xff /10) and all 12 G_TRI4 nibble recoveries
cross-checked twice against the N64 #else path and the raw BE-word bit
layout -- both CORRECT, no change.
- BUG: vtxoff read gdl->dma.par & 0xf. The PC Gdma_le shim maps .par to
bits 0-23 of word0 (packed length, 16-aligned) not the N64 byte-1
params field at bits 16-23, so vtxoff was silently 0 -> wrong
vtxbase[idx] for any G_VTX batch with v0 != 0. Now
((u32)gdl->words.w0 >> 16) & 0xf.
- Sibling ported: bgTestBulletHitBackground tail (post-hit G_SETTILE
back-scan for tileformat/tilesize) was itself an unported raw
((u8*)gdl)[0]/[1] + 64-bit words.w0<<11>>30 parser over the same
D85-widened 16-byte room DL. #ifdef PORT: opcode from bits 24-31,
byte1 from bits 16-23, (u32)-forced 32-bit width for the tilesize
shift. N64 path kept under #else.
- Diagnostic: GE_D154=1 logs the parsed cmd / vtx-index stream (call
header + per-TRI1/TRI4 idx) for the first 64 invocations so the
integrator/user can eyeball the parse. Verification is playtest-gated
(shoot a wall/floor on an idle machine) -- the no-input level sweep
never enters this function.
Build green (ntsc-final, 242/242). ABI/GBI-parse only, no game-logic
change. docs/PCPortResearch.md D154 + PORT-LEARNINGS §B updated.
D159 (RC1 / D149): texSwapAltRowBytes pre-swaps odd texture rows in u32
groups to cancel the N64 RDP odd-line TMEM address XOR during word block
loads. fast3d emulates no such XOR, so odd rows uploaded scrambled -> the
'interlaced' wallet-Bond photo and other large front-end I4/I8/IA8
textures. #ifdef PORT early-return; N64 body kept verbatim. Wallet photo
now renders as a clean grayscale portrait (still 180deg-rotated - D75
model-transform family, parked). -level_09 frame 200 coverage unregressed
(91.62 vs 91.66 golden = D117 noise), visual frame clean.
D164 (D76): constructor_menu00_legalscreen bounded its text loop on
&legalscreen_MRD assuming linker adjacency to legalpage_text_array;
mingw reorders file-scope globals (MRD lands 0x60 before the array) so
the do/while ran once. #ifdef PORT bounds on the array's own length.
Dam exit should play a scripted cutscene (Bond off the dam / bungee) before
the MISSION COMPLETE report; on PC it cuts straight to the report while
progression still works (bossReturnTitleStage fires). Add GE_D160=1 trace
points to localise whether the cutscene AI runs:
- bossReturnTitleStage entry (+ caller return addr)
- bondviewSetCameraMode (mode, stage, g_IntroSwirl, intro anim idx, caller)
- AI EndLevel/exit_level, CameraLookAtBondFromPad, CameraSwitch (tag lookup
result: cdef ptr / TagIndex<0 / tag NOT FOUND)
No behaviour change (all #ifdef PORT, getenv-gated).
gunUpdateAndFire's KF7 branch (`mdlhdr->Skeleton == &skeleton_gun_kf7`)
does `((f32*)stackpad2)[-8] = ...` then reads it back into
matrix_scalar_multiply -- a decomp register-allocation stackpad: a
scratch f32 the N64 compiler placed 32 bytes below the `u8 stackpad2[4]`
local. GCC's x86-64 stack layout is different, so the negative index
scribbles into an unrelated stack slot (return-address / saved-reg area)
-> 0xc000001d illegal-instruction the moment you fire a KF7.
Found via scripted-input (walk+fire) testing: 12 campaign levels survived
a firefight script, Caverns (-level_39) crashed reproducibly on a settled
machine at gunfire.c:817. addr2line -> gunUpdateAndFire. The KF7 is a
common campaign weapon (Facility/Surface/Caverns guards) so this would
hit the user early.
Fix: real `f32 kf7_flashscale` local under #ifdef PORT; N64 stackpad
expression kept under #else. Caverns now survives repeated fire to frame
1500+; -level_09 unregressed. PORT-LEARNINGS §A (stackpad hazard).
Drop the verbose raw-words / DESTROY obj-ptr-healthy lines now that D157
is root-caused + confirmed. Keep the compact per-criterion line
(type/ObjRefID/MinDif/stride/status) and the per-objective difficulty-gate
line -- both are the reusable signal for any future save/objective issue,
gated by g_savelogObjOnce + GE_SAVELOG so inert in normal play.
ROOT CAUSE of 'complete Dam, Facility stays locked'. The type-23
objective record's MinDificulty is a big-endian s32 at word 3; on N64 the
value is its low byte == offset 0xF, and struct objective_entry reads
'difficulty' as s8 @0xF. d88_propdefs.py bswap32's word 3, so on
little-endian the value moves to offset 0xC -> objective_entry.difficulty
@0xF reads 0 for EVERY objective. get_difficulty_for_objective() then
returns Agent(0) for all, so objectiveIsAllComplete() on Agent evaluates
objectives that should be difficulty-gated out (Dam obj 0 'Neutralize all
alarms' is Secret Agent; obj 1/2 are 00 Agent) -> they're incomplete ->
never returns TRUE -> end_of_mission_briefing() / the unlock write never
runs. User was right: you don't touch an alarm on Agent Dam.
Fix (D151-class, #ifdef PORT): reorder struct objective_entry's word-3
tail so 'difficulty' reads @0xC (post-swap position); N64 layout kept
under #else. Also fixes dump_objectives.py (was reading byte 0xC = the
BE high byte = always 0; hence LEVEL-OBJECTIVES.md said 'Agent' for
everything). Dam now decodes obj0=Secret Agent, obj1/2=00 Agent,
obj3=Agent. -level_09 coverage identical (phash delta is the M-30 RC2
texture change, not this).
Run 2 crit dump: Dam obj 0 walk is correctly synced (type 23 START stride
4, then 4x type 25 DESTROY_OBJECT tags 0-3, stride 2) and ObjRefID reads
right (0,1,2,3). All 4 DESTROY criteria evaluate INCOMPLETE -> obj 0 not
complete -> objectiveIsAllComplete=0. Dam Agent obj A = 'Neutralize all
alarms' (4 ALARM-tagged boxes). Need to know if the alarms are (a) not
found, (b) found still-healthy because the player didn't shoot them, or
(c) found still-healthy after being shot = destruction-detection bug.
Added obj=/prop=/healthy= to the per-alarm SAVELOG line.
While the objective-completion -> save-unlock chain is being fixed (M-30
blocker), GE_UNLOCK_ALL=1 flips get_debug_enable_all_levels_flag() so the
front-end mission-select offers all 20 solo levels instead of just the
next unlocked one. #ifdef PORT, env-gated, one-shot cached; retail
behaviour unchanged without the var. Verified: mission-select renders all
20 level tiles, no crash. -level_09 unregressed.
First GE_SAVELOG run showed bossReturnTitleStage stage=33 (Dam) with
objectiveIsAllComplete=0 -> end_of_mission_briefing never called -> no
unlock. The loop bails at obj 0 (get_status_of_objective != COMPLETE).
Need to see what the criteria walk reads. Added a crit-level dump (type,
ObjRefID, TextID, sizepropdef stride, first 3 words, currentstatus),
gated behind g_savelogObjOnce which bossReturnTitleStage sets only around
its own objectiveIsAllComplete() call so the per-frame poll doesn't
flood. -level_09 renders clean, 0 SAVELOG lines without the env var.
User: complete Dam -> 'previous' from debrief -> mission select, Facility
still locked. data/ge007.eep has no completion times -> the unlock save
isn't landing. Chain is bossReturnTitleStage (objectiveIsAllComplete?) ->
end_of_mission_briefing (guard on briefingpage/difficulty) ->
fileUnlockStageInFolderAtDifficulty -> fileOverwriteSaveSlotWithNewSave
(needs a DORESET spare slot) -> fileWriteSave (fileGamePakProbe) ->
EEPROM, then fileValidateSaves CRC-checks on reload.
GE_SAVELOG=1 (env, #ifdef PORT, no logic change) logs each link:
objectiveIsAllComplete + per-objective diff/status, the briefing guard,
the unlock args + free-slot lookup, the write (probe/block/crc), and the
per-slot validate CRC pass/fail. -level_09 unregressed (diag is
getenv-gated; the framediff move is the earlier RC2 mip fix).
fprintf(stderr) the model speed/playspeed/newspeed/oldspeed/timespeed/
elapsespeed/unkb0/unk88/anim values the first time modelTickAnim's
non-finite frame guard triggers, so the Facility-outro repro pins the
NaN source (likely a near-zero blend denominator or a misaligned Model
field). One-shot, stderr, #ifdef PORT.
Verified: -level_09 1500 frames crash-free, guard does not fire in
normal play.
After D155 the user re-ran the playthrough and it hung again at the end
of Facility -- same stack but frames=12962 frozen CONSTANT for minutes =
a true infinite loop, not D155's slow spiral.
The `while (1)` at model.c:3131 in modelSetAnimFrame2WithChrStuff steps
root-motion accumulation one anim frame at a time from framea to frameb.
frameb comes from modelTickAnim's `frame += playspeed * speed` per tick.
A cutscene anim transition with a near-zero blend/timespeed/unkb0
denominator (model.c:3436/:3477, guarded >0 but not against tiny values)
makes model->speed/playspeed huge or NaN -> frameb huge/NaN ->
floorFloatToInt garbage -> endframe-curframe ~= 2^31 -> effectively
infinite.
#ifdef PORT guards (N64 unchanged):
- modelSetAnimFrame2WithChrStuff: if frameb non-finite or |frameb| >= 1e6,
snap to framea (loop does ~0 iterations; modelConstrainOrWrapAnimFrame
at :3211 still clamps model->frameb to a real frame).
- modelTickAnim: after the tick loop, if frame/frame2 went non-finite,
fall back to the pre-loop model->animframe1/animframe2 so the NaN can't
poison model state every frame.
`!(x > -1e6f && x < 1e6f)` catches NaN too. Anim frames are 0..few-hundred
so 1e6 is a safe ceiling.
Likely deeper cause (needs the repro): these speed/blend fields are Model
members -- a D100/D140-style struct-pun misalignment would feed garbage
exactly when a cutscene sets an unusual transition. Dump
model->speed/playspeed/timespeed/unkb0 in modelTickAnim during the
cutscene if it still hangs.
Verified: build green; -level_09 frame 200 unregressed (guards never fire
in normal play). PORT-LEARNINGS §E.
User hit a "hang" (process alive, heartbeat dumps) at the end of the
Facility outro cutscene. Symbolicated mainThread stack: parked in
modelConstrainOrWrapAnimFrame <- modelTickAnim <- chrUpdateAnim <-
chrTick <- playerTick <- propsTick <- lvlRender across every dump.
Root cause: waitForNextFrame() (src/game/frametiming.c) passes
nextFrameTime = (osGetCount()-prev+387937)/775875 to updateFrameCounters
UNCLAMPED. osGetCount() is wall-clock on the port (D117), not VI-locked,
so a real-time stall (asset load at the cutscene->debrief boundary, made
worse by a local LLM hammering the box) makes nextFrameTime balloon to
hundreds/thousands. That becomes speedgraphframes -> g_ClockTimer, which
drives modelTickAnim's `while (numticks-- > 0)` per character per render
plus dozens of `for (i=0;i<g_ClockTimer;i++)` sim loops -> one frame does
thousands of anim ticks x N chars -> multi-second frame -> heartbeat
trips, and the slow frame feeds an even bigger delta -> unrecoverable
spiral. The N64 was VI-bound and never produced deltaFrames > ~2.
Fix: #ifdef PORT clamp nextFrameTime to FRAMETIMING_PORT_MAX_CATCHUP (6)
in waitForNextFrame(). N64 path verbatim. Timing-compensation class,
cf. D117/D134. PORT-LEARNINGS §E.
Verified: build green (ntsc-final); -level_09 unregressed (frame 200
nonclear delta 0.04pp, full-window crash-free). The clamp only engages
when a frame took >~100ms, so normal play is byte-identical.
bgTestRayIntersectionInRoom (bg.c) is the D135 sibling flagged since M-23:
an unported N64 GBI parser walking the 16-byte PC room DL with raw
((u8*)gdl)[k] / ((u32*)gdl)[i] 8-byte-Gfx byte/word indexing. It runs on
every shot that resolves against background geometry (bgTestBulletHitBackground),
so it AVs the first time you shoot a wall or floor in a real playthrough --
invisible to the no-input level sweep.
Ported #ifdef PORT (N64 path kept verbatim under #else):
- vtxoff / vtxbase / op read through the .dma view, matching the
already-ported bgBuildRoomVtxBounds.
- G_TRI1 vertex indices from (w1 >> 16/8/0) & 0xff, /10.
- the 12 G_TRI4 index nibbles recovered from (u32)gdl->words.w0/.w1,
each mapped from the N64 byte/halfword layout (table in the code
comment; cross-checked twice).
- both texturenum recoveries -> -1: the KSEG0 *(u16*)(w1-8) deref is not
valid for the port's converted GDLs (same as D135); texnum only selects
the bullet-impact decal/sound, parked with D77.
Build: green (ntsc-final). NOT VERIFIED -- a no-input GE_PCDUMP capture
never fires a bullet so it never enters this function; needs a firefight
into a wall on an idle machine. This session's box was too thrashed to get
a clean run (D153 driver-crash-under-load). Committed as WIP so the port
work isn't lost; next session: idle-machine wall-shoot test + -level_09
framediff, then mark verified.
§F/HANDOFF D154, PORT-LEARNINGS §B (D135 corollary).
D150 (src/str.c) — NULL-tolerant strcpy/strncpy/strcat under #ifdef PORT.
The watch BRIEF/OBJECTIVES pages build text with strcat(buf, langGet(id));
langGet() returns NULL on PC for a string bank the in-level flow never
loaded, and str.c's primitives are __nonnull__ builtins so a plain param
NULL check is optimised away — the guard launders the ptr through an empty
asm (GE_IS_NULL). NULL src -> empty string instead of an AV crash.
D151 (src/bondtypes.h) — watch data (objectives / mission background / M /
Q / Moneypenny) all rendered blank. propDef types 35 (WatchMenuObjectiveText)
and 23 (ObjectiveStart) store a plain s32 lang slot id in their 3rd word;
the decomp structs decode it as [u16 reserved][u16 text] reading text at
offset 0xA, which only works because the id is in the low 16 bits of a
big-endian word. d88_propdefs.py bswap32's the whole word -> text @0xA
reads 0 -> langGet(0) -> NULL. Fix: text is a full u32 at the word offset
under #ifdef PORT (N64 layout kept under #else). No converter change, no
sidecar regen. This is the D143/D150 "briefing text renders blank" root cause.
D152 (port/src/libultra.c) — mission-failed / objective-failed permanent
black screen. The D147 recursive-mutex behind osSetIntMask can be leaked
(libaudio has unbalanced / early-return mask calls; a transient thread can
acquire OS_IM_NONE and exit) -> mainThread + amMain both block forever in
alEvtq* -> gfx pipeline stalls. Live gdb.txt from the user's session
confirmed the deadlock (mainThread in the mission-fail audio fade-out
sndSetScalerApplyVolumeAllSfxSlot -> alEvtqPostEvent). Mitigation: model
the mask as a self-healing logical lock (owner/depth under s_imMx + condvar)
— normal microsecond critical sections behave as before, but a waiter
blocked > 2s concludes the holder leaked it, logs the stale owner + the
stealing caller's return address, and steals the section. Worst case is a
~2s audio hiccup + a log line naming the leak, instead of a hung game.
Proper narrow fix (dedicated ALEventQueue lock) still owed.
D153 investigated and dismissed — the "-level_09 crashes at frame ~900"
seen while regression-testing was D117/D134 host-scheduling flakiness under
heavy concurrent build+run load; on a quiet machine -level_09 runs past
frame 2400 clean with all three changes applied.
Build: green (ntsc-final). Golden framediff not run clean (same machine-load
flakiness); one quiet -level_09 run = 2400+ frames, 0 heartbeats.
src/game/lv.c (#ifdef PORT, after init_menus_or_reset on the title stage):
GE_STARTMENU=<id> boots the front end directly into a menu, skipping
intro + folder/mode/mission/difficulty select. GE_STARTMENU_PAGE=<n> picks
the mission_folder_setup_entries row (default 1 = Dam), GE_STARTMENU_DIFF
the difficulty. Sets briefingpage / selected_stage / gamemode=SOLO /
selected_difficulty / prev_keypresses so the target screen has usable
context. Lets WS2 screens be retested in seconds:
13 = MISSION_COMPLETE 12 = MISSION_FAILED
10 = BRIEFING 7 = MISSION_SELECT 6 = MODE_SELECT
No effect unless the env var is set; not compiled without PORT.
Verified: GE_STARTMENU=13 -> mission-complete renders, "Next" (A) transitions
to briefing -> mission-select with no crash/hang (D75 3D-model cosmetics
aside). -level_09 unregressed.
D143 (src/game/textrelated.c): langGet() returns NULL for a string slot the
current flow has not loaded (non-DEBUG builds; DEBUG substitutes a
placeholder). The N64 retail menu flow guarantees every front-end string is
resident before it is drawn; the PC flow does not yet, so a missing
briefing/objective/mode-select string reached textRender/textMeasure/
textWrap as NULL and faulted in the `*text` scan. Added #ifdef PORT NULL
guards at all four entry points -> degrade to "draw nothing" (blank text)
instead of crashing. User verified: front-end now navigable folder -> mode
-> difficulty -> level -> in-game.
D144 (port/fast3d/gfx_pc.cpp gfx_sp_matrix): a front-end 3D model
(MISSION COMPLETE dossier / mode-select wallets - D75 family) emits a
gSPMatrix whose pointer field was never resolved; w1 arrives as
0xFFFFFFFFFFFFFFFF, seg_addr() passes it through, and gfx_sp_matrix faults
reading addr[]. Guard: an unmapped/non-canonical addr -> substitute identity
(model draws with wrong transform, already a parked D75 cosmetic) so the
screen and its "Next" -> menu transition survive.
Verified: -level_09 unregressed (framediff 2/2 ok w/ HUD mask, no crash).
D142 (src/bondconstants.h): GCC/mingw gives an all-non-negative enum an
UNSIGNED underlying type, so a descending loop that relies on the counter
going negative to terminate spins forever. LEVEL_SOLO_SEQUENCE hit this in
fileGetHighestStageDifficultyCompletedForFolder (file2.c:859/887), called
every frame by the SELECT FILE screen -> whole game froze (no crash, just a
kernel-heartbeat stall). Fix: #ifdef PORT negative sentinel forces the enum
signed; first real enumerator stays 0, sizeof stays 4, no stored value
changes. Same trick DIFFICULTY already uses (DIFFICULTY_MULTI = -1).
Verified: front-end now boots -> intro -> SELECT FILE and stays alive 3000+
frames; menu chain navigable (user playtest).
port/src/input.c: GE_INPUTSCRIPT env var -- headless scripted controller-0
input for front-end/menu walkthroughs. "<frame>:<tok>[,<tok>]" entries;
buttons (A B Z START L R UP DOWN LEFT RIGHT CUP CDOWN CLEFT CRIGHT) pulse,
stick tokens (SUP SDOWN SLEFT SRIGHT / SNONE) sustain. When set it is the
sole controller-0 source (real kbd/mouse ignored) so a no-focus relative-
mouse window can't inject phantom deltas. Unset => zero effect.
D140 (src/game/bondview.h, bondview2.c): pause-menu crash in
bondviewRenderWatch -> bondviewTransformManyPosToViewMatrix(field_23C=NULL).
player +0x230 is a struct Model + RW pool punned into a 0x184-byte field run;
field_23C/watch_scale_destination/pause_watch_related_adjust alias
.render_pos/.scale/.animframe1. PC sizeof(struct Model) grows -> aliases
break -> NULL render_pos. Fix: real inline struct Model + u32 watchRwPool[192]
under #ifdef PORT; named reads redirect via GE_WATCH_* macros (N64 #else
verbatim).
D141 (src/game/gunfire.c): crash D140 exposed.
set_enviro_fog_for_items_in_solo_watch_menu walks bodymodel->Switches[]
(ModelNode* array) with raw byte offsets +0x48/+0x5c, j+=4 -> PC 8-byte
stride reads garbage -> AV in modelGetNodeRwData. #ifdef PORT uses
Switches[18 + (j>>2)] / Switches[23 + (j>>2)].
D118d (src/game/options.c): watch inventory list over-scrolls with keyboard
W/S (one press skips several items). GE fast-scroll is a raw per-frame level
check (joyGetStickY < -0x46); digital pads sit at max every frame. #ifdef
PORT drops the stick term -> nav via the latched single-step path. Feel
unverified headlessly.
Verified: build green (ntsc-final); -level_09 framediff 3/3, -level_20
crash-free (unregressed).
First real human WS6 playthrough. Each crash is a subsystem the 21-level
boot sweep never exercises.
- D135 (propobj.c) bgTestHitOnObj: unported N64 GBI parser — raw 8-byte-Gfx
byte/word indices desync on the PC 16-byte Gfx stream, walk off the DL.
Ported #ifdef PORT to gdl->words.w0/.w1 shifts (mirrors pd_port bg.c:3635).
Sub-fix: objHit:9717 div-by-0 — texnum recovery not portable, force -1.
- D137 (gunfire.c) gunDrawSight: s32 sp54 holds a Gfx*; 4-byte slot splices
adjacent stack into the pointer -> wild DL write on right-mouse aim.
#ifdef PORT: Gfx *sp54.
- D138 (snd.c) sndCreatePostEvent: parked audio never drains evtq->allocList,
per-objTick positional-sound posts stall the main thread ("1 room from the
end" freeze). #ifdef PORT no-op until audio lands (D77).
- D139 (cleanup_objects.c) cleanupObjects: read the propDef type via
(u8)obj[0] — correct only big-endian (type = low byte of the header word).
On LE that's extrascale -> walk never hits PROPDEF_END -> runs off the blob
-> frees garbage -> stage-unload crash. #ifdef PORT uses pdef->type.
UNVERIFIED (fast teardown test hit D140 first).
Open: D140 pause-menu watch-model NULL matrix; D118c aim-mode mouse-down
-> crouch. New harness tools_pc/repro_gdb.sh + build-pc/d136_cmds.txt.
-level_09 unregressed (91.65%). Docs: PCPortResearch §F D135-D140,
PORT-LEARNINGS §B/§C, HANDOFF M-23 + M-24 next-task.
Types 14/19/38/44 declare their Index{1,2,3} fields as a union with a
pointer, so on PC each is an 8B/8-aligned slot (Index1@8, Index2@16,
...) and the record ends in a *next the proplvreset2 walk writes. The
converter was laying the N64 tight-4-byte index words at PC bytes 4/8/12,
putting Index1 where the compiler reads Index2 and leaving Index2 zero.
Non-crashing but silently broke switch-doors, dual weapons, padlocked
doors, and safe/safe-item links.
- tools_pc/d88_propdefs.py: PROPDEF_PC_BYTES 14/19->32, 44->40; handler
emits Index{k} into low 4B of the 8-aligned slot at PC 8+8*k.
- loadobjectmodel.c sizepropdef() PORT: LINK/SWITCH/LOCK_DOOR->8,
SAFE_ITEM->10, TAG stays 6.
Verified: -level_09 framediff 3/3 PASS; -level_20 identical to a
freshly-built pre-D132 baseline (frame_000320 delta is pre-existing
stale-golden/Silo slowdown, not a regression); Archives/Streets/Dam
load+render+no-crash.
Remove closed D63 debug scaffolding (libultra.c + game-file probes)
D63 is a closed finding. Strips the dead TEMP D63 probe code:
- port/src/libultra.c: watchdog thread, activity ring, gun-barrel slot
watchers, and their call sites in osSendMesg/osRecvMesg/osSpTaskStartGo.
- #ifdef PORT probe blocks in blood_animation.c, dyn.c, front.c, rsp.c,
title.c (all env-gated / no side effects).
port/fast3d/gfx_pc.cpp still carries a D63 branch trail -- left for a
later pass. Build links clean; no behaviour change.
@
slotID>>10 indexes g_LangBanks[45]; slotID&0x3ff indexes the resolved
bank table. A bare -level_XX boot that reaches the cast/credits text path
(Cuba, bondviewRenderCredits -> D76 area) references banks the menu flow
never loaded -> OOB index / stale non-NULL bank pointer -> fault. langGet
already returns NULL for a missing string, so reject an impossible bank
index or an unmapped bank pointer the same way. Reduces (not eliminates)
the Cuba bare-boot credits-path crash; the level itself loads+renders
fine and the credits work through the real front-end flow.
The special-portal marker loop hardcoded the N64 8-byte bg_portal_data_entry
stride and controlbytes1@6: ((u8*)g_BgPortals)[(portal<<3)+6] |= 2. On PC
the struct is 16B (offset_portal widened to 8B) with controlbytes1@10, so
this scribbled PORTALFLAG_SPECIAL into the middle of a portal's offset_portal
pointer -> 0x0002_0000_xxxx garbage ptr -> crash on portal_pts->numPoints
(bg.c:5723) when a chr LOS check walked that portal. Only levels with a
specialportalarray entry (Control -level_23) triggered it.
#ifdef PORT uses g_BgPortals[portal].controlbytes1 |= PORTALFLAG_SPECIAL
(the struct accessor every other site already uses); N64 raw form kept
under #else. Control now PASSES (90.8%, was CRASH). Sweep 17 -> 18/21.
The libaudio bank tree (port/src/romdata.c) is Phase-3 parked; some levels
request a soundIndex that resolves to a scrambled/OOB ALSound* in the
converted bank, and sndSetupSound() then faults on sound->keyMap. Skip a
sound whose pointer is plainly not a mapped address rather than crash the
level. Surface1 -level_36 now loads+renders (77.5%, was CRASH). Sweep 16
-> 17/21. Bunker1/Dam unregressed.
criteria_picture (30) / criteria_roomentered (32) / criteria_deposit (33)
/ setup_objective_text (35) each end in a T*next list pointer that the
setup walk writes unconditionally (set_parent_cur_obj_* /
setup_briefing_text_entry_parent). On x86-64 the pointer widens 4->8B and
8-aligns at offset 16, making the struct 24B/6w (N64 16/20). d88_propdefs.py
emitted N64 size -> the runtime 8-byte ->next store clobbered the next
propdef record's header -> walk desync -> command indices drifted ~100 ->
setupDoor linkedDoor resolution landed on the wrong record -> crash.
Fixes C3r (Bunker2 -level_27), C4 (Depot -level_30), C6 (Surface2 -level_43)
in one change. Sweep 13 -> 16/21. Bunker1/Silo/Aztec/Archives/Egypt/Train
unregressed; d125_check.py 21/21 MATCH.
- tools_pc/d88_propdefs.py: PROPDEF_PC_BYTES[30/32/33/35]=24 + typed handler
- src/game/loadobjectmodel.c: sizepropdef PORT returns 6 for those types
tools_pc/d88_propdefs.py had a per-type handler table plus a generic
fallback. Six `inherits ObjectRecord` propDef types (47 TINTED_GLASS,
39 VEHICHLE, 40 AIRCRAFT, 45 TANK, then 13 AUTOGUN, 20 AMMO/MultiAmmoCrate)
had no handler and fell through to the generic arm, which bswap32's N64
word 1. For an ObjectRecord that word is [s16 obj][s16 pad] and needs a
half-swap, not a word-swap - so the model id `obj` landed in the wrong
16 bits, giving a garbage/OOB index into PitemZ_entries[] and a crash in
modelLoad (loadobjectmodel.c:393) / modelInitRwData (model.c:6249).
BUNKER1/Silo were unaffected because neither emits any of the six types.
Fix (converter + matching walk stride, no game logic):
- d88_propdefs.py: OBJ_TAIL_DESC handler - real 144B PC ObjectRecord
prefix + typed tail (pointers widened 4->8B/8-aligned, [u16|u16] words
half-swapped). PROPDEF_PC_BYTES 39/40 176/180->208, 45 224->248.
- loadobjectmodel.c sizepropdef() #ifdef PORT arm: VEHICHLE/AIRCRAFT
44/45->52, TANK 56->62 words (N64 values in the #else sizeof switch).
Verified: -level_33/34/35 clear the model-load chain (they now crash
further in, in unrelated chr/fast3d code - separate blockers);
-level_09 BUNKER1 and -level_20 Silo unregressed. Sidecars regenerated
(d88_emit.py --regen, 21/21). Full write-up: PCPortResearch.md D122
(uncommitted, rides with the in-progress docs restructure).
Bare `-level_XX` on PC booted with default memory pools and OOM-crashed
early: the N64 per-stage memallocstringtable[] auto-inject loop in
bossMainloop is gated on g_DebugAndUpdateStageFlag, which `-level_` leaves
to rmonGetToken() (returns 0 on PC).
Fix: #ifdef PORT block in bossInitMainthreadData that, when `-level_` is
present and no -m arg was given, looks up the stage's memallocstringtable
row and tokenSetString()s it - prefixed with `-level_XX` (and `-hard N`)
because tokenSetString replaces the whole token buffer and bossMainloop
must still see `-level_` or it boots the title stage.
Setting g_DebugAndUpdateStageFlag=1 (the old HANDOFF suggestion) is NOT
equivalent - it renders the full title-stage intro instead of loading the
level. Details in PCPortResearch.md sec.H D121.
Verified: bare `-level_09` loads BUNKER1 directly, matching the old
`-level_09 -ml0 -me0 -mgfx100 -mvtx50 -mt700 -ma150` repro.
- D119: ~28 weapons_held[]->chr puns in chraction.c read .act_*.attack_item,
aliasing WeaponObjRecord.weaponnum at 0x80 on N64 (act union @0x2C+84).
Pointer widening moves the union to ~0x38 on PC -> garbage negative item
id -> OOB g_ItemStats -> segfault the instant a guard fires. PUN_ATTACK_ITEM
macro reads weaponnum directly; #else branch identical -> N64 unchanged.
- D120: chrCreateBloodStain PointUsage[] chain walk cycles forever on PC
(d43_emit opcode-0x18 collision-record conversion incomplete). #ifdef PORT
caps + bounds-checks both walk loops so the game survives; converter fix
deferred to its own session.
- input.c: mouse-look per-poll delta (no stuck accumulator); mouse X = turn
(stick-X), A/D = strafe (C-buttons), mouse Y = look. D118a/D118b minor
bugs documented in HANDOFF.
-level_09 now survives the BUNKER1 guard firefight (45s+ crash-free, was
hard segfault ~frame 1200 every run).
D115: THROWMTX/THROWPOS/THROWPREV in gunfire.c were raw byte offsets
((u8*)g_CurrentPlayer + handnum*sizeof(struct hand) + 0xAD8/0xB08/0xB48).
Both the stride (PC sizeof(struct hand) ~0x968 vs N64 0x3B8, D102) and the
base offset are N64-sized, so on x86-64 the address lands inside the
inline gait Model / bondhead-matrix region of struct player and
matrix_4x4_copy(THROWMTX,..) scribbles 64 bytes of live render state on
every casing-ejecting shot. The offsets are exactly
hands[handnum].throw_item_pos_related[_prev] and its translation row -
macros now use those field accessors under #ifdef PORT; N64 build
unchanged. Full survey in docs/AUDIT-M6-player-offsets.md (10 sites,
3 HIGH fixed here, 2 MED open incl. the likely 'weapon model doesn't
draw' cause).
D113 (investigation, no code change): the 'portal-BFS under-reach / void'
premise is stale - the BFS output is correct for BUNKER1's topology
(rooms 26/27 are behind PORTALFLAG_DISABLED closed-door portals, culled
identically on N64). The void is the closed-door models obj 153/154 not
rendering in their doorway -> D114 track.
D114 (investigation, no code change): joint-matrix chain + d43_emit.py
float emission verified clean post-D112; basemtx is a proper
right-handed +Y-up lookat, joint render_pos coherent and upright. All
plan hypotheses disproven. Residual = a horizontal mirror in the shared
port/fast3d viewport / MP-matrix path vs GE's left-handed screen
convention (also explains the open D75 mirrored-text / logo items).
Still open.
Plan + agent reports: docs/PLAN-M6-playable.md. §F: D113/D114/D115.
BUNKER1's portal-visibility BFS (sub_GAME_7F0B7F84 -> sub_GAME_7F0B5864)
projects each candidate portal polygon to a screen-space bbox. When the
player straddles a portal's plane, sub_GAME_7F0B5528 emits z==0 near-plane
clip points; transform3Dto2DWithZScaling then uses inv_z = -1e20, sending
those points to +/-1e20-scale screen coords. On N64 the two clip points
bracket the view symmetrically, so the degenerate-box check (bg.c:1713)
or the downstream screensize clamp yields a full-screen box and the room
is kept. On x86 the exact garbage can come back min>max on a single axis
(or non-finite), slipping past that check -> the portal is dropped and
the room the player is walking toward vanishes, showing skyRender's
background fill through the doorway.
Fix: #ifdef PORT, treat any wildly out-of-range / non-finite projected
bound as degenerate -> full screensize (matches the N64 outcome).
Verified -level_09: per-frame visible-room count 1-3 -> 2-4; rooms beyond
near doorways (r11 from r27, etc.) now render. Reworks the GE_D104 probe
to dump the per-frame visible-room list (gated, capped).
Residual (separate): a bounded central sky gap + a blurry grey wall/
ceiling texture remain in the storage rooms — a dropped-poly / texture-
dimension issue in the D85 room GDL path, not visibility.
zbufClearCurrentPlayer (viewport.c) clears the N64 Z buffer with the
'point the colour image at the Z buffer and gDPFillRectangle it' idiom.
fast3d does not emulate that: gfx_dp_fill_rectangle bails when
color_image_address == z_buf_address assuming glClear already cleared
depth, but the frame-start clear_framebuffer(true,false) is colour-only.
fast3d expects G_CLEAR_DEPTH_EXT (0x44) for a real depth clear and GE
never emits it. Here the two addresses don't even match
(gDPSetDepthImage(z_buffer & ~0x3F) vs gDPSetColorImage(K0_TO_PHYSICAL))
so the packed-Z fill colour is scribbled onto the framebuffer instead.
Net: every in-level frame rendered against a stale/garbage depth buffer,
all ~160k room triangles failed the Z test, only skyRender's background
fill survived -> flat dark-blue screen + HUD.
PORT branch in zbufClearCurrentPlayer emits a bare G_CLEAR_DEPTH_EXT and
returns, skipping the N64 idiom (a no-op / harmful in fast3d anyway).
Verified -level_09: 90% flat sky-colour -> 2585 distinct colours, sky
16%; recognisable BUNKER1 (textured walls, storage racks, floor). 70s
crash-free. Adds GE_D104 room-render-pass probe in bg.c (gated, capped).
Remaining: dark-blue wedge (geometry gap) + brown band top third
(ceiling tex); then struct player weapon/offset pass; then D75(b).
gunfire.c punned the 1st-person weapon Model onto hand->field_B68 and its
RW-data pool onto hand->modeldatas (a fixed run at field_B68+0x20). N64
sizeof(struct Model) ~0xBC keeps modelInit()'s header writes (obj@0x08,
datas@0x10, scale, attachedto...) clear of the pool at +0x20. On x86-64
struct Model is 0xE8 (pointer fields widened 4->8): modelInit() writes
objinst->datas at offset 0x20 -- directly ONTO &hand->modeldatas -- so the
datas field and the pool base alias. The first rwdata record written by
modelInitRwData() then clobbers objinst->datas (bit 32 set), and
modelGetNodeRwData() returns 0x1_xxxxxxxx -> modelInitRwData faults.
objinst->obj (0x10) likewise lands on hand->mtxlist.
Add struct hand.weaponModel (inline struct Model) + weaponRwPool[192] under
#ifdef PORT and route gunfire.c through HAND_WEAPON_MODEL/HAND_WEAPON_RWPOOL
macros. On N64 hand->mtxlist aliased the punned model's render_pos, so set
weaponModel.render_pos = rwmtx explicitly on PORT. Same class as D53.2
(watch model) / D100 (player gait model). N64 build byte-identical.
Verified: -level_09 renders BUNKER1 with NO crash across 3 runs of 30-70 s
(previously: deterministic modelInitRwData fault at VI post ~601). Frames
render continuously (GE_PCDUMP: ~46% non-clear pixels, stable). The render
is partial -- bbox is the lower half of the screen -- consistent with the
still-open D75 3D-model rendering bugs, but the crash chain is clear.
sub_GAME_7F06DB5C (the anim2-blend variant of modelBuildGroupMatrices) does
sp1C = (s32)arg2->Parent, then modelFindNodeMtx(arg1, (ModelNode *)sp1C, 0)
-- the 32-bit-pointer idiom its sibling modelBuildGroupMatrices() was
already cleaned up for. On x86-64 the (s32) cast drops the 0x140000000
module base of a static ModelNode, so modelFindNodeMtxIndex() dereferences
0x4012xxxx and faults. It also stashes &sp48[sp54] (a RenderPosView*) the
same way and calls g_ModelJointPositionedFunc with a bogus 3-arg cast.
Under #ifdef PORT, do the pointer work at full width and use the 2-arg
g_ModelJointPositionedFunc signature -- mirrors modelBuildGroupMatrices'
parentnode handling exactly. N64 path unchanged.
Verified: -level_09 no longer faults in modelFindNodeMtxIndex; advances to
a modelInitRwData crash (fixed by D102).
Every use is `&g_CurrentPlayer->model` passed to a modelXXX(struct Model *)
function (animInit, modelTickAnim, modelSetAnimation, modelSetScale, ...) --
this field IS an inline `struct Model`. The decomp represents it as
`Model *model;` followed by ~45 `s32 field_59C..field_650` (roughly the N64
`sizeof(struct Model)`, ~0xB8 bytes). On x86-64 `struct Model` is ~0x2A8
bytes, so `animInit(&g_CurrentPlayer->model, ...)` wrote a 0x2A8-byte struct
into that 0xB8-byte hole, overrunning field_654 (the gait RW-data pool that
animInit is simultaneously pointed at), bondheadmatrices, and the
viewx/viewy/viewleft/viewtop fields. The clobbered `model->datas` came back
with bit 32 set -> modelInitRwData faulted at 0x1_70076xxx (a valid
player-block address | 0x100000000).
Fix (PORT only, N64 unchanged):
- struct player: `Model *model;` -> inline `struct Model model;`. sizeof grows
~0x2A0; D98's `mempAllocBytesInBank(sizeof(struct player) ...)` covers it.
- The gait RW-data pool: the fixed `&field_654` gap is only ~0x80 bytes and,
with the bigger inline model, no longer even clears the struct. Add a
dedicated `u32 gaitRwData[256]` at the end of struct player and point
animInit there (initBondDATAdefaults.c).
Verified: the modelInitRwData bit-32 fault is gone (GE_D51 trace: the player
gait model's modelGetNodeRwData now returns clean pointers into gaitRwData).
-level_09 still reaches VI post ~601, then hits a separate deterministic
crash (3/3): modelFindNodeMtxIndex (model.c:375), FAULT 0x4012c9c0 -- a
truncated ModelNode* (low 32 of a 0x1_4012c9c0 module address), called from
sub_GAME_7F06DB5C (model.c:1479). Another D86/D92-class truncation in the
model node pipeline.
model->animflipfunc is an s32 Model field that stores a function pointer.
It is only ever set to bheadFlipAnimation (initBondDATAdefaults.c:198,
bondhead.c:430 -- both for g_CurrentPlayer->model). On x86-64 storing the
64-bit pointer truncates it; bheadFlipAnimation sits at module+0x10100 so
modelTickAnim's indirect call (model.c:3534) jumped to 0x00010100 and
faulted -- deterministically ~frame 5 on -level_09.
Same fix pattern as D92 (Model.unka0): modelSetAnimFlipFunction stores a
bool flag, and modelTickAnim calls bheadFlipAnimation() directly under
#ifdef PORT. N64 build unchanged.
Verified: the 0x00010100 fault is gone (2/2 runs); -level_09 now reaches
VI post ~601 (was ~421) before a separate, deterministic crash in
modelInitRwData (model.c:6298, MODELNODE_OPCODE_BSP case): modelGetNodeRwData
returns a bad pointer 0x170076434 (low 32 = 0x70076434, a valid player-
model DRAM addr, with bit 32 set) -- a D52/D86-class RW-data pointer-math
bug on the player's animated model.
initBONDdataforPlayer allocates a fixed 0x2A80 bytes (US/JP; 0x2A70 EU) for
each player -- the N64 sizeof(struct player). On x86-64 the struct is far
larger (dozens of pointer fields widened 4->8 bytes), so every write past
~offset 0x2A08 runs off the block. The player block sits directly below
g_GfxBuffers[0] in MEMPOOL_STAGE, so g_CurrentPlayer->field_2A08 = ft4 in
bondviewRenderDebugBondView (reached every frame -- debug_render_raster
defaults to DEB_BOND_VIEW) wrote a small-negative-float angle onto the
master display list: the zbuf-clear gsDPSetRenderMode's w1 became 0xbd8efa35,
and fast3d aborted with 'Unknown GBI opcode 0xffffb9 at 0x70078330'.
Allocate (sizeof(struct player) + 0xF) & ~0xF under #ifdef PORT. N64 builds
unchanged.
Verified: -level_09 no longer hits the GBI-opcode fatal (was deterministic
at frame ~5). Renders further, then hits a separate corrupted-function-
pointer crash in modelTickAnim (model.c:3534) -- the D75-class animated-
model path, out of scope here.
bondviewPlayerTickDamageAndHealth computes damagetype = (s32)(health*8);
with health <= 0 (a lethal/near-lethal hit) that is negative and then
indexes g_DamageTypes[] below zero. N64 US absorbed the small OOB read;
on PC it segfaults (observed ~frame 5 in BUNKER1 when a guard shoots
Bond, fault ~0xff..401210e8). EU/JP already clamp negative to 0 right
here -- extend that clamp to PORT. N64 US build unchanged.
chrpropUpdateRoomList and its helpers (sub_GAME_7F0B21B0 / sub_GAME_7F0BA2D4,
both max=7; s32 rooms[7] scratch) build prop room lists of up to 7 entries,
then write prop->rooms[0..count] + a 0xff/-1 terminator. PropRecord.rooms is
only PROPRECORD_STAN_ROOM_LEN (4) bytes, so a prop overlapping >=4 rooms
(BUNKER1 patrol guards routinely span 4: observed rooms=[1a 13 14 10],
[13 10 14 0f], ...) gets 4 real IDs and no terminator inside the array.
chraiGetPropRoomIds() then does `for (i=0; self->rooms[i] != 0xff; i++)
roomids[i] = self->rooms[i];` -- with no terminator it runs off the end and
overflows the caller's `s32 sp48[PROPRECORD_STAN_ROOM_LEN]` stack buffer in
chrpropsRenderPass, corrupting that frame. The garbage gdl that results drives
a GBI write to the top of emulated DRAM (0x70800000 / 0x90000000) around
frame 5 -- the "runaway GDL append".
Fix: size the field (and the matching sp48 local, same macro) for 7 rooms +
terminator, PC only -- N64 build byte-identical. Plus a defensive bound in
chraiGetPropRoomIds so a missing terminator can never walk past
roomids[LEN-1]. GE_D96=1 env probes left in chrprop.c.
Verified: the deterministic bgScissorCurrentPlayerView / 0x70800000 fault at
~frame 5 is gone (5 runs). BUNKER1 renders further before a separate,
less-severe failure -- a corrupt frame-GDL slot ("Unknown GBI opcode
0xffffb9", w0 sign-extended, garbage w1) around frame 5-6, and a rarer
truncated-pointer segv in bondviewPlayerTickDamageAndHealth (bondview2.c:6174).
g_GfxSizesByPlayerCount / -mgfx is a BYTE budget sized for N64 8-byte Gfx
slots. On x86-64 a Gfx is 16 bytes, so the master display list needs 2x
the bytes for the same command count. Every render fn appends with a bare
'gdl++' and no bounds check, so once BUNKER1 emits real room geometry the
list overruns g_GfxBuffers[1]/[2], runs off the stage mempool, and faults
writing a GBI command at the end of the 8 MB emulated DRAM (0x70800000) --
non-deterministically, and corrupting room GDL blobs on the way (the
'unknown GBI opcode' / seg-9 0x90000000 faults were downstream of this).
Scale the gfx allocation by sizeof(Gfx)/8 under #ifdef PORT. Vtx/Mtx are
16/64 bytes on both targets so g_VtxBuffers is unchanged.
Verified: -level_09 runs 90 s+ (5000+ VI posts) with no crash where it
previously faulted within seconds. (Separate open issue: the kernel
heartbeat reports frames=0 -- render-loop completion needs a look.)
arg1[anim_index]->table is 'weapon_firing_animation_table (*)[]'; the
'(s32)table + (s32)(idx*sizeof(...))' form truncates the 64-bit pointer
to 32 bits (then zero-extends on the cast back), dropping the module
load-base high word (0x1_40000000 -> 0x4012xxxx) so panim_float->anim.anim
faults ~0x4012xxxx once a guard starts an attack in-level. It is plain
array indexing (the code's own comment says so) -- do it as such under
#ifdef PORT, both occurrences. Behaviour-identical on N64.
Verified: -level_09 no longer faults in chrlvInitActAttack across 8+ runs.