Commit Graph
100 Commits
Author SHA1 Message Date
James Dansereau a82e90d457 M-27: D146 hardening - guard gfx_sp_vertex/movemem/set_vertex_colors ptrs
A desynced/corrupt front-end display list (D75 family - the compiled
wallet/dossier/logo sub-DLs at seg5+offset) feeds wild pointers to the RSP
command handlers before the walk reaches an unknown opcode. Added a shared
fast3d_ptr_ok() mapped-range check; gfx_sp_vertex, gfx_sp_movemem and
gfx_sp_set_vertex_colors now bail on a wild pointer instead of faulting.
Pairs with the D146 unknown-opcode -> end-DL change.

Verified: -level_09 unregressed.
2026-08-30 15:30:16 -04:00
James Dansereau cea9ce0df9 M-27: D146 unknown-GBI-opcode -> end DL instead of abort(); tools_pc/debug.ps1
D146 (port/fast3d/gfx_pc.cpp): an unrecognised opcode in the DL walk called
sysFatalError() -> abort() -> whole process dies with no ge007.crash.log
(the crash handler does not cover abort). Seen on the real post-Dam
MISSION COMPLETE screen: "[FATAL] Unknown GBI opcode 0x40c03 at
0x70161a8c" - a front-end 3D model (dossier, D75/D144 family) whose display
list was never built, sitting at a valid DRAM address full of junk. D144
guarded the bad matrix pointer; the walk then hit more garbage. Now: log
loudly (rate-limited x20) and `return` to end the (sub-)DL so the frame
finishes. A real level-render desync is still visible in the log.

tools_pc/debug.ps1: launch the port under gdb from the repo root so a
playtest crash always leaves a backtrace (abort/fatal or silent exit).
-Menu <id> sets GE_STARTMENU; passes through -level_XX etc. Prints the
FATAL line + backtrace + crash log when the game exits.

Verified: GE_STARTMENU=13 mission-complete -> Next chain renders, no abort,
no crash (D75 model cosmetics aside).
2026-08-30 15:27:23 -04:00
James Dansereau 080056b125 M-27: GE_STARTMENU debug harness - jump straight to a front-end menu
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.
2026-08-30 15:11:44 -04:00
James Dansereau 36acf324fa M-27: D145 - ESC no longer hard-quits (it is the menu back key)
port/src/video.c, port/fast3d/gfx_sdl2.cpp, port/src/input.c: bare ESC was
wired to exit(0) in both event handlers. On the front-end / debrief / mission-
complete screens ESC is the natural "back / previous page" key, so a player
paging through the post-mission report with ESC quit the whole game -- a
clean exit(0), no crash log, looked like a crash. ESC now feeds the N64 B
button (back / cancel) through input.c alongside X/R/F; quitting is
window-close (X) or Alt+F4 only.

Verified: -level_09 boots crash-free, 8 frames rendered.
2026-08-30 15:05:12 -04:00
James Dansereau 70793eba4f M-27: D143 null-string text guard + D144 bad-matrix-ptr fast3d guard
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).
2026-08-30 14:47:37 -04:00
James Dansereau 94f677584a M-27: D142 file-select hang fix + GE_INPUTSCRIPT test harness
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.
2026-08-30 14:34:59 -04:00
James Dansereau f98414f8c6 M-27: front-end bucket — D140/D141/D118d pause+watch menu fixes
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).
2026-08-30 14:14:41 -04:00
James Dansereau 326564736d M-26: [D] [Debug] ini flags mirroring GE_PCDUMP / GE_INPUTLOG
- config.c: Debug.FrameDump (string) + Debug.InputLog (int).
  configGetFrameDump()/configGetInputLog() check the env var first
  (tools_pc/*.sh unaffected), fall back to the ini.
- video.c / input.c consumers switched to the accessors.
- GE_DETERM is design-only (D117), not mirrored.
- Verified: ini-only FrameDump=40-120:20 -> exactly 5 frames; GE_PCDUMP
  still overrides; -level_09 crash-free.
2026-08-30 13:00:52 -04:00
James Dansereau 710d02f802 M-26: [B/C] level_sweep STALLED verdict, raw mouse input opt-in
- level_sweep.sh: frames rendered then frozen with process alive ->
  STALLED (froze at N/M frames) instead of a false PASS (the Silo
  -level_20 fly-down / D117-D134 load case). Retried once like NO-FRAMES.
- input.c: Input.MouseRawInput=1 (default 0) sets SDL relative
  system-scale + warp hints off in inputInit -> no OS pointer accel on aim.
- docs: M-26-QOL-REVIEW.md + HANDOFF section.
2026-08-30 12:55:12 -04:00
James Dansereau db1834ca9b M-26: [A/E] atexit config+window persistence, --help/--version
- main.c: atexit(portAtExit) = videoSaveWindowState() + configSave(),
  fires on clean exit(0) (SDL_QUIT/ESC/close); abort() paths skip it.
- video.c: new [Window] ini section (Width/Height/X/Y/Maximized);
  sentinels (0 / -1) reproduce native-size + centered. videoSaveWindowState()
  snapshots live geometry via the SDL2 wapi backend. Video.Fullscreen
  now round-trips.
- main.c: --version / --help (-h) before crashInit — build id, usage,
  21-level -level_XX table.
- Verified: -level_09/-level_20 crash-free 6/6 frames, ge007.ini gains
  populated [Window] block on exit.
2026-08-30 12:55:06 -04:00
James Dansereau 905eebf447 docs: Silo -level_20 freeze was machine-load flakiness (D117/D134), not a real bug 2026-08-30 12:47:47 -04:00
James Dansereau aafa86d180 docs: M-25 QoL session summary + re-sweep (20/21, Silo freeze clarified) 2026-08-30 12:46:07 -04:00
James Dansereau 39dded2a17 M-25: [Input] ge007.ini knobs — mouse Y scale/smoothing, pad deadzone/trigger/invert-Y 2026-08-30 12:35:17 -04:00
James Dansereau bc97cdab0a M-25: playtest.sh — accept level names, --list, auto-symbolicate crash PCs 2026-08-30 12:31:08 -04:00
James Dansereau f6006bcdb4 M-25: [Video] ge007.ini knobs (vsync/fps cap/MSAA/filter/fullscreen) + config auto-migration 2026-08-30 12:30:00 -04:00
James Dansereau 645a301b41 docs: M-24 QoL review sheet (M-24-QOL-REVIEW.md) + cross-links 2026-08-30 12:21:17 -04:00
James Dansereau 9d136bc657 M-24: more playtest QoL — gamepad hotplug, wheel weapon-cycle, title FPS
port/src/input.c:
- inputRescanPads(): close + re-open all pads on hotplug. Game latches the
  connected mask at osContInit so a late pad merges into controller 0.
- inputPostWheel(): a wheel notch queues a 2-poll A-button press. GE's
  default scheme cycles weapon forward on a bare A edge (invButtons=A,
  bondview2.c:5162/5326). Both directions cycle forward.

port/src/video.c: pump SDL_MOUSEWHEEL -> inputPostWheel,
SDL_CONTROLLERDEVICEADDED/REMOVED -> inputRescanPads, and refresh the
window title with vidAvgFPS ~1 Hz via wmAPI->set_window_title.

-level_09 2/2 crash-free to frame 900, unregressed (91.6%). Build green.
2026-08-30 12:18:55 -04:00
James Dansereau 46fa579e54 M-24: playtest QoL — F12 screenshot + mouse frees on focus loss
port/src/video.c: F12 sets a request flag consumed in videoEndFrame (render
thread, GL context current) -> ppm/shot_NNN.ppm via the existing
gfx_opengl_dump_bound_fbo. SDL_WINDOWEVENT_FOCUS_LOST/GAINED now call
inputSetMouseGrab(0/1) so alt-tabbing away releases the trapped cursor.

port/src/input.c: add inputSetMouseGrab() — toggles SDL relative-mouse
mode (respecting MouseEnabled), zeroes the aim delta, and gates inputUpdate
mouse reads on a mouseGrabbed flag.

-level_09 3/3 runs crash-free to frame 900, unregressed (91.6%).
2026-08-30 12:08:22 -04:00
James Dansereau 546d803870 M-24: mouse-look rework (D118b/c) + real ge007.ini parser
port/src/input.c: mode-aware mouse->pad map matching bondviewProcessInput.
Aim mode (RMB held) drives the analog stick past +/-60 for proportional
yaw+pitch and emits no C-buttons; hipfire keeps digital C-up/C-down pitch.
Fixes D118b (mouse-Y inverted) and D118c (aim + mouse-down -> crouch, from
C-down being overloaded to crouch/lean/zoom in insightaimmode). D118a
(hipfire digital pitch vs analog yaw) residual, minor. Port-layer only.

port/src/config.c: implement configLoad/configSave (were TODO stubs).
Parses $S/ge007.ini ([Section] blocks, Key = value, #/; comments), clamps
ints to registered bounds, writes defaults on first run. New knob
Input.MouseTurnSpeed (hipfire yaw %) split from MouseAimSpeed.

Verified: -level_09 crash-free 900+ frames @ 91.65% (unregressed),
-level_20 crash-free, ini round-trips with no unknown-key warnings.
2026-08-30 11:59:52 -04:00
James Dansereau e9cf24ce7d docs: M-24 next-task — park D139/D140 in front-end bucket, in-level playtest first 2026-08-30 11:35:46 -04:00
James Dansereau 82e118a218 docs: HANDOFF M-23 committed-state fixup 2026-08-30 11:29:02 -04:00
James Dansereau 73820a0c24 M-23: WS6 Facility playtest — D135/D137/D138/D139 (firefight, aim, freeze, unload)
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.
2026-08-30 11:28:50 -04:00
James Dansereau 78c0f72157 level_sweep.sh: early-exit watchdog; M-22 re-sweep 20/21 PASS
The 45s watchdog is now a ceiling, not a flat per-level cost: a level is
killed as soon as its whole GE_PCDUMP frame set is on disk (PASS) or the
ppm count has been frozen for STALL_SECS (default 18). EXPECT_FRAMES is
derived from SWEEP_DUMP (start-end:stride). Full 21-level sweep ~7min
(was ~16).

M-22 full re-sweep after D134: 20/21 PASS. Facility(34) + Jungle(37) --
the M-20 re-verify failures -- both PASS clean, confirming D134 was their
cause. Only Cuba(54) crashes: known D129 residual (bare -level_54 hits the
cast/credits scroll referencing an unloaded text bank), parked with D76.
2026-08-29 23:57:03 -04:00
James Dansereau 46d4f03e5a D134: task-done SP/DP events must not be dropped into a full sched interruptQ
The frame-2 boot hang blamed on D117 flakiness since M-13 is a real bug:
osSpTaskStartGo posted OS_EVENT_SP/DP with OS_MESG_NOBLOCK into the 8-slot
scheduler interruptQ that the 60 Hz VI pacemaker also fills. A gfx task runs
the whole frame synchronously on the sched thread, so a slow early frame lets
retraces fill the queue and the done event is silently discarded -> curRSPTask
never clears -> pendingGfx never clears -> permanent stall.

portPostEventForce() drops the oldest (stale retrace) message and retries;
portPostVIEvent reserves 2 slots. -level_09: 6/6 boots to frame 600, 0
heartbeats (pre-fix 1/3).

Also hardens tools_pc/level_sweep.sh: 45s watchdog, lockdir serialization,
stray-process kill, one retry on NO-FRAMES.
2026-08-29 23:32:17 -04:00
James Dansereau 577f32f01f docs: M-21 — D132 applied + verified, handoff points to WS6 2026-08-29 23:16:43 -04:00
James Dansereau 5e9ea50add D132: fix propDef union-index slots for LINK/SWITCH/LOCK_DOOR/SAFE_ITEM
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.
2026-08-29 23:16:16 -04:00
James Dansereau e5f4290bc6 docs: M-20 — Facility + Jungle re-verified PASS on idle machine (concurrent-load flakiness, not regressions) → 21/21 2026-08-29 22:47:30 -04:00
James Dansereau 5f985e9756 docs: M-20 — D132 propDefs union-index layout divergence + D133 intro-render triage
D132 (propDef converter layout audit, static): converter cursor confirmed
correct vs real PC struct layout for every propDef type the 21 solo levels
emit (DOOR/ObjectRecord-prefix/VEHICHLE/AIRCRAFT/TANK/AUTOGUN/AMMO/
TINTED_GLASS/objective sub-records all MATCH; handler set is total). Real
divergence found for types 14/19/38/44 (LINK/SWITCH/LOCK_DOOR/SAFE_ITEM):
s32 IndexN fields share a union with a pointer -> 8B/8-aligned slot on PC,
converter emits at N64 tight 4B offsets -> indices misread -> switch-doors /
dual-weapons / locked-doors / safes silently fail to link (non-crash,
WS6 blocker). Proposed fix in D132, NOT applied - needs regen + game verify.

D133 (intro render triage): intro 'mostly black' is NOT a regression - it is
the D75/D76 parked-cosmetic steady state. Legal-screen coverage pixel-
identical (6677 non-clear px) between M-17 7159fff5 (worktree build) and
HEAD. Parked. Facility -level_34 + Jungle -level_37 re-verify failed this
pass -> flagged RE-VERIFY in LEVEL-STATUS, not investigated.
2026-08-29 22:37:11 -04:00
James Dansereau 71c205092a docs: M-20 — intro "mostly black" is D75/D76 steady state, not a regression
Built M-17 (7159fff5) in a scratch worktree, captured the same
GE_PCDUMP="20-900:20" window: coverage is pixel-identical to HEAD
(legal screen 6677 non-clear px / 2.17% on both builds). The 2D/text
layers draw; animated character-model layers never appear = open D75.
The M-17 "entire intro renders" handoff line was aspirational.

- HANDOFF: reworded Next-task items 2/3 (intro parked, not a regression);
  item 1 (BUNKER1 boot crash) already disproven by coordinator.
- LEVEL-STATUS: Facility -level_34 (boot crash 0x1400c3b77) and Jungle
  -level_37 (frame-2 hang) failed re-verify this pass on a lightly-loaded
  machine; flagged RE-VERIFY pending a clean-machine re-check.

PCPortResearch.md §F (D133) + PORT-LEARNINGS.md §E left uncommitted —
tangled with a parallel static-audit agent's in-flight D132 edits.
2026-08-29 22:36:30 -04:00
James Dansereau 106323aeaa docs: M-20 — BUNKER1 regression disproven (re-verified PASS); fresh Qwen static-audit tasks
- LEVEL-STATUS: -level_09 boot crash from M-19 did not reproduce on ef1ec523
  in 3 runs (1800 frames crash-free). Was sweep flakiness / D117 nondeterminism
  under machine load, not a real regression.
- HANDOFF-LOCAL-AGENT: replaced the 2 completed M-19 tasks with 3 fresh
  static-only audits (osVirtualToPhysical D131 corollary site sweep, in-place
  relayout D130 corollary sweep, glyph-index OOB audit).
2026-08-29 22:26:52 -04:00
James Dansereau ef1ec52366 @
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.
@
2026-08-29 22:13:14 -04:00
James Dansereau bb14baf3bf @
D131: restore truncated module ptr in seg_addr (Jungle -level_37)

explosionRenderPropSmoke passes a compiled .bss matrix symbol
(&dword_CODE_bss_8007A100) through osVirtualToPhysical() -- a u32-returning
shim -- into gSPMatrix, truncating the 0x1_00000000 module high word. w1
lands as 0x40xxxxxx and seg_addr() returns it raw -> wild deref in
gfx_sp_matrix when the first explosion draws (~frame 300).

Fix: seg_addr() restores the module high word for any fallthrough w1 in
[0x40000000, 0x70000000) (module fixed-based at 0x140000000, no ASLR;
DRAM/KSEG0/segmented/phys are all handled in earlier branches). Covers the
~30 latent osVirtualToPhysical(<compiled matrix/vtx symbol>) sites in
explosion/glass/blood/bondview2.

Verified: Jungle renders to frame 1500-2400+ crash-free at 91.7% content;
-level_20/-level_24 unregressed. Sweep 18->19/21 (note: -level_09 has a
separate pre-existing boot crash regressed since M-18 -- see HANDOFF).
@
2026-08-29 22:13:08 -04:00
James Dansereau 1c4efdaaf2 docs: audio plan (Phase 3 draft), texture glitch analysis, BACKLOG graphics-settings expansion
- AUDIO-PLAN.md: draft Phase-3 libaudio->SDL plan (D77), Acmd-stream
  execution + SDL device plumbing; supersedes the §6 audio paragraphs.
- TEXTURE-GLITCH-ANALYSIS.md: read-only diagnosis of texture artifacting
  (G_SETTEX no-op, mip-chain contamination, wrap period, palette
  off-by-one); fixes pending.
- BACKLOG.md: PC graphics settings workstream expanded with PD-parity
  baseline + stretch goals, config/video starting-point notes.
2026-08-29 21:34:00 -04:00
James Dansereau fda4c19159 D130: stage fontchar fields before in-place 24->32B relayout (C2 Facility+Runway)
romdataFixupFont re-lays out the N64 24B fontchar array into the PC 32B
array in place; for glyphs 0/1/2 dst overlaps src by less than the 24B
read span, so a forward field write clobbered a later field's source
mid-loop (glyph 1 width = bswap(index), glyph 2 pixeldata -> wild ptr).
Facility's level title 'Chemical Warfare Facility #2' rendered '#' with
the wild pixeldata -> gDPLoadTextureBlock -> fast3d AV in import_texture_i8.

Fix: stage all six N64 fields in a local f[6] before writing any.
Not the model-GDL relocation the D124-Facility addendum suspected
(disproved; D124 entry + BRIEF-C2gdl-model-reloc premise corrected).

Verified: -level_34 0/~14 runs, -level_35 0/4 (was ~3/4);
-level_09/20/24 unregressed. Sweep 18 -> 20/21 PASS; only Jungle C2m
(gfx_sp_matrix explosion-DL G_MTX) remains.

Docs: D130 in PCPortResearch §F/§H, LEVEL-STATUS 20/21, HANDOFF M-18,
PORT-LEARNINGS §A (in-place relayout aliasing + negative glyph index).
2026-08-29 21:33:56 -04:00
James Dansereau 7159fff544 docs: M-17 session — D126/D127/D128/D129, 13->18/21 levels pass
LEVEL-STATUS: Bunker2/Depot/Surface2 (D126), Surface1 (D127), Control
(D128) now PASS. Only C2/C2-GDL remains (Runway intermittent, Facility,
Jungle). Cuba loads+renders but bare-boot end-credits path still faults
(D76/D129). HANDOFF next task = C2-GDL model relocation (own session).
PORT-LEARNINGS: D126 corollary (trailing runtime list ptr in ROM record),
D128 (raw hardcoded N64 struct stride bites the same as Gfx/Vtx sizing).
2026-08-29 20:27:20 -04:00
James Dansereau af4e0697f7 D129: bounds-check langGet slot id + bank pointer
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.
2026-08-29 20:23:20 -04:00
James Dansereau 1657357ff7 D128: fix N64-stride raw portal-flag write in sub_GAME_7F0B37EC (C5 Control)
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.
2026-08-29 19:52:44 -04:00
James Dansereau b0ae1d18fb D127: guard sndPlaySfx against bogus ALSound* (C7 Surface1 -level_36)
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.
2026-08-29 19:39:53 -04:00
James Dansereau 73dfa8d81f D126: objective sub-record ->next pointer growth desyncs propdef walk
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
2026-08-29 19:35:19 -04:00
James Dansereau 060a7252e5 D125: fix d88_emit.py:374 8-byte slice / 4-byte literal (Aztec C3 crash)
emit_pad()'s widened-stan zero-fill assigned a 4-byte literal to an
8-byte bytearray slice. Python slice-assign with a short RHS silently
deletes the surplus bytes, so `out` shrank 4 B per pad+boundpad; once
len(out) dropped below a later leaf write's target offset, the write
clamped to empty-at-end and inserted there, drifting/truncating the
boundpad plink-name blob (Aztec pad33 "p138d2" -> "8d2") -> stanPackId
reject -> NULL stan -> door model=NULL -> crash propobj.c:13601.

Fix: 8 NUL bytes (only width-mismatched out[ write in the file).
All 21 Usetup*Z sidecars regenerated. Aztec (-level_28) now PASSES
(was C3 CRASH); Bunker1/Silo unregressed; d125_check.py 21/21 MATCH.
Sweep 12 -> 13/21.

Bunker2 (-level_27) still crashes: separate DOOR-tail linkedDoor
layout bug in d88_propdefs.py (C3r) -- linkedDoor is resolved from
linkedDoorOffset (read-before-write int id, D123 pattern). Folded
into the converter write-audit.

M-16 probes reverted; d125_check.py kept as the offline regression guard.
Generalisable quirk added to PORT-LEARNINGS.md §C.
2026-08-29 19:12:52 -04:00
James Dansereau 710d0b12b5 docs: correct stale 'opaque passthrough' propDefs comment in d88_emit.py (D122/D123/D125) 2026-08-29 16:39:58 -04:00
James Dansereau 7f9e8c7aee docs: D125 overseer note — d88_emit.py propdefs region-end/delta hypothesis 2026-08-29 16:38:47 -04:00
James Dansereau 9d118e0b13 M-13: 21-level sweep matrix + D123 (C1 fix) + D124 (C2 Jungle fix)
WS4 level sweep driven to completion: 12/21 solo levels PASS
(load + render + no-crash). Full matrix in docs/LEVEL-STATUS.md,
reusable runner tools_pc/level_sweep.sh, WS6 checklist docs/LEVEL-PLAYTEST.md.

Fixes:
- D123 (tools_pc/d88_propdefs.py): crash class C1 on 6 levels
  (Dam/Runway/Frigate/Statue/Streets/Cradle). D122 fallout - the widened
  Vehichle/AircraftRecord.ailist slot was zeroed instead of carrying its
  read-before-write integer AI-list id, so prop.c -> ailistFindById(0) ->
  GAILIST_AIM_AT_BOND -> ai() ran a CHR aim list with a NULL chr ->
  chrIsNotDeadOrShot(NULL). New OBJ_ID_WORDS arm. 4 levels -> PASS,
  Runway falls through to C2. BUNKER1/Silo unregressed.
- D124 (port/src/gimgfixup.c): crash class C2 on Jungle. Compiled
  explosion-DL pointer-sync keyed on an IMAGESEG marker that texLoad()
  had already erased -> compiled globalDL_0xNNN arrays kept link-time
  IMAGESEG words -> 0xABCDxxxx into fast3d on first explosion draw.
  Latent on every level. Detect the slot from the compiled array instead.

Diagnosed, NOT fixed (write-ups in PCPortResearch.md SF):
- C2-GDL (D124-Facility addendum): Facility/Runway - model texture-pool
  path yields a garbage tex->data; every model file loads at an odd
  bank base (mempAllocBytesInBank does no alignment). Suspect
  d43_emit.py/pcmodels sidecar texture-blob offsets (N64 8B vs PC 16B Gfx).
- C3/C6 (D125): Aztec/Bunker2/Surface2 - converted propDefs blob in RAM
  doesn't match d88_propdefs.py offline output (garbage after record 0);
  runtime sizepropdef() walk drifts ~100 records -> wrong door/modelid.
  d88_propdefs.py convert_stream + sizepropdef() PORT strides verified
  correct; suspect a dest-offset/pd_end bug in d88_emit.py propdefs emit.

Remaining: 9 crashes / 6 classes (C2-GDL, C3/C6, C2m Jungle G_MTX,
C5 Control BG portal, C4 Depot BG tile/room, C7 Surface1 sndSetupSound).
Priority order in docs/LEVEL-STATUS.md.

Also folds in the in-progress docs restructure (HANDOFF/AGENTS/CLAUDE,
PCPortResearch D121/D122 SF/SH entries, PLAN-linear-level-sweep,
HANDOFF-ARCHIVE, BACKLOG).
2026-08-29 16:36:18 -04:00
James Dansereau a8b174a829 D122: fix propDef obj half-swap for ObjectRecord-derived 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).
2026-08-29 15:26:22 -04:00
James Dansereau c53e1ac698 D121 (WS1): auto-inject per-level -m* pool row for bare -level_XX
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.
2026-08-29 15:02:26 -04:00
James Dansereau 1b48fa60b2 M-11: D114/D116 mirror deprioritised + GRAPHICS-BACKLOG log
Re-walked the glyph path a 4th time; confirmed ammo digits use the same
textrelated.c path (gunfire.c:5906 -> textRender -> textRenderGlyph), no
separate HUD-number renderer. Same contradiction as M-6/7/8: every stage
runtime-verified non-mirrored, glyphs still X-flip. Needs RenderDoc or the
asymmetric-texel experiment, not another static trace.

New finding: gfx_pc.cpp:1546/1551 D74 wrap-block is dead code (cms &
G_TX_WRAP with G_TX_WRAP==0) + OOB vertex-vs-texunit array index. Latent,
logged not fixed.

New docs/GRAPHICS-BACKLOG.md collects all cosmetic items (D74/D75/D76/
D77/D114/D116) below level-progression + crash priority per user.
2026-08-29 13:59:44 -04:00
James Dansereau da379d9644 M-10: D119 guard-attack crash fixed + D120 blood-stain hang guarded + input playtest fixes
- 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).
2026-08-29 13:48:05 -04:00
James Dansereau b531253229 docs: M-8 session wrap — weapon-model #5 not started (rate-limit), state + next steps 2026-08-29 13:22:06 -04:00
James Dansereau a967690875 docs: D118 SDL input layer (PCPortResearch §F, PORT-LEARNINGS, HANDOFF) 2026-08-29 11:11:08 -04:00
James Dansereau 8337a87fb2 D118: implement SDL input layer (kbd/mouse/gamepad -> N64 pad)
port/src/input.c was a stub; input only worked via libultra.c's minimal
contSnapshotFromKeyboard (no C-buttons, mouse, or gamepad).

- input.c: keyboard+mouse + SDL_GameController -> OSContPad. Analog stick
  = move, C-buttons = aim. Mouse-look bridged to digital C-buttons via a
  clamped per-axis accumulator (proportional press dwell). RMB/LT = aim
  mode, LMB/RT = fire. Config: ge007.ini [Input] MouseEnabled /
  MouseAimSpeed / MouseInvertY.
- input.h: add inputConnectedMask() + inputComputePad().
- libultra.c SI section: contSnapshotFromKeyboard() now delegates to the
  input module (single source of controller state); osContGetReadData()
  fills all MAXCONTROLLERS channels, not just controller 0.
- GE_INPUTLOG=1 gates a per-poll OSContPad trace (zero-cost when unset).

Build green (ntsc-final); boots -level_09 crash-free 35s. Live input
untested from headless agent - human playtest owed.
2026-08-29 11:11:08 -04:00
James Dansereau 06c601f9b3 docs: M-8 handoff — reprioritise (input layer #1, weapon model #2, D116 mirror deprioritised cosmetic) 2026-08-29 11:05:33 -04:00
James Dansereau 93b31c8c1d D116 part 3: buf_vbo + texture-upload runtime-verified non-mirrored; contradiction documented, deprioritised (cosmetic). GE_D116/vbo probe kept 2026-08-29 11:03:13 -04:00
James Dansereau ffc3112037 D117: framediff.py visual-regression tool; root-cause frame nondeterminism
Root cause (HIGH confidence): pure variable-timestep frame pacing, not
PRNG/uninit state. osGetCount() is wall-clock on PC (libultra.c:88), and
frametiming.c waitForNextFrame advances game logic by however many 60Hz
ticks of real time elapsed per render -> two runs diverge 32-69% of
pixels by frame 200-440. random.c seeding verified deterministic and
correct; the PRNG stream only diverges as a victim of the pacing jitter.

GE_DETERM fixed-tick mode: assessed NOT narrow (frame-coupled virtual
clock deadlocks the boss.c retrace gate / pre-first-frame waits;
robust version redesigns VI retrace + pacemaker semantics). Deferred
with a full design in PCPortResearch.md F/D117 per AGENTS.md.

tools_pc/framediff.py: structural/tolerant frame compare (16x12 grid
mean-colour delta + non-clear-% swing + 16x16 aHash), --mask for the
animated HUD, --exact for a future deterministic build, --update to
refresh tools_pc/golden/. Reads .ppm and .png (built-in PNG decoder).
Validated: two nondeterministic re-runs pass structural, --exact
reports the real 32-69%, a wrong-frame pair fails.

No game-logic or port-layer C changes; no probes left in tree.
2026-08-29 10:49:15 -04:00
James Dansereau e9dbaee7aa D116: static trace clears entire fast3d 2D texrect->vbo path; U-flip is downstream of buf_vbo (GL shader/ortho/sampler) 2026-08-29 10:42:17 -04:00
James Dansereau 42eacc4aae tools_pc: D115 golden frame baseline for regression diffing (-level_09 BUNKER1) 2026-08-29 10:38:23 -04:00
James Dansereau 8fdf473dc8 D116: probe finds HUD mirror is per-quad texture-U flip, not proportional-font-specific; ammo digits also mirrored; re-opens D114 2026-08-29 10:31:07 -04:00
James Dansereau 59d49a5e3f harness: CLAUDE.md auto-loads AGENTS.md + session/dispatch preflight checklists 2026-08-29 10:20:52 -04:00
James Dansereau a86868221d docs: D116 HUD text mirror is per-glyph texture-space flip, not screen mirror 2026-08-29 10:04:37 -04:00
James Dansereau 7bdb19e96f D115: fix gunfire THROW* raw struct player offsets; M-6 D113/D114 findings
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.
2026-08-29 09:53:00 -04:00
James Dansereau 64698f6b23 docs: subagent workflow + port-learnings cheat-sheet
Distilled from Chris Judnich's 'Decompiling a Nintendo 64 Game in 84 Days'
(saved under docs/reference/): explicit per-task budgets exposed to the
agent, a shared append-only learnings file, file-partitioned parallelism,
cheap pre-flight passes, and a standard investigation-subagent brief
template. PORT-LEARNINGS.md is a one-screen index of the recurring
N64->PC bug classes into PCPortResearch.md §F; AGENTS.md points to both.
2026-08-29 09:52:47 -04:00
James Dansereau 3a02ded079 D112: fix d43_emit put_f32 byte-swap off-by-one (skeletal models collapsed)
put_f32 reversed bytes doff+1..doff+4 instead of doff..doff+3, dropping
each float's MSB and pulling in a byte of the adjacent field. Corrupted
every f32 in converted model rodata (joint Origins, LOD distances, BSP
planes, bounding radii) for all sidecar-loaded models. Compiled-in
front-end models were unaffected, hence 'logo fine, every guard a line'.

render_pos[] joint matrices now coherent; BUNKER guards render as
humanoids and monitor screens draw content. Regen: python
tools_pc/d43_emit.py ntsc-final.
2026-08-29 09:21:12 -04:00
James Dansereau cb73ea0985 docs: D107 mip-tile fix; storage void root-caused as closed-door props not rendering (prop/model track, not visibility) 2026-08-28 22:18:01 -04:00
James Dansereau 1fe85339f2 D107: fast3d sampled unloaded mip tile for GE's LOD textures (blurry ceilings)
GE's room GDLs emit G_TL_LOD + G_TD_DETAIL for mip-mapped textures, but
gfx_detail_textures_enabled is false for this port. gfx_lod_tile_offset's
'rdp.tex_lod ? rdp.tex_detail : i' then returned tex_detail (1) for every
texel, so fast3d sampled GE's first mip (render tile 1). GE loads the
whole mip chain with one G_LOADBLOCK to TMEM 0 and just points tiles 1..5
at offsets inside it; fast3d keys loaded_texture[] by TMEM addr and has
no entry for tile 1's offset, so import_texture fabricated a 16x16 from
the top-left 256 bytes of the base image and magnified it across the
polygon -- the blurry brown/grey ceilings and wall panels in BUNKER1.

Fix: when detail textures are disabled, gfx_lod_tile_offset always
returns 0 (the base render tile, the only correctly-loaded level). Sharp
near surfaces, matching what the N64 shows at close range.

Verified -level_09: BUNKER1 ventilation room ceilings/walls/light panels
now render crisp instead of brown mush. Single-level textures (racks,
room 1) were already fine and are unaffected.
2026-08-28 22:02:25 -04:00
James Dansereau 5f6741a432 docs: D106 portal near-plane fix; residual BUNKER1 render items (bounded sky gap + blurry texture) 2026-08-28 21:44:02 -04:00
James Dansereau ad220f4e3d D106: portal near-plane projection garbage no longer culls the next room
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.
2026-08-28 21:43:24 -04:00
James Dansereau 5a5c73de56 D105: emit G_CLEAR_DEPTH_EXT — depth buffer was never cleared in-level
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).
2026-08-28 21:06:37 -04:00
James Dansereau 82f71855ca D103: native-res height from yScale, not hard-coded 480 (viewport was half-height)
osViSetMode hard-coded the fast3d native resolution to the visible
scanline count (pal ? 400 : 480) instead of the 320x240 CFB space GE
authors its GBI in. SCREEN_WIDTH/SCREEN_HEIGHT alias the native viewport;
RATIO_X came out 2.0 but RATIO_Y 1.0, so every gSPViewport/G_SETSCISSOR
was a 220px band (~46% of frame) shown in the lower half after invert_y.
gfx_current_native_aspect was wrong too (320/480).

Recover CFB height from vm->fldRegs[0].yScale (bufy*0x800/480, fr.c:417)
with a pal ? 272 : 240 fallback. comRegs.width already carried bufx.

Verified -level_09: GE_PCDUMP frames 46%/lower-band -> 91.7%/bbox
(0,20)-(639,459), correct letterbox + HUD placement. Reveals next layer:
room geometry (D85) and weapon model still don't draw (flat blue + HUD).
2026-08-28 20:38:07 -04:00
James Dansereau 7a72f37358 docs: RENDER CHAIN CLEAR — BUNKER1 renders with no fault (D101/D102 fixed); D75 model quality is next 2026-08-28 17:45:36 -04:00
James Dansereau 1d422d7dcc D102: give the 1P weapon Model + RW pool real storage (struct hand overlap)
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.
2026-08-28 17:40:48 -04:00
James Dansereau b6cc42b42c D101: sub_GAME_7F06DB5C stashes ModelNode*/RenderPosView* through an s32
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).
2026-08-28 17:40:48 -04:00
James Dansereau a24014dba6 docs: D100 partial (player.model inline struct); residual modelInitRwData garbage-index + modelFindNodeMtxIndex truncation 2026-08-28 17:17:20 -04:00
James Dansereau 0531aab76d D100: struct player.model is an inline Model, not a pointer (PC struct overlap)
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.
2026-08-28 17:15:38 -04:00
James Dansereau 16fc565d0b docs: D99 fixed (animflipfunc truncation); D100 modelInitRwData bad RW-data pointer is next 2026-08-28 17:00:58 -04:00
James Dansereau d356137f8c D99: animflipfunc s32 fn-pointer truncation in modelTickAnim
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.
2026-08-28 17:00:05 -04:00
James Dansereau 7d07bc2f3a docs: consolidate D95-D98 in PCPortResearch (prop room-list, damagetype, player struct); D99 modelTickAnim next 2026-08-28 16:55:19 -04:00
James Dansereau 62c4b2e4ff chore: gitignore .claude/ (Claude Code local state + agent worktrees) 2026-08-28 16:54:05 -04:00
James Dansereau 6d64c54909 docs: D98 fixed (player struct under-alloc scribbled master DL); modelTickAnim animflipfunc s32-truncation is next 2026-08-28 16:53:54 -04:00
James Dansereau 9ff1016fe1 D98: allocate the real PC sizeof(struct player), not the hardcoded N64 0x2A80
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.
2026-08-28 16:51:39 -04:00
James Dansereau 37454bc849 docs: D96 (prop room-list overflow) + D97 (damagetype clamp) fixed; 0xffffb9 master-DL slot corruption is next 2026-08-28 16:35:59 -04:00
James Dansereau 66d64d047c D97: clamp negative damagetype on PORT (US-build OOB read of g_DamageTypes)
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.
2026-08-28 16:35:25 -04:00
James Dansereau 1858b5d120 D96: PROPRECORD_STAN_ROOM_LEN 4 -> 8 on PC (prop room-list stack overflow)
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).
2026-08-28 16:31:12 -04:00
James Dansereau fea435acf5 docs: D95 mempool-ceiling fix (renders ~5 frames); runaway GDL append at frame 5 (prop render) is next 2026-08-28 16:12:36 -04:00
James Dansereau d13105147d D95: raise the PC mempool ceiling to reclaim ~4 MB of unused DRAM
The N64-fidelity mempool end (0x702F4400, tlbmanageGetTlbAllocatedBlock)
leaves ~5 MB of live, mapped, unused DRAM below the 8 MB top (only
animations_frame_buffer at 0x707FFD30 lives up there). PC structs are
larger everywhere, so the per-level -m* budget no longer fits and
MEMPOOL_STAGE OOMs (zbufAllocate spins in mempAllocBytesInBank). Raise
the ceiling to 0x70700000; the ~4 MB gain goes to MEMPOOL_STAGE.

With this + the reapplied 2x g_GfxBuffers (128f7644), -level_09 renders
~5 frames (previously: OOM hang, or non-deterministic overrun within
seconds). It then still faults in bgScissorCurrentPlayerView writing at
0x70800000 -- but deterministically at ~frame 5 and regardless of -mgfx
size (tested up to 1.6 MB/half), so that is a RUNAWAY GDL append in the
prop-render path (chrpropsRenderPass/chrpropRender), not a buffer-size
problem. Next blocker.
2026-08-28 16:11:49 -04:00
James Dansereau 128f7644f6 Reapply "D95: double the -mgfx master-DL buffer on PC (16-byte Gfx vs N64 8-byte)"
This reverts commit 9f53c260ee.
2026-08-28 16:09:30 -04:00
James Dansereau 4eedcd8c24 docs: D94 fixed (chraction ptr truncation); D95 open (PC -mgfx buffer half-capacity, naive fix OOMs stage pool) 2026-08-28 16:08:05 -04:00
James Dansereau 9f53c260ee Revert "D95: double the -mgfx master-DL buffer on PC (16-byte Gfx vs N64 8-byte)"
This reverts commit 6229d20a6d.
2026-08-28 16:06:45 -04:00
James Dansereau f96ad3ee78 D85: env-gated GE_D85DUMP probe -- dump post-texLoadFromGdl room GDL stream 2026-08-28 16:04:37 -04:00
James Dansereau 6229d20a6d D95: double the -mgfx master-DL buffer on PC (16-byte Gfx vs N64 8-byte)
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.)
2026-08-28 16:04:37 -04:00
James Dansereau 83b7287f07 D94: truncating (s32) pointer cast in chrlvInitActAttack anim-table indexing
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.
2026-08-28 16:04:36 -04:00
James Dansereau 89cbf727a6 docs: D85 texture wall CLEARED (BUNKER1 renders frames); non-deterministic crash cluster is next
Also documents the data/ junction-deletion + full sidecar-regen recovery.
2026-08-28 15:52:35 -04:00
James Dansereau b6f9e5113b docs: D85 texture-pool fixed (real texpool storage); frame-GDL overrun is the new wall 2026-08-28 13:27:29 -04:00
James Dansereau c18c8b8366 D85: give the stage texture pool (ptr_texture_alloc_start) real storage
The decomp declares ptr_texture_alloc_start as `struct texpool *` but no
code ever dereferences it as a pointer -- every use takes its address and
treats the storage itself as a `struct texpool` (texInitPool writes all
four members through &ptr_texture_alloc_start; texLoad / texFindInPool
read them back). On N64 the four members are 4 bytes each (16 total) and
overlap the following split-out symbols (ptr_texture_alloc_end etc.); the
layout happens to work. On x86-64 `struct texpool` is 32 bytes (four
widened pointers), so texInitPool smashes 24 bytes past the 8-byte
pointer global and, worse, reads of ->leftpos / ->rightpos (offsets 16
and 24) come back as unrelated BSS -- the stage texture pool then looks
permanently exhausted (texFreeBytesInBuffer() < 0) the very first time
texLoad() runs on the -level_09 path. Every room texLoad bails at its
out-of-memory guard without adding to the pool, texFindInPool() returns
NULL for every room texnum, and gfx_pc aborts with 'Bad size for RGBA
texture in tile 0: 00'.

Fix: under PORT, define ptr_texture_alloc_start as a real `struct
texpool` (not a pointer). All call sites already do
`&ptr_texture_alloc_start`, so they now get a correctly-typed
`struct texpool *` and the N64 build is untouched.

Verified: -level_09 stage texture pool now fills normally (no premature
OOM), ~630 room textures resolve, and the 'Bad size for RGBA texture'
FATAL is gone. Rendering now advances to a separate issue -- gfx_sp_matrix
dereferencing an unrelocated N64 segment address (0x90000000) from a room
GDL gsSPMatrix, the next D85 layer.

Also renamed the D85 texLoad pool-full probe (GE_D85TEX) from 'OOM' since
with this fix it now only fires on a genuinely full pool.
2026-08-28 13:26:23 -04:00
James Dansereau 419a50a677 docs: D85 stream-decode fixed (GDL widen+bswap); texture-pool load is the new wall 2026-08-28 13:14:57 -04:00
James Dansereau f8cb0ba406 D85: GE_D85TEX probe in texLoadFromGdl (room texture triage)
env-gated (zero cost when unset), left in place: logs each G_NOOP
texture marker's decoded texnum / type / whether texFindInPool resolved
it, and passes-through G_SETTIMG/G_SETTILE-class opcodes. Used to
establish that room GDL texnums decode correctly post-widen but every
lookup returns NULL -- the room texture bank isn't populated on the
-level_09 path.
2026-08-28 13:13:55 -04:00
James Dansereau 4958ca1c7a D85: widen + byte-swap raw N64 room GDL / Vtx blobs at load time
The per-room primary/secondary display-list binaries and the point-index
(Vtx) binary are copied verbatim by the offline bg converter (D80 left
the DL/point-index tail unconverted). Unlike model GDLs -- which the
pcmodels sidecar (D43/D50) pre-widens to 16-byte Gfx_le slots and
byte-swaps -- nothing converts the room blobs, so every PC consumer
(texCopyGdls, texLoadFromGdl, bgApplyDynamicCCRMLUT, bgBuildRoomVtxBounds)
strode 16-byte slots over 8-byte big-endian N64 data and decoded a
garbage GBI stream (cmd=00/01/02/52...). That is the D85 "room GDL
decodes to garbage" blocker.

Fix (runtime, not a sidecar -- the blob is RZ-compressed inside the bg
.seg and delta-sized, so an offline widen would have to rewrite the
whole compressed header offset table):

- bgWidenRoomGdl(): in-place, back-to-front 8->16 widen of the freshly
  decompressed blob, bswap32 on each word. w0's top byte becomes the
  opcode, matching port/shim/PR/gbi.h's Gfx_le. Called in
  bgLoadRoomPrimaryGdl / bgLoadRoomSecondaryGdl between bgDecompress and
  texCopyGdls; the widened (2x) size is what flows into texCopyGdls,
  texLoadFromGdl and usize_*_DL_binary. The room's memaAlloc block is
  sized from memaGetLongestFree() on first load and cached from the
  post-widen `used`, so the 2x expansion needs no budget-formula change
  (every N64 offset in the scratch math simply doubles).

- bgSwapRoomVtx(): bswap16 the 6 leading u16 fields of each Vtx
  (ob[3], flag, tc[2]); the 4 cn[] colour bytes are left alone. Vtx is
  16 bytes on both targets so no widening.

- bgLoadRoomSecondaryGdl also had `texLoadFromGdl((Gfx *)scratch,
  (Gfx *)expanded_size, ...)` -- a stray pointer cast of the size arg;
  fixed under #ifdef PORT, N64 path unchanged.

Verified: with this in, `-level_09` room GDLs decode to real GBI
(GE_D69BB dump: E7 RDPPIPESYNC / BA SETOTHERMODE_H / B9 SETOTHERMODE_L /
FC SETCOMBINE / BB TEXTURE / B7 SETGEOMETRYMODE preamble) and the
G_NOOP texture markers decode to sane texnums. Two downstream issues
remain (not stream-decode): room textures are never found in the pool
(texFindInPool returns NULL for every room texnum -> "Bad size for RGBA
texture"), and a frame-GDL buffer overrun in chrpropsRenderPass.
2026-08-28 13:13:55 -04:00
James Dansereau e61bc1fae1 docs: D93 resolved (null-room NULL guards); D85 root cause confirmed (8B vs 16B Gfx) 2026-08-28 13:05:16 -04:00
James Dansereau fe9c13af66 D85: NULL-guard the null-room (room 0) GDL patch/bounds walks
Room 0 has no geometry (csize_*_DL_binary == 0), so bgLoadRoomModelData
never assigns ptr_expanded_mapping_info and it stays NULL. Two consumers
then walk it unconditionally:

- bgApplyDynamicCCRMLUT: called with start=NULL, end=NULL; the end==NULL
  sentinel-scan branch dereferences address 0.
- bgBuildRoomVtxBounds: 'while (gdl[i].dma.cmd != G_ENDDL)' on gdl=NULL.

On N64 address 0 is readable RDRAM so both walks wander harmlessly; on PC
page 0 is unmapped. Add narrow #ifdef PORT NULL guards (same safety-net
pattern as the existing D85 bgBuildRoomVtxBounds vtx-bounds check).

These sat in front of the real D85 blocker: -level_09 now reaches the
documented 'Bad size for RGBA texture' assert (room GDL still decodes to
a garbage GBI stream -- 8-byte N64 Gfx vs 16-byte PC Gfx).
2026-08-28 13:03:20 -04:00
James Dansereau cfef983c66 docs: D90/D91/D92 resolved; D85 (room GDL) is now the render blocker 2026-08-28 12:39:18 -04:00
James Dansereau d2177db3a5 D92: two truncated pointers on the chr/AI spawn path
(a) chrAllocate's 5th param was declared s32 but callers pass
ailistFindById()'s AIRecord* -- the 64-bit pointer was truncated before
reaching init_GUARDdata_with_set_values, so chr->ailist held a bogus
address and ai() faulted dereferencing (AiListp + Offset)->cmd on the
first AI tick. Param widened to AIListRecord* under PORT.

(b) Model.unka0 is a 32-bit field that on N64 holds a function pointer
(always sub_GAME_7F01FC10). chr.c stored (s32)sub_GAME_7F01FC10 into it
and model.c subcalcpos called it back -- truncated jump target on PC.
Widening the field would shift the rest of Model, so under PORT the
setter stores a flag and subcalcpos calls sub_GAME_7F01FC10 directly.

BUNKER1 now loads and ticks through to the first render, where it hits
D85 (room GDL/texture decode: 'Bad size for RGBA texture').
2026-08-28 12:38:11 -04:00
James Dansereau b2a77d53ec D91: bg portal-descend truncated the D_800442FC element address
sub_GAME_7F0B7F84 (both variants) does `i = (s32) &D_800442FC[portalnum]`
then `*((u8 *) i) = depth` -- the (s32) cast drops the top 32 bits of
the array address on PC, so the byte store lands on a bogus low pointer
and faults during portal occlusion culling. Under PORT keep `i` as a
plain value (only used as an `if (i);` no-op afterwards) and write the
byte to D_800442FC[portalnum] directly.
2026-08-28 12:38:02 -04:00