Commit Graph
100 Commits
Author SHA1 Message Date
James Dansereau 81f02c7d3a docs: honesty pass on status + platform claims
- README: "not playable yet" callout under the screenshots (no audio,
  broken front-end models/text, expect crashes past the level intro).
- README/building.md/ci.yml: platform testing is Windows-only. The Linux
  build is provided but has had zero functional testing at runtime; the
  CI job is a CMake-configure smoke test, not a Linux-support claim.

Claude-Session: https://claude.ai/code/session_01UpTM1AcBRJuTetoZNcCD5V
2026-09-01 19:43:40 -04:00
James Dansereau 52539d10de Prepare for public release
- README, LICENSE (MIT for the port layer), NOTICE, CONTRIBUTING.
- Restructure docs: internals.md, porting-notes.md, dev-process.md +
  dev/agentic-development.md (the research write-up), dev/ (raw record).
- Replace decomp status-badge CI with a PC-port validate workflow.
- Remove session/agent scaffolding and stale planning docs from the tree.
- Add .gitattributes (normalize line endings to LF); add screenshots.

The pre-2026-08 history is the GoldenEye 007 decompilation by Larry Ficken
(KholdFuzion) and contributors, preserved unchanged. The PC-port work from
2026-08-17 on is mine.
2026-09-01 19:28:59 -04:00
James Dansereau 294e24cc26 M-32b: HANDOFF reconciliation (strip, D75 Bug 2 probe, RC3 inconclusive) 2026-08-31 15:53:51 -04:00
James Dansereau 652663aac6 D75 Bug 2: runtime probe + triage update
Adds env-gated GE_D75 probe in title.c sub_GAME_7F007F30 (capped 8 lines).
Booted the bare front end through the gun-barrel sequence:

- Bond chr model + gun model entirely absent; both Model instances are
  valid (non-NULL obj, numMatrices 21/1) -> 'model not loaded' ruled out.
- chrModelInstance->render_pos is valid and freshly written each frame
  (== that frame's dynAllocate mtxlist base, clean double-buffer swap,
  o2p returns it unchanged) -> the 'render_pos = stale dynAllocate arena'
  hypothesis (D115 #5) is RULED OUT; do NOT land a persistent render_pos
  buffer.
- Zero fast3d DL warnings during the sequence -> not the D144/D146
  corrupt-sub-DL family.

Failure is downstream in drawjointlist/dotube vtx/node-DL resolution or an
off-screen basemtx. Next step: a drawjointlist-level probe. Full write-up
in PCPortResearch.md 'D75 Bug 2 - RUNTIME PROBE'.

-level_09 nonclear stable (91.66->91.62, D117 pan noise); build green.
2026-08-31 15:53:05 -04:00
James Dansereau cde5505840 strip closed-Dxx debug scaffolding (D54/D63/D90) from compiled src/
Removes inert env-gated (GE_D54/GE_D63/GE_D90) probe blocks left in game
and libaudio TUs after those findings closed. All were #ifdef PORT +
getenv-guarded with no side effects; bg.c also drops a bare non-PORT
static (d63bgprimarycount) + entry log. No behaviour change.

Build green (ntsc-final, 10/10 link); -level_09 + -level_20 render
crash-free (19 frames each, Silo frame 300 @ 88.4% non-clear).
2026-08-31 15:41:03 -04:00
James Dansereau ae065c2082 M-31/M-32: reconcile 6-agent burst #2 (D154 vtxoff + sibling, D152+ audio-lock leak, RC3/D167 wrap-period knob, D75/D160 disproofs, docs) 2026-08-31 12:57:50 -04:00
James Dansereau dcf2002d71 docs: reconcile TEXTURE-GLITCH-ANALYSIS status table; add GE-ENV-PROBES.md; GE_D63 strip assessment 2026-08-31 12:50:12 -04:00
James Dansereau 840208b58d D75 triage (M-32): rule out D73 gu-scope gap; split into fast3d-mirror + dynAllocate-arena leads
Static-only pass. (a) D73 scope gap DISPROVEN: src/libultra/gu/* and
matrixmath.c are all endian-clean; the Rareware logo exercises the full
gu pipeline front-end and renders correctly.

D75 now splits into two independent bugs:
- Bug 1 (Nintendo/GE logo transform, wallet/Bond-photo 180deg): the
  parked D114/D116 shared fast3d viewport/MP-matrix mirror. Not game
  code; static tracing banned per PORT-LEARNINGS D2.
- Bug 2 (gun-barrel Bond + cast models entirely absent): category (b),
  independent of any transform. In-level skeletal models render via the
  identical joint path, so the discriminator is the front-end setup:
  render_pos pointed at a transient dynAllocate gfx arena (D115 MED #5)
  is the lead suspect and an in-scope #ifdef PORT fix candidate.

docs only: PCPortResearch.md D75 ADDENDUM + index row; PORT-LEARNINGS
section C (D73/gu scope now settled - stop re-auditing).
2026-08-31 12:49:42 -04:00
James Dansereau 40f177a982 RC3/D167: non-power-of-two texture wrap period (Video.WrapFix, default off)
fast3d never stored the N64 tile mask and wrapped textures purely at the
uploaded image size; the RDP wraps a WRAP render tile at 1<<mask, which GE
sets to ceil(log2(dim)) - so non-power-of-two surfaces (Depot room
textures) repeat at the next power of two on console and one image-width
too soon in the port ('textures repeat oddly').

- gfx_dp_set_tile: store masks/maskt on the tile.
- gfx_sp_tri1 hoisted pre-wrap block (the reworked D74 block, indexed by
  texunit): when WRAP and 1<<mask != tex_width, fold UV at 1<<mask and
  clamp the [dim,1<<mask) overflow band (no real texels) to the edge.
- GE_WRAPFIX env override (wins over the ini) for testing.

Behind the existing Video.WrapFix knob, default OFF -> byte-identical to
golden. Per-level captures (-level_09/-30/-34/-20, off vs on): no crashes,
no structural regression; Depot shows small localized ceiling/wall texel
changes. Kept default off pending a human visual check on Depot.

docs: PCPortResearch.md RC3/D167, PORT-LEARNINGS.md D, TEXTURE-GLITCH-ANALYSIS.md 6d.
2026-08-31 12:49:39 -04:00
James Dansereau dc5739bc93 D167: D152 addendum -- audio osSetIntMask audit + narrow lock-leak fixes
Static audit of every osSetIntMask(OS_IM_NONE) in the compiled audio code
(event.c, csplayer.c, synaddplayer.c, snd.c): all balanced on every exit
path, including alEvtqPostEvent's freeList-empty early return. No unbalanced
same-thread leak exists -- the D152 s_imLock wedge is not a missing restore.

Two real defects fixed (#ifdef PORT, N64 verbatim under #else):

1. snd.c sndSetSfxSlotVolume walked the live ALSoundState list and posted to
   the shared ALEventQueue without holding OS_IM_NONE, unlike its twin
   sndDeactivateAllSfxByFlag. On PC: unguarded walk racing amMain + one lock
   acquire/release per matching sound. During the mission-failed fade-out
   (sndSetScalerApplyVolumeAllSfxSlot -> sndApplyVolumeAllSfxSlot -> here,
   per frame) that is a per-frame lock-acquire storm. Now holds the mask once
   across the walk; sndApplyVolumeAllSfxSlot batches its slot loop under one
   recursive hold.

2. port/src/libultra.c: portThreadWrapper now calls imThreadExitRelease() when
   a host thread returns from its entry -- if it still owns s_imHeld, release
   the orphan (+ LOG_ERROR). Kills the 'transient thread acquired OS_IM_NONE
   and exited' leak (the §F dump's own alternate hypothesis) and the re-wedge
   when a new thread reuses the dead pthread id and imAcquire mis-detects
   recursion. Steal-lock kept as backstop.

Build 240/240 green; -level_09 frame 300 renders 91.7% non-clear crash-free;
-level_20 boots crash-free. Fade-out repro is playtest-gated (not headless-
reproducible) -- user to confirm no black screen + no D152: log lines.
2026-08-31 12:49:06 -04:00
James Dansereau 75641fb0b7 M-30: D154 re-audit — fix vtxoff, port bgTestBulletHitBackground G_SETTILE sibling, add GE_D154 diagnostic
Re-audited the M-28 bgTestRayIntersectionInRoom GBI-parser port (bg.c):

- G_TRI1 (w1>>16/8/0 & 0xff /10) and all 12 G_TRI4 nibble recoveries
  cross-checked twice against the N64 #else path and the raw BE-word bit
  layout -- both CORRECT, no change.
- BUG: vtxoff read gdl->dma.par & 0xf. The PC Gdma_le shim maps .par to
  bits 0-23 of word0 (packed length, 16-aligned) not the N64 byte-1
  params field at bits 16-23, so vtxoff was silently 0 -> wrong
  vtxbase[idx] for any G_VTX batch with v0 != 0. Now
  ((u32)gdl->words.w0 >> 16) & 0xf.
- Sibling ported: bgTestBulletHitBackground tail (post-hit G_SETTILE
  back-scan for tileformat/tilesize) was itself an unported raw
  ((u8*)gdl)[0]/[1] + 64-bit words.w0<<11>>30 parser over the same
  D85-widened 16-byte room DL. #ifdef PORT: opcode from bits 24-31,
  byte1 from bits 16-23, (u32)-forced 32-bit width for the tilesize
  shift. N64 path kept under #else.
- Diagnostic: GE_D154=1 logs the parsed cmd / vtx-index stream (call
  header + per-TRI1/TRI4 idx) for the first 64 invocations so the
  integrator/user can eyeball the parse. Verification is playtest-gated
  (shoot a wall/floor on an idle machine) -- the no-input level sweep
  never enters this function.

Build green (ntsc-final, 242/242). ABI/GBI-parse only, no game-logic
change. docs/PCPortResearch.md D154 + PORT-LEARNINGS §B updated.
2026-08-31 12:48:16 -04:00
James Dansereau 6f45df9dde M-30b docs: 6-agent burst results (D158/D159/D161/D164/D165/D166/D160) merged + verified 2026-08-31 12:25:07 -04:00
James Dansereau deada1bb5f M-30 docs: with-input crash sweep results (21/21 crash-free, D158 holds) 2026-08-31 12:18:33 -04:00
James Dansereau 99c5926924 D165/D166: front-end 1:1 pointer P-controller + speed-proportional hipfire pitch pulses
port/src/input.c only (port-layer, no game-code change):

D165 - the M-30 menu 'pointer mode' fed mouse velocity into the stick, but
front.c frontUpdateControlStickPosition integrates the stick as a velocity
into a screen-pixel cursor -> velocity^2 feel. Replace with a P-controller:
maintain menuEst{H,V} (our estimate of the game cursor, stepped each poll with
front.c's exact recurrence via menuCursorStep), accumulate menuTgt{H,V} from
raw mouse motion, emit stick = clamp(6.0*(target-est), +/-70). Gain 6.0 =>
per-poll error decay 0.45, no overshoot, ~8-poll settle, parks under the
game deadzone with no drift. Input.MenuPointerMode (default 1) keeps the old
velocity mode as a fallback; Input.MenuPointerSpeed scales the mapping;
GE_INPUTLOG prints an est/tgt/eff/stick trace. No mouse motion => emitted
stick unchanged => prior behaviour exactly.

D166 - hipfire mouse pitch was a fixed digital threshold while yaw is analog.
Emit the C-button as a duty-cycled pulse whose frequency scales with mouse-Y
speed (fast = solid hold as before, slow = sparse taps). Input.HipfirePitchSpeed
tunes it. Aim-mode (RMB, already analog) untouched.

Build green; -level_09 (~360 frames) + GE_STARTMENU=6 smoke crash-free.
Feel-check owed (headless can't drive the mouse).
2026-08-31 12:18:15 -04:00
James Dansereau d23802ff9b D161: Depot ceiling blue-speckle — CI tile with G_TT_NONE decoded as palette
The Depot roof (CI8 tile drawn with gsDPSetTextureLUT(G_TT_NONE)) was
decoded against a stale rdp.palette, producing blue/magenta noise that
aliased into radial rays on the receding ceiling plane. A CI-siz tile
with the TLUT disabled must be treated as raw intensity (I). Route
CI4/CI8 -> I4/I8 in import_texture() when palette_fmt == G_TT_NONE.

Adds GE_TEXDUMP (env-gated PPM + param dump) as an inert diagnostic.
-level_09/-30/-34 sweep PASS, unregressed.
2026-08-31 12:16:19 -04:00
James Dansereau 8dfc578f4e D159: no-op texSwapAltRowBytes on PC (fixes combed front-end photos); D164: legal-screen loop bound
D159 (RC1 / D149): texSwapAltRowBytes pre-swaps odd texture rows in u32
groups to cancel the N64 RDP odd-line TMEM address XOR during word block
loads. fast3d emulates no such XOR, so odd rows uploaded scrambled -> the
'interlaced' wallet-Bond photo and other large front-end I4/I8/IA8
textures. #ifdef PORT early-return; N64 body kept verbatim. Wallet photo
now renders as a clean grayscale portrait (still 180deg-rotated - D75
model-transform family, parked). -level_09 frame 200 coverage unregressed
(91.62 vs 91.66 golden = D117 noise), visual frame clean.

D164 (D76): constructor_menu00_legalscreen bounded its text loop on
&legalscreen_MRD assuming linker adjacency to legalpage_text_array;
mingw reorders file-scope globals (MRD lands 0x60 before the array) so
the do/while ran once. #ifdef PORT bounds on the array's own length.
2026-08-31 12:15:32 -04:00
James Dansereau e295ee7ed8 D164 docs: root-cause D76 disclaimer screen (linker-adjacency loop bound in front.c, not an image-table bug) 2026-08-31 12:10:52 -04:00
James Dansereau 93692a1470 D160 docs: Dam level-end cutscene (D148) trace + diagnostic write-up 2026-08-31 12:10:22 -04:00
James Dansereau fc844fb9b4 D160: env-gated diagnostics for the Dam level-end cutscene (GE_D160)
Dam exit should play a scripted cutscene (Bond off the dam / bungee) before
the MISSION COMPLETE report; on PC it cuts straight to the report while
progression still works (bossReturnTitleStage fires). Add GE_D160=1 trace
points to localise whether the cutscene AI runs:
 - bossReturnTitleStage entry (+ caller return addr)
 - bondviewSetCameraMode (mode, stage, g_IntroSwirl, intro anim idx, caller)
 - AI EndLevel/exit_level, CameraLookAtBondFromPad, CameraSwitch (tag lookup
   result: cdef ptr / TagIndex<0 / tag NOT FOUND)
No behaviour change (all #ifdef PORT, getenv-gated).
2026-08-31 12:09:45 -04:00
James Dansereau d439dfeaeb D158: KF7 muzzle-flash stackpad2[-8] negative stack write -> crash on fire
gunUpdateAndFire's KF7 branch (`mdlhdr->Skeleton == &skeleton_gun_kf7`)
does `((f32*)stackpad2)[-8] = ...` then reads it back into
matrix_scalar_multiply -- a decomp register-allocation stackpad: a
scratch f32 the N64 compiler placed 32 bytes below the `u8 stackpad2[4]`
local. GCC's x86-64 stack layout is different, so the negative index
scribbles into an unrelated stack slot (return-address / saved-reg area)
-> 0xc000001d illegal-instruction the moment you fire a KF7.

Found via scripted-input (walk+fire) testing: 12 campaign levels survived
a firefight script, Caverns (-level_39) crashed reproducibly on a settled
machine at gunfire.c:817. addr2line -> gunUpdateAndFire. The KF7 is a
common campaign weapon (Facility/Surface/Caverns guards) so this would
hit the user early.

Fix: real `f32 kf7_flashscale` local under #ifdef PORT; N64 stackpad
expression kept under #else. Caverns now survives repeated fire to frame
1500+; -level_09 unregressed. PORT-LEARNINGS §A (stackpad hazard).
2026-08-31 11:59:17 -04:00
James Dansereau 3e5720c01b D74: rework sub-tile UV pre-wrap as opt-in Video.WrapFix (was dead + OOB)
The D74 block in gfx_sp_tri was inert (guard `cms & G_TX_WRAP` == `& 0`,
always false) AND OOB (`tex_width2[i]` indexed by the vertex i=0..2, but
the array is per-texunit [2]). SMALL-FIXES B1: the naive in-place
correction boot-crashed -level_09.

Rework per the B1 plan: precompute per-texunit `wrap_s/t[t]`,
`wrap_tw/th[t]`, `wrap_uls/ult[t]` once in the texunit setup loop (needs
the original cms/cmt before the CLAMP-clear), keyed on 'clamp bit not
set' + window smaller than the uploaded image; apply per vertex indexed
by `t`. Gated behind `Video.WrapFix` (default 0 -- new/never-run path).
Verified: WrapFix=0 unchanged; WrapFix=1 -level_09 renders clean through
frame 375, no boot crash, no visible corruption. Visual fix on wrapped
triangle textures (room panels) needs a level with them + an N64
reference -- Depot per B1.
2026-08-31 11:40:42 -04:00
James Dansereau e86ad05ba8 M-30 docs: SMALL-FIXES status refresh (B3/B4/B5 done, RC4 retracted, D2 caveat) 2026-08-31 11:16:54 -04:00
James Dansereau 1b0b566b90 M-30 docs: flag rebuild+regen requirement before next playtest 2026-08-31 11:15:33 -04:00
James Dansereau 1bc4928a55 d43_emit: validate PointUsage[] round-trip + index-range + non-zero (D120)
Confirms the D120 fix converter-side: 1469 PointUsage regions / 70177
s16 entries across the model set (every guard/character has several) now
byteswap-round-trip, every index < numVertices, and no region is
all-zero (the pre-M-30 bug). Guards against a future emit-gap
regression.
2026-08-31 11:13:53 -04:00
James Dansereau ef3de6c102 M-30 docs: session state + next-task summary at top of HANDOFF 2026-08-31 11:12:08 -04:00
James Dansereau cb5295fe85 GE_SAVELOG: trim criterion dump to essentials (D157 confirmed fixed)
Drop the verbose raw-words / DESTROY obj-ptr-healthy lines now that D157
is root-caused + confirmed. Keep the compact per-criterion line
(type/ObjRefID/MinDif/stride/status) and the per-objective difficulty-gate
line -- both are the reusable signal for any future save/objective issue,
gated by g_savelogObjOnce + GE_SAVELOG so inert in normal play.
2026-08-31 11:10:58 -04:00
James Dansereau 0083cdc10e docs: D157 §F entry + D120 marked FIXED 2026-08-31 11:09:41 -04:00
James Dansereau 6364ddaa4b M-30 docs: RC2 verification notes + framediff phash-noise clarification 2026-08-31 11:08:48 -04:00
James Dansereau d92fe0ef08 M-30: 21/21 level sweep PASS (first all-green incl Cuba) — no regressions from D157/RC2/D120 2026-08-31 11:02:23 -04:00
James Dansereau 742f2f820e D120: emit the opcode-0x18 PointUsage[] s16 chain (was zero-filled)
d43_emit.py reserved the PointUsage region (add_region) during layout but
the emit pass never wrote it -> all zeros -> chr.c's blood-decal walk
'index = PointUsage[index]' cycled 0->0 forever (capped by the #ifdef
PORT guard at chr.c:3328, so decals were missing/wrong, not a hang).
Now byteswaps each s16 index (numVertices entries, negative-terminated,
no remap). Full regen chain re-run, round-trip validation passes. The
chr.c guard stays as a safety net (SMALL-FIXES B3). Also: retract RC4
(palette off-by-one) in TEXTURE-GLITCH-ANALYSIS -- the current RGBA5551
decode is correct; the doc's 'spec' was ARGB1555.
2026-08-31 10:59:26 -04:00
James Dansereau d0b17f1788 M-30 docs: D157 CONFIRMED by user playtest (Dam Agent -> save -> persist) 2026-08-31 10:50:33 -04:00
James Dansereau e610c57733 M-30 docs: D157 root cause (objective difficulty byte-offset) in HANDOFF + PORT-LEARNINGS 2026-08-31 10:26:02 -04:00
James Dansereau 2fde8a5d3f M-30: regenerate LEVEL-OBJECTIVES.md with fixed difficulty reader (D157) 2026-08-31 10:25:24 -04:00
James Dansereau 4cd8ccaeac D157: objective MinDificulty read at wrong byte on LE -> campaign never unlocks
ROOT CAUSE of 'complete Dam, Facility stays locked'. The type-23
objective record's MinDificulty is a big-endian s32 at word 3; on N64 the
value is its low byte == offset 0xF, and struct objective_entry reads
'difficulty' as s8 @0xF. d88_propdefs.py bswap32's word 3, so on
little-endian the value moves to offset 0xC -> objective_entry.difficulty
@0xF reads 0 for EVERY objective. get_difficulty_for_objective() then
returns Agent(0) for all, so objectiveIsAllComplete() on Agent evaluates
objectives that should be difficulty-gated out (Dam obj 0 'Neutralize all
alarms' is Secret Agent; obj 1/2 are 00 Agent) -> they're incomplete ->
never returns TRUE -> end_of_mission_briefing() / the unlock write never
runs. User was right: you don't touch an alarm on Agent Dam.

Fix (D151-class, #ifdef PORT): reorder struct objective_entry's word-3
tail so 'difficulty' reads @0xC (post-swap position); N64 layout kept
under #else. Also fixes dump_objectives.py (was reading byte 0xC = the
BE high byte = always 0; hence LEVEL-OBJECTIVES.md said 'Agent' for
everything). Dam now decodes obj0=Secret Agent, obj1/2=00 Agent,
obj3=Agent. -level_09 coverage identical (phash delta is the M-30 RC2
texture change, not this).
2026-08-31 10:25:16 -04:00
James Dansereau 9c23e5bcb6 GE_SAVELOG: dump obj/prop/healthy per DESTROY_OBJECT alarm criterion
Run 2 crit dump: Dam obj 0 walk is correctly synced (type 23 START stride
4, then 4x type 25 DESTROY_OBJECT tags 0-3, stride 2) and ObjRefID reads
right (0,1,2,3). All 4 DESTROY criteria evaluate INCOMPLETE -> obj 0 not
complete -> objectiveIsAllComplete=0. Dam Agent obj A = 'Neutralize all
alarms' (4 ALARM-tagged boxes). Need to know if the alarms are (a) not
found, (b) found still-healthy because the player didn't shoot them, or
(c) found still-healthy after being shot = destruction-detection bug.
Added obj=/prop=/healthy= to the per-alarm SAVELOG line.
2026-08-31 09:58:08 -04:00
James Dansereau 375fb91fd6 GE_UNLOCK_ALL=1: mission-select shows every solo level (playtest unblock)
While the objective-completion -> save-unlock chain is being fixed (M-30
blocker), GE_UNLOCK_ALL=1 flips get_debug_enable_all_levels_flag() so the
front-end mission-select offers all 20 solo levels instead of just the
next unlocked one. #ifdef PORT, env-gated, one-shot cached; retail
behaviour unchanged without the var. Verified: mission-select renders all
20 level tiles, no crash. -level_09 unregressed.
2026-08-31 09:44:25 -04:00
James Dansereau 27c0544ce3 M-30 docs: narrow save blocker to objectiveIsAllComplete (Dam obj 0) 2026-08-31 09:35:46 -04:00
James Dansereau a1257941ea GE_SAVELOG: dump per-criterion objective walk (type/ObjRefID/stride/words)
First GE_SAVELOG run showed bossReturnTitleStage stage=33 (Dam) with
objectiveIsAllComplete=0 -> end_of_mission_briefing never called -> no
unlock. The loop bails at obj 0 (get_status_of_objective != COMPLETE).
Need to see what the criteria walk reads. Added a crit-level dump (type,
ObjRefID, TextID, sizepropdef stride, first 3 words, currentstatus),
gated behind g_savelogObjOnce which bossReturnTitleStage sets only around
its own objectiveIsAllComplete() call so the per-frame poll doesn't
flood. -level_09 renders clean, 0 SAVELOG lines without the env var.
2026-08-31 09:35:10 -04:00
James Dansereau b22cd7469a M-30 docs: campaign-progression blocker + GE_SAVELOG diagnostic in HANDOFF 2026-08-31 08:49:14 -04:00
James Dansereau db5b36bc3b GE_SAVELOG: trace the campaign-unlock -> EEPROM chain (progression not persisting)
User: complete Dam -> 'previous' from debrief -> mission select, Facility
still locked. data/ge007.eep has no completion times -> the unlock save
isn't landing. Chain is bossReturnTitleStage (objectiveIsAllComplete?) ->
end_of_mission_briefing (guard on briefingpage/difficulty) ->
fileUnlockStageInFolderAtDifficulty -> fileOverwriteSaveSlotWithNewSave
(needs a DORESET spare slot) -> fileWriteSave (fileGamePakProbe) ->
EEPROM, then fileValidateSaves CRC-checks on reload.

GE_SAVELOG=1 (env, #ifdef PORT, no logic change) logs each link:
objectiveIsAllComplete + per-objective diff/status, the briefing guard,
the unlock args + free-slot lookup, the write (probe/block/crc), and the
per-slot validate CRC pass/fail. -level_09 unregressed (diag is
getenv-gated; the framediff move is the earlier RC2 mip fix).
2026-08-31 08:48:58 -04:00
James Dansereau 385b792a0c M-30 docs: RC2 mip-texture fix + Y-invert fix in HANDOFF 2026-08-31 08:25:52 -04:00
James Dansereau ddef038238 fast3d RC2: clip mip-contaminated texture uploads to base height (+ GE_DTEX probe)
Reported: menu / room textures look 'interlaced' (alternating garbage
rows). Root cause (docs/TEXTURE-GLITCH-ANALYSIS.md RC2): GE's
texGetDepthAndSize() packs base level + every LOD mip into one
gDPLoadBlock, so fast3d's upload height (size_bytes / row) runs ~1.3x
taller than the base image and the mip bytes render as rows below it.
GE_DTEX=1 confirmed it: 64x64 I4 uploaded 64x87, 65x65 -> 65x95, etc.

Fix in import_texture(): when LOD is active (rdp.tex_lod) and the load
is a plain full-width block (not a windowed gfx_dp_load_tile - the D74
sub-tile / Rare-logo case, which is lod=0), clip size_bytes to the
SETTILESIZE base-tile height. GL then builds correct mips from a correct
base. New knob Video.FixMipTextures (default 1; =0 restores old upload,
verified byte-identical to golden -level_09 3/3). BUNKER1 with the fix
on: side-wall textures visibly cleaner, geometry/HUD unchanged; Silo
unregressed. -level_09/-level_20 framediff WILL flag with the fix on
(textures changed for the better) - refresh golden after the user
confirms the menu + Depot look right.

New GE_DTEX=1 env probe logs load params for the first 64 textures.
2026-08-31 08:25:34 -04:00
James Dansereau 8dc952774b input: fix inverted Y in front-end pointer mode (mouse-down moved cursor up) 2026-08-31 08:10:34 -04:00
James Dansereau b32a740986 M-30 docs: user batch defect list (#2 #3 #4 #5) in HANDOFF 2026-08-31 08:08:02 -04:00
James Dansereau cbc83c24ab input: front-end pointer mode - mouse drives the menu cursor on both axes
Reported: on SELECT FILE the mouse only moved side-to-side; up/down
needed W/S. The front-end cursor is stick-driven (frontUpdateControl
StickPosition reads joyGetStickX/Y); the port only fed mouse-X to the
stick in menus (mouse-Y went to C-buttons, which the menus ignore).

New: when current_menu != MENU_RUN_STAGE (and != MENU_INVALID, so bare
-level_XX in-game aim is untouched), mouse velocity drives stick X+Y as
a pointer, no C-buttons/aim-band, LMB=A (select) RMB=B (back). Reads the
game global current_menu directly for UI context only - no logic change.
New knob Input.MenuPointerSpeed (%, default 100). -level_09 3/3
framediff; FILE_SELECT boots clean 900 frames.
2026-08-31 08:07:23 -04:00
James Dansereau 7d31654f2e D2/D146: strip unconditional D63 debug trail from gfx_pc.cpp
The D63 branch-trail dump fired ~10 sysLogPrintf lines + a full trail
walk on EVERY unknown GBI opcode, unconditionally. On the front-end
menu (D149 corrupt model DL) this ran per-frame and collapsed the menu
to ~5fps with input starved. Removed the trail statics, the per-G_DL
recording, and the default-case dump; the rate-limited D146 'ending DL'
log stays. -level_09 3/3 framediff, -level_20 91.6% unregressed.
2026-08-31 07:56:35 -04:00
James Dansereau b9c333d74f M-29 Track A: regenerate LEVEL-OBJECTIVES.md for all 21 solo levels 2026-08-31 07:44:03 -04:00
James Dansereau 290e85c50d M-29 docs/tools: SMALL-FIXES notes, LEVEL-OBJECTIVES decode, animgen dump helpers 2026-08-31 07:42:37 -04:00
James Dansereau 87d8e065d5 M-29 docs: D156 in HANDOFF 2026-08-30 21:45:58 -04:00
James Dansereau 390fdc50c5 D156: one-shot diagnostic when the anim-frame guard fires
fprintf(stderr) the model speed/playspeed/newspeed/oldspeed/timespeed/
elapsespeed/unkb0/unk88/anim values the first time modelTickAnim's
non-finite frame guard triggers, so the Facility-outro repro pins the
NaN source (likely a near-zero blend denominator or a misaligned Model
field). One-shot, stderr, #ifdef PORT.

Verified: -level_09 1500 frames crash-free, guard does not fire in
normal play.
2026-08-30 21:45:43 -04:00
James Dansereau f887165ff4 D156: guard NaN/blown-up anim frame -> unbounded frame-step loop (Facility outro hang)
After D155 the user re-ran the playthrough and it hung again at the end
of Facility -- same stack but frames=12962 frozen CONSTANT for minutes =
a true infinite loop, not D155's slow spiral.

The `while (1)` at model.c:3131 in modelSetAnimFrame2WithChrStuff steps
root-motion accumulation one anim frame at a time from framea to frameb.
frameb comes from modelTickAnim's `frame += playspeed * speed` per tick.
A cutscene anim transition with a near-zero blend/timespeed/unkb0
denominator (model.c:3436/:3477, guarded >0 but not against tiny values)
makes model->speed/playspeed huge or NaN -> frameb huge/NaN ->
floorFloatToInt garbage -> endframe-curframe ~= 2^31 -> effectively
infinite.

#ifdef PORT guards (N64 unchanged):
- modelSetAnimFrame2WithChrStuff: if frameb non-finite or |frameb| >= 1e6,
  snap to framea (loop does ~0 iterations; modelConstrainOrWrapAnimFrame
  at :3211 still clamps model->frameb to a real frame).
- modelTickAnim: after the tick loop, if frame/frame2 went non-finite,
  fall back to the pre-loop model->animframe1/animframe2 so the NaN can't
  poison model state every frame.
`!(x > -1e6f && x < 1e6f)` catches NaN too. Anim frames are 0..few-hundred
so 1e6 is a safe ceiling.

Likely deeper cause (needs the repro): these speed/blend fields are Model
members -- a D100/D140-style struct-pun misalignment would feed garbage
exactly when a cutscene sets an unusual transition. Dump
model->speed/playspeed/timespeed/unkb0 in modelTickAnim during the
cutscene if it still hangs.

Verified: build green; -level_09 frame 200 unregressed (guards never fire
in normal play). PORT-LEARNINGS §E.
2026-08-30 21:43:39 -04:00
James Dansereau f33894f2a2 M-29 docs: D155 in HANDOFF 2026-08-30 21:37:47 -04:00
James Dansereau d8ca9a0c51 D155: clamp per-frame deltaFrames — fixes Facility outro-cutscene hang
User hit a "hang" (process alive, heartbeat dumps) at the end of the
Facility outro cutscene. Symbolicated mainThread stack: parked in
modelConstrainOrWrapAnimFrame <- modelTickAnim <- chrUpdateAnim <-
chrTick <- playerTick <- propsTick <- lvlRender across every dump.

Root cause: waitForNextFrame() (src/game/frametiming.c) passes
nextFrameTime = (osGetCount()-prev+387937)/775875 to updateFrameCounters
UNCLAMPED. osGetCount() is wall-clock on the port (D117), not VI-locked,
so a real-time stall (asset load at the cutscene->debrief boundary, made
worse by a local LLM hammering the box) makes nextFrameTime balloon to
hundreds/thousands. That becomes speedgraphframes -> g_ClockTimer, which
drives modelTickAnim's `while (numticks-- > 0)` per character per render
plus dozens of `for (i=0;i<g_ClockTimer;i++)` sim loops -> one frame does
thousands of anim ticks x N chars -> multi-second frame -> heartbeat
trips, and the slow frame feeds an even bigger delta -> unrecoverable
spiral. The N64 was VI-bound and never produced deltaFrames > ~2.

Fix: #ifdef PORT clamp nextFrameTime to FRAMETIMING_PORT_MAX_CATCHUP (6)
in waitForNextFrame(). N64 path verbatim. Timing-compensation class,
cf. D117/D134. PORT-LEARNINGS §E.

Verified: build green (ntsc-final); -level_09 unregressed (frame 200
nonclear delta 0.04pp, full-window crash-free). The clamp only engages
when a frame took >~100ms, so normal play is byte-identical.
2026-08-30 21:37:34 -04:00
James Dansereau eea9388217 M-29 docs: B1/B2 outcome in HANDOFF (3-point opt-in, B2 not filtering) 2026-08-30 21:10:11 -04:00
James Dansereau 944b28bd0d M-29: wire up opt-in N64 3-point texture filtering (BACKLOG B1 groundwork)
The sm64ex 3-point filter shader path already existed in gfx_opengl.cpp but
was never reachable from config, and its min-filter row was all GL_NEAREST
(no mip sampling) so it would shimmer badly.

- gfx_opengl.cpp: 3-point min-filter row -> GL_LINEAR_MIPMAP_{NEAREST,LINEAR}
  so minification uses the mips it already force-generates at upload;
  anisotropy default 0 -> 4 (0 was invalid for GL_TEXTURE_MAX_ANISOTROPY).
- video.c: Video.TextureFilter now accepts 2 = 3-point + trilinear.
  DEFAULT STAYS 1 (bilinear) -- 3-point is more console-authentic but
  softens textures at normal viewing distance, and (tested) did NOT fix
  the Depot roof garbage. That is a separate texture-data/UV/effect bug,
  not filtering -- BRIEF-B2 updated, next step is a GE_DTEX probe.

Verified: default (bilinear) -level_09 frame 320 unregressed vs golden
(nonclear delta 0.00pp, phash 11); build green; 1200-frame -level_30 run
crash-free with 3-point active.
2026-08-30 21:09:53 -04:00
James Dansereau 69e3670f2c M-29 docs: race-to-release plan, audio deferred, B2 Depot texture diagnostic
HANDOFF: M-29 session summary + the 3-track plan (A=playtest gate,
B=audio DEFERRED, C=solo quick wins in priority order).
BRIEF-B2-depot-textures.md: captured evidence + leading hypothesis that
the Depot roof garbage is mostly BACKLOG B1 (missing filtering/mips),
not a decode bug; decode path audited clean.
2026-08-30 21:01:53 -04:00
James Dansereau d0d18b2472 M-29: file-backed EEPROM saves (BACKLOG B5 / Phase 4)
port/src/libultra.c: osEeprom{Probe,Read,Write,LongRead,LongWrite} were
no-op stubs, so campaign progress (unlocked missions, times, difficulty)
was lost on every quit. Implement a real 16Kbit (2048-byte) EEPROM backed
by $S/ge007.eep: lazy-load on first access, write-through on every write,
bounds-checked against the 256-block device. Pattern mirrors the PD port
(pd_port/port/src/libultra.c). Probe now reports EEPROM_TYPE_16K so the
decomp save logic (src/game/file2.c, already compiled) runs unchanged.

Verified: build green (ntsc-final); -level_09 boots, the game writes
data/ge007.eep (2048 B, checksum @block 0 + save_data slots @block 4),
600+ frames crash-free; -level_09 unregressed. File persists across runs.
2026-08-30 20:52:47 -04:00
James Dansereau 609bb732b9 M-29: aim sensitivity fix (BACKLOG B3 / SMALL-FIXES B4)
port/src/input.c: RMB manual-aim mouse sensitivity overshot badly at the
old MouseAimSpeed=50 default (25 px/poll saturated the aim band, so a
flick ran GE's (stick-60)/10 curve at full rate for many polls). Lower
the default to 25 and expose the band itself as Input.AimBand (int 5..40,
default 20 -> unchanged behaviour at the new speed). Port-only, config-
backed, fully reversible via ge007.ini.

port/fast3d/gfx_pc.cpp: annotate the dead D74 texture-wrap block
(SMALL-FIXES B1). The in-place correction (G_TX_CLAMP test + per-texunit
index) activates never-run code and crashes -level_09 at boot; it needs
the hoist-out-of-vertex-loop rework + per-level visual verification, not
a one-liner. Left inert with a NOTE; no behaviour change.

Verified: build green (ntsc-final); -level_09 + -level_20 crash-free for
a full ~75s capture window on a settled machine (D153: earlier back-to-
back runs crashed under build/run thrash, not a regression).
2026-08-30 20:49:50 -04:00
James Dansereau 52ec6d837c M-28: D154 (WIP, unverified) — port bg.c room-geometry hit-test GBI parser
bgTestRayIntersectionInRoom (bg.c) is the D135 sibling flagged since M-23:
an unported N64 GBI parser walking the 16-byte PC room DL with raw
((u8*)gdl)[k] / ((u32*)gdl)[i] 8-byte-Gfx byte/word indexing. It runs on
every shot that resolves against background geometry (bgTestBulletHitBackground),
so it AVs the first time you shoot a wall or floor in a real playthrough --
invisible to the no-input level sweep.

Ported #ifdef PORT (N64 path kept verbatim under #else):
 - vtxoff / vtxbase / op read through the .dma view, matching the
   already-ported bgBuildRoomVtxBounds.
 - G_TRI1 vertex indices from (w1 >> 16/8/0) & 0xff, /10.
 - the 12 G_TRI4 index nibbles recovered from (u32)gdl->words.w0/.w1,
   each mapped from the N64 byte/halfword layout (table in the code
   comment; cross-checked twice).
 - both texturenum recoveries -> -1: the KSEG0 *(u16*)(w1-8) deref is not
   valid for the port's converted GDLs (same as D135); texnum only selects
   the bullet-impact decal/sound, parked with D77.

Build: green (ntsc-final). NOT VERIFIED -- a no-input GE_PCDUMP capture
never fires a bullet so it never enters this function; needs a firefight
into a wall on an idle machine. This session's box was too thrashed to get
a clean run (D153 driver-crash-under-load). Committed as WIP so the port
work isn't lost; next session: idle-machine wall-shoot test + -level_09
framediff, then mark verified.

§F/HANDOFF D154, PORT-LEARNINGS §B (D135 corollary).
2026-08-30 20:34:15 -04:00
James Dansereau 57e24a374b M-28: watch-menu text (D150/D151) + audio-lock deadlock mitigation (D152)
D150 (src/str.c) — NULL-tolerant strcpy/strncpy/strcat under #ifdef PORT.
The watch BRIEF/OBJECTIVES pages build text with strcat(buf, langGet(id));
langGet() returns NULL on PC for a string bank the in-level flow never
loaded, and str.c's primitives are __nonnull__ builtins so a plain param
NULL check is optimised away — the guard launders the ptr through an empty
asm (GE_IS_NULL). NULL src -> empty string instead of an AV crash.

D151 (src/bondtypes.h) — watch data (objectives / mission background / M /
Q / Moneypenny) all rendered blank. propDef types 35 (WatchMenuObjectiveText)
and 23 (ObjectiveStart) store a plain s32 lang slot id in their 3rd word;
the decomp structs decode it as [u16 reserved][u16 text] reading text at
offset 0xA, which only works because the id is in the low 16 bits of a
big-endian word. d88_propdefs.py bswap32's the whole word -> text @0xA
reads 0 -> langGet(0) -> NULL. Fix: text is a full u32 at the word offset
under #ifdef PORT (N64 layout kept under #else). No converter change, no
sidecar regen. This is the D143/D150 "briefing text renders blank" root cause.

D152 (port/src/libultra.c) — mission-failed / objective-failed permanent
black screen. The D147 recursive-mutex behind osSetIntMask can be leaked
(libaudio has unbalanced / early-return mask calls; a transient thread can
acquire OS_IM_NONE and exit) -> mainThread + amMain both block forever in
alEvtq* -> gfx pipeline stalls. Live gdb.txt from the user's session
confirmed the deadlock (mainThread in the mission-fail audio fade-out
sndSetScalerApplyVolumeAllSfxSlot -> alEvtqPostEvent). Mitigation: model
the mask as a self-healing logical lock (owner/depth under s_imMx + condvar)
— normal microsecond critical sections behave as before, but a waiter
blocked > 2s concludes the holder leaked it, logs the stale owner + the
stealing caller's return address, and steals the section. Worst case is a
~2s audio hiccup + a log line naming the leak, instead of a hung game.
Proper narrow fix (dedicated ALEventQueue lock) still owed.

D153 investigated and dismissed — the "-level_09 crashes at frame ~900"
seen while regression-testing was D117/D134 host-scheduling flakiness under
heavy concurrent build+run load; on a quiet machine -level_09 runs past
frame 2400 clean with all three changes applied.

Build: green (ntsc-final). Golden framediff not run clean (same machine-load
flakiness); one quiet -level_09 run = 2400+ frames, 0 heartbeats.
2026-08-30 20:25:02 -04:00
James Dansereau 81a0c0d253 M-27 docs: HANDOFF — full front-end loop DONE (D142-D147), test tooling, D148/D149 parked 2026-08-30 18:52:30 -04:00
James Dansereau 566bc4cd06 gitignore gdb.txt / gdb-hang.txt (debug.ps1 output) 2026-08-30 18:51:42 -04:00
James Dansereau 84516ad69b M-27 docs: D142/D147 bug classes (PORT-LEARNINGS D3/D4), D148 Dam cutscene + D149 front-end model in GRAPHICS-BACKLOG 2026-08-30 18:51:35 -04:00
James Dansereau 48bac33068 M-27: D147 - osSetIntMask is a real recursive lock (audio-queue race -> hang)
The hang seen near the end of Dam (and latent everywhere a sound effect
plays): a door finishes opening, objTick -> doorPlayCloseSound0 ->
sndPlaySfx -> alEvtqPostEvent walks g_sndPlayer's ALEventQueue insert list
on the main thread while the audio-manager thread (amMain -> alAudioFrame ->
sndPlayerVoiceHandler -> sndRemoveEvents) mutates the same list. Both guard
with osSetIntMask(OS_IM_NONE) -- which the port stubbed as a no-op -- so on
PC there is no mutual exclusion: the list corrupts and alEvtqPostEvent's
for-loop spins forever (thread-dump confirmed both threads inside the evtq
under a bogus/circular link).

osSetIntMask now maps to one process-wide recursive mutex: OS_IM_NONE
acquires, the OS_IM_ALL token it returns releases on the paired restore,
other masks (OS_IM_VI in sched.c) pass through. The decomp never blocks
while holding OS_IM_NONE (N64 contract), so it cannot deadlock. Covers
every cross-thread critical section in libaudio + sched, not just this one.

Verified: -level_09 1200 frames, 0 heartbeats, 91.66% coverage (golden).
Thread dump captured via CTRL_BREAK to the batch gdb + fresh attach.
2026-08-30 15:40:59 -04:00
James Dansereau 51212024e8 M-27: debug.ps1 builds by default (-NoBuild to skip) so playtests never run a stale exe 2026-08-30 15:30:32 -04:00
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