Commit Graph
100 Commits
Author SHA1 Message Date
James Dansereau ef10985343 docs: sidecar build step + D176/D182 Surface/file-select bug reports (M-35)
- building.md: document the d43_emit.py / d69_emit.py sidecar generation
  as an explicit required step (4), split Run to step 5. A plain clone
  needs no ROM to build but the sidecars are required to run (D179).

- GRAPHICS-BACKLOG.md: expand D176 (Surface exterior: black sky +
  comb-interlaced tree-line, screenshots) and add D182 (SELECT FILE
  background degrades after navigate-away-and-back: Bond image stops
  drawing + comb interlacing). Both observed this session, not fixed.
  D176(b) and D182(2) interlacing share the D159 texSwapAltRowBytes
  family recurring on texture re-import.

- docs/img/bugs/: d176 our-build vs reference, d182 backout capture.

Claude-Session: https://claude.ai/code/session_01GULf6knbsqfsnW39tMr9WT
2026-09-02 09:54:25 -04:00
James Dansereau 433907b566 port: absolute menu cursor + dual-pump event fix + auto window size (M-35)
Follow-on to ad748654 (D180). Playtested this session: ESC-to-free now
works; Dam -> Surface seamless on Agent.

- Dual SDL event pump race: port/src/video.c (host thread) and
  port/fast3d/gfx_sdl2.cpp (render thread) both drain the same SDL queue,
  so either loop can dequeue a given event. ESC-to-free / click-to-lock /
  mouse wheel / focus events were being eaten by the render-thread pump
  and lost. Now handled in BOTH pumps (inputReleaseCapture /
  inputNotifyClick / inputPostWheel / inputSetMouseGrab).

- Menu pointer rewritten for capture mode. front.c's cursor integrator
  caps at ~5.75 virtual px/poll, so the D165/D169 P-controller fed from
  an absolute mouse position was always laggy/floaty and could not reach
  the bottom of the grid. With MouseCaptureMode=1 the front end has a
  real free OS cursor: write cursor_h_pos/v_pos directly from the
  absolute mouse position and emit a zero stick. Legacy relative path
  (MouseCaptureMode=0) unchanged, and its estimate clamp now tracks the
  live front-end rect (was pinned to 320x240 -> right/bottom unreachable).

- OS cursor hidden while the window is focused (applyCursorVisibility);
  shown again on focus loss.

- Window.Width/Height <= 0 now auto-fits a 4:3 window to ~85% of the
  desktop (gfx_sdl2 gfx_sdl_init) instead of 640x480 native. video.c
  passes 0 through; [Window] persistence still pins whatever you leave.

Verified: build-pc.sh ntsc-final green 240/240; -level_09 framediff
3/3 within threshold (nonclear d <= 0.01pp, phash = documented D117).

Claude-Session: https://claude.ai/code/session_01GULf6knbsqfsnW39tMr9WT
2026-09-02 09:54:17 -04:00
James Dansereau 10b278311f docs: M-35 QoL run — review sheet, playtest checklist, options-menu plan
- M-35-QOL-REVIEW.md: per-change file:area breakdown, config keys with
  ranges/defaults, verification evidence.
- M-35-PLAYTEST-CHECKLIST.md: the human handoff — what to do, what
  correct feels like, which key tunes it. Item 0 (default-config
  regression) first.
- OPTIONS-MENU-PLAN.md: WI-3 design — recommends a port-layer fast3d 2D
  overlay over a src/ menu edit; resume checklist.

Claude-Session: https://claude.ai/code/session_01GULf6knbsqfsnW39tMr9WT
2026-09-02 01:30:01 -04:00
James Dansereau 849ee512cd port: Game.ScreenShakeIntensity route-(b) hook in viShake (D181)
First behavioral src/ hook under the AGENTS.md #2 documented-exception
path. src/fr.c viShake() gets one #ifdef PORT line multiplying the
incoming amplitude by portScreenShakeScale (extern f32, registered
0.0-10.0 in port/src/video.c, committed with WI-1). Default 1.0f is an
exact no-op so headless golden dumps are byte-unaffected; 0.0 disables
explosion/effect screen shake. N64 build keeps the original line under
#else. findings D180/D181 + F-index.

Claude-Session: https://claude.ai/code/session_01GULf6knbsqfsnW39tMr9WT
2026-09-02 01:26:41 -04:00
James Dansereau ad74865404 port: click-to-lock mouse capture + absolute menu cursor + aim retune (D180)
WI-1: Input.MouseCaptureMode (0 = legacy always-grab, default; 1 =
Quake/Nightdive-style click-to-lock). In mode 1 the OS cursor is free
until a click lands in the game window; ESC, focus loss, or entering
any front-end menu frees it again, and re-entering a stage while armed
re-locks so unpausing needs no click. Mouse buttons are withheld from
the game while the cursor is free in-stage (no phantom fire). Controller
input paths are untouched. reconcileGrab() runs once per controller-0
poll off the game's current_menu global (UI context only).

WI-2: when capture mode is on and the cursor is free in a menu, the
D165/D169 pointer P-controller takes its target from the real cursor's
absolute window position mapped onto the live virtual front-end rect
(getPlayer_c_screen*), for true 1:1 tracking. The relative-delta path
(legacy / grabbed) is unchanged.

B3: Input.MouseAimSpeed default 25 -> 16 (aim mode still overshot at 25).

Build 241/241; -level_09 framediff 3/3 within threshold (unregressed);
GE_STARTMENU menu boot crash-free. Feel-checks owed (headless can't
drive the mouse).

Claude-Session: https://claude.ai/code/session_01GULf6knbsqfsnW39tMr9WT
2026-09-02 01:26:33 -04:00
James Dansereau 91b7c5aa4e port: config system — add Float/UInt option types with clamps
Modelled on the PD port's configRegister{Float,UInt}. Needed for the
in-game video/options menu work (WI-3): sliders that store fractional
or unsigned values (sensitivity, screen-shake, fov). configSave() now
flattens all four option kinds into one section-grouped pass so mixed
int/float keys in the same [Section] stay together, and the
newly-registered-key migration check covers them.

Claude-Session: https://claude.ai/code/session_01GULf6knbsqfsnW39tMr9WT
2026-09-02 01:26:24 -04:00
James Dansereau def0e0ac56 release: Windows alpha bundle pipeline
- tools_pc/bundle-win.sh: package a Windows build for distribution — exe +
  MinGW/SDL2/zlib runtime DLLs + README + license texts, into
  dist/goldeneye-pc-port-<ver>-win64/{,.zip,.zip.sha256}. Hard-fails if a
  ROM image or an oversized blob lands in the bundle; verifies the DLL
  closure with ldd.
- tools_pc/dist/README.md.in: the bundled README template (ROM-supply
  instructions, run steps, controls, legal), @VERSION@-substituted.
- CI: build tag trigger (v*); windows-build now packages the bundle every
  run and uploads it; new release job (tags only, contents: write scoped
  to it) attaches the bundle to a DRAFT pre-release for a human to
  smoke-test and publish.
- README: Download section + Legal reworded to cover pre-built binaries
  (engine only, no game data, permissive runtime libs bundled with their
  licenses).
- .gitignore: /dist/ (root bundle output; tools_pc/dist/ template stays
  tracked).

Cutting a release: tag vX.Y.Z-alpha, push; CI builds + drafts the
pre-release; smoke-test with a ROM, then Publish.

Claude-Session: https://claude.ai/code/session_01GULf6knbsqfsnW39tMr9WT
2026-09-02 00:44:54 -04:00
James Dansereau d55d5e065f findings: log D173–D178 from QA playtest reports
New observed-but-not-investigated entries from user QA:
- D173 third-person Bond model spawns too high (level start + Cuba credits)
- D174 no visible blood effect (likely unverified D120 decal fix)
- D175 in-game stutter/brief hang on door open (Runway, Surface)
- D176 skybox broken on Surface
- D177 ladders non-functional — progression blocker, high priority
- D178 pre-mission briefing objectives listed incorrectly

Cross-referenced to the nearest known finding families; next-step
pattern-match notes in GRAPHICS-BACKLOG.md.

Claude-Session: https://claude.ai/code/session_01GULf6knbsqfsnW39tMr9WT
2026-09-02 00:27:21 -04:00
James Dansereau 4e2b721ac8 docs: README hook, discoverability, Pages site, game-behaviour reference
- README: lead with what it is + the AI-agent-collaboration research
  framing + current status in the first paragraph; rename H1 to
  "GoldenEye 007 PC Port"; add a live CI badge.
- New "How this differs from the other GoldenEye PC projects" section:
  this is a source port of the N64 original; the Recomp projects
  binary-translate the Xbox 360 XBLA remaster (different game, no shared
  code).
- CITATION.cff for GitHub's "Cite this repository" widget / Zenodo.
- GitHub Pages site off /docs (_config.yml + index.md landing page).
- agentic-development.md: searchable title + abstract.
- .github/social-preview.{png,html,md}: repo social-preview card styled
  after the GE mission-briefing screen, kept editable (CSS source + a
  regen/how-to doc beside the PNG).
- docs/dev/game-behavior-reference.md: distilled the raw GameFAQs
  walkthrough dump down to the behaviour-defining parts (combat/AI model,
  difficulty scaling, per-level objective sets, timers, weapon data,
  original-game quirks). Credits: Brett "Nemesis" Franklin, marshmallow.

Claude-Session: https://claude.ai/code/session_01GULf6knbsqfsnW39tMr9WT
2026-09-02 00:27:05 -04:00
James Dansereau 0c2673f636 CI: bump actions/checkout and actions/upload-artifact v4 -> v7
Latest majors; clears the pending Dependabot bumps so the repo goes
public with a clean slate. setup-msys2@v2 is current.

Claude-Session: https://claude.ai/code/session_01GULf6knbsqfsnW39tMr9WT
2026-09-01 22:54:26 -04:00
James Dansereau 4ca11e4386 Prepare repository for public release
- README: attract-mode stills + demo GIF as the hero row, status/warning
  callouts, metrics table, "Contents" index, doc-index table; simplified
  the two-agent workflow diagram (LR -> TD, fewer edges) for GitHub web
- Table-of-contents indexes added to internals.md, porting-notes.md,
  agentic-development.md, AUDIO-PLAN.md
- CI: add Windows/MSYS2 build job, minimal token (contents: read),
  persist-credentials: false, concurrency group; run on main + master.
  validate job regenerates the US ROM-asset symbol file only and configures
  ntsc-final (PAL/JP need a region ROM; no ROM is used in CI)
- .github: SECURITY.md, issue forms + chooser config, PR template,
  labels.yml, dependabot (Actions, monthly)
- Do not track the proprietary SGI IRIX IDO toolchain or SGIImageViewer.exe;
  document in SetupGuide.md / building.md how to obtain IDO for N64 ROM
  builds (the PC port does not need it)
- Stop tracking two committed *.pyc files; drop stale screenshots and a
  redundant .gitignore line; misc doc cross-reference fixes

Claude-Session: https://claude.ai/code/session_01GULf6knbsqfsnW39tMr9WT
2026-09-01 22:42:52 -04:00
James Dansereau 5baa53eb61 tools_pc/debug.ps1: fix DLL-not-found on launch from a plain shell
The exe links MSYS2 mingw64 DLLs (SDL2, libgcc, libstdc++, libwinpthread,
zlib). Run from a normal PowerShell those aren't on PATH, so gdb reported
"During startup program exited with code 0xc0000135" and nothing ran.
Prepend the mingw64 bin dir (derived from -Gdb) to PATH before launching.

Also relax ErrorActionPreference around the gdb call so its stderr
progress lines don't raise NativeCommandError noise mid-run.

Claude-Session: https://claude.ai/code/session_01UpTM1AcBRJuTetoZNcCD5V
2026-09-01 20:28:13 -04:00
James Dansereau e69d7b58a9 docs/img: attract-mode screenshots (bunker1, silo, dam)
Pre-spawn intro-camera views captured with GE_PCDUMP after the D168
flip fix (so they're upright). Not yet wired into the README - awaiting
placement direction (keep archives-1 / bunker1-1 as the top pair).

- attract-bunker1.png: control room + "Satellite Control Bunker" caption
- attract-silo.png: guards + toxic-waste drums, silo hangar behind
- attract-dam.png: guards on patrol, control panel, snowy peaks

Claude-Session: https://claude.ai/code/session_01UpTM1AcBRJuTetoZNcCD5V
2026-09-01 20:22:44 -04:00
James Dansereau 142e9c8f26 D168 verified; goldens regenerated; D75 shrunk; D169 marked fixed
Built the migrated tree (MSYS2 mingw64 + ROM + sidecar regen) and ran
-level_09 with GE_PCDUMP: HUD text and the exit arrow now read
right-side-up. D114/D116 confirmed as capture artifacts.

- tools_pc/golden/*.png regenerated from a fresh correctly-oriented run
  (replaces the stopgap in-place flip); stray v*.png removed
- D75 shrunk: with upright captures the gun-barrel Bond RENDERS fine
  (walk + fire) -- the "absent" reports were the flipped capture. The
  Nintendo logo is genuinely broken (renders as 2 white blobs shifted
  left -- a real logoinst transform bug, not a flip). Cast roll not
  re-captured.
- D169 entry -> FIXED with the build/verify note

Claude-Session: https://claude.ai/code/session_01UpTM1AcBRJuTetoZNcCD5V
2026-09-01 20:22:32 -04:00
James Dansereau 231a8a91bf D169: level-select mouse pointer reaches the whole grid
The front-end pointer P-controller clamped its target/estimate to a
320x240 field (MENU_CURSOR_HI_H/_V 300/220); GE's front end runs 440x330,
so the pointer saturated before the outer mission-select grid columns and
bottom row -- only an inner ~3x3 was selectable.

port/ only, no game-logic change: the menuMode branch now derives its
clamp bounds from the live virtual screen (getPlayer_c_screenwidth/height
/left/top, src/game/bondview.c) -- [screenleft+20 .. screenleft+w-20] x
[screentop+20 .. screentop+h-20], matching front.c
frontUpdateControlStickPosition -- and seeds the estimate from the real
cursor_h_pos/cursor_v_pos instead of a 320x240 centre guess. Falls back
to the old constants when the front-end screen isn't set.

Builds + links clean (engine symbols resolve). GE_STARTMENU=7 boots
crash-free; -level_09 framediff 3/3 within threshold. Interactive
feel-check still owed (headless can't drive the mouse).

Claude-Session: https://claude.ai/code/session_01UpTM1AcBRJuTetoZNcCD5V
2026-09-01 20:22:23 -04:00
James Dansereau fa54f8fb90 docs: add operating manual, fix cross-references after doc restructure
- Commit CLAUDE.md + AGENTS.md so a fresh clone has the operating manual and
  the ABI/no-game-logic rules (trimmed of stale phase status; the personal
  pd_port path removed; points at the split docs).
- Sweep every stale doc reference across the tree after the restructure:
  PCPortResearch.md -> internals.md / dev/findings.md,
  PORT-LEARNINGS.md -> porting-notes.md,
  AGENT-WORKFLOW.md -> dev-process.md,
  docs/{GRAPHICS-BACKLOG,HANDOFF-ARCHIVE,GE-ENV-PROBES,LEVEL-*,TEXTURE-GLITCH-ANALYSIS,AUDIT-M6-*,AUDIO-PLAN}.md -> docs/dev/.
- Drop the superseded docs/PLAN-linear-level-sweep.md.
- docs/HANDOFF.md + docs/BACKLOG.md stay local-only (rolling working files);
  CLAUDE.md now handles their absence in a fresh clone.
2026-09-01 20:01:09 -04:00
James Dansereau 0ac5dfb2fa D169: level-select mouse dead-zone root-cause writeup
Front-end mouse pointer can't reach the outer mission-select grid cells
(only an inner ~3x3). The D165 P-controller in port/src/input.c hard-codes
a 320x240 virtual field; GE's front end runs 440x330, so the pointer
target/estimate saturate at 300/220 before the grid's outer split points
(317 / 235.5). File-select/mode/main-menu unaffected (targets inside the
320x240 sub-box). Fix proposed (derive clamp bounds from
getPlayer_c_screenwidth/height, seed estimator from the real cursor) but
NOT applied - no build in the migrated tree to verify link + regression.

Claude-Session: https://claude.ai/code/session_01UpTM1AcBRJuTetoZNcCD5V
2026-09-01 19:45:17 -04:00
James Dansereau 203549f57e D168: fix vertically-flipped GE_PCDUMP captures; close D114/D116
gfx_opengl_dump_bound_fbo wrote glReadPixels output (GL bottom-left
origin) straight into a top-row-first P6 PPM, so every GE_PCDUMP / F12
capture was upside-down. This is the entire source of the D114/D116
"HUD/text X-mirror" - four sessions chasing a fast3d mirror that never
existed, misreading inverted screenshots of asymmetric content. The
developer confirms the game renders correctly on real hardware.

- writer now emits rows top-to-bottom
- tools_pc/golden/*.png flipped in place to match + golden/README.md
- D114/D116 reclassified as capture-orientation artifacts (not deleted);
  D75 "logo mispositioned" / RC1 "photo 180 deg" flagged for re-judging
- annotations in findings.md, GRAPHICS-BACKLOG.md,
  TEXTURE-GLITCH-ANALYSIS.md, porting-notes.md D2, GE-ENV-PROBES.md

Not runtime-verified (migrated tree has no ROM/toolchain) - needs a
fresh capture to confirm text reads normally.

Claude-Session: https://claude.ai/code/session_01UpTM1AcBRJuTetoZNcCD5V
2026-09-01 19:44:39 -04:00
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