Commit Graph
98 Commits
Author SHA1 Message Date
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 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 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 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 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 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 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 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 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 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