554 Commits
Author SHA1 Message Date
James Dansereau 0e8c2ce213 docs: fix reconciliation date in GE-ENV-PROBES.md (2026-09-28, not 10-07) 2026-09-28 10:29:52 -04:00
James Dansereau 500b83dfdb README: final Background stats for the v0.4.0 tag (1005 commits) 2026-09-28 10:28:04 -04:00
James Dansereau 8e4c3cfce2 Merge pull request #112 from jkdansereau/release/v0.4.0
release/v0.4.0: v0.4.0 feature + fidelity release (widescreen, aim system, rebinding, crosshair, rumble, options rebuild, 60 fps fix)
2026-09-28 10:27:40 -04:00
James Dansereau d267d2311e docs: reconcile GE-ENV-PROBES.md for v0.4.0 (31 new rows); fix env-drift checker to see envflag.h wrapper reads; drop README hero row 2026-09-28 10:25:31 -04:00
James Dansereau 7dd7b819e3 release notes: add Tank aim speed (Input.TankAimScale) to the options reference table — it was described in the tanks bullet but missing from the table 2026-09-28 10:11:27 -04:00
James Dansereau 668a2b629b gallery: drop the four v0.2.0 playtest stills (Facility, Bunker 1, Silo, Bunker 2) — gallery is now all-v0.4.0 attract (12 shots); manifest + contact sheet refreshed; readme link count 16 -> 12 2026-09-28 10:09:24 -04:00
James Dansereau 48c37a3c7e readme: trim hero row to a single caption — drop the second caption line, fold the stills link into the gif caption 2026-09-28 10:07:42 -04:00
James Dansereau e7f4540b15 release: v0.4.0 screenshot set — 12 intro-attract stills (3072x1728 widescreen) downscaled to 1600px JPG, replacing the v0.2.0 playtest shots per level; Control/Streets/Jungle added; 6 superseded duplicates dropped; manifest + contact sheet refreshed; README gains a press-kit hero row (Streets/Dam/Runway/Aztec); preview gif re-cut at 400px/15fps/128c (smoother, 15 MB vs 23.6) 2026-09-28 10:06:52 -04:00
James Dansereau efef63fd7f release: v0.4.0 preview media — 24 s montage from live v0.4.0 play sessions (Runway tank opener, Dam, Caverns, Aztec, Bunker 2, Surface 2); release-notes image ref re-pointed to the v0.4.0 tag; README/index captions refreshed 2026-09-28 09:52:52 -04:00
James Dansereau 4f66db12d6 v0.4.0 wrap-up: D407 RESOLVED — tank board-ordering fix, board-animation lockout, menu-accept B gate, in-tank aim routing + Input.TankAimScale; PC Options wheel direction fixed (D407(b)); all 10 TANKDBG probes stripped (src/game zero-diff); D408 logged (SkipIntro skips the failure dossier — Skip intro marked EXPERIMENTAL); release notes + readme stats 2026-09-28 09:28:28 -04:00
James Dansereau 76e9202dfd release: v0.4.0 notes — tank board/exit bullet + PC Options paging follow-up; readme final stats (995 commits, 285 findings D1-D407) 2026-09-28 08:26:09 -04:00
James Dansereau 2b75f3ed8e findings: D407 logged — tank board/exit dead on PC after the D378/D393 use/reload split (port-only fix; live tank drive owed for sign-off) 2026-09-28 08:26:09 -04:00
James Dansereau 460cfd77e0 port: D407 — tank board/exit via the B bit in tank states; PC Options page-edge highlight clamp + mouse-wheel/W-S paging (D407(b)) 2026-09-28 08:26:09 -04:00
James Dansereau a8a9f2932e readme: final project stats (992 commits, 284 findings D1-D406); mark the agentic-development case study as a v0.3.0-era snapshot (through 20 Sep / D321), with the README carrying the final numbers 2026-09-28 02:31:31 -04:00
James Dansereau 9e71e7990b release notes: page indicator wording matches the shipped UI (Page p/N marker + bottom hint) 2026-09-28 02:27:56 -04:00
James Dansereau 73dd0c4ca0 readme and docs site: v0.4.0 release status, release news section, masthead tagline, and accuracy pass
- v0.4.0 status framing (the most complete release to date; the common
  defect classes fixed; a short list of known caveats) and the pre-1.0
  note, on both the README and the Pages home page.
- Dated release news section with links to the GitHub release tags.
- Keyword-reinforced meta descriptions; site masthead tagline.
- Accuracy: options section list per the shipped overlay, Xbox 1.1 (Jinx)
  controller roles (D394), Select opens the overlay, Deck 2x MSAA / 250%
  preset, background stats, footer last-updated stamp.
2026-09-28 02:27:56 -04:00
James Dansereau ebf8e4c05d findings: D406 resolved -- front-options pagination validated on the Steam Deck 2026-09-28 02:26:56 -04:00
James Dansereau a5924517fc port: front-options page hint one row below the last entry (D406d) -- pitch-aligned, not floating at the paper edge 2026-09-28 02:25:51 -04:00
James Dansereau 0c4b60e440 findings: D406c -- page hint on every page (11 rows/page), chevron wording dropped 2026-09-28 02:25:00 -04:00
James Dansereau 4b66494813 port: front-options page hints on every page, clearer wording (D406c)
11 rows per page now (was 13) so the bottom line of the paper is always
free for the page hint -- page 1 of a multi-page section shows
'Down: next page' after all (a full 13-row page had no room and no
hint, reported from the Deck). Hint wording drops the confusing
chevrons: 'Down: next page' / 'Up: previous page'.
2026-09-28 02:24:36 -04:00
James Dansereau 58691e5c5e port: front-options slider bars clear long labels; explicit page indicators (D406b)
Per user reports on the v0.4.0 front screen: the X/Y axis look
sensitivity (controller) labels ran into the fixed 232px slider track.
Bars now start 10px past each row's measured label (rows too long for
any track show the value only); drag hit-test and A-hold mapping use
the same per-row origin. Page 2 of a multi-page section now carries an
explicit dim 'Page p/N' marker right of the title plus a bottom hint
('Down: more rows >' / 'Up: previous page <') wherever the page has
room below; the (p/N) title suffix is retired. GE_FRONTOPTS_WIDTHS
one-shot label-metrics log added (same env-gated diagnostic
convention).
2026-09-28 02:21:22 -04:00
James Dansereau 8e58376d46 port: front-end PC Options pagination for long sections (D406)
INPUT (17 visible rows) outgrew the fixed 440x330 paper: the 18px pitch
was sized before the D356 profile row, so a full page's last row landed
at y=335 (off the bottom of the screen) and rows past the silent 14-row
cap (Crouch mode, Reset to defaults, Bindings...) were unreachable from
the front screen. Sections now page at 13 rows (the largest that fits);
the vertical stepper crosses page boundaries in both directions and the
title carries a (p/N) marker. F10 overlay untouched (it scrolls its own
window). Headless verification (SkipIntro boot + scripted D-pad stepping
+ GE_FRONTNAVLOG): page 1 -> page 2 down and back up, all 17 rows
reachable. GE_FRONTOPTIONS_AUTO test hook added (diagnostic-only, same
convention as GE_FRONTOPTIONS_SECTION). Findings D406 + index; release
notes: options section list corrected to the shipped Input/Gameplay/HUD/
Graphics/Audio/Display and the D406 line added.
2026-09-28 02:09:03 -04:00
James Dansereau 82476f3dd2 readme: v0.4.0 voice pass over a web-1.0 structure
Plain declarative prose throughout the overview and section intros - no
product-page register, no first-person winks. Structure carries the period
feel: top anchor-nav line (middot separators), a dated news line for the
release, --- section rules, and a last-updated footer stamp.
2026-09-28 01:34:43 -04:00
James Dansereau 1b290b2012 findings: D405 table row 4-cell -> 3-cell (pointer merged into title cell) so the index generator buckets it; D405 now indexes as CLOSED 2026-09-28 01:32:13 -04:00
James Dansereau 64913054ce release: v0.4.0 site/README consistency + Web 1.0 re-skin
- Docs site: drop jekyll-theme-cayman for a hand-rolled _layouts/default.html
  (masthead band + middot text nav + footer with last-updated stamp and legal
  line; head re-plumbed with {% seo %}, viewport, favicon); head-custom.html
  fully rewritten to the chosen design tokens - charcoal/off-white/amber,
  Libre Baskerville headings over system sans, square corners, 1px borders,
  flat (no gradients/shadows), mermaid wrapper re-skinned.
- docs/index.md: v0.4.0 status (native widescreen, aim system, rebinding,
  crosshair, rumble, rebuilt overlay); Honest status replaced by the v0.4.0
  release-notes known issues; body anchor-nav line; legal footer moved to
  layout.
- security-and-fidelity-status.md: front matter so {% seo %} titles it.
- README.md: v0.4.0 truth pass (status, Working/Known issues, Deck preset
  2x MSAA / 250% per code, background stats 981 commits / 283 findings,
  comparison table, GEPD-default controls paragraph, earlier builds) plus
  markdown-native motifs matching the site: top anchor-nav line, --- section
  rules, last-updated footer stamp, license badge in the amber accent.
- release-notes: SFTP sideload mention (leftover from the Deck session).
2026-09-28 01:32:13 -04:00
James Dansereau ad20cf210a release: v0.4.0 notes as source of truth - feature-organized, every claim code-verified (labels, options, defaults, D248 root cause) 2026-09-28 01:17:59 -04:00
James Dansereau 889f2e5d5e release: notes logic pass - rainbow fix moved to Graphics; Known issues = actual residuals only (D335b F10 overlay stretch replaces the widescreen heads-up) 2026-09-28 01:15:27 -04:00
James Dansereau b1ee5a3655 release: notes completeness pass - 60fps fix (D248), D202 mixer, D393 pad defaults, D314/D290/D342 polish 2026-09-28 01:14:35 -04:00
James Dansereau 2dd2d9dd35 release: comprehensive v0.4.0 notes - full v0.3.0..v0.4.0 arc (native widescreen, aim rework D332-D338/D404, HUD scale, rainbow fix, stability wave) 2026-09-28 01:12:56 -04:00
James Dansereau f84b8023dd release: v0.4.0 release notes (what's-new since v0.3.0 + true residual known-issues set) 2026-09-28 01:10:23 -04:00
James Dansereau 2621163f03 D405: record post-merge verification (sweep 21/21, MSAA=16 boot check, link ritual) 2026-09-28 01:09:17 -04:00
James Dansereau ed58593215 graphics backlog: track issue #105 (Dam water-reflection cyan fill residual) 2026-09-28 01:07:01 -04:00
James Dansereau 50e3426aca fast3d: clamp MSAA to GL_MAX_SAMPLES and bind the default framebuffer as GL name 0 (PR #106, italoarruda; security-reviewed 2026-09-28, D405)
Port-layer only: resolve_msaa_color_buffer clamps msaa_level to the driver's
GL_MAX_SAMPLES (pre-fix: unallocated multisample renderbuffers -> black
screen when MSAA is set beyond driver support); the resolve/copy paths now
bind current_framebuffer ? framebuffers[i].fbo : 0 (the default framebuffer
is GL name 0, not framebuffers[0]). Affects both Windows (opengl32) and
Linux builds. Code file from the PR head (fc092d99); docs in house style,
label rebased D323 -> D405 (collision with our G_TL_LOD finding).
2026-09-28 01:03:00 -04:00
James Dansereau 4b7cae63f9 v0.4.0 final review: D404 pad-centred aim (N64-parity quadratic curve) + full campaign-playtest sign-off bookkeeping
- input.c: padDirectCompute() — pad twin of the mouse Centred aim; the natural
  pitch path routes AIMMODE_CENTRED through it, AIMMODE_N64 keeps classic
  stick-deflection behavior. Parity pass uses bondview2's verbatim N64
  quadratic curve (sign(v)*clamp(defl/70,+-1)^2 * fov/60 * 3.5 deg/poll), so
  full-stick Centred matches N64's top rate exactly.
- optionsoverlay: aim rows device-neutral (Aim style / Aim range).
- findings.md: D404 RESOLVED (user-validated on Steam Deck, both passes);
  21 sign-off status updates from the user's full campaign playtest
  (D175/D224/D235/D236/D240/D241/D245/D249/D281/D286/D287/D289/D291/D294/
  D303/D306/D307/D308/D311/D320/D322/D75/D387/D395/D396/D399/D400); stale
  options-wave rows closed (D346-D391 batch); D383/D394 kept PARTIAL with
  v0.5.0 scopes; D281 root cause -> v0.5.0; D258 -> NTSC-only for v0.4.0.
- README: Known issues = the true residual set (D318, D75 Rareware logo,
  AllUnlocked EXPERIMENTAL, NTSC-only); stale v0.3.0 clause removed.
- AGENTS.md: native-reexec guard status re-confirmed for 2026-09-28.
2026-09-28 01:02:09 -04:00
James Dansereau 39d1fd381e input: implement Wave A controller options + Turok-verbatim labels (v0.5.0)
Port-layer only (port/src/input.c, optionsoverlay.c); zero src/game edits.
All new knobs off/neutral at default = byte-identical at defaults.

New controller input options (CONTROLLER-INPUT-PLAN Wave A):
- Input.PadLookSensX/Y: per-axis right-stick look sensitivity (100 = native)
- Input.PadSouthpaw: swap fire<->grenade triggers (applied after edge capture,
  so edge-detection stays consistent)
- Input.PadDeadzoneL/R: per-stick deadzone; legacy Input.PadDeadzone key kept
  registered (no row) + one-time migration in inputInit() seeds any unset L/R
  from it (C-init sentinel PAD_DZ_UNSET) before the first configSave
- Input.PadLookSmooth: 0-10 per-pad EMA low-pass on the natural-pitch look
  output (default 0 = off), reset on pad hot-unplug
- Input.MouseYScale: pre-existing registered key finally gets an INPUT row

Verbiage baseline (Turok PC options menu, user-provided verbatim): Mouse
horizontal/vertical sensitivity, X axis/Y axis look sensitivity, Look
smoothing, Southpaw, Vibration (was "Rumble strength"), Deadzone
(left/right stick). GE keeps one flat INPUT section (D356), so the two
Invert-look rows retain their (mouse)/(controller) suffixes.

Display baseline (Turok scales): sensitivity + deadzone rows render 0-100
with 50 = default (existing calibratedDefault path); look smoothing renders
0-10 via a new dispMax "N/M" row formatter (alongside the existing N/100 and
N/255 special cases).

Tested on a real pad (Windows build) - all six options behave as intended.
2026-09-28 00:30:21 -04:00
James Dansereau 83ece6c370 docs: add CONTROLLER-INPUT-PLAN (v0.5.0 controller/input options wave)
Low-hanging-fruit controller/input options ported from the Turok PC port's
menu, scoped as port-layer-only (zero src/ edits): right-stick look X/Y
sensitivity, southpaw trigger swap, per-stick deadzone (with the legacy
single-deadzone migration), look smoothing, and a mouse-vertical-sensitivity
row. All off-by-default (byte-identical at defaults, golden gate preserved).
Documents what the GE port already has, the per-item code shape + insertion
points in input.c, and the Tier B/C items explicitly out of scope (head bob,
bloom/shadows, death-cam, uncapped-FPS -> UNLOCKED-FPS-PLAN, controller
rebind -> PR #109).
2026-09-28 00:00:11 -04:00
James Dansereau d934a93253 docs: D403 resolved — Deck front-end logo/copyright page confirmed fixed
After regenerating the Deck's pcmodels/pccg sidecars from the current
converter (M-197 blob fix + D303) and pushing them, the user re-checked
the front end on the Steam Deck and confirmed the Nintendo/GoldenEye logo
+ copyright/legal page now render correctly. D403 was the stale-sidecar
test artifact all along; no code was ever at fault.
2026-09-28 00:00:11 -04:00
James Dansereau 7f6d745f25 docs: D403 disambiguation performed — regenerate + push Deck sidecars
The Deck bundle's stale 0.3.0-era pcmodels/pccg sidecars were regenerated
from the current Python-source converter (prepare-assets.py + d43/d69/d88,
ALL CHECKS PASSED) on the NTSC ROM and pushed to the Deck (sha256-verified),
preserving the user's ge007.eep + ge007.ini. Awaiting the user's by-eye
front-end recheck (logo/copyright page) to close the loop.
2026-09-27 23:54:08 -04:00
James Dansereau 001690568f docs: log D402/D403/D404 + Steam Deck v0.4.0-pre test results
- D402: first clean Linux build fix (shim include-cycle + _SHIFTL).
- D403 (new): Deck front-end logo/copyright page broken + graphics not
  updated is a test artifact — the bundle was seeded with 0.3.0-era
  sidecars that predate the D75/M-197 blob fix and D303; not a code
  regression. Disambiguation: regenerate the Deck's data/ sidecars.
- D404 (new): 'Aim style: Centred (PC)' is inert on a gamepad; the row's
  doc comment over-promises (applies to pad trigger) but the centred
  path is mouse-only (input.c:1783, D337 device-aware gate).
- Deck test confirmations: D175 gains a low-utilisation (~15-30%)
  frame-pacing/present data point; D395 pad-binding retest satisfied
  (not stuck); D401 rumble + D287 idle/resume-from-sleep confirmed on
  the Deck's Steam Controller / SteamOS.
2026-09-27 23:53:03 -04:00
James Dansereau ed991dd1cb port: D402 — fix the first clean Linux (glibc/SteamOS) build
The release/v0.4.0 branch had never been compiled on Linux; two
port-layer include-resolution spots that MinGW tolerated break under
glibc. Shim-only fixes, zero src/ edits, no rule-2:

- port/shim/PR/ucode.h + port/shim/bondconstants.h: break the
  bondconstants.h -> ultra64.h -> pc_protos.h -> bondtypes.h ->
  bondconstants.h first-include re-entry by deferring pc_protos.h during
  bondconstants.h's own ultra64.h pass (_PORT_DEFER_PC_PROTOS).
- port/shim/PR/gbi.h: define _SHIFTL/_SHIFTR when absent, so video.c's
  gbi.h macros keep their mbi.h definitions on Linux.

Builds + links clean on Ubuntu 24.04 (glibc 2.39 <= SteamOS 2.41).
2026-09-27 23:53:03 -04:00
James Dansereau 9fb381eab1 M7 (v0.4.0, D387): ship AllUnlocked as loudly-documented EXPERIMENTAL/irreversible
Per the user's release decision, keep the toggle (default OFF) but make it
impossible to miss in-game: the row is now labelled 'All unlocked
(EXPERIMENTAL)' and its ON value renders 'ON - UNSAFE' in both the F10
overlay and the front PC Options screen (both read the shared row table).
ON value now bright-highlights like a normal On. Port-only, display-only
(config still stores 0/1); a safe architecture is v0.5.0 work.

Housekeeping: track docs/dev/M7-ALLUNLOCKED-PLAN.md (records the decision);
gitignore the stray repo-root Python/ install; refresh the D387 status.
2026-09-27 23:27:49 -04:00
James Dansereau 471eecd6d3 D401: Rumble Pak -> real gamepad haptics (re-promotes D224/M-87)
Port layer only (zero src/ edits), per docs/dev/RUMBLE-PLAN.md:
- port/src/input.c: inputRumbleSupported/inputRumble -> SDL_GameControllerRumble;
  detection via SDL_GameControllerHasRumble (SDL >= 2.0.18) else
  SDL_JoystickIsHaptic + known-type Windows fallback; zeroed on pad close.
  Single global gRumbleScale (dflt 0.5) registered as Input.RumbleScale.
- port/src/libultra.c: the two M-87 blockers resolved - g_contStatus[i].status
  now sets CONT_CARD_ON for connected rumble-capable pads (so
  joyRumblePakInit arms) and osPfsInit returns PFS_ERR_DEVICE for them (so
  osMotorInit is attempted); osMotorInit/Start/Stop drive the haptics API.
- port/src/optionsoverlay.c: one global 'Rumble strength' ROW_SLIDER
  (Input.RumbleScale, 0.05 detents, %.2f) in the INPUT section +
  kResetDefaults entry. The four per-player Input.PlayerN.RumbleScale keys
  were dropped (user decision 2026-09-28); stale keys in an existing ini
  log four 'unknown key' LOG_NOTE lines and self-clean on next configSave.
- Real-pad haptics confirmed working by the user (2026-09-28).

Build env: build-pc.sh self-heal guard revision - re-exec now writes a
self-contained .ps1 (msys->native argv conversion mangles $-bearing
-Command strings; file write is conversion-free), builds PATH explicitly,
picks TMP from writable candidates (GetTempPath honours the broken
inherited TMP), self-logs to build-pc/ge007-native-reexec-*.log, and
treats a null LASTEXITCODE as 'did not execute' (PS 5.1 piped native
commands error non-terminating). OPEN: the guard still fails inside
build-pc.sh while the identical standalone .ps1 invocation works - AGENTS.md
documents the workaround. docs: RUMBLE-PLAN.md (new, tracked), findings.md
D401 entry + index rows, QOL-INVENTORY D224 -> DONE (D401),
findings-index.csv regenerated.
2026-09-27 23:23:05 -04:00
James Dansereau a548fc4de6 port: D397 — fix file-select/gun-barrel comb via RDP scanline-range snap in gfx_draw_rectangle
Root cause H2 confirmed: the gun-barrel / file-select vertical gradient is
built from 0.75-native-pixel-tall gSPTextureRectangle row strips (title2.c
(<<2)-1 bottom idiom). On the N64 RDP the scan converter floors the U10.2
edges to whole scanlines, so each strip rasterizes to exactly one scanline;
gfx_draw_rectangle passed the fractional edges straight to the GPU's
continuous rasterizer, aliasing the 0.75px pitch into a periodic 'comb'
(fixed ~3-native-row period; window period = 3*scale — confirmed by a
3-scale 640/1024/1280 GE_PCDUMP test).

Fix (port-layer, surgical): after the sub-pixel offset, floor the top edge to
its scanline and the bottom edge to the last covered scanline (the RDP
scanline range [top_scan, bot_scan+1]) before NDC conversion. No-op for
whole-pixel rects (edges already 4k/4k-1), so only sub-pixel strips change
and normal fills are untouched.

Also: remove the temporary GE_D397SH / D397SHM env-gated shader probes
(gfx_opengl.cpp, gfx_pc.cpp) now that the root cause is settled.

Docs: findings.md D397 -> RESOLVED (+3-scale confirmation + fix record),
findings-index.csv regenerated, porting-notes.md section D15 marked resolved
with the landed fix.

Verified by playtest: gun-barrel + file-select gradient render clean, comb
gone.
2026-09-27 22:42:43 -04:00
James Dansereau 2a60b8a912 port: D397 — D397SHM input-mapping probe; H1 LERP-factor variant ruled out
New env-gated D397SHM probe in gfx_generate_cc dumps which G_CCMUX
inputs a combine feeds as vInput1..N. For the gun-barrow row-strip
shader (id0=0000000010001218): vInput1=PRIMITIVE, vInput2=ENV_ALPHA,
so the front.c LERP renders out = PRIM + (TEXEL-PRIM)*(envA/255).

The PD ground-truth port feeds the identical env_color.a/255 factor for
the same ENV_ALPHA LERP input, so the LERP factor is not a port-side
scaling bug (H1's 1/255-vs-1/32 variant refuted). A uniform factor
cannot produce the *periodic* comb, which points to H2 (row-strip
geometry / 256-tri vertex-ring wrap). Next: Q3 gfx_flush() probe +
Q5 native FBO capture. Findings §D397 + handoff updated.
2026-09-27 22:15:17 -04:00
James Dansereau 65875ad45b port: D397 — retract the ineffective CI->I import block; correct the finding record
Per scratch/d397_fs/HANDOFF-D397.md: the stale-RENDERTILE hypothesis is
REFUTED (D161 already routes CI + G_TT_NONE to the I8 import; the GE_TEXI
logs show palfmt=0 on all 299 row strips), so the D397 block added to
import_texture had no effect (12px frame diff) and is removed.

The finding's true state: data path proven clean (byte-exact I8 uploads
vs the ground-truth PGM); MSAA/mips/noise/quant/cache ruled out.
Leading cause H1 = the file-select custom-CC LERP feeding ENV_ALPHA 0x14
as a 20/255 mix factor (black texels render ~0.92x prim = the measured
27/28/17 census); the N64 RDP 5-bit coefficient scaling (gmain.s / PD
CC engine) is the open root-cause question. H2 = possible separate
row-strip-geometry comb. GE_D397SH probe stays in-tree until the root
cause lands. findings table + tail section + CSV corrected.
2026-09-27 22:05:19 -04:00
James Dansereau 2ba72eb75f docs: v0.4.0 plan/README/backlog/porting-notes refresh for the modern-options wave
- V0.4.0-PLAN: 2026-09-27 refresh (M-wave state, D393-D397 session
  additions, M7 deferral decision).
- README: known-issues + options documentation for the landed
  crosshair/controller/options work.
- GRAPHICS-BACKLOG / porting-notes: D392/D397 and crosshair entries.
- .gitignore: per-landing sweep capture dirs.
2026-09-27 22:04:07 -04:00
James Dansereau a2b2f78dd1 port: D397 — file-select background + gun-barrel comb look: stale RENDERTILE import (fix in tree, by-eye owed)
Root-caused the D397 interlaced/comb symptom on the file-select
background and intro gun-barrel (first display, not round-trip):
titleRenderFolderMenuBackgroundLines loads 299 one-pixel 440-byte I8
rows with gDPLoadTextureBlock(I, 8b) and samples G_TX_RENDERTILE but
never emits gSPSetTile, so the tile's fmt/siz is whatever the previous
screen left (CI/4b spectrum-screen leftover, D116 probe). fast3d ran
the I8 bytes through import_texture_ci4 against a stale/all-zero
16-entry palette -> periodic black rows (GL texture-cache artifact,
not a ground-truth dither — no ROM RLE decode needed).

- gfx_pc.cpp import_texture: route the import through the source's
  fmt/siz when a CI/RGBA tile genuinely disagrees with an I8 8b load
  (mirrors the D229 case); real CI/RGBA textures untouched.
- gfx_opengl.cpp: GE_D397SH env-gated shader-formula probe for the
  file-select gunbarrel combine (label initially written as D398,
  renamed to avoid the PC-Options-label finding collision).
- findings: D397 row -> FIX LANDED (by-eye + PCDUMP verification
  owed); tail section notes the fix. CSV regenerated.

Cross-ref D182(2) (same symptom family), D159/D183 (disproven leads),
D229 (mirror-case precedent).
2026-09-27 22:04:03 -04:00
James Dansereau 545b2c7792 port: v0.4.0 modern controller + options-UI wave (D371-D388, D393-D396)
Input (input.c/h):
- D380/D386: single effective PC layout; ini values ARE the bindings
  (no hidden defaults); versioned migration materialises effective
  GEPD defaults once (inputMigrateBinds, bindsVersion).
- D385: keyboard keys and pad buttons 1..5 share the two bind slots;
  legacy extra ini tokens are migration-only. D386: SDL button-number
  correction in the D385 defaults.
- D383/D384: opt-in inputBindingProbe (restores ini state); the PC
  settings editor must not navigate with live game keys.
- D393/D394: Xbox 1.1 Jinx face map — A use, X reload, Y weapon
  cycle, B owned-gadget cycle (no N64 equivalent, port-side), L3/R3
  crouch, LB aim; retires LB/RB weapon cycling.
- D395: both options UIs refuse pad-initiated keyboard/mouse capture;
  pad B cancels a capture started by keyboard/mouse.

Options UI (optionsoverlay.c/h):
- D388: nested Input -> Bindings / Gameplay -> HUD roots in both
  F10 and front-options; redundant Low-end toggle retired (FPS cap /
  MSAA remain independent, stored values preserved on migration).
- D391: hit bands follow rendered glyph baselines; dead zone +
  out-of-card click rejection.
- D396: F10 pad B backs/closes a page (closes at root), A/X
  accepts/toggles without shifting sliders; left/right still adjust.

All behind #ifdef PORT; N64 input/menu paths untouched. M7 (D387
AllUnlocked save-safety) intentionally NOT in this wave.
2026-09-27 22:04:00 -04:00
James Dansereau 4bd49f739f port: v0.4.0 M2 crosshair wave (D373/D379/D381) — opt-in hide/colour/size/style, default N64-intact
gunDrawSight (gunfire.c, #ifdef PORT) now routes through portCrosshair*
accessors: hide flag, colour presets + custom RGB, 100% default size,
and an opt-in beta crosshair sprite. With all defaults the original
red sprite, 16-unit half-size and N64 combiner are untouched; non-PORT
builds keep the original literals and control flow.

- D373: crosshide/tint plumbing (was rule-2 sign-off PENDING at M2
  authoring; the PORT-guarded pattern in gunDrawSight is the sign-off
  record).
- D379: only recoloured reticles switch to the texture-alpha silhouette
  combine (portCrosshairApplyTintCombine); the original keeps the N64
  G_CC_FADEA commands.
- D381: size/style/colour surface + live config apply
  (videoRequestLiveConfigForKey) in video.c.

Rule-2 framing: port-layer feature behind the gunDrawSight #ifdef PORT;
zero N64-path change.
2026-09-27 22:03:53 -04:00
James Dansereau 368daf1242 port: D392 — train-intro soldier pose: read firing-table fields by name on PC (ABI/layout only)
chrlvUpdateAimendbackShoulders read the leading anim union of
weapon_firing_animation_table through raw (f32*) slots [12/13/16/17].
On PC the union widens four->eight bytes, so those offsets alias the
wrong fields and the intro soldier hunched with the hand through the
torso (D392, vs the 1964 reference).

The #ifdef PORT arm reads the intended limits/arm fractions by name
(config->max_up/max_down, free_arm_frac_up/down); the non-PORT arm is
untouched. No logic or behavior change beyond the pointer-width
misread; D392 finding + Train aim-angle probe confirm, user visually
verified against 1964.
2026-09-27 22:03:48 -04:00
James Dansereau c214fa7815 D398-D400: PC Options label work + findings.md tail-section recovery
- D398: replace D348 procedural slider glyph with ROM-native IMG_DOT
  texture (texSelect + display_image_at_position, Copy/Erase pattern);
  superseded by D400.
- D399: file-select bottom row shifted 15px left as a block (user
  sign-off); selectpos.f[0] behind a new #ifdef PORT in front.c.
- D400: icon removed on user sign-off; PC Options is now a text-only
  label exactly like Copy/Erase (one textMeasure + one textRender,
  game's packed 0xEBD879FF gold). Fixed two measurement bugs:
  textMeasure(textheight, textwidth) args were passed swapped (label
  35px above the bar, hit band transposed) and the bare label measured
  h=0 vs 14 for newline-terminated Copy/Erase (7px low); height now
  measured with kLabelNL. LABEL_GAP 20->28, LABEL_X 343->351.
  PCDUMP frame 195: tops aligned 279.8 vs 279.3, 31px clearance from
  Erase. By-eye check owed.
- docs: findings.md had been truncated to its section table (686
  lines), losing all ## Dxx tail sections D376-D400. Recovered:
  table D376-D400 rows + body/sections D001-D375 from HEAD;
  D376-D387, D396-D399 sections recovered verbatim from pi session
  transcripts; D398 section recovered from the 2026-09-28 session;
  D388-D395 rebuilt as explicit reconstructed stubs from the
  table/CSV one-liners (original detail unrecoverable); D400 section
  written fresh. findings-index.csv regenerated and verified
  byte-identical to the pre-truncation copy.

Build clean (ntsc-final). Verified: PCDUMP frame 195 (no overlaps,
tops aligned), CSV matches pre-truncation backup.
2026-09-27 21:58:48 -04:00
James Dansereau 2e0643c70f v0.4.0 D375: crouch now emits C-down (the engine's real crouch input), not B
bondview2.c reads crouchDown = insightaimmode && (buttons & (D_JPAD|D_CBUTTONS))
(5318/5579): crouch is C-down WHILE IN AIM MODE, never the B button. M3's
IA_CROUCH -> GE_CONT_B (and the CrouchMode=1 latch) was inert, so crouch
never fired in the GEPD layout. Hold + latch now emit GE_CONT_D (0x0004).
N64-faithful side effect: S (IA_BACK, also C-down) crouches while aiming.
User re-test: aim (RMB/Left Shift) + Left Ctrl; hold or F10 Toggle.
2026-09-27 16:47:55 -04:00
James Dansereau fca07bdd9a v0.4.0 M2 (D373): crosshair on/off + colour; D374: GEPD becomes the default key layout
M2 (the wave's only rule-2 item, sign-off recorded via user direction):
#ifdef PORT hook in gunfire.c gunDrawSight -- Video.CrosshairHide skips
the draw, Video.CrosshairColor tints the RGB (7 named tints, 0 = the
N64 white); non-PORT builds keep the original literals. F10 GRAPHICS
rows + kResetDefaults.

D374 (M3 playtest fix, user decision): GEPD is now the DEFAULT key
layout (Input.Layout 0 = GEPD, 1 = N64-era binds; a mouse FPS layout is
the natural default and the N64 never had a keyboard). The preset is
now the true effective default: only EXPLICIT per-key [Input.Bind]
overrides beat it; persisted values equal to the N64 defaults count as
unset. This fixes the playtest bug where a stale persisted
Fire=Left Ctrl masked the GEPD preset and made Ctrl fire+crouch at the
same time. Boot log reports the applied layout. One-off: pre-D374
Layout=1 (ex-GEPD) inis mean N64 now -- flip to 0 (release note).
2026-09-27 16:43:04 -04:00
James Dansereau b95b54ecc7 v0.4.0 M5: low-end performance preset row (D372)
Video.LowEndMode (default off) in F10 VIDEO: ON writes FpsCap 30 +
MSAA x1 (restart row); OFF restores the compiled defaults (60/2) rather
than per-user pre-preset values. The key records the last selected
preset; the effect flows through the two real knobs.
2026-09-27 16:30:32 -04:00
James Dansereau 40ef53e395 v0.4.0 M3: GEPD key-layout preset + crouch/reload bindings (D371)
Input.Layout (0=FPS default, 1=GEPD) + Input.CrouchMode (hold/toggle) rows
in F10 INPUT (Turok-standard functional section; the planned MODERN bucket
is dropped per D356). The GEPD preset acts as the per-key effective
default under [Input.Bind] overrides; new IA_RELOAD/IA_CROUCH actions emit
GE_CONT_B (unbound by default, N64-original). Toggle crouch latches B with
a menu-boundary latch drop. optionsoverlay rowSetCommit hook re-derives
the binds via inputLayoutApply(). inputRebuildBinds no longer warns on
intentionally-empty defaults. Boot + ini round-trip + F10 page verified;
in-game feel-check owed (D371).
2026-09-27 16:29:18 -04:00
James Dansereau c2c6fbdd68 checkpoint: F10 GE-style refresh + distance defaults retune, D360-D370 series (local WIP; D370 pending user acceptance) — pre-wave baseline for the v0.4.0 modern-options wave 2026-09-27 16:18:20 -04:00
James Dansereau d8971fded0 options UI: settle file terminology on the end-user term 'Profile' (D359)
'Game file' (the manual's term) reads, in modern PC usage, as the
program's own files, not the user's save. 'Profile' is the clearest
end-user term for what these files actually are -- a per-player
bundle of progress + per-file settings -- and '(per profile)'
expresses the tag's scope where '(save)' read as a verb.

Strings (both UIs: front options + F10 overlay):
- front top row: 'Save file' / 'File N'  ->  'Profile' / 'N'
- section annotation: '(File N)' / '(no file)'  ->  '(Profile N)' / '(none)'
- per-file row tag: '(game file)'  ->  '(per profile)'
- F10 unavailable value: 'Select game file'  ->  'Select a profile'

Display layer only: identifiers and ini keys (Bond.* rows,
watchSettings*, ini) are untouched. D357 plan section 4 is the
terminology record; new D359 finding + index row; manual
'game file' -> 'profile' sync noted as a TODO (port-owned doc,
not in this repo). Build + headless smoke clean.
2026-09-27 15:03:24 -04:00
James Dansereau 44d2b4353a options UI: anchor all file terminology on the manual's 'game file' (English review)
Full-string audit of the settings UI (front options + F10 overlay):
- front top-row label 'Save file' -> 'Game file' (the instruction
  manual's term)
- per-file row tag '(profile)' -> '(game file)', both UIs
- F10 unavailable value 'Select file' -> 'Select game file'
- kept: 'File N' values, the '(File N)' section annotation and the
  '(no file)' unavailable state (short forms; the annotation's host
  word is the section title, which says the file number)
- 'Bond file' now appears only in identifiers/config keys
  (Bond.* rows, watchSettings* names, ini keys), never user-visible
Docs: D357 plan section 4 is now the terminology record (final string
table); D358 finding notes the (profile) -> (game file) supersession.
Build + headless smoke clean.
2026-09-27 15:00:01 -04:00
James Dansereau 8775ca6435 docs: D358 (D356 level-1 overlap fix) finding + index/CSV 2026-09-27 14:55:54 -04:00
James Dansereau 4147b55865 untrack local build-pc-cmd.bat (machine-specific paths); gitignore it 2026-09-27 14:55:44 -04:00
James Dansereau f8f94cdd87 frontoptions: fix level-1 overlap (save-file row painted under the first content row) + off-by-one item mapping; (save) tag -> (profile); D357 values plan
- The front options screen numbers items 0..s_rowN (0 = the top
  save-file row, k = content row k-1), but the level-1 draw loop painted
  content row k at rowY(k) -- the save row's own line -- and mapped
  s_hl (item-numbered) straight into s_rowIdx (content-numbered): the
  'Save file' row overlapped the first content row, highlights/adjusts
  hit the wrong row, the last content row was dead, and s_rowIdx[s_hl]
  could read one past the built rows. Content rows now draw at rowY(k+1)
  and all s_hl -> s_rowIdx uses take s_hl - 1 (A-press, L/R repeat,
  reset-maintain); hit-test/cursor were already item-numbered and are
  untouched.
- Per-file row tag '(save)' -> '(profile)' (maintainer request), both
  UIs (front options + F10 overlay) plus the stale comments.
- docs/dev/D357-SETTINGS-VALUES-PLAN.md (PROPOSAL): row-by-row audit of
  current defaults/ranges/steps/displays vs the PD port (local checkout)
  and the Turok PC list (maintainer notes); proposals: mouse sensitivity
  as a x multiplier, deadzone as a whole-% key (D333 legacy-alias
  pattern), texture filter '3-Point' -> Bilinear, audio step 128 -> 328
  (whole-%), MSAA +16x, FPS-cap preset grid, FOV degrees; kResetDefaults
  lockstep gate + GE_WSPROBE_RESET re-verification.

Build clean (248 targets), headless menu boot smoke clean; visual
check of the front options screen + F10 owed to the maintainer.
2026-09-27 14:55:41 -04:00
James Dansereau fe57d73924 optionsoverlay/watchsettings/frontoptions: D356 settings regroup — functional sections, save-file scoping, per-section reset-to-defaults (Gate E)
- 5 sections (INPUT/GAMEPLAY/GRAPHICS/AUDIO/VIDEO, all below MAX_PROWS):
  DISPLAY+GRAPHICS merged into GRAPHICS; AUDIO carries the watch music/FX
  sliders out of the Bond-file page; VIDEO keeps the pure video rows;
  __HdrMouse/__HdrPad merged into INPUT; __HdrBond/__BondFile retired for
  the front-only top save-file row (frontoptions.c)
- save-file scoping: per-file sections (GAMEPLAY, AUDIO) annotated
  '(File N)'/'(no file)' via the new Row.saveScoped flag +
  watchSettingsActiveFolder(); per-file rows in mixed sections carry a dim
  '(save)' tag; the D353 optionsRowIsBondChooser special-case is
  generalized (both UIs)
- N64-watch rows Bond.Look / Bond.AimControl commented out of the menu
  surface (D181/D216 style: keys + watch paths stay live)
- per-section 'Reset to defaults' replaces the Turok rows (plan Gate E):
  edge-triggered two-step arm->confirm within 3 s, one commit, disarm on
  timeout / navigate-away / close (optionsResetMaintain/Clear);
  rowResetSection walks the section's declared range: watch rows ->
  BLANKSAVEDATA through the D352 commit path (front direct, stage queue),
  ini rows -> kResetDefaults[] (the port's C initializers); __Resolution
  is a documented exclusion (action-backed)
- GE_WSPROBE_RESET probe: front tick-120 prepare/dispatch/verify, stage
  tick-120 dispatch / tick-121 verify; a direct -level_XX boot starts its
  timer from phase 0 (front prepare is front-only)
- docs: findings.md D356 + index + CSV, WATCH-SETTINGS-PLAN status,
  OPTIONS-MENU-PLAN pointer, plan doc status -> LANDED

Verified headless: GE_WSPROBE_RESET front (all 5 sections failures=0,
__Resolution untouched, file-2 scope isolation OK) and -level_33 (6
commands queued, all sections verified post-drain, saved music/sfx = 0xFF);
GE_WSPROBE / GE_WSPROBE_FRONT regressions clean; 248/248 build + link.
2026-09-27 14:49:08 -04:00
James Dansereau 95f8f5f8b5 docs: D356 settings-regroup plan with tightened reset/save-file contracts (maintainer sign-off) 2026-09-27 14:03:06 -04:00
James Dansereau 2b4c5ba7dc watchsettings.c: resync in-stage BGM (track 1) on Music commit and level entry (D355) 2026-09-27 13:53:36 -04:00
James Dansereau 61263ad303 watchsettings.c: front-end F10 routing + transparent default Bond-file selection, auto-init fallback, headless probes (D354) 2026-09-27 13:48:03 -04:00
James Dansereau 462675cf47 optionsoverlay.c: Turok-style regroup - DISPLAY/GRAPHICS split, BOND FILE absorbs music/FX, designated rows, overflow log, file-scope title annotation (D353) 2026-09-27 13:32:39 -04:00
James Dansereau 8e9bee6ef0 watchsettings.c: D350-bridge robustness - hold-not-drop at stage transitions, chooser spinlock, queue coalescing, warning logs (D352) 2026-09-27 13:32:18 -04:00
James Dansereau 1303c0ebe1 D350: expose watch-backed Bond settings in PC options and F10
Add a game-thread bridge and field-scoped per-file persistence for PC UI edits, with an explicit file chooser; keep watch-only edits session-only. Fix D351 row initializer offsets for units and auto-hide. Document verification and review follow-ups.
2026-09-27 13:20:21 -04:00
James Dansereau 98a24a07f4 Docs: close D343 (NTSC verified; PAL/JP not capturable locally) 2026-09-27 12:30:14 -04:00
James Dansereau 050c3981d6 D348: mini slider glyph beside the file-select PC Options label
No settings icon exists in the game art (mainfolderimages holds only
copy/del/select-arrow/X/check/dot), so a miniature slider control is drawn
instead: three menu-box tracks with offset knobs in the 14px gap after Erase
(10px wide, 2px margins), INK_DIM tracks, knobs matching the label ink
(white / gold while hot). Tracks the dynamic label x (JP-safe), text position
unchanged; hot region extended to include the glyph. PCDUMP pixel-verified.
2026-09-27 12:24:56 -04:00
James Dansereau 7e07d9e018 D347: F10 overlay controller value-adjust + hold-to-repeat
D-pad left/right and stick-X now adjust the selected row's value (A/X/B/Y
kept); 18/4-frame hold-to-repeat added to value adjust and up/down nav,
matching the options screen's D345(e)/(f) cadence. Port-only; user
controller test verified.
2026-09-27 12:18:36 -04:00
James Dansereau 7ff9ddbe14 D345/D346: options-screen pointer + keyboard nav fixes; settings wording pass
D345 (a-g): re-assert window on menu change; cursorToItem skips snap while
the 1:1 pointer is live; W/S emit stick in menus under NaturalPitch;
F10-style discrete row stepping + arrow left/right value adjust replace raw
drift (first pass shipped broken, fixed); screen-placed cursor parks in the
left gutter off the row text.

D346 (+b): labels/values aligned to Nightdive/Turok + PD-port conventions
(title-case On/Off, Frame rate cap, Anti-aliasing/None, Anisotropic
filtering, units in values not labels, deadzone as %, self-explanatory
auto-FOV toggles); VIEW section merged into VIDEO (MAX_PROWS 12->15,
ROW_DY 20->18). Display-only: config keys and stored values untouched.

Port-only; no src/game edits. User playtest verified.
2026-09-27 12:12:32 -04:00
James Dansereau 835035916d D343: PC Options joins the file-select bottom bar; Copy/Erase shift left
Rule-2 layout edit (user sign-off 2026-09-27): Copy 247/225 -> 222/200,
Erase 357/335 -> 304/282 under #ifdef PORT; their click boxes follow.
"PC Options" is drawn by the port hook right after Erase's measured text
end (min x 352), so wider localised labels push it right. The top-right
label is gone.
2026-09-27 11:01:58 -04:00
James Dansereau 13473d5ec6 D343: PC options as a GE front-end screen (MENU_PC_OPTIONS)
Replaces the M2 panel. Built like the Cheat Options screen: open dossier
(tabs + blank CLASSIFIED page), black Zurich ink via frontPrintText, GE's
highlight box, PREVIOUS tab as back, the game's crosshair. Page 0 is a
numbered section list; each section lists the F10 overlay's rows with a
value column (ON in the cheat red) and slider bars.

Game code (Rule 2, user sign-off 2026-09-27): MENU_PC_OPTIONS enum value
and five one-line dispatch cases, all #ifdef PORT. The file-select label
now enters the screen via frontChangeMenu. The M2 button hand-off in
input.c is gone; F10/Select don't open the overlay on this screen.
2026-09-27 10:58:27 -04:00
James Dansereau e4cf012410 D343/D344: record user verification on PC 2026-09-27 10:41:25 -04:00
James Dansereau fd82dda717 D343 M2: GE-styled options panel on file select; D344 docs
The file-select Options label opens a panel drawn in the same hook:
sections as tabs, rows from the F10 overlay's table via an exported row
API, the game's crosshair as pointer (input.c passes the stick, hands the
buttons to the panel). Quit to desktop now goes through videoRequestQuit.
Also repairs two escaped-newline typos in the D343 docs.
2026-09-27 10:38:16 -04:00
James Dansereau 461ea871c2 D344: orderly quit so exit never lands inside the GL driver; GE_QUITFRAME
Quit (quit event, window close, Alt+F4, from either SDL pump) used to
exit(0) immediately while the render thread could be mid-frame in the
driver: the likely trigger of the host's 0x119 bugchecks. Now quits are
requested; the render thread glFinish()es, unbinds the context and parks
at a frame boundary, and the host exits after (max 2 s). GE_QUITFRAME=<n>
gives the harness a clean self-quit; golden_gate.sh uses it.
2026-09-27 10:38:16 -04:00
James Dansereau c07af11fae D343: record user playtest (all but a physical controller confirmed) 2026-09-27 10:07:22 -04:00
James Dansereau 413014b8f6 D343: file-select Options label opens the settings overlay; F10 idle-timer fix
One Rule-2 #ifdef PORT draw hook in constructor_menu05_fileselect (user
sign-off 2026-09-27). The label's hit test and open request live in
optionsoverlay.c; the open is applied on the input thread. While the
overlay is open on file select, g_MenuTimer is held at 0 so the screen no
longer drops to the legal screen after 30 s.
2026-09-27 10:01:27 -04:00
James Dansereau bd8ade5c1e D307: note camera reaction timing is tick-based and faithful 2026-09-27 00:31:57 -04:00
James Dansereau 60f12f2bba D245 follow-up: water base-level crop never ran (coloured dashes)
The crop tested loaded_texture.line_size_bytes, which is the whole
LoadBlock (1400 B), so the water still uploaded 32x43 while UVs were
normalised to 32 rows; the mip rows showed as coloured dashes. Test the
tile line size instead. User-verified on Frigate; golden 3/3.
2026-09-27 00:31:24 -04:00
James Dansereau 4ba1454ba8 D342: file-select folders vanished after backing out of a file
The PC wallet model (0x1664C B, 0x17000 reservation) loads at the start of
ptr_logo_and_walletbond_DL, but the file-select scratch DL and the
briefing text still used the N64 offset +0xA000 -- inside the model.
After main menu -> back the model is not reloaded, so the overwritten
folder bodies/photos disappeared. Both now use +0x17000 (same 0x6e000
budget). User-verified; golden gate 3/3. D341 also user-verified.
2026-09-27 00:16:18 -04:00
James Dansereau 5c643e3acb D341: intro gun-barrel blood drip drew static garbage
Two fast3d defects. (1) The 1-cycle blood texture rectangle sampled a
stale tile-1 CI8 declaration: the title leaves G_TL_LOD set and the D236
detail-base rule redirected texunit 0 to tile 1. LOD tile selection is
2-cycle-only on the RDP, so texrects outside 2-cycle now sample their own
tile. (2) The drip image is regenerated in place in the dyn pool (reached
via the KSEG0 mirror); the address-only cache key froze it on its first
image. Dyn-pool textures now key on a content hash. Headless-verified,
golden 3/3. A/B: GE_DYNTEXHASH_OFF.
2026-09-26 19:20:06 -04:00
James Dansereau f242992360 D245: water tiled at the wrong size + s16 sky/water vertices; D242 closed
The IsWater 32x32 CI8 image (drawn through an RGBA16 tile, D229) was
imported with its mip chain as 32x43 and normalised as 16 wide, so the
pattern ran at 2x and wrapped every 43 rows instead of the N64's 32:
fold steps made it jump. Import the base level only and use CI8
geometry in the tri path. Sky/water fans now go through a port-only
G_FLOATVTX_EXT (float clip positions + S/T) instead of s16 Vtx, removing
the tc shift and wScale quantisation jitter. User-verified vs 1964;
golden 3/3; Win + Linux build. A/B: GE_D245_OLDVTX / GE_D245_OLDTEX.
2026-09-26 18:56:52 -04:00
James Dansereau d5f41d6a37 D242: headless death auto-advances without input; KIA screen needs a menu-launch check 2026-09-26 18:22:39 -04:00
James Dansereau d061ca3d12 D303: model converter copied 1/4 of each star muzzle flash's vertices
The op-22 DisplayListPrimaryRecord count is the number of star-flash
arms; dorottex (and PD's modelRenderNodeStarGunfire) read count*4
vertices. d43_emit/d43_convert sized the array as count vertices, so
3/4 of every muzzle star came from neighbouring sidecar bytes and drew
as long spikes (M16 "shooting up"). Size it 16*4*count. Emitter
round-trip checks all pass; M16 renders a clean star headless.

Also TEMP GE_STARTWEAPON (bondview_r.c) for weapon repros. Re-run
tools_pc/d43_emit.py to regenerate data/pcmodels-*.
2026-09-26 18:15:17 -04:00
James Dansereau 00b0006971 D307: security cameras read the mount pad as their look-at pad
CCTVRecord's own s32 `pad` (the look-at pad) shadows the inherited
ObjectRecord `pad` (mount pad) under IDO. The PC struct renamed it
`lookpad` to satisfy GCC, which silently re-pointed setupCctv's reads at
the mount pad, so every camera's look vector was its lens offset and the
lens faced the wall. Read `lookpad` on PC (#ifdef PORT, A1 class);
matches PD's setupCreateCctv (lookatpadnum). User-verified vs 1964,
golden gate 3/3. Porting-notes gains the duplicate-member rename trap.
2026-09-26 18:00:40 -04:00
James Dansereau 40b6994504 Findings: 1964 comparisons — D174/D308 faithful; D307/D303/D245 confirmed port bugs; D242 N64 reference 2026-09-26 17:46:57 -04:00
James Dansereau 116a9db798 Findings: close D183 D298 D202 D315; D220 merged into D303; D242/D303 to 1964 list 2026-09-26 17:39:29 -04:00
James Dansereau 035d54dd66 Findings bookkeeping: user status calls + stale-row cleanup (M-201)
User-confirmed fixed/closed: D289 D285 D160 D156 D152 D235; approved
closes D286 D296 D85, D323 parked as an enhancement; D218 shipped as
DrawDistance. Shipped QoL rows D180-D216 and D333-D338 marked fixed;
D318/D320/D321 fixed via D329; D236 sub-passes D265-D280 closed.
D186/D339/D202/D207/D174/D308 annotated with user direction.
2026-09-26 17:36:48 -04:00
James Dansereau 506cd7ed11 D207/D241: shot-sound handle probe; live captures refute drop/starvation
TEMP GE_D207S (gunfire.c, #ifdef PORT, read-only) logs every player
shot-sound decision (H1/H2/SKIP). Four user captures incl. a 7-weapon
stress test at 60 fps: 0 SKIP, 0 voice drops, audio production at rate.
The one captured drop-out ran under GE_AUDIOTRACE, whose audio-path file
I/O is a likely cause. Still open as intermittent.
2026-09-26 17:26:06 -04:00
James Dansereau b8e34c7fac D287: user-confirmed fixed (idle-unfocused click-in, mouse checks) 2026-09-26 16:55:58 -04:00
James Dansereau 8f838316c0 Q7: declare atoi/lround for port TUs; D207: ZHOLD/ZREL harness, findings
optionsoverlay.c called atoi and lround through implicit int declarations
(GE's stub stdlib.h/math.h shadow the CRT, D324 class); atoi joins the
other K&R decls in port/shim/stdlib.h, lround gets a local prototype.
Windows + Linux builds clean of both warnings.

GE_INPUTSCRIPT gains ZHOLD/ZREL (sustained fire) for automatic-weapon
audio repros. D207/D241: headless burst-fire at 60 vs 30 fps with the
alarm forced logged zero voice drops at both caps; owed a real-play
capture. Golden gate 3/3.
2026-09-26 16:53:54 -04:00
James Dansereau 0097469810 D291/D306: close Dam intro truck glitch as faithful; add repro probes
D291 (user-captured) is D306: Dam intro camera 5, truck over the walkway
camera; reproduces at FovScale 100. All PC-specific leads refuted with
evidence: FOV, depth precision/DrawDistance, depth state (every truck
triangle is opaque z-tested), near-plane handling, wheel transforms. The
image is a depth-correct view of the authored model from under the truck.
GE_ZF's earlier "real fighting" was a +-255 z quantisation artefact.

TEMP tools: GE_INTROCAM=<n> pins the level intro's first camera
(bondview_r.c, #ifdef PORT, PRNG draw still consumed); GE_D306C depth-state
census in gfx_pc.cpp. Golden gate 3/3.
2026-09-26 15:54:46 -04:00
James Dansereau f1207e915b D252: re-sync compiled explosion DLs on every stage load; D314 confirmed
gimgSyncCompiledGlobalDLs (D68 shim) only copied texLoad's resolved
texture pointers into the compiled globalDL shadows while a slot still
held the 0xABCD IMAGESEG marker, i.e. only for the first stage (title).
Every later stage kept pointers into the title's freed texture pool, so
explosion smoke/fire/debris rendered as rainbow garbage once the stage
overwrote that memory (Facility: past the lockers). Learn the slots once,
re-sync them every stage. User-verified; golden gate 3/3.

Also: TEMP GE_D252POOL texpool/sync probe; GE_D157 F12 burst and caps
(uncapped colour match made live play 4-6 fps). D314 F10 flicker
user-confirmed fixed. Findings M-201, porting-notes C2.
2026-09-26 11:43:30 -04:00
James Dansereau aedc98e247 D252/D314/D291: explosion-debris investigation, overlay wheel race fix, Dam truck notes
- D252 (M-200): the 'rainbow confetti' is the explosion debris
  (explosionInitFlyingParticles). Statically clean for colour; 4 headless
  repros including the user's grenade-launcher trigger are all clean. Still OPEN.
- d320repro (TEMP): new explode= and cheat= actions for headless explosion
  captures; documented in GE-ENV-PROBES.
- D314: optionsOverlayScroll (host thread) rebuilt s_visIdx/s_visN and wrote
  s_sel while the scheduler thread draws the overlay. Wheel notches are now
  queued atomically and applied in HandleInput (D287 pattern). Emit and
  HandleInput share the scheduler thread. Headless file/mission select is
  pixel-stable; likely fixed together with D287, needs a user check.
- D291: truck AI mapped (ai_9 / 0x040a, path 7); headless walk did not reach it.
2026-09-25 23:20:38 -04:00
James Dansereau d07ed0f2d0 D287: apply SDL mouse-mode/cursor changes on the window thread only
input.c called SDL_SetRelativeMouseMode / SDL_ShowCursor from the scheduler
thread (every controller poll) and from the host thread's focus/click
handling at the same time. SDL's mouse API is not thread-safe; on Windows
relative mode starts/stops the raw-input thread, and clicking back into the
window after being unfocused is exactly when both threads flip it - the D287
freeze trigger. Callers now record the wanted state; inputApplyMouseRequests()
applies it on the host thread from videoPumpEvents(). Queue-deadlock leads
ruled out statically. Win+Linux build clean, golden 3/3; freeze re-test owed.
2026-09-25 22:37:46 -04:00
James Dansereau 2ed84ed38b Q7 audit fixes: missing returns in front.c menus, getenv prototype, dram.c shift UB
- front.c: constructor_menu07_missionsel / constructor_menu0D_missioncomplete
  fall off the end while the caller uses the Gfx* result; return DL under
  AVOID_UB (porting-notes D6). Old/new objects disassemble identically at -O2,
  so no behaviour change on this compiler; clang/arm64 no longer rely on luck.
- libultra/audio/event.c: include <stdlib.h> under PORT so getenv() is not an
  implicit int-returning declaration (D324 class).
- dram.c: DRAM_SIZE >> 32 on a 32-bit unsigned long (Win64) was UB.
Golden gate 3/3.
2026-09-25 22:30:48 -04:00
James Dansereau edddf87429 D322: prefer DirectSound over SDL2's WASAPI backend on Windows
Four simultaneous drain monitors on the same USB interface: WASAPI (512 and
2048 samples) fell to 81-88% drain for ~30-50 s every ~317 s and lost 1.4% of
playback, while DirectSound stayed at exact real time. Each WASAPI shortfall
overflowed the audio queue and dropped blocks (the periodic garbled bursts).
audioInit() now hints directsound,wasapi; SDL_AUDIODRIVER still overrides.
Golden gate 3/3; 60 s GE_D204 run clean after load.
2026-09-25 22:25:13 -04:00
James Dansereau 2a20a93318 D322: periodic 'garbled' bursts are the host audio device, not the port
A no-game SDL drain monitor (new tools_pc/sdl_drain_monitor.c, opens SDL
exactly like port/src/audio.c) shows the maintainer's Focusrite USB interface
draining at 88-95% for ~15 s every ~312 s, matching the in-game overflow
bursts' period. Level loads, combat, the SDL pull pattern and a
pacemaker-vs-vsync beat were ruled out. Remaining D322 items: the 8-voice SFX
cap (D207/D241) and #87's progressive decay. porting-notes: measure the host
device before chasing periodic audio glitches.
2026-09-25 19:15:22 -04:00
James Dansereau 82548a0fe9 findings: 2026-09-25 release playtest close-outs + D322 capture
D311 FIXED (root cause D336; 0 anomaly lines incl. Caverns combat), D288
CLOSED, D294 and D330 user-confirmed, D235 still unreproduced. D322: pools
healthy (leak hypothesis refuted); SFX soft-cap saturation in firefights
(D207/D241) plus a new periodic SDL output-queue overflow every ~314 s that
starts at the title screen, so it should reproduce headless.
2026-09-25 18:40:26 -04:00
James Dansereau 7baf490020 tools_pc: golden_gate.sh — one-command per-patch golden gate
Pins data/ge007.ini to a defaults-only 640x480 window (restored on any exit),
captures Bunker 1 at 640-1120:240 and runs framediff.py against the golden
baseline. --level/--dump give a capture-only smoke run (exit 0 = frames
captured and no crash). Full framediff output goes to scratch/.
2026-09-25 16:46:03 -04:00
James Dansereau 251fcc4194 docs: fold drop-in ROM Part B backlog (issue #85) into release-dropin-rom-plan 2026-09-25 10:47:59 -04:00
James Dansereau 684dee9127 D231: park mute-on-focus-loss/screenshot QoL (deprioritized; branch retired from GitHub) 2026-09-25 10:45:11 -04:00
James Dansereau 709959c66a docs: cut-content research round 2 — decomp-only discoveries + full music alignment
New findings not covered by TCRF's articles (full detail in local
backlog/cut-content/RESEARCH.md):

- The shipped game's M16 pickup text still reads 'an AR33 assault rifle'
  (LpropobjE PROPOBJ_STR_2A, propobj.c:10471); unused 'US AR33' strings and
  PROP_CHRM16 comments survive too.
- Klobb rename scar verified: GUN_STR_6B is the only out-of-alphabetical-order
  weapon name in LgunE.c, exactly as TCRF's prerelease page describes.
- cartrifle + wristdart: full pickup/prop resources, no ITEM_ enum, no weapon
  stats — cut weapons missing from TCRF's unused-items list (wrist dart = the
  AMMO_DARTS hardware; decomp comments it '(BETA)').
- All four Bonds mapped: CUFF_/BOND_ Connery/Dalton/Moore enums + the
  actor-switch code in bondview2.c with every branch flattened to Brosnan;
  no former-actor models exist in the ROM (80 C*Z chr models enumerated).
- Cuba (LlenE.c) is the only orphan level with recoverable text: cut Natalya
  dialogue, title card, alternate credits cast.
- Orphan-level geometry confirmed present in ROM (obseg.h segment decls);
  what's missing is mission setup, not geometry.
- Music enum vs Notes-page GS index aligned slot-for-slot through 0x2C with
  four local mismatches past it (49/52/54/59) pending in-game audition.
2026-09-25 10:44:44 -04:00
James Dansereau 019ec2f233 D226: cap HUD scale at 150% (user playtest: works, little benefit above 150) 2026-09-24 11:40:43 -04:00
James Dansereau 062f605520 plan: reconcile v0.4.0 rows (D226, D237, D238, D294, D311) + session additions (D336-D339, #105 note) 2026-09-24 11:29:21 -04:00
James Dansereau cd651000ca D226: findings write-up (implemented, verified) 2026-09-24 11:29:03 -04:00
James Dansereau 3072035417 D226: HUD scale option (Game.HudScale 75-200%) — fast3d G_HUDSCALE_EXT rect scaling about per-element anchors
Ammo (per-hand corner), bottom messages/title card (bottom-left) and top
dialogue (text origin) scale with crisp glyphs; dialogue and bottom
messages are width-capped because their line breaks are authored. 100%
emits nothing (golden 3/3). Captures verified at 150/200%; Win + Linux.
2026-09-24 11:28:29 -04:00
James Dansereau 0de97b9c67 D311: likely root-caused by D336 (head-bob alias read); headless Caverns clean at 2x pool; close after a live combat session 2026-09-24 11:13:42 -04:00
James Dansereau 36de85e789 D339 (#92): GE_PERFSTAT perf probe + measured CPU budget (CPU likely not the low-end bottleneck; GPU suspect)
Env-gated per-frame timing: render phases, tris/batches, game-thread busy,
GL_TIME_ELAPSED GPU timer. Measured ~2.1 ms/frame total CPU on the heaviest
levels here; the GPU timer is only meaningful when GPU-bound. Target-hardware
numbers needed before a low-end preset. Golden 3/3; Win + Linux builds.
2026-09-24 11:11:47 -04:00
James Dansereau 02c361c909 D337/D338: user feel check ('Seems good'); legacy AimStyle migration exercised live 2026-09-24 10:57:15 -04:00
James Dansereau 827ab54288 D338: Aim range option — PC (default, full-screen GEPD feel) or N64 (original 65% stick limits, camera turn from ~49%)
Scales the mouse accumulator's screen mapping and edge-scroll threshold; aim
entry adopts through the game's screen mapping so it never jumps. Measured:
PC 98%/72%, N64 65%/49%. Golden 3/3; Win + Linux builds.
2026-09-24 10:52:42 -04:00
James Dansereau 2d2f56b050 docs: GE_AIMLOG + scripted mouse/aim tokens in the probe catalogue (D337) 2026-09-24 10:40:26 -04:00
James Dansereau 1a33cc09b5 D337: fold aim modes (N64 via PD integration default, Centred (PC) opt-in); Q/L + Toggle aim routing; per-device mouse integration; scripted-mouse aim harness
- Input.AimMode 0=N64 (D332 PD-integration, default) / 1=Centred (PC); legacy
  AimStyle/PdMouseAim migrated; hidden Input.AimLegacyGepd fallback.
- Mouse aim routing follows RMB/Aim, Q/L (the game's second aim button), and
  insightaimmode (Toggle aim, pad): removes the accidental Q 'FPS aim'.
- The PD game hook only feeds the mouse while the mouse is the active aim
  device, so pad/keyboard stick aim is not swallowed.
- Harness: GE_INPUTSCRIPT MDX/MDY/RHOLD/LHOLD/RREL + GE_AIMLOG; measured old
  vs new: same screen range and edge-scroll, jitter equal headlessly.
Golden 3/3; Win + Linux builds.
2026-09-24 10:40:16 -04:00
James Dansereau d3b4b9cd0a D336: user-confirmed (Facility>Frigate, Runway>Frigate; 0 D156 in a 14-level session) 2026-09-24 10:12:03 -04:00
James Dansereau 91564f79b6 docs: porting-notes on dead placeholders after inlining a split struct (D336); probe catalogue 2026-09-24 09:58:09 -04:00
James Dansereau f73599f449 TEMP diagnostics: GE_D235_NEXT (load a different stage after abort) and D85TEX per-stage texture-pool size/range line 2026-09-24 09:57:55 -04:00
James Dansereau c5f0d1b741 D335a follow-up: reset the scissor after the pillarboxed front end so the squeezed scissor can't carry into the first stage frames 2026-09-24 09:57:55 -04:00
James Dansereau 38354e1c7e D336: fix Frigate 'everything breaks but the gun' — head-bob anim read uninitialised alias fields (A1, D100 follow-up)
bheadAdjustAnimation read player->field_5C0 / animFlipFlag, which on N64
are raw-offset aliases of the inline player.model (model.animframe1 /
model.gunhand). Since D100 made model a real inline struct on PC they were
separate, never-written fields: Bond's head-bob start frame came from
uninitialised player-struct bytes (2e32 etc.), poisoning his model anim
(D156) and wrecking the scene. The D294 2x room pool only moved the player
struct onto dirtier memory. Read/write the real model fields.

Found with gdb hardware watchpoints (field never written). Headless repro
Runway -> Frigate -> turn now clean, D156 0; golden 3/3; Win + Linux.
2026-09-24 09:57:55 -04:00
James Dansereau b74d2738fd backlog: D335b F10 overlay pillarbox (polish) 2026-09-23 15:40:54 -04:00
James Dansereau 8e07bced9d D334/D335: 16:9 smoke sweep over 16 more levels — all render, no crashes, no slabs 2026-09-23 15:40:09 -04:00
James Dansereau 89a9875542 README: drop the fixed gun-barrel widescreen note (D335a) 2026-09-23 15:29:57 -04:00
James Dansereau 9232ea180f D335: wrap end credits; fix D335a gun-barrel bar leak (full-canvas scissor under the pillarbox)
Verified at 16:9: -level_54 ending subtitles/cast roll keep their shape;
gun-barrel backdrop now clips at the 4:3 edge. Emitted only for windows
wider than 4:3. Win + Linux builds clean.
2026-09-23 15:29:48 -04:00
James Dansereau 66b33f7f45 D226: design refinement — HUD scale needs a fast3d span transform (boxes + text + icons), not per-glyph scaling 2026-09-23 15:00:38 -04:00
James Dansereau 91bd0f84b7 D237: F10 overlay category headers (DISPLAY/VIEW/MOUSE/CONTROLLER/GAME) + SkipIntro, Show FPS, pad invert/deadzone/trigger rows
New non-selectable ROW_HEADER kind: navigation, wheel and clicks step over
headers. Wheel scroll already existed. Port-only; Win + Linux builds clean;
overlay capture verified.
2026-09-23 14:59:37 -04:00
James Dansereau 7a541da877 plan: widescreen done (D334/D335) 2026-09-23 14:57:07 -04:00
James Dansereau ae1b39ff25 README: native widescreen replaces the stretched-4:3 known issue and roadmap entry (D334/D335) 2026-09-23 14:56:50 -04:00
James Dansereau f62bceaf88 D335: native widescreen, HUD + front end (Option B Phase 3)
HUD elements now keep 4:3 proportions via fast3d's EXT aspect modes (the PD
pattern): watch, gauge bars, messages/title card, upper text and countdown
CENTER; ammo RIGHT (right hand) / LEFT (left hand). The front end is
pillarboxed (whole menu_jump_constructor_handler CENTER) and the menu
pointer is mapped into the pillarbox. port/include/hudaspect.h emits only
when native widescreen is on and the window is wider than 4:3, so 4:3 and
native-off stay byte-identical. Rule-2 approved.

Verified with 16:9/21:9 captures (HUD, watch, title card, legal, file
select, gun barrel); golden 3/3; Win + Linux builds. Gun-barrel circle
leaks into the bars while sliding in -> GRAPHICS-BACKLOG D335a.
2026-09-23 14:56:29 -04:00
James Dansereau d5d9a36a6f D334: native widescreen, world side (Option B Phase 2) — Video.NativeWidescreen
Generalise GE's own N64 16:9 mode (aspect = vp_ratio*0.75*16/9, the TV
pre-squash) to the real window aspect: the port's canvas->window stretch is
that TV, so the world renders undistorted at any aspect (Hor+, vertical FOV
kept). All of GE's 16:9 special cases generalised under #ifdef PORT (camera
aspect NTSC+EU, crosshair sprite width, rocket viewmodel offset); Rule-2
approved. Phase-4 vertical FOV boost is skipped while native is on (would
double-count); room-pool aspect term kept. Default on; F10 row.

4:3 identity (golden 3/3); native 16:9 centre == 4:3 frame (diff 4.3 vs
12.4 stretched); 21:9 clean on Bunker/Facility/Dam; Win + Linux builds.
HUD still stretched (Phase 3 next).
2026-09-23 14:44:46 -04:00
James Dansereau 639a112b9b D333 (#104): Input.AimStyle — N64 / PD damp / centred FPS-style aim on RMB
One F10 selector ("RMB aim style") instead of stacking toggles. CENTRED
routes RMB-held mouse motion to hipDirectCompute (FOV-scaled) and emits no
aim stick, so the game's aim integrator keeps the crosshair centred: the
behaviour the reporter found on Q (game L button), now on RMB. PD DAMP is
D332 (PR #96); legacy Input.PdMouseAim=1 migrates to it. Default N64 =
unchanged. Port-only.

Windows + Linux builds clean; bunker1 golden 3/3. Feel check owed.
2026-09-23 14:34:24 -04:00
Dan Turner 05e5525db9 input: fix reticule jitter in aim mode — adopt the PD mouse-aim model (D332, PR #96)
Opt-in Input.PdMouseAim (F10 "PD mouse aim"): in aim mode the port only
accumulates the mouse position and the game's own crosshair integrator is
driven by it with PD's mouse damp (0.01), instead of GEPD's per-poll
overwrite fighting the weapon's ~0.8 per-tick damp (write*damp^k jitter).
Hook: #ifdef PORT block in sub_GAME_7F067FBC (aim-mode branch only),
identity when off; Rule-2 sign-off given by the maintainer.

Integrated locally from PR #96 with review fixes: finding renumbered
D337 -> D332; GE_PDMOUSEAIM lookup cached (GE_ENVSTR; it ran every poll
and aim tick); "live env" comments corrected. Windows build clean; bunker1
golden 3/3. Feel A/B owed.
2026-09-23 14:29:27 -04:00
James Dansereau 3a7c5d06c5 plan: fix D235 commit ref after amend 2026-09-23 13:34:24 -04:00
James Dansereau c728c42e81 golden: move bunker1 window to 640-1120:240 (frame 400 sat on the nondeterministic intro camera; 3 captures agree) 2026-09-23 13:32:20 -04:00
James Dansereau 971c2d9f62 D281: AllUnlocked on a fresh install now yields the game's own fresh-slot layout
D259 seeded only the volumes on all-zero slots. Because the AllUnlocked read
patch re-CRCs every slot, a fresh eep survived validation as five folder-1
slots with options 0: sight-on-screen off (no crosshair while aiming = the
"RMB aim broken" report) plus auto-aim/look-ahead/ammo off, and the game
reset/rebuilt the duplicates mid-validate. All-zero slots now become what
fileValidateSaves + fileBuildWriteNewSave would build (folder saves from
BLANKSAVEDATA via the game's own setters, leftovers free).

Verified: fresh eep + AllUnlocked + SAVELOG -> 00/21/42/63/80, all CRC OK,
no repair writes. Golden 3/3 (x2 reruns); Windows + Linux builds clean.
2026-09-23 13:27:28 -04:00
James Dansereau 27a560cd0b plan: reconcile v0.4.0 bucket states with the findings index (D75/D195/D197/D235/D288/D290/D294, getenv sweep) 2026-09-23 13:19:19 -04:00
James Dansereau d9d7cd3586 perf: cache getenv in hot-path probes (post-D302 sweep)
Swept all 178 getenv sites; 97 were uncached. The load-time/one-shot ones
stay as-is. The 25 on per-frame/per-node/per-string/audio-thread paths
(langGet, modelGetNodeRwData, modelRenderNodeGundl, skyRender and its poly
path, chrpropRender, frozen-camera tick, amDmaCallback, alRaw16Pull,
_decodeChunk, __CSPHandleMIDIMsg, sndHandleEvent, audioSetNextBuffer,
configGetInputLog/configGetFrameDump) now go through GE_ENVFLAG/GE_ENVSTR
(port/include/envflag.h): one getenv per site per process, exact since the
env is fixed. All sites are inside PORT probe blocks; no game logic touched.

Windows + Linux builds clean (only pre-existing, D324-audited warnings);
bunker1 golden 3/3; GE_INPUTLOG still gates on (12 lines with scripted input).
2026-09-23 13:18:32 -04:00
James Dansereau 94dff8116f D331: shell casings now render (D264 pattern, third site)
The casing draw read g_DefaultCasingModelRenderData (u32[15] {0,1,3,..})
as the wider PC ModelRenderData: flags landed on a 0, so subdraw() drew
nothing and the read overran the array. Use the explicit template.

Verified: PP7 casing ejects and tumbles on native Windows (none before);
bunker1 golden 3/3; Windows + Linux builds clean.
2026-09-23 13:13:14 -04:00
James Dansereau a5804d10b8 D290: fix missing controller model on the watch Control Options page (D264 pattern, second site)
watchRenderController built its ModelRenderData from a raw 64-byte read at
D_80035D04+0x3c, which on N64 is the {0,1,3,...} template spanning into
watchControllerButtonBases. On PC the struct is wider and the globals aren't
at those offsets, so flags read 0 and subdraw() drew nothing. Use the
explicit template, as D264 did for the item preview. Also fix the three
pointer-width/raw-Gfx sites on the same path that now execute: two u32
render_pos casts (fatal under Linux PIE), a (s32) pointer arg, and the
dual-controller branch's hand-encoded G_MTX word (now gSPMatrix).

Verified on native Windows (headless watch walk + capture); bunker1 golden
3/3; Windows and Linux builds clean. D291: headless check noted (needs play).
2026-09-23 13:07:31 -04:00
James Dansereau 30cf76b812 D288: re-check on current build — does not reproduce (dense Silo-intro capture + colour scan); D271 A/B ruled out as the fixer 2026-09-23 12:58:21 -04:00
James Dansereau 2a6bc2bb4a D235: headless reload verification; TEMP GE_D235_RELOAD / GE_D235_OFF harness
Scripted watch Abort on -level_09 -> title stage -> GE_D235_RELOAD re-enters
the level via bossSetLoadedStage() (the front end's mission-start call)
after a front-end dwell. With the fix: stale entries purged at every
transition; the reloaded level renders cleanly. A/B with GE_D235_OFF (reset
skipped): 588-788 stale entries survive yet all textures stay correct, for
both an identical and a divergent (intro skipped) first-load order. So the
reported trigger isn't captured headlessly on Bunker 1; the fix stays
correct by construction. D235 stays OPEN pending a real die -> mission
select -> restart playtest (A/B with GE_D235_OFF).

Both switches are TEMP, env-gated, inert when unset; bunker1 golden 3/3.
2026-09-23 11:29:34 -04:00
James Dansereau c1b5b6e519 D330 (#103): mouse no longer drives the stick inside the in-level watch
Auto-Aim / Look Ahead "wouldn't stay on": in the watch, current_menu is
still RUN_STAGE and hipDirectCompute declines on its !watch gate, so mouse
motion fell through to the legacy stick fallback (~3 counts/px/poll). A
~15 px flick crossed the watch's +/-0x2E threshold, which on a selected
option picks OFF/ON (game_option_toggle_input) and otherwise flips pages.
The mouse now contributes nothing while the watch is open or paused;
keyboard and pad drive the watch as before. Save/load round trip ruled out.

Build clean; bunker1 golden 3/3. Mouse feel-check owed (needs real motion).
2026-09-23 11:09:52 -04:00
James Dansereau f89311e9e3 golden: re-baseline bunker1 at 400-880:240 (post-D248 60 fps); defaults-only ini pin
The 2026-09-09 set was captured at 30 fps (2 sim ticks/frame). Since D248
the port renders at 60 fps (1 tick/frame), so frames 200/320/440 landed in
the intro cutscene (nondeterministic camera, D117) and the golden failed on
every build, with or without D329. Moved the window to 400-880:240 (the same
sim ticks the original set sampled, now in settled gameplay); two independent
native-Windows captures agree 3/3.

verify.sh's ini pin rewrote only Width/Height, which kept personal
FovScale/DrawDistance/MSAA settings and failed the gate. It now writes a
defaults-only 640x480 ini (and pins/removes one when none exists).
2026-09-23 11:03:21 -04:00
James Dansereau 7af146a1d0 D329: fix 60 fps guard anim pin (D318/D320 root cause) - tick-granularity divergence
The D318/D320 guard freeze is not a platform-invariant lock. modelTickAnim
folds numticks into one modelSetAnimFrame2WithChrStuff call, which only
advances framea/frameb when floor(frame) crosses an integer. At 1 tick/frame
(port at 60 fps) and attack speed 0.5-0.8, a pair left equal by the aim-hold
endframe clamp is re-emitted stale after the endframe is raised, so the
model pins forever. At >=2 ticks/frame (N64's normal regime) it cannot.

- model.c (#ifdef PORT, Rule-2 sign-off): when the loop did not run and the
  current endframe no longer blocks the next frame, re-derive frameb as
  modelSetAnimFrame does. GE_D329=0 disables, GE_D329LOG=1 logs triggers.
- Verified: lockstep clk=1 no longer pins (Facility ai_19, Control ai_9,
  WSL + native Windows, watchdog off); 0 triggers at clk=2; user playtest
  confirms N64-matching Ourumov timing. Bunker1 golden failure predates it
  (identical with the fix off).
- d318bprobe (TEMP, env-gated): attack-state probe + GE_D318B_CLK lockstep.
- d320repro: fix jump=<chr>,... parser (documented form never parsed).
- findings D329 + D318/D320/D321 corrections, porting-notes D13.
2026-09-23 09:53:10 -04:00
James Dansereau 35483f9892 plan: track mod support and HD texture packs as researched post-1.0 items
Both investigated 2026-09-22 against the MIT PD port fork; neither is v0.4.0
work. Recorded here rather than only in the gitignored notes backlog so the
branch carries the conclusions.

Mod support (D325-D328): community mods repack the ROM, so vanilla offsets are
useless; the fix is an offline importer plus a mod-directory override layer, not
a loader rewrite. GE's file table is in the ROM at 0x252c4 inside the
1172-compressed data segment at 0x21990 (681 exact matches vs filelist, 0
mismatches; hardcode-free discovery reads Random Eye's own table to 704/726
byte-identical). Remaining effort is the mod-dir override layer; added levels
stay blocked by rule 2.

HD texture packs (issue #100): PD's texpack architecture studied - identity, not
decoding, is the problem. Our gfx_pc.cpp is the same lineage and GE_TEXDUMP is
already most of a dumper. Step 1 (address -> texturenum registry) is the only
novel design decision and is a src/game touch needing #ifdef PORT.
2026-09-22 23:20:09 -04:00
James Dansereau 5910350e8e D328: mod support is viable — GE's file table is in the ROM; D327's 'ruled out' corrected
Reviewed the PD PC port fork at perfect-dark-dabs-mod-dabs-mod (MIT, same
license as this tree; AGENTS.md already names the PD port a standing reference)
and verified its approach against our own ROM.

Architectural flip: PD does not load a modded ROM. modimport.c states our exact
constraint ('the port cannot run the patched ROM - it runs decomp code and reads
the ROM at fixed offsets') and instead diffs the patched ROM against stock,
extracting what differs into a mod directory its loader already reads. Mod
support is an offline importer plus a file-override layer, not a loader rewrite.

geconvert.c parses a GoldenEye US ROM directly and gives the constants:
DATA_ROM 0x21990, DATA_VRAM 0x80020d90, FILES_AT 0x252c4, 12-byte records with
size by differencing adjacent addresses - exactly obInit.

Verified here: 1172 magic at 0x21990, raw-inflate yields a 247120-byte data
segment (the same inflate port/src/rzdecomp.c already wraps), table at 0x252c4
with 727 rows, and 681 exact offset+size matches against filelist.u.csv with 0
mismatches.

Then reimplemented discovery with NO hardcoded offsets (scan for 1172 in the
first 4 MB, inflate, scan for a >=500-row run counting from 0) and validated on
two real mods by content:
- Random Eye: its own table found at seg 0x219f0 / table 0x25304; 704/726 files
  byte-identical to vanilla at the extracted offsets, vs 0/803 at vanilla
  offsets in D326. The 22 remainder are fully accounted for: 2 real changes
  (UsetupcaveZ, UsetuptraZ), 1 NULLFILE differencing artifact, 19 zero-size
  unused bg slots.
- Goldfinger 64 (24 MB): table read correctly, 19/726 identical as expected of a
  total conversion.

D327's scans were run over uncompressed ROM bytes, so its mechanism guess was
right and its conclusion wrong; amended here (section heading, inference
paragraph, status line and index row). Also noted: PD's rompatch.c is an MIT
VCDIFF/BPS/IPS decoder, which removes the GPLv2 xdelta3 constraint D325 flagged.

Remaining work is the mod-directory override layer (we have no analogue of PD's
romfile.moddir); rule-2 limit for added levels unchanged. No code changed.
2026-09-22 23:15:50 -04:00
James Dansereau bbc32957e1 D325-D327: ROM-mod (xdelta) compatibility investigation + settled plan
Docs-only. Records the mod-compatibility investigation so it is not re-derived
when mod support is picked up. No code changed.

Measured two shipped community mods against a vanilla NTSC-U ROM:
- Goldfinger 64 v1.0 (12.8 MB xdelta): ROM 12 -> 24 MB, 797/803 vanilla
  filelist.u.csv slices differ.
- Random Eye v1.1 (286 KB xdelta): ROM size byte-identical, yet 96.5% of bytes
  and 803/803 slices differ; content preserved but shifted (+7216/+7392, one
  file -229888).
Both die at tools_pc/d43_emit.py:273 with a zlib error on a slice taken at a
hardcoded vanilla offset. Cause: the file table is compiled in (ob.c ->
file_resource_table.inc.c -> romassets_<region>.s) and the converters reuse the
same vanilla offsets. ROM size is not the constraint; ROM code is never
executed, so ASM hacks are inert regardless.

D325 - Goldfinger 64 probe + mod taxonomy.
D326 - Random Eye repacks too, so D325's hypothesised in-place tier is empty.
D327 - plan settled: reading the table from the ROM is ruled out by measurement
  (hw_address absent in all four encodings tested, 0 hits for the
  segment-relative form; two structural fileentry scans found nothing);
  content-addressed offset remapping with gap inference is the viable route,
  since obInit differencing proves files are contiguous.

Also recorded: GE maps its 0x7F000000 game segment directly from ROM,
uncompressed, uniquely signature-findable at ROM 0x34b30.

The 1964GEPD fork reviewed in D327 is GPL-2.0 against this MIT tree; no code
was copied or vendored, facts re-derived from our own ROM and decomp.
2026-09-22 22:51:21 -04:00
James Dansereau 97c7e38845 D320: resolve sweep via forced repro — ai_19/ai_9 pins confirmed, ai_12 clean; watchdog generalized (table-driven + uncapped repeat rescue); D318 N64-manifestation corrected (unverified; 1964 deliberate-timing A/B armed)
Also commits the prior session's carried-uncommitted set: D294 room-pool
fix (sqrt + 2x cap, user-verified state), D324 stdlib-shim prototypes,
D323 explicit-LOD census + tools_pc tooling, GRAPHICS-BACKLOG row.
texdump*/ probe artifacts gitignored.
2026-09-22 22:30:32 -04:00
James Dansereau 12490b95e6 plan: mark D195/D197 stale in front-end batch (both closed per findings) 2026-09-21 23:54:18 -04:00
James Dansereau eaa137d6a9 D306/D308: investigate z-fighting — depth precision == N64, real fighting confirmed; GE_ZF flicker probe in-tree 2026-09-21 23:53:39 -04:00
James Dansereau 2efbbef964 D303: investigate extra long muzzle flash — pipeline + data ruled out; GE_D303 capture probe in-tree 2026-09-21 23:41:05 -04:00
James Dansereau 61766e8aef D307: investigate CCTV facing-backwards — no port-side bug found; GE_D307 diagnostic in-tree
setupCctv orientation path traced end-to-end and every input verified
N64-faithful against the ROM for Bunker1: PcctvZ (not GcameraZ) switch
data bit-for-bit, boundpads match the pccg sidecar once the legitimate
load-time room-scale (pad->pos *= get_room_data_float2(), ~1.8542) is
applied, record config words incl. unkCC match; all matrix/atan2 helpers
are byte-identical src/game code. PC orientation values are what the N64
computes; if the symptom genuinely differs from N64 the remaining suspect
is fast3d's handling of the non-orthonormal lens matrix (unk84 row 0
scaled 0.1x). Blocked on user repro detail.

Also: plan/README/findings refreshed for M-197 (D75 fixed, user-confirmed)
and M-198 (D236 closed); D252 retired; findings index regenerated.
2026-09-21 23:26:18 -04:00
James Dansereau 0503d33564 D236: strip the investigation probes now that the finding is closed
D236 was root-caused and fixed in the previous commit, so its diagnostic
scaffolding comes out:

  GE_D236RM / GE_D236ORDER / GE_D236ALPHA / GE_D236ZFIX  gfx_sp_tri1
  GE_D236RAW                                             bg.c room load
  GE_D236BT                                              chr.c + model.c
  port_d236bt_dump()                                     host stack backtrace
  g_d236_last_vtx_seg / _raw                             G_VTX trackers

Also drops the <windows.h> include that only the backtrace dumper needed, and
rewords the one D288 comment that cross-referenced GE_D236RM. src/game/bg.c,
chr.c and model.c are back to their pre-probe state -- no game-code diff from
D236 remains. The pass-26 fix in gfx_lod_tile_offset is untouched.

Doc rows removed from GE-ENV-PROBES.md; gen_env_probes.py --check and
gen_findings_index.py --check both pass. Build clean. The Surface 1 spawn
capture is unchanged across the strip (settled frame 560 within threshold;
the intro-pan frames never match run-to-run, D117).
2026-09-21 22:53:04 -04:00
James Dansereau 425237f93f D236 (M-198): fix Surface 1/2 tree cards; root cause was a DETAIL tile binding
GE's texLoadFromGdl picks a handler per G_NOOP texture reference from w0 & 7.
TEXTURETYPE_LOD/MIPMAP (texHandleType0/Type2) issue ONE load block at TMEM 0,
so tiles 0..n are LOD levels of that one image. TEXTURETYPE_DETAIL
(texHandleType1, src/game/tex.c) issues TWO: the detail texture at TMEM 0 and
the real base image at TMEM texGetSizeInBytes(detail,0) on tiles 1+.

gfx_lod_tile_offset folded every G_TL_LOD draw to tile 0 (D107 -- correct for
the first two, wrong for DETAIL), so the port drew the detail texture and never
imported the base image at all. All four Surface 1 treeline cards
(tex1198-tex1201, RGBA5551 64x17 cut-outs) are DETAIL bindings sharing one
detail texture, tex2465: an opaque 32x32 IA8 noise tile. That is the same
texture D267/D276 measured at 1023/1024 texels opaque and called the mystery
"noise painter" -- hence 20+ passes chasing decode, alpha, mip and geometry
theories for what was a binding bug.

Fix (port layer only): under G_TL_LOD, if tile fi+1 differs from tile fi in
BOTH TMEM address and fmt/siz, sample tile 1. Asymmetric on purpose -- LOD
levels of one image always share a format, so a mip chain can never trip it,
and a detail pair that happened to share a format just keeps the old
behaviour. Config Video.DetailBaseTile, default on.

Verified: Surface 1 idle spawn goes from an opaque tiled noise wall to discrete
snow-covered pines with ground/snow unchanged; Surface 2 shows a discrete night
treeline; Depot renders clean; bunker1 golden framediff is bit-identical with
the fix on and off. Offline scan of every level's bg: DETAIL bindings exist
only in sevx (84), depo (39), sevb (4), sev (4), so every other level is
provably untouched. User play-test confirmed.

Also refuted and removed pass 25's fix direction: cut-out mip suppression
changed nothing, and its alpha census showed the wall is painted by a fully
opaque texture -- retiring the mip / 1-bit-alpha / XLU-smear branch. The
GE_D236P25 probe is gone with it.

tools_pc/gen_env_probes.py: stop counting a "GE_FOO[" log-line TAG in the doc
as a documented env var. GE_TEXDUMP_ADDR prints "GE_TEXA[...]", which is not
the name of any getenv(), so the check reported a permanent phantom GONE.

Generalised as docs/porting-notes.md D12; full trail in findings.md D236 pass
26. Carries the pending D75/M-197 sidecar-converter fix and the env-gated
D236/D75 probes, which the doc gates require to land alongside their rows.
2026-09-21 22:48:10 -04:00
James Dansereau 327e6fe1a7 D75 (M-196): refute slot-overwrite hypothesis; narrow logo bug to texture-modulate render path
New env-gated GE_D75D probe (gfx_pc.cpp) captures per-frame V/T token stream
+ per-triangle rejection/emission accounting for the front-end logo window.

Findings (bare front-end boot, logo = frames ~244+, nv=93/nt=1018):
- Slot-overwrite (M-195's leading hypothesis) REFUTED: the DL is streamed
  (V TTT...VTTT interleaved), max triangle vertex index = 15, so no
  cross-batch vertex clobbering can occur.
- Rejection gates are not the cause: of 1018 tris/frame, trivial-reject=0,
  backface-cull ~520 (normal half), ~500 reach GL, all on-screen (NDC in
  [-1,1]^2). Not the invisible flag; vertex colors [0,255]; depth normal.
- PPM analysis: logo window renders 0-33 content px/frame (the 'tiny
  fragment'), vs title quads at 8.36% coverage.
- Narrowed cause: combine mode 0x009ffe4f19ffe4f1 parses to
  rgb a=TEXEL0 c=SHADE (texture-modulate); gfx_generate_cc sets
  used_textures[0]=1, but the material still renders black-on-black. Next
  step: trace where the TEXEL0 sampler is lost between combiner build and
  draw (shader_id0 -> GL program / prg[tm] / texture upload).

Port-layer fast3d only; no rule-2 sign-off needed. Probes env-gated, inert
without GE_D75D (verified: 0 lines in a normal run).
2026-09-21 06:00:28 -04:00
James Dansereau d4440759cd Merge main into release/v0.4.0 (docs: D322 log + mission-count fix) 2026-09-21 05:26:42 -04:00
James Dansereau 5ea053667f docs: correct solo-mission count 21 -> 20 (level_54 'Cuba' is the ending-credits sequence, not a mission)
The campaign has 20 playable missions (SP_LEVEL_MAX // 20 in
bondconstants.h; mission_folder_setup_entries in front.c has exactly 20
entries). The old '21' counted -level_54 (LEVELID_CUBA), which the front
end jumps to after the final mission for the extended cast display +
credits scroll (do_extended_cast_display / bondviewRenderCredits, D129) -
it is not a selectable mission.

- README, docs/index.md, .github/release-notes.md,
  docs/security-and-fidelity-status.md: 21 -> 20 in current-status claims
- AGENTS.md phase status: same correction
- docs/dev/LEVEL-STATUS.md + LEVEL-PLAYTEST.md: relabel the level list;
  counting note explains historical '21/21 PASS' sweep results = 21 stage
  boots (20 missions + ending sequence)
- social-preview card: stat line 21 -> 20, PNG regenerated (1280x640)
2026-09-21 05:23:11 -04:00
James Dansereau aebcf178e6 D75 (M-195): GE_D75V per-batch vertex probe + findings/probes doc updates
- gfx_sp_vertex probe (env GE_D75V=lo-hi): logs per G_VTX batch the
  transformed x/y/z/w min/max, raw first-vertex ob, dest_index, and the
  MP translation row. Proves the logo transform is correct and that every
  batch encodes v0=0 (slot-overwrite hypothesis).
- videoGetFrameCount() accessor for frame-gated probes.
- findings.md: M-195 entry in the D75 section (emission + transform
  eliminated; G_VTX v0=0 overwrite is the leading suspect).
- GE-ENV-PROBES.md: register GE_D75V, bump reconciled line.
2026-09-21 05:14:57 -04:00
James Dansereau bb508e5ce4 D235: wire videoResetTextureCache into per-stage reset (stale GL texture cache); D311: arm speed-poison choke-point probes
D235 (wrong textures after dying/restarting a level) -- mechanism found
+ fix (M-193). M-108's refutation of the game-side CI-cache theory stands,
but the port has its own TextureCacheMap (port/fast3d/gfx_pc.cpp) keyed by
(source address, palette addrs, fmt, siz, size_bytes, palette_hash) with NO
content check. Stage-texture source addresses live in MEMPOOL_STAGE, which
the per-stage while(!done) block in boss.c resets on every transition
(death/restart included). After the reset, entries keyed into that arena are
stale; if the texpool places a different image at an address that held
another one in the previous run of the level, the lookup hits and returns
the old pixels -- 'wrong textures' only on the reload path, never on a cold
boot. The N64 has no such cache (RSP re-reads DRAM every frame), so this is
a pure port-side defect: videoResetTextureCache() (port/src/video.c) existed
with zero callers and is now called in that block after memaReset
(#ifdef PORT). Re-uploads are pixel-identical; cost is one-time during the
loading screen. GE_D235=1 logs the purged entry count per transition
(cold boot = 0, death/restart > 0) as mechanism evidence; new
gfx_texture_cache_count() accessor backs it. Full 21-level golden sweep:
21/21 PASS (pixel-identical). Live death/restart confirmation is a playtest
item.

D311 (upstream poison source, M-194): every write of model->speed funnels
through modelSetAnimSpeed (startframe<=0 path) or modelSetAnimation2 -- both
now log 'D311: ... ANOMALY' when the incoming value or the resident speed is
non-finite/|v|>1e4 (#ifdef PORT, unconditional-on-fire like the D156
guards; silent in healthy play). The line captures in/resident/startframe/
oldspeed/newspeed/timespeed + model ptr so the next Caverns occurrence pins
the injecting call. Static pass: sub_GAME_7F06FE90 callers all pass
arg2=16.0f (denominator safe); the old->new lerp only circulates values;
bheadAdjustAnimation is the one path where a live player-velocity
corruption reaches model->speed.
2026-09-20 23:46:43 -04:00
James Dansereau 8d3bd5732a D320/D288: extend GE_D318T with generic pin detector; refine GE_D288 probe; cache bg.c hot-path getenvs
D320 (Phase 1 item 1): d318TimelineTick (GE_D318T=1) now also runs a
generic D318-class pin DETECTOR for the sweep's flagged lists (Facility
ai_19, Control ai_9, Depot ai_12): any chr in ACT_ATTACK with a live-fire
attacktype (no TARGET_AIM_ONLY bit) whose modelGetAnimFrame is byte-stable
for 600 ticks logs one 'D320T: PIN?' line with AI-list id/offset/atk/ent/
mot/unk54, repeating every further 600 stable ticks. Detection only -- the
recovery watchdog stays hardcoded to ai_22 by design (D320's explicit
instruction: generalize only after a second list actually freezes in play).
Legit states excluded/self-clearing: monologue pre-aims keep the AIM_ONLY
bit; working fire cycles re-init through chrlvTickAttackCommon so their
frame does not stay byte-stable for 10 s. Smoke: 90 s Facility run, zero
false positives, existing D318T timeline unaffected.

D288 (Phase 1 item 3): per M-155's revised next step, the GE_D288 probe
now skips any_behind_camera==1 triangles (the x/w divide is meaningless for
w<=0 -- that subset was pure false-lead noise) and logs the render-mode
word (oml) so a live capture can ID the culprit asset against level tables.
Re-ran bare -level_20: 1965 sane hits in the 550-755 window, still zero
hits for the target color RGB(83,62,44) -- consistent with M-155's working
theory that the bare boot's scripted camera never reaches the bug state;
headless probing is at its limit here, live capture is next.

bg.c getenv caching (D302-class sweep follow-up): GE_D104 (per room pass,
per frame), GE_D63 primary+secondary render logs (per room, per frame), and
the two GE_D154 inner-GDL-walk sites (per bullet hit) now use the cached
static pattern. All #ifdef PORT blocks; zero behavior change when unset.
2026-09-20 23:27:22 -04:00
James Dansereau 56ac2ab870 Phase 0 hygiene: remove GE_OBJT probes (D321 closed; M-192 item 7)
The D321 stage-flag writer probe (chrSetStageFlags/chrUnsetStageFlags in
chraction.c, GAS_RELEASING destruction timestamp in propobj.c) did its job
(D321 trigger chain mapped + closed); all three blocks were #ifdef PORT
diagnostics with no N64-build or runtime effect when unset. Dropped the
GE-ENV-PROBES.md row too (drift check passes).

getenv hot-path sweep (D302 follow-up, HANDOFF M-192): function-level scan
of all 171 GE_* getenv sites found no uncached calls at frame/triangle rate
-- the only candidates are snd.c's event-rate probe guards (all #ifdef PORT)
and bossReturnTitleStage (once per title return). fast3d has zero raw
'if (getenv(' sites.
2026-09-20 23:10:57 -04:00
James Dansereau 7bb43260de D322: log issue #87 long-session audio degradation + ship GE_D322 pool-telemetry probe
User report (v0.3.0 full campaign): audio progressively degrades from Silo;
by Caverns/Cradle the OST is inaudible and SFX come and go; restarting the
game restores it. Logged as D322 with static triage: level teardown audit
clean, mixer stateless per frame, D202's covered leak class is one shape
only (ownerless count==-1 loops) validated single-level only, and D305
already records a pool/list accounting desync. Ranked hypotheses:
voice-pool exhaustion/counter drift > queue starvation > evtq saturation.

Probe: GE_D322=1 prints sfx=n/8 (SFX soft cap), evtq=n/64, pv free/lame/alloc
(24 physical synth voices) + pipeline depth/drops every 5 s of wall clock,
read with GE_D204. sndD322PoolSummary() walks the ALSynth lists under the
D285 lock; NB drvr is a pointer field (first build faulted on &drvr).
Smoke-tested 40 s headless -level_09: pools coherent, no crash.

Also adds the missing D321 row to the findings index (entry existed at file
tail with no table row, so it was absent from the CSV too).
2026-09-20 23:10:57 -04:00
James Dansereau 2d9e673129 D322: log issue #87 long-session audio degradation + ship GE_D322 pool-telemetry probe
User report (v0.3.0 full campaign): audio progressively degrades from Silo;
by Caverns/Cradle the OST is inaudible and SFX come and go; restarting the
game restores it. Logged as D322 with static triage: level teardown audit
clean, mixer stateless per frame, D202's covered leak class is one shape
only (ownerless count==-1 loops) validated single-level only, and D305
already records a pool/list accounting desync. Ranked hypotheses:
voice-pool exhaustion/counter drift > queue starvation > evtq saturation.

Probe: GE_D322=1 prints sfx=n/8 (SFX soft cap), evtq=n/64, pv free/lame/alloc
(24 physical synth voices) + pipeline depth/drops every 5 s of wall clock,
read with GE_D204. sndD322PoolSummary() walks the ALSynth lists under the
D285 lock; NB drvr is a pointer field (first build faulted on &drvr).
Smoke-tested 40 s headless -level_09: pools coherent, no crash.

Also adds the missing D321 row to the findings index (entry existed at file
tail with no table row, so it was absent from the CSV too).
2026-09-20 23:09:03 -04:00
James Dansereau fbc48899ec social-preview: bump card to v0.3.0 (status line; stats refreshed to 199 findings, ~24k C/C++, ~7k Python) + regenerate PNG 2026-09-20 18:07:09 -04:00
James Dansereau 470c239502 docs(agentic-development): velocity chart daily -> single weekly-totals chart (6 bars) 2026-09-20 18:00:19 -04:00
James Dansereau c8309307b9 docs(agentic-development): simplify commits-velocity chart to weekly totals (6 bars) - daily x-axis labels overlapped in narrow renderers 2026-09-20 18:00:09 -04:00
James Dansereau 77ffbbdd64 docs(agentic-development): handoff-workflow flowchart LR -> TD so GitHub doesn't scale the wide diagram down to illegible size 2026-09-20 17:55:54 -04:00
James Dansereau 5d23d2bdfe docs(agentic-development): split commits-per-day chart into August/September halves - 32 x-axis labels overlapped in the rendered mermaid 2026-09-20 17:55:21 -04:00
James Dansereau 4f403ed6d5 docs: deep v0.3.0 accuracy pass — README Background stats (36 days/16 Aug-20 Sep, ~835 commits, D1-D321/199 findings); agentic-dev timeframe 'August' -> 'August-September', gantt gains the 9/18-9/20 final fix sweep + release review bar, findings count 311 -> 321 2026-09-20 17:52:59 -04:00
James Dansereau e153028e07 docs(agentic-development): v0.3.0 release date 9/18 -> 9/20; refresh milestone stats (36 days, ~835 commits, D1-D321/199 entries, M-192, velocity chart through 9/20) 2026-09-20 17:37:25 -04:00
James Dansereau 4827bc868a release-notes/readme: cutscene fix covers all cutscenes (not just Dam); drop never-shipped locked-camera bullet; merge hipfire + F10 sensitivity into one mouse/input bullet 2026-09-20 17:35:16 -04:00
James Dansereau 92dbd015b5 Register GE_OBJT in the env-probe index (CI drift check) 2026-09-20 17:24:16 -04:00
James Dansereau 75e5902717 D321: map the D318 trigger chain (tanks -> chr 254 gas cascade -> combat bit, ~3.5s authored delay; derail same-tick; PC faithful to N64) + GE_OBJT probe extensions (AI-list ID, GAS prop timestamps) 2026-09-20 17:21:24 -04:00
James Dansereau b3b0ff74db v0.3.0 release review: watchdog re-arm, rule-2 sign-offs, GPL source removal, repo hygiene
Per the 2026-09-19 adversarial pre-ship review (findings.md M-192):

- d318watchdog.c: one-shot per OCCURRENCE, not per process -- re-arm after
  600 ticks of signature absence so a Facility replay in the same session
  (AllUnlocked) is still covered.
- model.c: RULE-2-SIGNOFF records for the retained M-183/M-185 and D311
  defensive clamps (user-approved retention as the stability-maximizing
  option; stale 'Threshold: 50.0' comment corrected to 10.0f; both marked
  post-release removal candidates pending full cutscene re-verification).
- reference/mouse-injector/: removed from the public repo (GPLv2 source +
  prebuilt discord-rpc.lib in an MIT project; never compiled here).
  Provenance stub README remains; path gitignored for local re-vendoring.
  AGENTS.md / GEPD-INPUT-PLAN.md / optionsoverlay.c refs updated.
- scratch/: untracked + gitignored (19 session logs/probe sources were
  committed in the release window).
- findings-index.csv: D317 (and 5 other stale rows) now CLOSED -- added
  CLOSED to gen_findings_index.py's verdict vocabulary.
- docs: D310 perf gauge (no change warranted), GE_DEBUG_UNLOCKALL noted in
  security-and-fidelity-status.md, release notes gain the missing D318
  watchdog bullet + FOV-clamp clarification.

Build verified: ./build-pc.sh ntsc-final clean; gen_findings_index.py
--check and gen_env_probes.py --check pass.
2026-09-20 10:51:12 -04:00
James Dansereau 63315030f8 docs: v0.3.0 release prep — final review pass
- GE-ENV-PROBES.md reconciled (18 new vars documented, GE_D243X3 tombstoned
  after the M-170 permanent fix removed it) — CI gen_env_probes.py --check passes
- release notes gif alt text: ~32 s -> ~12 s (matches README/index captions)
- agentic-development.md stats regenerated for the month-one milestone
- docs/index.md status bumped to v0.3.0, honest-status synced with README
- .gitignore: local/ working-junk dir
2026-09-20 10:09:33 -04:00
James Dansereau 448444ed38 D320 second pass: re-verify mechanism + high-risk rows against source; settle AI command-ID mapping
Review of the D320 entry itself. All mechanism claims confirmed against
source (chrHasStoppedOrPatroling ACT_ATTACK exclusion; update fn only moves
endframe via chrlvAttackActionRelated, never modelSetAnimation; ai_9/ai_12
structures re-read in full). Two precision corrections:
- ai_19 row: the ONLY progression out of the label 0x34 loop is own-flag
  bit 0x01 -> list 0x1504; the stop-check path re-aims at 0x08 (loops).
  Nothing in the list sets that bit (level's only chr_bitfield_set_on(,0x01)
  targets chr 0x43).
- ai_12 row: the update lands mid-hold by construction (aim cmd jumps to it
  on success); the 180-range-flag wait between is unreachable on this path.

Also settled the AI command-ID mapping for future reference: bondaicommands.h
IDs == cmdbuilder enum values from aicommands.def order (naive CMDNAME count
is off by 2; cross-checked on 4 known commands). if_guard_bitfield_is_set_on
= 0x96 = AI_IFMyFlags2Has (executing guard's OWN flags2); the per-chr family
(chr_bitfield_*) is 0x97-0x99.

Also audited d318watchdog.c in this pass: chraiGetAIListID and
sub_GAME_7F025560 signatures match; call form mirrors chraction.c:7396;
dead c78 can never satisfy the signature (ACT_ATTACK vs ACT_DIE/ACT_DEAD
disjoint); g_randomSeed is u64 as written. No changes needed.
2026-09-19 09:01:01 -04:00
James Dansereau cc169874ce D318: port-side watchdog + timeline probe for the Facility ai_22 softlock; D320: D318-class sweep of all level AI scripts; gfx_pc GE_D116 printf format fix
- D318 (RESOLVED): as-authored latent race — gas bit mid-monologue derails
  c78's pre-aim hold into a permanent ACT_ATTACK anim pin
  (actor_fire_or_aim_at_target_update moves only the endframe, never
  re-inits; N64-identical). NOT a port bug. Port-side watchdog
  port/src/d318watchdog.c (hooked in lv.c after g_GlobalTimer += ) detects
  the pinned state and re-inits the attack on confirm; GE_D318T=1 timeline
  diagnostic included. random.c gains an env-gated probe (GE_RANDPROBE) for
  bit-exact PRNG verification. Scratch evidence (dis/seqcmp scripts, addr
  dumps, session handoff) parked per house practice.
- D320 (OPEN): swept every level's AI scripts for the same aim-hold ->
  _update shape: 17 lists in 8 levels. Facility ai_19 and Control ai_9 are
  HIGH (non-self-healing, same as ai_22); Depot ai_12 MED-HIGH (objective
  bit gated behind a stop-check). Bond-combat loops likely safe (sustained
  fire stays inside the shoot window by design; exercised every playtest).
  Classified as-authored latent race(s), faithfully reproduced. Probe-verify
  path + generalize-the-watchdog recommendation documented in findings.md.
- gfx_pc.cpp: GE_D116 debug printf used %f for uint32_t args and %d for
  unsigned tile fields; casts added, build warning-free.

Full ntsc-final configure+compile+link PASS. Accuracy sweep write-up in
docs/dev/ACCURACY-AUDIT-2026-09-20.md (PRNG bit-exact; A1/shift32/wrapv/
PORT-hygiene/Gfx-Vtx/osVirtualToPhysical sweeps all clean or already fixed).
2026-09-19 00:38:14 -04:00
James Dansereau 276d0124dc D312/D313: fix bgBuildRoomVtxBounds G_VTX vertex-count misread on PC (Gfx_le dma.par bit shift) — n=16 batches collapsed to single-point bounds, failing the per-batch AABB pretest in bgTestBulletHitBackground; projectiles passed through those walls and bullets showed no impacts. One-line #ifdef PORT fix (recover params byte from words.w0 bits 16-23); N64 path untouched. User-repro verified (Runaway: mines collide, bullet impacts land on wall plane). 2026-09-18 18:12:14 -04:00
James Dansereau 340d6a595f D316/D317: fix F10 overlay mouse click mapping + row hit-band; D319: FpsCap 30/60 toggle
D316: optionsOverlayHandleInput() mapped clicks with a naive window-size
scale that ignored the safe-area crop, and overlayRowAtY()'s hit-band was
asymmetric (biased downward) -- together these could land a click on the
row below the one clicked. Fixed by adding gfx_get_ui_screen_rect() (reuses
the real forward transform from gfx_adjust_viewport_or_scissor) to invert
clicks correctly, and centering the row hit-band.

D317 (reported as a crash toggling Game.AllUnlocked): confirmed instant
close, not a hang -- closed as a symptom of D316 (the hit-band bias let a
click on "All unlocked" fall through to the adjacent "Quit to desktop" row).

D314 (F10 flicker on main menu): mechanism still unpinned; landed an
env-gated diagnostic probe (GE_D314=1) for a future repro run. Confirmed
still present after the D316 fix (different bug class) -- deprioritized
per user call.

D315 (menu scaling main-menu vs in-game): disclosed as a known issue,
deferred to v0.3.1.

D319: Video.FpsCap's free 0-360 slider was mostly a dead zone (D186: sim
tick ceiling caps it at 60/50, and values under 30 already got force-
uncapped) -- replaced with a plain 30/60 cycle (ROW_FPSCAP, mirrors the
existing ROW_MSAA pattern); default changed to 60 so a fresh config shows
a reachable value. Uncapped stays valid at the config level for old inis
but is no longer offered in the menu.
2026-09-18 16:47:48 -04:00
James Dansereau 69cc9790ac README: credit f1zz1ec0ke for LLM consultation (v0.3.0) 2026-09-18 13:27:32 -04:00
James Dansereau ce05ae7187 Park D309/D310/D311 session evidence logs + referenced diagnostics in scratch/
- Probe logs preserved as evidence: pre-fix standing-still guard lines,
  v1 Trevelyan regression (local-list embedded PRINT strings), clean
  post-fix Caverns run (global PRINTs size=1/global=1 hit once each incl.
  the D309 spin record; local PRINTs NUL-scanned with valid landings; zero
  LABEL-NOT-FOUND / D156 / watchdog lines), D311 freeze watchdog+stack dump.
- D309/D310 diagnostic files referenced by findings.md: frozen thread
  stacks, live m_RunToBondPersistent byte dump, AI-list decoders (original
  NUL-scan + fixed global-only sizing), command name table.
2026-09-18 13:22:50 -04:00
James Dansereau c67479866c D310 v2: scope PRINT sizing fix to global AI lists; D311: close D156 guard hole
D310 (RULE-2-SIGNOFF, user-confirmed fixed): the uniform 1-byte AI_PRINT
size broke level-local lists, which embed a NUL-terminated string after
the PRINT byte (Caverns Trevelyan ran circles with glitched anims).
chraiitemsize is back to byte-identical decomp; new PORT-only
d310ItemSize() applies the 1-byte size only for lists confirmed global
via chraiGetAIListID, used at ai()'s AI_PRINT step and chraiGoToLabel's
label scan. Local-list behavior unchanged from the original.

D311 (new, guarded): live freeze in Caverns diagnosed via GDB — Bond's
own anim tick spun ~3.1e9 iterations in modelSetAnimFrame2WithChrStuff:
animframe1 poisoned to -1.67e31 upstream, and the D156 guard validated
only frameb, so 'frameb = framea' passed both garbage values through and
floorFloatToInt saturated them to far-apart ints. Extend the guard to
validate both framea/frameb (snap bad one to the valid other, else 0)
and heal model->animframe1/2 before the fallback re-reads them. Same
file/pattern as the accepted D156 guards, #ifdef PORT only. Upstream
poison source (oldspeed=-1.75e18 at transition start; timespeed=0,
unkb0=0) recorded open in the D311 entry.

Post-fix Caverns playtest: guards dash-pause-resume, Trevelyan pathing
normal, Caverns->intro clean. Probe log: global PRINTs size=1/global=1
(hit once each), local PRINTs NUL-scanned/valid landings, zero
LABEL-NOT-FOUND, zero D156 fires, zero watchdog freezes.
2026-09-18 13:18:16 -04:00
James Dansereau 0c700852e2 D310 (RULE-2-SIGNOFF): size AI_PRINT records at their true 1-byte width in chraiitemsize (#ifdef PORT) — fixes D309 spin + Caverns standing-still guards 2026-09-18 12:55:06 -04:00
James Dansereau 0f8bd90aaf D309: extend GE_D309 probe to AI_PRINT step-ons and label-not-found restarts (diagnosis only) 2026-09-18 12:43:48 -04:00
James Dansereau 8be34ba1f6 D309: add GE_D309 diagnostic probe at chrGoToBond failure branches (diagnosis only, #ifdef PORT) 2026-09-18 12:31:46 -04:00
James Dansereau ede16415f2 docs: D309 diagnosed — Caverns->intro complete freeze, phantom GotoNext(lblZero) via AI_PRINT NUL-scan mis-size 2026-09-18 12:25:33 -04:00
James Dansereau 3085775d52 Merge branch 'main' into release/v0.3.0
# Conflicts:
#	.github/release-notes.md
#	README.md
#	docs/dev/findings-index.csv
#	docs/dev/findings.md
#	src/snd.c
2026-09-18 10:55:16 -04:00
James Dansereau 444b172cb8 docs: v0.3.0 release prep — D251 closed, security page published, notes cleanup
- release-notes: drop the internal 'carried forward' meta paragraph; move the
  D251 bottom-row duplication out of Known Issues into the F10 fix bullet
  (no longer occurring per user live playtest of the D304 build)
- findings: close D251's last symptom; mark D304 live F10 playtest done;
  de-stale D266/D271 'uncommitted' cells (landed in 532092c7); regen index CSV
- README: link the security & fidelity status page
- docs/security-and-fidelity-status.md: publish — re-verified for v0.3.0 on
  2026-09-18 (bundle contents, no-networking source check, asset converter);
  D271 now landed; D284 RNG history reworded as found-and-fixed pre-v0.3.0

Verified: build-pc.sh ntsc-final clean (243/243); tools_pc/verify.sh sweep
21/21 PASS on this tree (scratch/verify_sweep_v030_final.json).
2026-09-18 10:54:00 -04:00
James Dansereau 416396e504 D302/D304/D225/D164/D252/D305: perf regression, F10 menu overhaul, small fixes pass
- D302: fix a reintroduced D250-class perf regression -- 4 uncached getenv()
  calls added by D236 diagnostic passes 10/12/14/16 were running on every
  triangle draw in gfx_sp_tri1; cached with the established pattern.
- D304: F10 mouse-sensitivity menu overhaul -- widened Input.AimModeSens'
  clamp to match Input.MouseTurnSpeed, then collapsed both plus the
  aim/turn link toggle into a single Input.MouseSensitivity control per
  user request (avoids decoupling into a confusing state). Fixed two real
  overlay bugs found along the way: overlayUpdateScroll() bottom-anchored
  the clicked row on every click (not just when off-screen), and
  overlayRowAtY()'s hit-test scanned rows past the last one actually drawn,
  letting a click near a scroll boundary silently hit the wrong row
  (reproduced as "All unlocked" triggering "Quit to desktop" instead).
- D225: GE_DEBUG_UNLOCKALL=1 dev-tooling toggle (file2.c), default off.
- D164/D76: docs-only correction -- the legal-screen one-line fix was
  already committed 8dfc578f4; findings.md just never caught up.
- D252: root-caused and fixed a 6+-session-old rainbow-particle bug --
  explosionRenderPart() wrote 5 vertices into a 4-vertex allocation,
  corrupting the next particle/effect's data in the shared vertex pool.
  User-confirmed live: "Rainbow went away."
- D305: Steam Deck SIGSEGV recurrence past D285's own fix -- sndHandleEvent's
  preempt-scan dereferenced its list-walk pointer before checking it for
  NULL. Added the missing guard. User-verified crash-free on the Deck
  across multiple sessions, including toggling Game.AllUnlocked off/on.

D236 pass-16 diagnostic (GE_D236RAW, bg.c) carried along from a prior
session's in-progress investigation; unchanged, env-gated, zero-cost unset.
2026-09-18 09:56:23 -04:00
James Dansereau e7c76cf8eb D236 pass 13: live capture demotes draw-order theory, redirects to tree-card fog
User played live with GE_D236ORDER=1, walked toward Surface1's tree line;
wall bug still reproduced. Tree-card count reached 121 tris (vs 2-4 in
deterministic boot), and draw order flips favorably at high counts (tree
drawn last, as pass 12's theory needed) -- but the bug persisted anyway in
that same zone, demoting draw-order as the dominant cause.

Redirects to a lead D280 flagged but never closed: the tree class's own
fog computation (bimodal 25/255 per vertex, never verified against N64,
unlike the noise class's fog which D280 already confirmed correct). Not
attempted this pass -- next session's starting point.

Not v0.3.0-blocking, parked again per standing user priority.
2026-09-18 02:36:49 -04:00
James Dansereau db823d0261 D243 M-189/M-190: fix Bond cutscene root cause, camera tracking, mouse-lock; D236 pass 12 diagnostics
D243/D173/D292 (Bond cutscene family) -- root cause found and fixed:
bondview2.c's cutscene body-model buffer layout reserved space using a
hardcoded 32-bit sizeof(Model) literal that never accounted for 64-bit
pointer widening (real size 232 vs reserved ~188 bytes), so the model's
own animation-data buffer overlapped its tail fields (playspeed/animrate/
unkac/unkb0/unkb4), corrupting them on every cutscene body-model creation.
Fixed by computing the real sizeof(Model), matching the identical idiom
already used one line above for ModelFileHeader. User-confirmed live.

Follow-up camera-tracking fix: replaced the old shake-workaround freeze
(which also blocked legitimate per-shot tracking) with a write-every-tick
scheme plus an instant re-seed of the look-at filter's accumulator at each
teleport, eliminating the old catch-up shake without freezing anything.

Second bug found in the same review: mouse-look was never gated during
scripted/no-control camera states (an already-documented but never
implemented safety gate in port/src/input.c's hipDirectCompute). Fixed by
adding the missing check, reusing gameScriptedCameraActive() (renamed from
D243's d243mCutsceneActive, since it's no longer D243-specific -- also
used by model.c's M-183/M-185 clamps, which were separately found to be
dead code for real players (gated on the diagnostic env var, not just
camera mode) and fixed to be unconditionally active.

D236 (Surface tree wall) pass 12: new mechanism found (both competing
texture classes are Z_UPD=0 decal-mode draws, so on-screen precedence is
decided by draw order, not depth) and a headless capture finally landed on
the actual bugged view -- but that view has zero tree-class triangles
present, so the mechanism couldn't be tested there. Two new env-gated
diagnostic probes (GE_D236ORDER, GE_D236ZFIX) left in tree for the next
pass; not a fix, not v0.3.0-blocking.

All findings.md summary-table rows touched this session cross-checked
against their full entries and corrected where stale (D219/D252/D246/D247
had been fixed in a prior session but their index rows still said
OPEN/NOT-A-BUG).
2026-09-18 02:30:17 -04:00
James Dansereau 643785c2c5 Widescreen FOV Phase 4, D246/D247 overscan crop, D219 rainbow-spark fix
- Widescreen/FOV Phase 4: automatic vertical-FOV scale by window aspect
  ratio (Video.WidescreenAuto, default on), composed before the existing
  manual Video.FovScale multiplier. New symmetric FOV floor (20 deg)
  alongside the existing 160 deg ceiling.
- D247/D246: the game's own TV-overscan safe-area viewport inset (real on
  N64, hidden by CRT overscan, visible as black bars on PC with no crop)
  now gets cropped instead of shown -- gfx_adjust_viewport_or_scissor
  remaps against the actual captured gameplay viewport bounds (vertical)
  plus a measured, resolution-independent 1-logical-unit trim (horizontal)
  instead of the fixed VI canvas size. New Video.SafeAreaCrop toggle,
  default on.
- D219 (rainbow bullet-spark colors), a real 32->64-bit ABI bug:
  bullet_spark_render (glass2.c) read the spark's stored RGBA color via a
  hardcoded raw byte offset instead of the named s_bullet_spark struct
  field -- on 64-bit PC an earlier void* field's widening + alignment
  padding shifts the true offset by 4 bytes, so the read landed in
  unrelated f32 velocity fields instead. Fixed by reading the named
  fields. Also fixed an adjacent UB (a 16-byte Vtx read from a 4-byte
  global) in the same function. This turned out to be the actual root
  cause behind the D252 "lingering particle sprites after an explosion"
  report as well (same render path via objHit and friends) -- closed as
  a duplicate.
- Moved docs/dev/WIDESCREEN-FOV-PLAN.md to docs/dev/notes/ (gitignored
  local notes), matching this project's convention of keeping
  in-progress planning docs out of the tracked tree.

User live-verified all of the above interactively this session.
2026-09-18 01:10:27 -04:00
James Dansereau 814ecf5fce D243 M-183/M-185: playspeed/endframe clamp workarounds for Bond animation issue
Added two #ifdef PORT workarounds in modelTickAnim() to address D243's
Bond position/animation issue during scripted camera modes:

M-183: Clamp playspeed > 10.0 to 1.0 (prevents rapid advancement ~97 frames/tick)
M-185: Clamp corrupted endframe values (< 0.0 or > 1000.0) to 100.0

Both are env-gated on GE_D243M and apply only during scripted camera modes.
The clamp fixes prevent the rapid animation advancement but Bond's animation
still 'repeats' — he can't complete his animation during the Dam abseil cutscene.

Root cause analysis: playspeed/endframe corruption is consistent with the
D100/D140 struct layout/punning family (32→64-bit pointer widening causes
fields to be read from wrong memory locations). Playspeed observed at 388.82
(expected ~1.0), endframe as huge negative values like -604462909807314587353088.0.

Updated findings.md §F table entry for D243 to reflect partial fix status.
2026-09-17 23:10:59 -04:00
James Dansereau 4ccab22ce2 D243: ship permanent fix for Dam abseil camera shake (M-170)
Convert the GE_D243X3 experiment into an always-on fix by removing
the env-gating. The fix freezes field_488.pos (the camera's look-at
target derived from the model's animated-skeleton transform) during
POSEND camera mode, re-baselining only when a legitimate shot-change
teleport fires (detected via the d243TeleportEpoch counter incremented
by d243NotifyTeleport() in chrai.c's AI_TRYTeleportingChrToPad handler).

This eliminates the camera shake while allowing legitimate inter-shot
repositioning. The fix is decomp-faithful - it targets the camera's
reaction to the animation cycle rather than suppressing Bond's own
animation, which was found to be playing correctly.

Also includes:
- GE_DEBUGMENU diagnostic in boss.c (prior session)
- GE_D243SWIRL probe in bondview2.c (prior session)
- M-178 finding documentation (rapid animation advancement analysis)
- D243 biganim probe in model.c (for future investigation)
2026-09-17 22:38:52 -04:00
James Dansereau 6938d5644d docs: add vendored GEPD Mouse Injector reference + input-overhaul plan
Both were already documented as standing repo content (AGENTS.md's
critical-files table, several findings.md entries citing D194/D300's
ground truth) but never actually committed -- add them so the repo
matches what's already written about it. GPLv2, read-only reference,
never compiled as part of this build (see reference/mouse-injector/README.md
for provenance).
2026-09-17 19:31:33 -04:00
James Dansereau ee4ea96258 docs(D300): correct stale status -- user already live-confirmed hipfire fix
findings.md and the index still said "pending manual playtest" / "pending
build-verify", but HANDOFF.md records the user played it live this session
and confirmed better feel. Regenerated the index via gen_findings_index.py.
2026-09-17 19:30:38 -04:00
James Dansereau 2fe0e1c83e fix(D301): NULL-guard fileGetIsCheatUnlocked() against unvalidated saves (GitHub #87)
Closes the unguarded save deref at its single choke point instead of
patching each call site (front.c mode-select, file.c briefing check).
Build-verified, crash-free smoke test; live confirmation against the
exact #87 repro still owed (see finding for detail).
2026-09-17 19:30:05 -04:00
James Dansereau 52a3e9d938 D173/D243: add CONSOLIDATED NEXT STEPS block for D173 (M-87..M-176); correct a self-introduced level-34 doubt
D173 gets the same "read this one block, not the whole history" treatment
D243 already has -- established facts, what's ruled out, and the concrete
next step (a D252-style raw ROM byte dump of the extending_left_hand clip
at frame 95) in one place.

Also retracts an unfounded doubt raised earlier this session about whether
-level_34 is really Facility -- a hand-count of setup_text_pointers[] alone
suggested otherwise, but multiple already-established findings in this same
file (D130, D203, D217, D255, and a full independent static decode of
UsetuparkZ.c during the Trevelyan/Ourumov investigation) confirm it is;
"ark" is just Facility's internal decomp codename, not Park/Archives.
Checked before letting the doubt stand.
2026-09-17 16:20:28 -04:00
James Dansereau f6c3cfe421 D173 M-176: user's live N64 comparison confirms genuine divergence, narrows to an asset-decode bug
User directly compared this port's level-start sequence against real N64
from memory: N64 shows Bond grounded, arms at sides, for 1-2s before he
draws his gun and surveils left-right (~0.25-0.5s later) as the camera
moves to first-person. This port shows Bond floating from the very first
frame his model appears -- never briefly grounded.

Traced the second phase too: chrlvTickBondIntro (chraction.c:7663, also
decomp-verbatim) is a watchdog that switches Bond from the held
"extending_left_hand" pose to fire_standing_draw_one_handed_weapon_fast
once the first pose wraps -- confirmed firing live. The two-phase structure
matches the user's description exactly; only the first phase's visual
(grounded vs floating) diverges.

Checked one more candidate: c_item_entries[BODY_Formal_Wear].pov = 1.058,
a small plausible-looking scale constant, not an obvious corruption --
doesn't distinguish good data from bad on its own.

With theory (b) (faithful N64 behavior) now ruled out by direct comparison,
and the selection/scale code confirmed clean, this narrows to bucket (a):
the extending_left_hand clip's own keyframe data being mis-decoded for this
port around frame 95. Next step is a D252-style raw ROM byte dump, not more
source tracing.
2026-09-17 16:17:30 -04:00
James Dansereau ca01cb9ba9 D173 M-174/M-175: root-caused the intro puppet's animation to a decomp-verbatim table entry
M-174: tried firing_animation_groups[group][sub] as the source of the
puppet's initial animation -- zero hits, ruled out (negative result logged
so it isn't re-guessed next time).

M-175: instrumented modelSetAnimation itself (the one choke point every
caller funnels through) with a __builtin_return_address probe filtered to
the player's own bodyModel, then resolved the logged addresses with
addr2line. Found the exact call site on the first try: bondviewSetCameraMode
(bondview2.c:936-942), inside CAMERAMODE_SWIRL's intro-fly-in setup. It
reads stage_intro_anim_table[] (bondview.c:355), a decomp ground-truth
table with zero #ifdef PORT in the whole call chain -- entry [0] is
{PTR_ANIM_extending_left_hand, 95.0, -1.0, 0.02}, and PTR_ANIM_extending_
left_hand is confirmed index 61 in animation_table_ptrs1[], an exact match
to the aidx=61/animframe~96 measured live on both Dam and Facility in M-172.

Conclusion: the selection logic is fully decomp-verbatim, not a port bug.
If the puppet genuinely floats while playing this held pose, the remaining
candidates are narrowed to exactly two: (a) this specific clip's keyframe
data is mis-decoded for this port at/around frame 95 (asset-pipeline bug,
D252-style raw-byte dump would confirm), or (b) this is real N64 behavior
and something else (camera framing, or just how it always looked) is the
actual source of the "floating" reports. Distinguishing the two needs N64
reference footage or a raw asset dump, not more source tracing -- this
thread is static-exhausted for this session. Captures archived at
scratch/d173/ (gitignored, local).
2026-09-17 15:48:59 -04:00
James Dansereau 1b48577367 D173: correct M-173's write-up before it goes stale -- chrTick is shared, not a separate player draw path
Caught this while writing the next-step paragraph, before committing to a
wrong claim: the render-position computation (subcalcmatrices, chr.c:2777)
runs inside chrTick, which is called identically for the player's own prop
and every guard -- there is no separate "player draw path" for it to skip a
step in. The real divergence is which INPUT feeds that shared machinery:
bondview2.c's firing_animation_groups[group][sub] table, reached only by
the player's own per-tick animation-select call, versus guards' own
AI-driven animation selection. Retargets the next step at comparing that
specific table entry's keyframe data against a comparable guard-idle entry,
rather than a code-path diff that doesn't actually exist.
2026-09-17 14:52:11 -04:00
James Dansereau 9b75d75934 D173 M-173: control experiment confirms bug is isolated to player-puppet draw path, not generic chr rendering
Added a before/after prop->pos probe spanning all of chrTick (not just the
player), gated on the existing d243mProbeActive(). Checked four background
guards on Dam with different actiontypes/animations/positions: all show
~zero before/after gap, nothing like the player puppet's ~328-unit float
measured in M-172. Rules out "every chr's root joint sits above prop->pos by
design, normal rendering just hides it" -- normal chr rendering is correctly
grounded. Narrows the remaining lead to the player-specific bodyModel draw
call (bondview2.c, near the existing D243M: draw/f488write probes) versus
the generic chrRenderProp path every other chr uses. Diagnosis only, no
game-code changes. Capture archived at scratch/d173/ (gitignored, local).
2026-09-17 14:51:28 -04:00
James Dansereau 447ef22162 D173 M-172 cont'd: cross-level replication (Facility) + ruled out two more candidates
Facility (-level_34) reproduces an almost identical ~329-unit float vs Dam's
~328, despite completely different ground heights/positions -- strong
evidence of a fixed constant rather than a level- or animation-scale-driven
effect. Dispatched a local Qwen triage pass on solo_char_load; it raised
modelSetAnimTranslationScale/pov as a candidate but correctly self-qualified
it as inconsistent with the now-cross-level-confirmed near-constant offset.
Own follow-up trace ruled out two more candidates: the puppet's position
setup matches the normal guard init path exactly (setsuboffset after
init_GUARDdata_with_set_values), and the puppet's first animation's nonzero
start frame (~96) is a legitimate firing_animation_groups table constant,
not carried-over garbage state. Leading theory now: the ~328-unit elevation
is baked into that animation's own keyframe data (root/pelvis joint height),
and whatever normally compensates for a chr's third-person render staying
grounded isn't reached by this puppet's draw path. Next step (compare a
normal guard's raw root-joint gap the same way) is static/no-live-access,
logged in findings.md.
2026-09-17 14:47:49 -04:00
James Dansereau 6f604c14cf D243 M-170/M-171 + D173 M-172: playanim probe refutes empty-driver theory; first quantified D173 measurement
D243: added a GE_D243M-gated probe on AI_PlayAnimation. Live data shows the
abseil's scripted fall animation (decoded anim_id 0xb1) lands on Bond's own
real player chr/model (isPlayerChr=1), directly refuting the standing M-145
"empty, model-less driver chr" theory. The animation plays and loops
correctly (aidx stable, animframe cycling normally) -- this reframes the
whole bug: the render-position "cycling" this session's GE_D243X3/X4 freeze
experiments were built to suppress is very likely the animation's own
legitimate looping root motion, not corruption. GE_D243X4 (Bond's own
render_pos freeze) is flagged as actively wrong and should not ship;
GE_D243X3 (camera field_488.pos) is on firmer ground but should ideally
damp on loop-wrap, not freeze-forever. Next step (camera-shake vs loop-wrap
correlation) needs a fresh unfiltered capture, logged as open.

D173: used the same probes to capture the intro/pre-control puppet cutscene
autonomously (no player input needed to progress it) on Dam. First-ever
quantified measurement: prop->pos.y is rock-steady at the correct ground
level (60.3) for the puppet's whole on-screen lifetime, while its rendered
root sits ~324-328 units higher throughout -- and the gap barely moves
across three different attached animations, arguing against an
animation-root-motion cause and pointing at a fixed root-joint/base-transform
offset in the puppet's spawn setup (solo_char_load) instead. Measurement
only, no code change; capture archived at scratch/d173/ (gitignored, local).
2026-09-17 14:42:48 -04:00
James Dansereau 8ba1be14f2 D243 M-169: real teleport signal wired in, root mechanism confirmed decomp-faithful
Implements next-steps (b) and (c) from the D243 CONSOLIDATED NEXT STEPS
block. (c): static analysis shows the render_pos-cycling mechanism (chrTick's
cutscene-unaware animation dispatch) is unmodified decomp code with no gate
on either platform, so it's decomp-faithful by construction -- clears the
way to ship a port-layer fix without a rule-2 sign-off. (b): chrai.c's
AI_TRYTeleportingChrToPad now notifies a new epoch counter right after its
real setsuboffset() call; GE_D243X3/GE_D243X4 re-baseline on that signal
instead of freezing forever, targeting the M-168 "camera looks wrong way for
later shots" / "Bond still looks stuck" incompleteness.

Build/link/crash-safety verified headlessly (two 25-30s -level_09 smoke
runs, both crash-free). Live Dam-to-abseil playtest still owed before this
can replace the diagnostic gating with a shipped default.
2026-09-17 13:59:47 -04:00
James Dansereau b69cdbe6af D243/D148: consolidated next-steps synthesis for the cutscene bug family
Adds a single "read this, not the whole M-107..M-168 saga" block to the
D243 findings.md entry: what's established (two independent render
paths, the stale-animation mechanism, the wrong-chr animation
targeting, camera shake fixed diagnostically, model duplication fix
incomplete because it breaks legitimate per-shot repositioning) and
three concrete, not-yet-attempted next steps (delta-threshold
re-baseline, hook AI_TRYTeleportingChrToPad directly, determine the
actual N64-vs-PC divergence point before committing to either).

GRAPHICS-BACKLOG.md's D148 row (last meaningfully updated ~M-87,
long stale) rewritten to match -- it was the only other place a future
session would look for cutscene-bug status and didn't reflect any of
this session's work.
2026-09-17 13:46:10 -04:00
James Dansereau d2caaa09bc D243 M-168: render_pos freeze -- 2 crashes fixed, stable but incomplete
Extended M-167's freeze technique to render_pos (Bond's own draw
position), aiming at the model-duplication symptom M-167 didn't fix.
Two live crashes along the way, both from the same root cause: model
pointers get reused by the allocator across different logical model
instances, so any attempt to skip subcalcmatrices() based on inspecting
render_pos's value or caching pointer identity can be fooled into
treating a brand-new, uninitialized instance as already-validated --
leaving render_pos at its fresh tombstone value for a later consumer
(sub_GAME_7F06C768, computing zDepth) to crash on.

Fixed by never skipping subcalcmatrices() at all -- it always runs now,
guaranteeing render_pos is never left invalid regardless of model
lifecycle. The freeze is purely a contents copy on top of a
now-guaranteed-valid buffer. Live-playtest stable (no crash, full
session).

But the result is informative-negative: freezing unconditionally also
suppresses the LEGITIMATE per-shot camera/model repositioning
(AI_TRYTeleportingChrToPad moves Bond between several distinct shots
during the cutscene), not just the illegitimate intra-shot cycling --
so the camera now looks the wrong direction for later shots and Bond
still appears stuck. Both GE_D243X3/GE_D243X4 remain diagnostic
experiments, not real fixes. Documented two next-step candidates
(delta-threshold re-baseline, or hooking the teleport call directly)
for distinguishing legitimate repositioning from the cycling bug.

porting-notes.md gets the generalized lesson: freezing a value and
freezing a pointer target are different operations -- the latter needs
the call that keeps the pointer valid to always run, with the freeze
applied to contents afterward, not to whether the call happens.
2026-09-17 13:43:08 -04:00
James Dansereau 53219c18ae D243 M-167: camera shake mechanism confirmed live -- first positive result
M-163's live playtest smoothed prop->pos completely (log-verified) but
the visible camera shake was unchanged -- the camera's look-at filter
doesn't read prop->pos at all. It reads field_488.pos, a separate
variable computed from render_pos (the model's own animated-skeleton
transform), independent of prop->pos even though both happened to show
the same period-4 cycling symptom through a probe that only logged one
of them.

Retargeted the suppression at field_488.pos's actual write site
(bondview2.c:10495-10497, GE_D243X3-gated, freezes it while POSEND is
active). User's live playtest: "yep camera shake is gone now" -- the
first positive result across the whole D243 investigation.

Not a real fix -- GE_D243X3 unconditionally freezes state rather than
correcting why the model's skeleton animation cycles in the first
place. User also reports Bond's own rendered body still visibly
duplicates/glitches, expected since this only freezes the camera-feed
copy, not the render_pos draw-path read -- likely the same underlying
mechanism observed through a different consumer, not yet isolated.

porting-notes.md D11 gets the generalized lesson: confirm the actual
consumer reads the field you fixed before spending a live-test cycle
on it -- two fields can show the same symptom from one shared root
cause while being genuinely independent data paths.
2026-09-17 13:06:57 -04:00
James Dansereau e4a870484b D173/D292: cross-reference D243's chrUpdateAnim mechanism finding
The Dam abseil cutscene investigation (D243 M-160/M-164/M-165) traced a
general pattern likely shared by the whole "third-person world model
transform/lifecycle" bug family already flagged at M-145: chrUpdateAnim
reads a puppet's animated root-joint position back into its logical
position, so a transition that moves the puppet without also updating
its animation state produces a position that visibly lags/diverges,
tracking the stale animation instead of the intended spot. Plausible
explanation for D173's level-start float and D292's death-sink, not
confirmed for either -- flagged as the thing to test with a live capture
using the same aidx=/animframe= technique D243 M-166 just added, rather
than re-deriving a new mechanism from scratch next time either is
investigated.
2026-09-17 12:50:42 -04:00
James Dansereau d9c7705654 D243 M-162..M-166: corrected decisive experiment + deep root-cause tracing
Two third-party reviews caught a real flaw in the earlier M-161 negative
experiment: it restored prop->pos AFTER chrTick() returned, but
render_pos/field_488.pos are computed INSIDE that same call -- so M-161's
"no change" result never actually tested M-160's overwrite hypothesis.
Verified this directly against the capture data (a clean period-4
same-frame before/after-chrTick offset proves the lane-advance happens
entirely inside chrTick) before accepting the review.

M-162: corrected save/restore placement (chr.c), Z-only. Live playtest
confirmed the mechanism -- Z went smooth in the log -- but the visible
glitch persisted because that capture's chr cycled on Y instead.

M-163: widened the experiment to the whole prop->pos vector. Built and
smoke-tested; not yet live-playtested (exe was busy/user stepped away) --
this is the ready-to-test build for the next session.

M-164/M-165/M-166 (static tracing, no build needed): chrUpdateAnim reads
the model's root-joint position back into prop->pos after advancing
whatever animation is attached (getsuboffset) -- not an arbitrary
overwrite. The cutscene's teleport (AI_TRYTeleportingChrToPad) re-anchors
the root joint but never touches animation state; the real rappel
animation commands target a separate invisible driver chr (M-145), not
the visible one. Traced the Dam exit script (UsetupdamZ.c) directly and
found no animation-set command for Bond anywhere in it -- his head
animation gets forced back to an idle/walk loop by
bondviewFrozenMoveBond's own tick, but the *phase* within that loop is
inherited, not reset. This raises a real possibility that the mechanism
is decomp-faithful and the actual PC/N64 divergence is upstream of the
cutscene code entirely (which animation phase Bond happens to be on when
the cutscene grabs him). Extended D243M: onscreen with aidx=/animframe=
(reusing GE_D193A's reverse-resolve technique) so the next live capture
can test this directly.

Docs updated throughout (findings.md, findings-index.csv,
GE-ENV-PROBES.md); HANDOFF.md refreshed with full session state (local,
gitignored).
2026-09-17 12:49:19 -04:00
James Dansereau fec396f53b D300: hipfire direct camera look (WI-1); D243 M-159/160/161 investigation
D300: new hipDirectCompute() in port/src/input.c writes vv_theta/vv_verta
directly for hipfire mouse-look, mirroring GEPD's linear px->degree model
and the existing D194 aim-mode pattern -- replaces the N64 stick-curve
routing that saturated at ~13 px/poll (issue #89). Ported GEPD's
dead/watch/pause safety gates. Input.MouseDirectLook=1 default, escape
hatch to the legacy path. User-confirmed better feel in a live playtest.

D243: added a permanent onscreen-visibility probe (D243M: onscreen,
chr.c) to test whether chrTick's posIsOnScreen gate explains the
abseil cutscene's frozen render position (M-159). A live capture
refuted that and found the real mechanism instead: chrTick overwrites
prop->pos with a stale value via its own chrUpdateAnim/modelTickAnim
animation-position logic, racing the AI script's scripted write
(M-160). A decisive throwaway experiment neutralizing that overwrite
produced no visible change in a live playtest (M-161) -- the mechanism
is real but isn't the dominant visible cause. Experiment reverted;
D243 parked pending a new lead after ~15 session-passes without a fix.

Docs: findings.md/findings-index.csv/porting-notes.md/GE-ENV-PROBES.md
updated throughout.
2026-09-17 10:56:41 -04:00
James Dansereau 7b405abc63 fix(D299): Game.SkipIntro crash selecting most save slots
fileValidateSaves() -- the only place saves[] is populated from EEPROM
-- is called exclusively from the legal screen's init, which
Game.SkipIntro's boot-to-file-select shortcut skips entirely. saves[]
was left all-zero, so fileGetSaveForFoldernum() returned NULL for
every folder except whichever one a zeroed save_data happened to
decode to, crashing file-select's unguarded fileGetIsCheatUnlocked()
for the rest. Call fileValidateSaves() from the shortcut too
(port-layer completeness fix, zero game-logic change).

Live-verified by user: all 4 save slots now selectable with
SkipIntro=1, intro still correctly skipped.
2026-09-17 09:43:44 -04:00
James Dansereau 85a165d74b porting-notes: findings-index.csv is generator-derived — edit the §F table row, not the CSV 2026-09-17 08:50:48 -04:00
James Dansereau f70284b5be D219/D252 M-158: fix §F table status cells (index CSV is generator-derived — update the source row, not the CSV) 2026-09-17 08:50:33 -04:00
James Dansereau 30dcbea085 D219/D252 M-158: rainbow bug not reproducible in 6 identical dev-box runs; classified flaky/environment-dependent; probes left live for next occurrence 2026-09-16 23:15:45 -04:00
James Dansereau 6d34a880b3 D219 M-158: GE_D157 gains D157T (texture-identity-keyed vertex full-path) + D157I (import census) probes 2026-09-16 23:15:45 -04:00
James Dansereau 145d3f46ac D219 M-157 correction: measured G_LIGHTING for glass2.c sparks, theory doesn't hold up
Went back and actually tested the G_LIGHTING hypothesis from the
previous commit instead of leaving it as an untested code-reading
inference. Two things prompted this: (1) noticed a code comment near
the existing GE_D172 probe documenting a live result never written up
in findings.md -- "G_LIGHTING is ruled out (5000/5000 samples off)" for
explosion.c's particles specifically, meaning the sibling case this
session's theory leaned on was already empirically disproven, not
merely unconfirmed; (2) that's exactly the kind of unchecked assumption
this session's own rule-2 taxonomy work was written to prevent.

Added GE_D157 (gfx_pc.cpp, gfx_sp_vertex, env-gated, cached getenv):
logs geometry_mode/LIGHTING + resulting shaded RGB whenever a vertex's
raw color bytes match one of glass2.c's g_BulletSparkColors[] entries.
Re-ran the identical Bunker1 repro that produced the magenta/green/cyan
screenshots -- only 2 hits, both LIGHTING=off, both unaffected white,
no hit at all for the pale-yellow value the door-spark path should
produce. Reading: G_LIGHTING likely isn't the mechanism here either
(consistent with the sibling case), though the probe may simply not be
catching the real spark vertices yet (exact-byte-match is fragile).

Corrected the D219 finding to flag the "cross-validates M-110" framing
from the previous commit as superseded, not delete it -- the reasoning
trail (including the mistake) stays visible so a future session doesn't
repeat it. Root cause of the visually-confirmed rainbow bug remains
open; next steps revised to a texture-identity-keyed probe variant.

Added GE_D157 to GE-ENV-PROBES.md. No src/game changes. findings-index
regen was a no-op.
2026-09-16 22:25:39 -04:00
James Dansereau c58797c2da D219 M-157: first visual capture of the rainbow-particle bug, cross-validates M-110's G_LIGHTING lead
Chasing D252 further, got the first-ever screenshot proof of this bug
family: a plain bullet-impact spark on a Bunker1 door cycling magenta ->
purple -> green -> teal -> purple across consecutive shots, captured
headlessly via GE_INPUTSCRIPT (Z fire pulses) + GE_PCDUMP together, then
reading the resulting PPMs directly (PIL) instead of only grepping a
probe log.

Traced the call chain (glass2.c's bullet_spark_render, a different code
path from both D219's explosion.c particles and D252's IMAGE_FIRE_N
system) and ruled out three mechanisms with actual code evidence before
finding one that fits:
- Not D252's fire-tile system (GE_D252 got zero hits on this repro).
- Not a struct-layout/ABI bug (sImageTableEntry is 12 bytes on both
  32- and 64-bit, no pointer members -- checked before assuming, per
  this session's own rule-2 bucket-(a) discipline).
- Not the primitive color (g_BulletSparkColors[1], a hardcoded literal
  for world-prop hits -- always pale yellow).
- The texture itself (explosion_smokeimages, IA8, verified via the
  asset table) has no color channels and decodes correctly.

That leaves M-110's G_LIGHTING-not-cleared mechanism (already proposed,
unconfirmed, for explosion.c) as the one that fits: bullet_spark_render
never clears G_LIGHTING either (only ORs in G_CULL_BACK), so if lighting
is on, fast3d reinterprets the vertex color bytes as a lighting normal
and computes a scene-light SHADE color instead -- independently
converging on the same mechanism at a second call site, for a visually
distinct symptom (bullet sparks vs explosions). Flagged honestly as
unresolved bucket (b) vs (c): bullet_spark_render's missing lighting
clear is decomp-verified N64 ground truth, so a src/game fix needs the
port-layer alternative ruled out first, per docs/dev-process.md's own
new taxonomy -- not assumed just because the mechanism fits.

Also documented the headless-visual-capture technique itself
(GE_INPUTSCRIPT + GE_PCDUMP together, then actually opening the PPMs)
in porting-notes.md as a reusable method -- several other long-open
visual findings (D290/D291/D294) have been stalled purely for lack of
eyes on the pixels, not for lack of a working probe.

No src/game or port/ changes -- diagnosis only, screenshots are
session-local (scratch/, gitignored). findings-index.csv regen was a
no-op.
2026-09-16 22:20:49 -04:00
James Dansereau 1e1041c6ba D252: note GE_D172 re-run on the same repro found nothing (no combiner data) 2026-09-16 18:18:05 -04:00
James Dansereau 46b72b76fa D252 M-156: GE_D252 probe finally fired, decode confirmed correct by hand
Reused the D219/M-90 scripted-fire technique (GE_INPUTSCRIPT Z pulses) on
a bare -level_33 boot to get bullet-impact sparks headlessly -- got 14
distinct fire-stage texture dumps (scratch/d252_raw_*.bin, gitignored).
Hand-decoded all 14 as RGBA5551 big-endian and averaged the opaque
texels per stage: a perfect, smooth white-hot -> yellow -> orange ->
deep-red -> fade-out progression, zero off-hue content anywhere. This
independently reconfirms M-141's width/height/byte-order conclusion and
goes further -- it's the first time this investigation actually looked
at the decoded numeric pixel values rather than reasoning about the
decode formula, and it rules out the source asset data and the decoder
entirely. The bug is confirmed downstream: GL upload, the texture
cache, or the G_CC_INTERFERENCE combiner.

Also checked and refuted M-141's texture-cache-collision lead: all 14
captured stages have distinct texture_addr values, so TextureCacheKey
(gfx_pc.h) can't be colliding between stages the way that theory
required. Recorded a concrete next step (correlate a live GE_PCDUMP
rainbow frame against which of the now-known-correct 14 stages was
bound) for the next session.

No src/game or port/ changes -- diagnosis only. findings-index.csv
regen was a no-op (no new Dxx label).
2026-09-16 18:16:33 -04:00
James Dansereau d5fd08f40c D288: frame-gate the GE_D288 probe, re-run, still inconclusive
Gated GE_D288 to num_dls 550-755 (the reported bug window, same pattern
GE_D236RM already uses) and raised the bbox threshold from 0.15 to 0.5.
Volume dropped 116k -> 28,883, but that's still not the isolated signal
-- traced the remaining noise to a bad assumption in the probe itself:
bbox_frac from a raw x/w divide is meaningless once any_behind_camera=1
(w<0 or near-zero), which is exactly why the surrounding D233 code already
defers to GL's own clipper for that case rather than trusting the divide.
Restricting to any_behind_camera=0 (2,128 hits) still found no match for
the target color (83,62,44); the persistent hits across the window look
like static level geometry, not the transient bug triangle.

Working theory: the bare -level_20 boot's scripted intro camera may not
reach the same state the original visual bug report came from -- the same
"deterministic boot flag != deterministic camera path" pattern this
session's D236 passes independently hit on a different level. Recorded a
revised next-step list in findings.md; a live human repro is now more
promising than further headless probing of this specific approach.

No src/game changes. findings-index.csv regen was a no-op.
2026-09-16 18:10:43 -04:00
James Dansereau 08865bedeb D288 M-155: ran the GE_D288 probe, result inconclusive -- needs frame-gating
Ran the M-152 next-step probe (GE_D288=1 GE_PCDUMP=560-745:5, backgrounded,
25s bare -level_20 boot). Two problems surfaced with the probe itself, not
the underlying bug: it fires 116k times in ~1500-2000 frames (the
bbox_frac>0.15 threshold catches ordinary near-camera geometry throughout
the whole run, not just the reported ~562-741 window), and the target
color signature (83,62,44) never appears in the log. No GE_PCDUMP PPM
output was produced either, so no visual cross-check was available.
Confidence unchanged from M-152; this is a probe-refinement finding, not
a root-cause finding. Recorded the concrete next step (frame-gate GE_D288
on num_dls the way GE_D236RM already does) in the D288 entry.

Also caught and documented a real tooling gotcha: a backgrounded
ge007.x86_64.exe launch from this Bash tool doesn't reliably die to
`pkill -f` (process shows as bare `ge007.x86_64` in ps, no .exe suffix) --
a stray instance ran ~90s past its intended capture window before being
caught via `taskkill //F //IM ge007.x86_64.exe`. Added to porting-notes.md
so the next session uses taskkill from the start.

findings-index.csv regen was a no-op (no new Dxx label).
2026-09-16 18:07:47 -04:00
James Dansereau 7e81fb5e55 D236 pass 11: GE_INPUTSCRIPT nav attempt, no working path found, one new data point
Tried to keep the -level_36 deterministic boot's camera in the outdoor
room past its scripted indoor transition, to re-run GE_D236RM facing the
tree line directly (the concrete next step pass 10 named). Three
GE_INPUTSCRIPT variants (sustained SDOWN, sustained SUP, a START-skip +
delayed SUP) each changed the pre-transition room (curRoom 7/12/13) but
none held the camera outdoor past the transition, which fires at roughly
the same real time regardless of direction and earlier when START skips
faster -- timer/event-driven, not position-driven.

One new data point: the START-skip run's very first two GE_D236RM
samples hit count=10 then 6 (vs pass 10's flat max of 4) before
collapsing back to 2 then 0 within ~100 draw-list calls. Still far short
of D280's ~720 free-run figure -- best current explanation is that
D280's original number came from a genuinely different, later, closer
vantage than anything reachable in this boot's opening seconds, not a
CPU-side instantiation bug at this specific vantage.

No src/game or port/ file touched (GE_D236RM unmodified from pass 10).
Added the "opening cutscene isn't fully input-inert, but its transition
may still be time-gated not position-gated" quirk to porting-notes.md.
findings-index.csv regen was a no-op (no new Dxx label).
2026-09-16 18:04:08 -04:00
James Dansereau b73742ca2c fix: cache the GE_AUDIOTRACE getenv() check in alEvtqPostEvent (D250)
The D202/M-71 [EVTQ-DROP] diagnostic probe (gated at 56d2dfd6 after
being found unconditional) called getenv() uncached on every dropped
event post -- a real per-item hot path during audio event-pool
exhaustion, unlike the sibling GE_AUDIOTRACE probes in
synthesizer.c/csplayer.c/load.c which only run at one-shot init/load
call sites. Cached with the standard static-int pattern (D250, M-120),
matching the dozens of existing GE_* probe checks already using it.

Not yet re-linked -- build-pc is occupied by a concurrent investigation
this session; verify with a clean build + /linkcheck before the next
push.
2026-09-16 17:59:45 -04:00
James Dansereau 23da815dda D236 pass 10: refute "nondeterministic instantiation", it's room occlusion
Per the rule-2-clarification pass-10 brief in findings.md, applied the new
three-bucket taxonomy to D280's leading theory that the 0x0c184b50
discrete-tree-card render-mode class instantiates inconsistently across
runs of the deterministic -level_36 boot (~4 tris vs ~720).

Added a small env-gated diagnostic (GE_D236RM, port/fast3d/gfx_pc.cpp
gfx_sp_tri1) counting that class's per-frame triangle count, heartbeat-
logged every 300 frames even at zero. Three independent -level_36
launches (one with sustained GE_INPUTSCRIPT camera rotation) produced
byte-identical per-frame counts, falsifying the "run-to-run dependent"
premise outright. Cross-referencing with the existing GE_D104 room-census
probe in the same run shows the count drops to zero at the exact frame
the player's curRoom transitions from 12 (17-20 rooms drawn, an outdoor
vista) to 18 (1-5 rooms, an enclosed indoor space) via the boot's own
scripted intro camera — ordinary portal/room occlusion, not a bug. None
of buckets (a)/(b)/(c) apply; no src/game or port/ fix made or warranted.

The original "wall not discrete sprites" symptom is untouched by this
pass — narrowed the real next step to getting a camera into room 12
facing the tree line directly, in docs/dev/findings.md.

Added the generalisable "deterministic boot flag != deterministic camera
path" quirk to porting-notes.md §E. findings-index.csv regenerated.
2026-09-16 17:58:01 -04:00
James Dansereau 1c60da6f46 docs: clarify rule #2's diagnostic scope + define rule-2 sign-off process
Widens the written scope of AGENTS.md non-negotiable #2's ABI exception to
match established practice (runtime struct/union misreads, not just
ROM-serialized structs — the D209/D210/D255 pattern), states explicitly
that diagnosis into src/game is never restricted (only the fix is), and
defines a real three-bucket taxonomy + sign-off procedure (dev-process.md
§7) for the rare genuine-behavior-difference case, replacing the
undefined ad hoc "ask the user" gesture that's been referenced without
being defined across ~10 places in the docs.

Mirrors the same framing into CONTRIBUTING.md and the PR template for
external contributors. Extends porting-notes.md §A1 with D255 as a third
worked example and a tell-tale-signs checklist, explicitly distinguishing
it from D284 (same epistemic shape, different mechanism, no rule-2
question at all). Appends a ready-to-dispatch pass-10 brief to the D236
entry applying the new taxonomy to its actual next step.

Docs-only; no code/build changes. Per docs/dev/notes/RULE-2-CLARIFICATION-PLAN.md.
2026-09-16 17:45:58 -04:00
James Dansereau deac53f892 docs: D243 M-154 fix 2 (second probe crash + final deref-free tick path) 2026-09-16 17:09:36 -04:00
James Dansereau 14fc657412 D243 M-154 fix 2: tick probe never dereferences render_pos (second abseil crash)
The NULL/-1 sentinel guard from 85aa1a74 was not sufficient: the second
live run crashed at the identical PC/fault address, now PAST the guard
(disassembly confirms it compiled correctly). Between CREATE and the
first draw pass, render_pos holds arbitrary stale AnimModelSlot contents
-- run 1 drew the 0xFF tombstone (-1), run 2 drew some other bad value.
Sentinel-whack-a-mole is the wrong shape for a probe.

Final fix: drop the raw= read from the tick line entirely (rp=%p stays --
logging a pointer costs no deref, and the -1 -> real transition frame is
still captured). No data lost: the pre-view root translation is already
logged by the D243M chrdraw census in drawjointlist, where validity is
guaranteed (the game's own gSPSegment runs moments later) and draw-time
is the more accurate sample anyway (tick-time would read the previous
frame's matrix buffer). chrdraw probe gains the same -1 rejection as
belt-and-braces. Smoke: 30 s bare boot, 4,051 chrdraw lines, no crash.
2026-09-16 17:09:19 -04:00
James Dansereau 2a1f1b2472 findings: D243 M-154 crash-fix note + D298 corrected (full HUD, suspected debug-launch artifact)
- D243: M-154 fix paragraph — probe-side AV at the abseil CREATE
  (render_pos = 0xFF tombstone until instcalcmatrices runs during the
  draw pass); guarded; transition frame now logged.
- D298: user corrected the symptom — the FULL HUD is missing, not just
  the aim-mode crosshair; suspected debug-launch artifact; per explicit
  instruction, do not raise until seen in a regular release-targeted
  build. Entry re-scoped and parked accordingly.
2026-09-16 16:57:40 -04:00
James Dansereau 85aa1a74e9 D243 M-154 fix: guard tick-probe render_pos read against the -1 tombstone
First live abseil run crashed inside the new probe (AV, fault addr
0xFFFFFFFFFFFFFFFF, PC in playerTick bondview2.c:10419) on the first
tick after CREATE: bodyModel->render_pos is assigned by
instcalcmatrices() during the draw pass (model.c:2453), i.e. AFTER the
probe point, so on tick 1 post-CREATE it still holds the reused
AnimModelSlot's stale 0xFF tombstone (-1). The game only dereferences
it post-chrTick, so it is legitimately unset here; the M-153-era
fields never touched it, which is why the earlier capture survived.

Guard the raw= read against NULL and (RenderPosView*)-1; rp=%p stays
unconditional so the -1 -> real transition frame is captured (a D243
data point in itself). Probe-only; zero game-code impact. Smoke: 30 s
bare boot clean, 4,059 chrdraw lines.
2026-09-16 16:57:39 -04:00
James Dansereau 90c87fa227 findings: D298 filed — aim-mode crosshair missing (regression, parked per user)
User-reported 2026-09-16 during the M-154 re-run build: the aim-mode
crosshair no longer renders, suspected to come from earlier v0.3.0 work.
Explicitly parked ('don't fix, just for context'). Entry records the
symptom, rules out the probe-only M-154 commits and D295's menu-buffer
widen, points at the crosshair draw path (crosshairimage / bondview.h
show-hide flag / gunfire.c D215 renderdata family) and a bisect plan.
2026-09-16 16:50:59 -04:00
James Dansereau ce8427cff2 docs: D243 M-154 — user confirmed double Bond (one frozen copy per shot); corrected log reading; census probe documented
- findings.md ## D243: M-154 entry. User symptom sharpened (one new
  frozen Bond per camera shot; old copies persist, never animate).
  Corrects M-153: the POSENT window has three phases and in phase 3
  prop->pos ITSELF cycles period-4 through three z-lanes while y falls
  smoothly — the rendered model tracks it (per-lane pose offsets), so
  it teleports every frame rather than freezing. H1 vs H2 named;
  probe design + smoke results recorded. §F table cell updated.
- GE-ENV-PROBES.md: GE_D243M row updated (chrdraw census, new tick
  fields, dedupe note).

HANDOFF.md (gitignored) got the matching LATEST block with the
re-capture command.
2026-09-16 16:47:31 -04:00
James Dansereau 1b3476a401 D243 M-154: chrdraw census probe + extended tick fields (GE_D243M)
Discriminates the two remaining hypotheses for the Dam-abseil frozen
Bonds (H1 multiple Model instances vs H2 framebuffer accumulation):

- objecthandler.c drawjointlist (both region variants): new
  'D243M: chrdraw' line per CHR render submission while a scripted
  camera mode is active (POSEND/INTRO/SWIRL/FADESWIRL), logging Model*
  + raw root-matrix translation. A second Bond instance = extra lines
  with distinct model pointers at the same frame. All CHR draws funnel
  through here (chrRenderProp; player path bondview2.c:10883, generic
  chrprop.c:443).
- bondview2.c tick probe extended: propptr/chrptr (phase-3 A,A,B,C
  position cycle could be pp->prop rotating among three PropRecords),
  rp= render_pos pointer (change = buffer reassignment/aliasing),
  raw= pre-view-transform root translation (model-matrix vs
  view-matrix origin of the cycle).
- bondview2.c: d243mProbeActive()/d243mGetFrameCounter() accessors;
  camera gate folded into active() so objecthandler.c needs no
  bondview.h include. D250 cached getenv kept.

#ifdef PORT only; silent when GE_D243M unset or during FPS play.
Smoke: 45s bare -level_33 boot -> 4,053 chrdraw + 1,066 tick lines,
clean link, no behaviour change.
2026-09-16 16:47:31 -04:00
James Dansereau 8d42ac5ee2 D287 M-152: correct false evidence citation, static architecture trace
scratch/d287_freeze_gdb.txt, cited as this finding's freeze-time
evidence, does not contain it -- grepped for every specific number the
write-up quotes (frame 17616, the three heartbeat stall times) and got
zero hits. The file on disk is an unrelated clean normal-exit capture.
No real freeze-time thread state has actually been analyzed for this bug
yet; corrected the entry so a future session doesn't treat it as
narrowed-and-confirmed.

Also traced the consumer/producer architecture statically (mainThread's
only OS_MESG_BLOCK send is rsp.c:271's osSendMesg into the scheduler's
cmdQ; GE_D60 doesn't cover sends), and found portHeartbeatCheck hard-caps
at 3 reports/process -- "heartbeat stopped logging" in the original
write-up is that cap, not new evidence. Both quirks added to
porting-notes.md. No code change -- diagnostic-only pass, needs a real
live capture next.
2026-09-16 16:26:39 -04:00
James Dansereau 695ff7ec09 docs: D243 M-153 — live GE_D243M capture analyzed; double-instance refuted, period-4 render cycle named
The user's interactive Dam->abseil run with GE_D243M=1 GE_D160=1 landed
(351 KB log, 2991 probe lines; preserved locally at scratch/d243/).
Findings (## D243 M-153 + §F table cell):

- Single Bond model throughout (one idx/model pair, one draw/frame); no
  REMOVE/CREATE window around the abseil — M-145's double-instance and
  bodyModel==0 re-seed-window hypotheses are refuted on PC.
- Two divergent position streams: prop->pos runs the smooth scripted
  void sweep; the model's rendered position (field_488.pos = render
  readback, the only writer under POSEND) freezes on dam-surface points
  then locks into a strict period-4 cycle through four fixed world
  points. The lagged filter (field_3C4, starting at the exact -22832
  M-143 observed) chasing that cycle is the observed camera shake.
- Next: code dive chrTick->render_pos for what produces the four fixed
  points; D173/D292 are the same family.
2026-09-16 16:25:08 -04:00
James Dansereau a9f370bdac cleanup: strip dead GE_D255 correlation probe from objFree
D255 was root-caused and fixed for real at M-140 (a 32-bit pointer
truncation in vtxstore_fix_refs, not a destroy-path race). This
objFree/getenv("GE_D255") trace was a correlation aid for the
now-superseded race hypothesis and never fires against the real fix;
HANDOFF already flagged it as strip-candidate scaffolding. The
vtxstore.c comment documenting the actual fix is untouched.
2026-09-16 16:20:25 -04:00
James Dansereau 190141ff53 docs: D230 status was stale OPEN despite M-121 fix in the same entry
The §F summary table row and the entry's own inline Status line still
said "OPEN -- static-analysis pass done (M-108)" even though the same
paragraph documents the RAW16-wavetable byte-swap fix landed at M-121
(v0.2.0). Left a future session likely to re-investigate an already-fixed
bug. Corrected both to FIXED, noting the still-owed by-ear pass.
2026-09-16 16:17:53 -04:00
James Dansereau 895118c205 D290 M-152: static investigation, decomp side ruled out
Traced the watch controller-graphic render path end to end: page gating,
model/item wiring, ROM resource-table entry, and the generic equip/load
pipeline all check out byte-matched or generic-and-proven-elsewhere.
Rules out src/game/ with high confidence. Best-narrowed hypothesis (not
confirmed): a fast3d translucent-blend edge case for the one-off
PROP_TYPE_PLAYER draw used during the watch's fade-in, matching this
project's recurring one-off-combiner-mode bug class. Falsifier for the
next live session recorded in the finding. No code change.
2026-09-16 16:15:30 -04:00
James Dansereau fbc2a533ae D288 M-152: static investigation, add GE_D288 near-plane probe
Read gfx_sp_tri1 against D233/D106/D271's existing guards. D233's
trivial-reject fix is ruled out as a direct cause -- GL clips well-formed
clip-space geometry correctly downstream of it. Found a new, previously
uncatalogued unguarded site: the G_CULL_BOTH backface-cull cross-product
divides raw vertex w with no epsilon guard, which can NaN/Inf near w=0
and silently skip both cull branches. Not yet confirmed against a live
repro. Added GE_D288 (cached-getenv, D250-safe) to catch the culprit
triangle's w/bbox at draw time on the next -level_20 run.

No game-logic change; port/fast3d/ diagnostic only, inert unless
GE_D288 is set.
2026-09-16 16:13:33 -04:00
James Dansereau 56d2dfd672 fix: gate EVTQ-DROP audio diagnostic behind GE_AUDIOTRACE
The D202/M-71 evtq-exhaustion probe in alEvtqPostEvent was left
unconditional, so shipped builds wrote an unbounded log line on every
dropped event -- found reading GitHub #87's session logs, which showed
real sustained event-pool exhaustion. Gate it on GE_AUDIOTRACE, the same
switch the sibling probes in synthesizer.c/csplayer.c/load.c already use.
2026-09-16 16:06:44 -04:00
James Dansereau fd065071cc findings: D243 M-151 probe landed; GE-ENV-PROBES + index updated 2026-09-16 15:58:37 -04:00
James Dansereau 4a6fb1b940 D243 M-151: GE_D243M model-lifecycle probe for the Dam-abseil two-Bonds/shake
#ifdef PORT, cached getenv, zero behaviour change when unset. Three probes
in bondview2.c:
- D243M: CREATE/REMOVE at both bodyModel sites (solo_char_load /
  bondviewRemovePlayerBody) with camera mode at the transition;
- D243M: tick per playerTick() call while the third-person model exists or
  POSEND/INTRO is active (idx, model ptr, cam modes, prop->pos,
  field_488.pos, filter fields field_3B8 + field_3C4/8/C);
- D243M: draw per on-screen render_pos submission (two lines at the same
  frame number = second Bond instance).

Smoke-tested on a bare -level_33 boot: level-intro cutscene exercises the
full lifecycle (CREATE frame 534 cam=SWIRL -> per-frame tick+draw -> REMOVE
frame 1127), FPS play silent, no crash. Next: user's interactive Dam run to
the abseil with GE_D243M=1 GE_D160=1.
2026-09-16 15:57:51 -04:00
James Dansereau 5071185f8c findings: D295 M-148/M-149 + D297 filed/closed (M-150); porting-notes D8/D9
- D295: full M-148 root-cause/repro record (difficultytext overflow ->
  high-bit byte -> langGetJpnCharPixels NULL deref; OOB theory retracted;
  pre-D284 save fingerprint identified), verdict FIXED (M-149).
- D297: new entry — v0.3.0 wipes all pre-fix saves (D284 side effect);
  scope verified (5 slots only, block-0 is a factory seal), FIXED (M-150).
- D284: correct the false 'savegames are unaffected' claim.
- porting-notes: D8 instance (difficultytext[4]); new D9 — changing a
  PRNG/CRC implementation invalidates persisted derived state; sweep
  rewrites data/ge007.eep gotcha in E.
2026-09-16 15:28:44 -04:00
James Dansereau f1e2c7cbb8 D297 M-150: eeprom-shim migration of pre-D284 save CRCs
v0.3.0 silently wiped all v0.2.x saves on first launch: stored slot CRCs
are fingerprints of the PRNG that wrote them, and D284 changed the PRNG.
One-time migration in the eeprom shim's read path (block 4 only): a slot
that fails the current-CRC check but validates under the legacy PRNG is
re-stamped with current-PRNG checksums before fileValidateSaves sees it,
then persisted once. Runs before geEepromPatchAllCheats so cheat patches
never reach the file. Corrupt/fresh slots untouched (game's own wipe path).

- port/src/legacycrc.c: isolated file carrying the deliberately-wrong
  pre-D284 PRNG (verbatim from 0de99b4d^, reviewer-verified bit-exact) +
  lockstep copy of the crc.c loop; header says when it may be deleted.
- tools_pc/d295_crc.py: reference CRC model, bit-exact vs compiled C for
  both PRNGs (used to verify migrated files).

Verified: user's #87 artifact migrates 5/5 slots with progress intact and
is idempotent on relaunch; native post-fix saves, corrupt slots, and fresh
eeps all behave per design; 21/21 sweep PASS.
2026-09-16 15:28:37 -04:00
James Dansereau 4fc54293f9 D295 M-149: fix #87 crash — port-side JPN-cache seed + PORT buffer widen
langGetJpnCharPixels NULL deref (language.c:320): any rendered string byte
>= 0x80 in non-JP mode indexed the unallocated JPN glyph cache. Two
signed-off fixes:
- port/src/main.c: seed g_JpnCacheCacheItems/g_JpnCharCachePixels from
  static port buffers before osStartThread (exact langInit construction,
  _Static_assert on pixel-stub size) so a stray high-bit byte takes the
  normal cache-miss path instead of crashing.
- src/game/front.c: widen difficultytext[4]->[32], missiontext[18]->[32]
  under #ifdef PORT (D8 class; '00 Agent' + newline overflowed the decomp
  buffer into textpos.p[0], the direct trigger). N64 build untouched.

Verified: previously-crashing input runs clean, 21/21 sweep PASS.
2026-09-16 15:28:33 -04:00
James Dansereau 6f28156956 findings: D243 M-145 - two-Bonds observation confirms third-person model lifecycle bug
Static trace confirms user's hypothesis architecturally: the Dam exit
script runs on a model-less BG driver chr, while the visible Bond is the
player's own third-person model teleported via chr_try_teleporting_to_pad
(CHR_BOND_CINEMA). Ties the abseil camera shake (M-143/M-144) and the
double-Bond image to one model-lifecycle mechanism; cross-refs D173/D292.
2026-09-16 09:38:02 -04:00
James Dansereau 08dbd581e9 findings: D285 live-Deck verification (Archives, dual grenade launchers, rocket-cheat guards, v0.2.2) 2026-09-16 09:37:02 -04:00
James Dansereau 768ac8d837 release-notes: drop stale per-release changelog re-dumps
.github/release-notes.md re-included the full "What's new since v0.2.0"
and "since v0.1.0" sections in every release body, even though that
content already lives permanently on those releases' own GitHub pages.
Keep only the delta since the immediately previous release, with a link
to the Releases page for full history.
2026-09-16 09:21:40 -04:00
James Dansereau 2e8511539b v0.2.2 hotfix: README + release notes + D285 live-verification
Documents the four hotfix-batch changes since v0.2.1 (D255, D285, D282,
D293) in README Status and .github/release-notes.md, and folds in
2026-09-16 real-Deck confirmation that D285's audio-thread lock holds
under sustained overlapping-explosion combat (Archives, dual grenade
launchers + rocket-launcher-cheat guards).
2026-09-16 09:13:43 -04:00
James Dansereau 2c19689c62 findings: D243 M-144 — user confirms real N64 does NOT show the abseil shake
Closes the open question M-143 ended on: this is a genuine PC-only
divergence, not an original-game quirk. Since bondviewUpdatePlayerCollisionPositionFields's
smoothing-filter math is decomp-exact and already g_ClockTimer-scaled, the
bug must be upstream of the filter -- either the position warp itself is
wrong/larger on PC, or a call-ordering desync around the CAMERAMODE_INTRO
transition. Narrows the next step to a single probe: log raw field_488.pos
and field_3B8 (pre-filter) around the warp tick, instead of the open-ended
"needs N64 hardware" framing M-143 left off on.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-16 00:17:08 -04:00
James Dansereau 91ba14d2e6 findings: D243 M-143 — name the shake mechanism (unreset position-smoothing filter)
Live Dam-abseil capture with the new lookatpad/gBondViewCutscene GE_D243CAM
probes (added this session) fires the gBondViewCutscene branch. Its pos2
read is g_CurrentPlayer->field_3C4/8/C, a leaky-integrator low-pass filter
of Bond's real position (bondviewUpdatePlayerCollisionPositionFields) that
is never reset when a scripted cutscene warps his position out from under
it -- the filter takes ~1s to catch up, and a fixed eye with a look-at
target sweeping through that catch-up is the reported shake. The filter's
own update is g_ClockTimer-scaled (frame-rate-correct by the project's
usual standard), so this looks decomp-faithful rather than a port-specific
timing bug -- open question is whether real N64 hardware shows the same
transient, which would make this not a port defect at all.

D146 stays fully decoupled (zero hits on a third consecutive live run).

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-16 00:13:30 -04:00
James Dansereau 57b6c6d67c D243: instrument the two untested CAMERAMODE_POSEND branches (lookatpad, gBondViewCutscene)
M-141/M-142 ruled out the pad-orbit branch (GE_D243CAM: zero hits across two
live Dam-abseil captures). The Dam cutscene must reach one of the other two
CAMERAMODE_POSEND sub-paths instead. Adds the same GE_D243CAM-gated
osSyncPrintf to both, right before their early returns: pos/pos2 for the
look-at-pad branch, plus theta/verta (the spherical angles driving pos2's
offset) for the gBondViewCutscene branch, since a jump/jitter in either is
the leading candidate for the reported shake.

Build-verified (release/v0.3.0, 11/11 changed objects relink clean); all
three D243CAM log lines confirmed present in the built binary. Awaiting the
next live Dam-abseil repro (Phase 1 in docs/HANDOFF.md) to see which branch
actually fires and what the logged values show.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-16 00:00:42 -04:00
James Dansereau 09fdd86eef optionsoverlay: add D293 quit-to-desktop row to the F10 overlay
No discoverable in-UI way to quit (Deck/controller-only setups had only
window-close/Alt+F4). Adds a ROW_ACTION row kind (not config-backed, same
pattern as the existing __Resolution row) that triggers the identical
configSave()+exit(0) path SDL_QUIT already uses. Build-verified on Windows
and Linux; verify.sh dam PASS, no regression.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 23:52:50 -04:00
James Dansereau 1970ab3732 fix(D285): lock sndHandleEvent's preempt-scan against sndSetupSound's writer
sndHandleEvent's pool-exhaustion preempt-scan read/walked the shared
D_800243E4 active-ALSoundState list with no osSetIntMask lock, while
sndSetupSound (the list's insert path, reached from sndPlaySfx calls
across 15 game files) already locks it -- an asymmetric-locking gap,
the same D4/D147/D152 "N64 interrupts-off isn't a real lock on PC"
class. Wraps the scan in the identical lock/unlock pair sndSetupSound
already uses, unconditionally (no #ifdef PORT, matching that function's
own style -- osSetIntMask is genuine N64 API and the port's shim is a
process-wide recursive lock, safe to nest). Audited the rest of the
function: the scan is the only D_800243E4 touch point in it.

Build green; -level_09/-level_20 unregressed (1400+ frames, no crash,
no D152 lock-stuck warnings). Not yet gdb/live-confirmed against a
real reproduced crash -- a GE_INPUTSCRIPT-driven stress attempt this
session didn't visibly activate the scripted-input harness for a
reason not root-caused; the honest bar met here is "correct by D4/D152
precedent, build/sweep-clean," not "reproduced-then-fixed."

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 23:49:30 -04:00
James Dansereau 97678b2980 fix(D282): front-end menu nav always uses left stick, matching F10-overlay convention
Menus read the same stick_x/stick_y channel as in-level gameplay, and
naturalPitchMode (SOLITARE-style, default-on) routes that channel to
the right stick for continuous look. Since front.c's menu nav shares
the channel, it silently inherited the right-stick routing, which is
unintuitive on a modern pad/Deck outside a running stage. The gamepad
poll already computed an unused padMenuMode flag for shoulder-button
gating; reused it to always source stick_x/stick_y from the left
stick outside RUN_STAGE, regardless of naturalPitchMode. Port-layer
only, inert during gameplay -- verified via -level_09/-level_20
unregressed; in-menu feel needs a human controller playtest.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 23:48:20 -04:00
James Dansereau 1ba83ba4b0 fix(D255): correct 32-bit pointer truncation in vtxstore_fix_refs
Root-caused via a live Steam Deck session over SSH: sub_GAME_7F09BAC4
(vtxstore_fix_refs) read an active OBJ record's Model* through
((Model*)((ChrRecord*)prop->chr)->chrflags) - a union pun onto
ChrRecord.chrflags, which is typed CHRFLAG, a 4-byte enum. On 32-bit
N64 this is bit-exact (the enum and a pointer are both 4 bytes, so
casting the enum's value to Model* reproduces the real stored bytes).
On 64-bit PC, Model* is 8 bytes but the enum read is still only 4 -
the cast reads just the pointer's low 32 bits as an int and
zero-extends it back to a 64-bit address, discarding the real high
bits. This is the D3x pointer-width ABI class from AGENTS.md - a
struct field misread across the 32-to-64-bit pointer-width transition.

This was actually flagged as suspect (a) in the very first D255
investigation, then wrongly deprioritized in favor of a "destroy-path
race" theory once a register capture happened to show Rdi=(nil) and
looked like it confirmed a different, unrelated NULL-model theory. It
didn't: (nil) is exactly what a 32-bit-truncated read produces
whenever the real pointer's low 32 bits happen to be zero - a
coincidence that pointed two sessions' worth of investigation at the
wrong field. Proof: reading the same record through the correctly
64-bit-typed ObjectRecord.model field (this session's diagnostic
probe) was always a valid, non-NULL pointer - the object's model was
never actually missing.

Fix: read the pointer through ObjectRecord.model (the same union
memory, correctly typed) instead of through the mistyped
ChrRecord.chrflags alias, at both use sites in the function. Identical
bytes and identical result on N64; correct on 64-bit PC.

Verified live on the user's own Steam Deck hardware over SSH, running
their own prepared ROM assets: held through the entire Facility level
including repeated terminal destruction (the most consistent prior
repro) and through Caverns' radio-objective terminals (the second
confirmed level), zero crashes. Windows rebuild + full 21-level
verify.sh sweep also clean.

The separate audio-thread crash (D285, caught during this same live
session) is unrelated and unaffected by this fix.
2026-09-15 23:48:17 -04:00
James Dansereau 24fb22ca80 D243: live playthrough decouples shake from D146; add GE_D243CAM probe
Live Dam abseil cutscene captures (2 runs, both diagnostic probes on):
the visible camera shake reproduces every time, but the D146 stale-memory
burst fired on neither run -- the two symptoms M-107/M-109 treated as
related are actually independent. Added an env-gated GE_D243CAM probe to
bondviewFrozenCameraTick's pad-orbit CAMERAMODE_POSEND branch to check for
g_GlobalTimerDelta jitter; zero hits rules out that branch entirely. The
Dam cutscene uses one of the other two CAMERAMODE_POSEND sub-paths
(g_CameraLookAtBondPad or gBondViewCutscene), neither instrumented yet --
next session's concrete next step per docs/HANDOFF.md.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 23:44:07 -04:00
James Dansereau 8c28453517 findings: fix D255's stale OPEN status (was FIXED+verified, never updated)
The M-140 fix writeup existed in two places -- an orphaned paragraph right
after the table row, and the pre-fix narrative in the tail ## D255 section
-- but neither ever updated the table row's own status cell, so
findings-index.csv (generated from the table) reported a real, fixed,
live-verified crash as still OPEN. Consolidated into one writeup in the
tail section (which findings-index.csv derives its verdict from per this
file's own ctx-opt convention) and fixed the row's status cell to match.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 23:25:23 -04:00
James Dansereau e978e526be findings: D249 static review rules out two suspects, narrows to portal BFS
Confirmed correct level IDs (Streets=29, Egyptian=32, not the finding's
original level_20 guess). Source review clears both the unscaled-far-clip
and LOD-distance-multiplier hypotheses (bgfog.c/video.c both already scale
consistently). Narrowed to bg.c's portal/room-visibility BFS as the one
remaining suspect -- could be a real bug or just N64 behavior newly visible
on PC's sharp display; needs a live screenshot + location before touching
this decomp-verbatim system, not more static tracing.

Headless GE_PCDUMP long-sightline attempts on Streets/Statue/Egyptian did
not transfer from the D245 technique -- none of them have a scriptable path
to an open sightline without real level navigation.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 23:20:44 -04:00
James Dansereau 7412a9818d sky.c: D245 headless probe + opt-in fixed-shift A/B candidate
Adds a GE_D245V env-gated probe dumping the water fan's raw S/T span each
call, and a GE_D245_FIXEDSHIFT env var (skyPortForceFanShift) that forces a
constant tc-shift k instead of the per-frame adaptive pick. Both are
diagnostic/opt-in only -- default behavior is unchanged (verify.sh
frigate/"surface 2" PASS with the env vars unset).

Headless GE_INPUTSCRIPT rotation on Frigate found k genuinely flips (kT
4->3) during ordinary view turning, which the original single-frame D227V
trace never caught -- new evidence for why the M-116 allowShift=TRUE fix
didn't hold up live. The fixed-k candidate is NOT the default; it needs a
live A/B (unset vs 3/4/5) to know if it actually looks better, not another
blind ship.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 23:06:47 -04:00
James Dansereau e108d6bf2e findings: log D295, campaign-halting Control Center crash (GitHub #87)
00 Agent campaign crashes at end of Control Center, then crashes on every
subsequent relaunch — points at corrupted persisted state (save/EEPROM),
not a one-off frame bug. Possibly-related audio degradation reported from
Streets onward, unconfirmed. Not investigated; needs the user's crash log
and save file before further work.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 22:52:11 -04:00
James Dansereau a3637c40e1 optionsoverlay: add D293 quit-to-desktop row to the F10 overlay
No discoverable in-UI way to quit (Deck/controller-only setups had only
window-close/Alt+F4). Adds a ROW_ACTION row kind (not config-backed, same
pattern as the existing __Resolution row) that triggers the identical
configSave()+exit(0) path SDL_QUIT already uses. Build-verified on Windows
and Linux; verify.sh dam PASS, no regression.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 22:49:42 -04:00
James Dansereau acf80a874f agents: findings table row now points at the CSV lookup path (consistency with preflight bullet) 2026-09-15 22:39:21 -04:00
James Dansereau 5d7b18a035 Merge docs/llm-context-optimization: LLM context optimization (P0+P1+P2)
Independently validated (fresh-session audit from git + snapshot); all
findings fixed. Docs-only: findings-index.csv lookup path, HANDOFF split,
~127 fat §F cells migrated verbatim to ## Dxx tail sections, CI size
budgets. Preflight ~123k -> ~6.5k tok.
2026-09-15 22:38:00 -04:00
James Dansereau 6cb430c4a9 claude.md: point findings lookup at findings-index.csv (was stale in-file §F index; validator-relevant for Claude Code sessions) 2026-09-15 22:30:24 -04:00
James Dansereau 44ea596411 findings: validation fixes — D252 derived status now reflects M-141 (both hypotheses refuted, root cause open); normalize D221/D227 append flags to 'appended 2026-09-15 ctx-opt' 2026-09-15 22:24:48 -04:00
James Dansereau 81cb276ed5 hygiene: untrack docs/dev/notes/ctx-opt/consolidate_ab.py (dir is gitignored; validator finding) 2026-09-15 22:24:48 -04:00
James Dansereau 5684d30bd4 agents: caveat for large multi-pass ## Dxx sections (grep-within / offset-limit, don't slurp) 2026-09-15 21:55:31 -04:00
James Dansereau c2b42db275 ci: recalibrate §F size budget to 180 KB (table ~55 KB + legacy narrative ~110 KB)
The 150 KB threshold assumed the §F region was table-only, but it also
holds ~92 KB of pre-ctx-opt D24–D33 bullet narrative plus the M-111/M-113
write-ups. The regression signal is fat ROWS re-accumulating (table is now
~55 KB), so the budget sits ~15 KB above today's 166 KB total.
2026-09-15 21:52:07 -04:00
James Dansereau 093e428639 findings.md: has-section dedup, rows 20-27 (D280 D275 D274 D273 D272 D284 D285 D286)
Same protocol: topics were compressions of their own §H entries (verified
by probe), statuses (the verdicts) kept verbatim. Skipped as already-thin:
D240-D243, D247 (one-liner + short status each).
§F table region: 226 KB -> 166 KB.
2026-09-15 21:51:22 -04:00
James Dansereau e9bb415488 findings.md: has-section dedup, rows 16-19 (D217 D271 D276 + skips)
- D217: 7.5 KB status was a digest of the §H M-86→M-97 entries (each
  'See full write-up: §H ...'); only the closing verdict was missing —
  appended it (PR #47 + PR #49 merged, white residual → D228, M-105
  stale-status correction).
- D271/D276: topics were compressions of their own §H entries; statuses
  (the verdicts) kept verbatim.
- Skipped as already-thin/substantive: D220, D132 (compact audit summary),
  D183, D179, D178, D182, D181.
2026-09-15 21:50:34 -04:00
James Dansereau 2a84927e3f findings.md: finish D227 shrink (status cell was missed in the batch-2 pass)
The earlier edit replaced only D227's topic cell; its 11.9 KB status
survived. That status was a digest of the full write-ups, which exist:
§H '## D227' entries M-94/M-95/M-96/M-98/M-100/M-100b (in findings.md) and
M-99 (full entry in HANDOFF-ARCHIVE.md). Digest wording survives in git
history + snapshot.
2026-09-15 21:49:31 -04:00
James Dansereau e85175639c findings.md: has-section dedup, rows 11-15 (D206 D207 D205 D176 D177)
Same protocol. Three sections ended before their final disposition — the
newer verdicts were appended verbatim to the section first (flagged):
- D206: M-77c user by-ear A/B = PASS (section still listed it as 'still owed')
- D207: M-77c MONITORING disposition
- D205: M-78 CLOSED (root cause was D206)
D176's defect-(b) summary (grey diagonal static, D183 0/166) has no section
home — kept as the row's substantive topic instead of a bare pointer.
2026-09-15 21:48:28 -04:00
James Dansereau 45427eaf0e findings.md: has-section dedup, rows 1-10 (D159 D161 D186 D200 D221 D227 D229 D228 D204 D202)
Each cell checked line-by-line against its ## Dxx section(s); cells were
compressed summaries of the sections, so rows shrink to one-liner + pointer
and status keeps its verdict. Two cells carried facts NEWER than their
sections — appended verbatim to the sections first (flagged in-section):
- D221: human mouse-playtest confirmation (section still said 'eyeball owed')
- D227: M-100b human play-test confirmed (section ended 'still owed')
D228's status was the post-split FIX write-up, absent from its pre-fix
M-97 section — appended as a second ## D228 pass-section.
Exact pre-shrink wording survives in git history + notes/snapshot-pre-ctx-opt/.
2026-09-15 21:45:46 -04:00
James Dansereau 00a877cc53 findings.md: D70–D121 §H cross-check — shrink covered rows to pointers, migrate D120
Semantic coverage check of each §F row labeled D70–D121 against the §H
narrative (done directly, not delegated):
- D85, D86·D87, D88.4, D121: thin rows, fully covered by §H entries —
  topic reworded as one-liner + 'see §H' pointer, status kept verbatim
- D117, D118: fat cells whose content (M-52 addenda; M-24 mouse-look
  rework incl. D118b/c) is fully in §H — shrunk to one-liner + pointer
- D120: fat cell with NO section and no §H coverage (sole record of the
  M-30 PointUsage fix) — pure verbatim move to a new ## D120 tail section,
  row becomes a pointer; verified byte-identical vs pre-migration snapshot
- D75: LEFT AS-IS — its status cell holds a v0.2.0-pre playtest result not
  present in §H; shrinking would lose data
- D119·D120: left as-is — sole record of D119 (§H mentions it only in passing)
Range rows (D70–D74, D89–D92, …) stay as-is per plan.
2026-09-15 21:41:50 -04:00
James Dansereau 14eb1f8330 findings.md: shrink the 3 multi-label rows with per-sublabel sections to pointers
D76·D164, D165·D166, D180·D181 each point at existing ## Dxx sections that
fully cover the cell content (verified line-by-line; cells were compressed
summaries of the sections). Status cells kept verbatim. The remaining two
multi-label rows (D86·D87, D119·D120) are thin §H-era summary rows with no
sections — deferred to the D70–D121 §H cross-check.
2026-09-15 21:40:23 -04:00
James Dansereau e2b8ec3432 findings.md: consolidate the (a)/(b) duplicate-label sections (D152+, D233, D234)
Each label had a full writeup + a short 'Duplicate row' triage pass.
Keep the full writeup; append the short pass's genuinely-unique content
verbatim under a dated merge note (D233/D234: GRAPHICS-BACKLOG.md logging +
triage hypotheses; D152+: fully subsumed — noted as such). Both §F table
rows stay (cell 1 frozen) and both now point at the single merged section.
Nothing deleted: earlier text in git history + notes/snapshot-pre-ctx-opt/.
2026-09-15 21:39:38 -04:00
James Dansereau 8687846bee findings.md: header documents the ctx-opt migration + CSV lookup path + 4 known index gaps
Cell 1 of the §F table is frozen, so four rows (D69·D78–D84, RC3·D167,
the D88.x range row, D118d) are invisible to findings-index.csv — LABEL_RE
can't parse their label format. Documented in the file header alongside
the lookup convention and a record of what the 2026-09-15 migration did
(including the one non-verbatim step: D250/D252/D255 derived statuses).
2026-09-15 21:39:33 -04:00
James Dansereau 9c9d4a0997 docs(findings): migrate D250/D252/D255 to tail sections (ctx-opt P1, manual)
These three rows were fat topic with NO status cell (2-column rows). Topic
moved verbatim to new ## Dxx sections; table rows get a substantive one-liner
+ a status DERIVED from each entry's own verdict text (D250 FIXED / D252 OPEN
known-issue / D255 OPEN pending rule-#2 sign-off) — the derivation is flagged
in-section for auditability. CSV regenerated, label set unchanged (171); all
three topics verbatim-checked against the snapshot.
2026-09-15 21:30:27 -04:00
James Dansereau 9a8f1c9162 docs(findings): migrate D211/D219 to tail sections (ctx-opt P1, structurally-odd follow-up)
Pure moves following the 6208276d formatting repair: topic/status cells moved
verbatim to new ## Dxx sections; table rows now carry one-line summary +
pointer. CSV regenerated, label set unchanged (171); both sections
verbatim-checked against the snapshot.
2026-09-15 21:29:31 -04:00
James Dansereau 6208276d2b docs(findings): repair pre-existing table formatting in D211/D219/D255 (NOT a content edit)
Formatting fixes only — text preserved verbatim, no content added/removed:
- D211: the HUD ammo display '7 | 93' contained a literal unescaped pipe,
  splitting its status cell in two; wrapped that token in backticks.
- D219: a call-chain used pipes as separators between separate code spans
  (5 cells instead of 3); merged into one continuous code span — same text,
  backtick positions moved.
- D255: row lacked the trailing pipe its neighbours have; added it so the
  table is well-formed 3-column (status cell empty, as in D250/D252).

These were pre-existing CSV corruption (garbled/truncated status_raw), not
migration artifacts. CSV regenerated; label set unchanged (171).
2026-09-15 21:29:20 -04:00
James Dansereau 29dd9ac882 ci: warn on dev-docs size budgets (ctx-opt P2)
Warn-only (non-fatal): findings.md §F table region > 150 KB means fat rows
are accumulating again (cell content belongs in ## Dxx sections; lookup path
is findings-index.csv); porting-notes.md > 120 KB. Lit until Bucket B of the
ctx-opt migration lands (~266 KB today). HANDOFF.md is gitignored so its
> 30 KB budget stays a local check.
2026-09-15 21:27:58 -04:00
James Dansereau dd2e95e229 docs(findings): repair D233/D234 disambiguation markers (were inserted before '##')
The previous commit's (a)/(b) insertion landed at the line start, producing
' (a)## D233 — …' lines that no longer parse as headings. Moved the markers to
after the label: '## D233 (a) — …'. 117/117 migrated sections now have valid
headings; CSV check + label set still green.
2026-09-15 21:24:21 -04:00
James Dansereau 1726221030 docs(findings): disambiguate duplicate D233/D234 section headings (a)/(b)
Two table rows each carry the same label+topic with a status progression
(earlier vs updated pass). Both migrated verbatim in batch 6; heading markers
are formatting only. Flagged for Bucket B consolidation.
2026-09-15 21:23:47 -04:00
James Dansereau 60b1e4ff61 docs(findings): migrate fat §F rows D271-D294 to tail sections (ctx-opt P1, batch 8/8)
12 rows: topic/status cells moved verbatim to new '## Dxx' sections at file
tail; table rows now carry a one-line summary + pointer. CSV regenerated, label
set unchanged (171). Nothing deleted.
2026-09-15 21:23:27 -04:00
James Dansereau 63b4d79ce6 docs(findings): migrate fat §F rows D244-D270 to tail sections (ctx-opt P1, batch 7/8)
20 rows: topic/status cells moved verbatim to new '## Dxx' sections at file
tail; table rows now carry a one-line summary + pointer. CSV regenerated, label
set unchanged (171). Nothing deleted.
2026-09-15 21:23:27 -04:00
James Dansereau ce4ec5a387 docs(findings): migrate fat §F rows D218-D243 to tail sections (ctx-opt P1, batch 6/8)
21 rows: topic/status cells moved verbatim to new '## Dxx' sections at file
tail; table rows now carry a one-line summary + pointer. CSV regenerated, label
set unchanged (171). Nothing deleted.
2026-09-15 21:23:26 -04:00
James Dansereau 5446cc295b docs(findings): migrate fat §F rows D201-D216 to tail sections (ctx-opt P1, batch 5/8)
10 rows: topic/status cells moved verbatim to new '## Dxx' sections at file
tail; table rows now carry a one-line summary + pointer. CSV regenerated, label
set unchanged (171). Nothing deleted.
2026-09-15 21:23:20 -04:00
James Dansereau 4ea303e25e docs(findings): migrate fat §F rows D184-D199 to tail sections (ctx-opt P1, batch 4/8)
Also disambiguates the two '## D152+' headings from batch 2 as (a)/(b)
(formatting only; content untouched — flagged for Bucket B consolidation).

14 rows: topic/status cells moved verbatim to new '## Dxx' sections at file
tail; table rows now carry a one-line summary + pointer. CSV regenerated, label
set unchanged (171). Nothing deleted.
2026-09-15 21:23:19 -04:00
James Dansereau 2d50dd2319 docs(findings): migrate fat §F rows D169-D181 to tail sections (ctx-opt P1, batch 3/8)
9 rows: topic/status cells moved verbatim to new '## Dxx' sections at file
tail; table rows now carry a one-line summary + pointer. CSV regenerated, label
set unchanged (171). Nothing deleted.
2026-09-15 21:22:48 -04:00
James Dansereau 415abb4438 docs(findings): migrate fat §F rows D152-D168 to tail sections (ctx-opt P1, batch 2/8)
15 rows: topic/status cells moved verbatim to new '## Dxx' sections at file
tail; table rows now carry a one-line summary + pointer. CSV regenerated, label
set unchanged (171). Nothing deleted.
2026-09-15 21:22:47 -04:00
James Dansereau cd6f0b5e83 docs(findings): migrate fat §F rows D123-D151 to tail sections (ctx-opt P1, batch 1/8)
16 rows: topic/status cells moved verbatim to new '## Dxx' sections under a
provenance divider at file tail; table rows now carry a one-line summary +
pointer. CSV regenerated, label set unchanged (171). Nothing deleted.
2026-09-15 21:22:39 -04:00
James Dansereau d7d84c09f5 docs: HANDOFF archive receives M-140 -> ~M-12 block (ctx-opt P0)
Companion to the local split of docs/HANDOFF.md (gitignored rolling
working file — that half of the move is working-tree-only, backed up in
the pre-edit snapshot). Appends lines 137-5764 of the pre-split HANDOFF
(### ARCHIVE — M-140 onward, ~444 KB) after the existing M-2/M-3 brief,
with a divider documenting coverage and the newest-first ordering of
the moved block. Content verbatim; CRLF->LF normalised to match this
file and .gitattributes (eol=lf). Verified: live head + moved chunk
rejoin byte-identical to the original (CR-stripped); appended region
cmp-clean against the snapshot.

Archive intro corrected: it claimed 'through session M-11' but held only
M-2/M-3; the M-4-M-11 gap is pre-existing, noted as such.
2026-09-15 21:11:53 -04:00
James Dansereau eb184b6df1 docs: point auto-loaded context at findings-index.csv, not the §F fat table (ctx-opt P1.5)
AGENTS.md findings bullet + HANDOFF-ARCHIVE.md intro pointer now steer
cold-starting agents to docs/dev/findings-index.csv (grep this, then read
the specific ## Dxx entry) instead of linear-reading the ~100k-token §F
table. Base: release/v0.3.0 @ bf24bef8.
2026-09-15 21:11:00 -04:00
James Dansereau bf24bef8a9 docs(D294): correct trigger condition - ground level, not elevated vantage
User clarified the black-geometry drop-out happened at normal ground
level during normal progression near the statue, not from the
elevated ramp with wide sightlines as the first write-up assumed.
Retracted the "widened FOV frustum from elevation" framing of the
room-visibility hypothesis; kept the underlying room-transition/
streaming-timing guess since it doesn't depend on elevation.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 20:38:43 -04:00
James Dansereau a001f1c137 docs(D294): new finding - Statue ground briefly renders black near the elevated CCCP-monument ramp, self-recovers
User report at Video.FovScale~130 + max draw distance/LOD: ground
geometry near an elevated wide-visibility vantage point drops out to
solid black, then recovers 10-30s later on its own. Doesn't cleanly
match D222 (NPC/object culling, FIXED), D249 (default-FOV far
geometry, doesn't self-recover), or D218 (fog/tint boundary, not
missing geometry) -- logged as its own finding with those three as
neighbors, not a confirmed duplicate of any. Leading unconfirmed
guess: a portal/room-visibility BFS not accounting for the widened
FOV frustum.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 20:37:28 -04:00
James Dansereau b6ed6b1d36 docs: fold D289 into D255 (likely same terminal crash), consolidate D173/D292 as one Bond-positioning thread
User corrections on the playtest batch: D289 (Dam computer-room
crash) is most likely the same computer-terminal SIGSEGV D255 already
fixed this session, not a separate mystery -- needs a re-test on the
post-fix build before treating as new. D292 (Bond sunk into floor on
death) is confirmed the same "Bond's world model positioned wrong"
family as D173/D148, not a separate bug -- cross-referenced both
directions so the split-across-two-files writeup (GRAPHICS-BACKLOG.md
D148 + findings.md D173) is easier to find as one thread covering
level-start floating, cutscene spin, and now death-pose sinking.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 20:35:52 -04:00
James Dansereau 4c2643c239 docs: log Steam Deck playtest feedback batch (D289-D293 new, cross-refs to D173/D219/D243/D246)
User completed a Dam-through-Runway Steam Deck playtest and reported
9 items. Four corroborate already-open findings (D173 third-person
Bond floating at level start, D246 fullscreen edge strips -- Deck's
own default preset sets MSAA=4, exactly this row's leading suspect,
D219 purple/blue particle colors widened to blood decals, D243 Dam
end-cutscene camera glitch -- second independent confirmation). Five
are new: D289 (Dam computer-room crash, non-reproducing), D290 (watch
controller-page missing pad graphic), D291 (Dam truck texture
glitches), D292 (death-pose Bond sunk into floor, possibly D173-
adjacent), D293 (no way to quit the game from the UI -- confirmed in
code, SDL_QUIT via window-close/Alt+F4 is the only path; a QoL gap on
Deck/controller-only setups).

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 20:33:07 -04:00
James Dansereau d475884456 docs(D288): promote D171 (Silo stray triangle) with user-reproduced, visually-documented repro
User reported a stray triangle during the Silo missile-shaft camera
pan (previously D171, parked since M-34 as "observed, not
investigated"). Reproduced headlessly, precisely bounded to frames
~562-741 of a bare -level_20 boot: a large, flat-shaded, untextured
brown triangle (uniform RGB(83,62,44)) grows then shrinks across the
pan, consistent with a real world-space triangle mis-transformed near
the camera plane rather than a HUD element or garbage vertex-color
read. Points at the D106/D233 near-plane/w-clip neighborhood in
port/fast3d/gfx_pc.cpp, not yet confirmed at the code level. Evidence
saved to docs/img/bugs/. Also noted: foreground `timeout N ...` launches
of the exe from this session's Bash tool produce zero GE_PCDUMP output;
backgrounding the launch works reliably -- worth remembering next time.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 20:27:54 -04:00
James Dansereau a2b028737b docs: fix Ourumov spelling in D263 quoted report title
Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 20:19:28 -04:00
James Dansereau 791bf6b7c0 docs(D263): confirm Ourumov/Trevelyan standoff timeout fallback is by design, not a bug
User reproduced the "stuck standoff" 3-4 times; traced to a hard
timeout in ai_22 (label 0x13, UsetuparkZ.c) that fires the execution
unconditionally after ~2.5 min if no other trigger fires first. User
waited it out and confirmed it resolves normally with dialogue -- not
a freeze, not a new bug. Documented so it isn't re-flagged.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 20:17:17 -04:00
James Dansereau 269de57941 Merge docs/d287-freeze-finding: new D287 freeze finding + D252 bullet-impact confirmation
Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 20:00:49 -04:00
James Dansereau 86fdcc1db6 docs(D287): new finding - real non-recovering freeze after long idle + refocus; D252 confirmed on bullet-impact sparks too
D287: user hit a live freeze under debug.ps1 after ~15-20 min idle,
refocus-triggered, non-self-recovering (distinct from D286's single
fire-then-recover shape). Evidence preserved at
scratch/d287_freeze_gdb.txt (untracked, matches this repo's existing
scratch/ convention). Leading lead: gfxFrameMsgQ (32-slot scheduler
client queue) full while the VI-retrace pacemaker keeps ticking,
pointing at the main/render thread's consumer stalling rather than
the scheduler. Not root-caused; GE_D60 diagnostic already exists and
is the recommended next probe.

D252: user confirmed live the rainbow-hue bug still reproduces on
plain bullet-impact sparks, not just explosions -- a much cheaper
repro for the next GE_D252 capture.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 20:00:43 -04:00
James Dansereau 79d33a0a07 Merge fix/d285-audio-thread-race: D285 audio-thread SIGSEGV fixed via D4/D152-class lock
Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 19:35:26 -04:00
James Dansereau 07b25f4b45 fix(D285): lock sndHandleEvent's preempt-scan against sndSetupSound's writer
sndHandleEvent's pool-exhaustion preempt-scan read/walked the shared
D_800243E4 active-ALSoundState list with no osSetIntMask lock, while
sndSetupSound (the list's insert path, reached from sndPlaySfx calls
across 15 game files) already locks it -- an asymmetric-locking gap,
the same D4/D147/D152 "N64 interrupts-off isn't a real lock on PC"
class. Wraps the scan in the identical lock/unlock pair sndSetupSound
already uses, unconditionally (no #ifdef PORT, matching that function's
own style -- osSetIntMask is genuine N64 API and the port's shim is a
process-wide recursive lock, safe to nest). Audited the rest of the
function: the scan is the only D_800243E4 touch point in it.

Build green; -level_09/-level_20 unregressed (1400+ frames, no crash,
no D152 lock-stuck warnings). Not yet gdb/live-confirmed against a
real reproduced crash -- a GE_INPUTSCRIPT-driven stress attempt this
session didn't visibly activate the scripted-input harness for a
reason not root-caused; the honest bar met here is "correct by D4/D152
precedent, build/sweep-clean," not "reproduced-then-fixed."

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 19:35:22 -04:00
James Dansereau 6f278ad4d4 Merge investigate/d243-fresh: D243 trace ported, build-verified, D250-fixed; still needs interactive playthrough
Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 19:31:02 -04:00
James Dansereau 556f4434f3 D243: port GE_D243 front-end MENU_* trace onto release/v0.3.0, build-verify, fix uncached getenv
Cherry-picks the GE_D243 diagnostic (764a1f26, previously stuck on an
unmerged, un-build-verified branch) onto release/v0.3.0. The prior
session's "toolchain blocker" was this session's own TMP/TEMP-vs-Bash-tool
gotcha (documented in porting-notes.md this session), not a real
CMake/ninja issue -- now explained and worked around. Also fixes an
uncached getenv("GE_D243") per menu_init() call (D250's own rule: this
runs ~once/frame during front-end states) to a single cached check.
Build-verified; smoke-tested via a bare -level_33 boot (no crash, no
D243 lines, as expected since bare -level_XX skips the front end).
Still needs a real interactive playthrough to actually correlate
against the D146 stale-DL burst -- documented as the next step in
findings.md D243 (M-141), including an unconfirmed D173-connection
hypothesis raised by the user.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 19:30:58 -04:00
James Dansereau beef5a37f8 Merge feat/d282-menu-nav-leftstick: D282 front-end menu nav now defaults to left stick
Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 19:28:07 -04:00
James Dansereau bb1e658ea5 fix(D282): front-end menu nav always uses left stick, matching F10-overlay convention
Menus read the same stick_x/stick_y channel as in-level gameplay, and
naturalPitchMode (SOLITARE-style, default-on) routes that channel to
the right stick for continuous look. Since front.c's menu nav shares
the channel, it silently inherited the right-stick routing, which is
unintuitive on a modern pad/Deck outside a running stage. The gamepad
poll already computed an unused padMenuMode flag for shoulder-button
gating; reused it to always source stick_x/stick_y from the left
stick outside RUN_STAGE, regardless of naturalPitchMode. Port-layer
only, inert during gameplay -- verified via -level_09/-level_20
unregressed; in-menu feel needs a human controller playtest.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 19:28:03 -04:00
James Dansereau e4211cced7 Merge fix/d252-rainbow-particles: D252 width/height suspect refuted, diag probe added; D283 doc confirmed accurate
Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 19:25:26 -04:00
James Dansereau 4b1d9bc21c docs(D252): refute width/height suspect via authored asset data; add GE_D252 diag probe
The prior "prime suspect" (import_texture_rgba16 width/height derivation)
assumed line_size_bytes comes from the tile's MASK_16 wrap period, but
assets/oddtextures.c's actual gsDPSetTile for the fire render tile sets
line=4 explicitly, giving the correct 16x14 dimensions -- matching the
pre-existing is_fire_shape==16x14 filter already hardcoded into the
GE_TEXDUMP dumper. Neither named D252 hypothesis (width/height, GEVR's
byte-group-order alternative) survives; root cause stays open. Adds an
env-gated GE_D252 diagnostic (dims + raw texel dump) for whoever next
gets an interactive session to trigger the actual fire draw.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 19:25:15 -04:00
James Dansereau e0167c8266 Merge docs/gevr-triage-adopt: cheap GEVR-triage doc adoptions
Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 19:18:17 -04:00
James Dansereau 8df8ad3c35 docs: adopt cheap GEVR-triage items (crash heuristic, frame-rate notes, process lesson, widescreen pre-flight)
Folds in the zero-code-risk ADOPT items from scratchpad/GEVR-TRIAGE.md
(a prior survey of the sibling GEVR VR port, same n64decomp/007
lineage, reference-only): a crash-triage heuristic for non-canonical
x86-64 pointer faults, two frame-rate-assumption notes that pre-empt a
future uncapped/high-refresh rabbit hole, a process lesson about
grep-built site lists standing in for a real read, and the
portal-scissor site map as pre-flight for whenever widescreen work
resumes.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 19:18:07 -04:00
James Dansereau eb0928889e Merge docs/d286-caverns-freeze: D286 Caverns freeze finding + log-unit fix 2026-09-15 18:55:32 -04:00
James Dansereau 65728e9f0e docs(D286): new finding - user-confirmed real freeze on Caverns, Steam Deck
Caught during the same live Deck session used to fix D255: the game's
own hang watchdog (portHeartbeatCheck) fired once, no frame rendered
for ~8s at frames=46404, dumped all-thread state, then rendering
resumed and the session continued to a clean close - not a crash, a
self-recovered stall. User confirmed by direct observation this was a
real, visible freeze (not a Deck-side system hiccup), happening early
in a Caverns playthrough, not tied to an explosion or terminal (rules
out D255/D285 as the cause).

Not root-caused - no distinguishing log line precedes it. Logged as
open with no repro trigger identified rather than guessed at further.

Also fixes a cosmetic units bug spotted while reading the watchdog
code: the heartbeat log printed a raw microsecond delta with a "ms"
label (port/src/libultra.c, no /1000 conversion) - now divided
correctly. No behavior change, log output only.
2026-09-15 18:55:26 -04:00
James Dansereau 2ec57eb40d Merge fix/d255-vtxstore-pointer-truncation: root-caused and fixed D255 2026-09-15 18:42:14 -04:00
James Dansereau 7ef0395a3f fix(D255): correct 32-bit pointer truncation in vtxstore_fix_refs
Root-caused via a live Steam Deck session over SSH: sub_GAME_7F09BAC4
(vtxstore_fix_refs) read an active OBJ record's Model* through
((Model*)((ChrRecord*)prop->chr)->chrflags) - a union pun onto
ChrRecord.chrflags, which is typed CHRFLAG, a 4-byte enum. On 32-bit
N64 this is bit-exact (the enum and a pointer are both 4 bytes, so
casting the enum's value to Model* reproduces the real stored bytes).
On 64-bit PC, Model* is 8 bytes but the enum read is still only 4 -
the cast reads just the pointer's low 32 bits as an int and
zero-extends it back to a 64-bit address, discarding the real high
bits. This is the D3x pointer-width ABI class from AGENTS.md - a
struct field misread across the 32-to-64-bit pointer-width transition.

This was actually flagged as suspect (a) in the very first D255
investigation, then wrongly deprioritized in favor of a "destroy-path
race" theory once a register capture happened to show Rdi=(nil) and
looked like it confirmed a different, unrelated NULL-model theory. It
didn't: (nil) is exactly what a 32-bit-truncated read produces
whenever the real pointer's low 32 bits happen to be zero - a
coincidence that pointed two sessions' worth of investigation at the
wrong field. Proof: reading the same record through the correctly
64-bit-typed ObjectRecord.model field (this session's diagnostic
probe) was always a valid, non-NULL pointer - the object's model was
never actually missing.

Fix: read the pointer through ObjectRecord.model (the same union
memory, correctly typed) instead of through the mistyped
ChrRecord.chrflags alias, at both use sites in the function. Identical
bytes and identical result on N64; correct on 64-bit PC.

Verified live on the user's own Steam Deck hardware over SSH, running
their own prepared ROM assets: held through the entire Facility level
including repeated terminal destruction (the most consistent prior
repro) and through Caverns' radio-objective terminals (the second
confirmed level), zero crashes. Windows rebuild + full 21-level
verify.sh sweep also clean.

The separate audio-thread crash (D285, caught during this same live
session) is unrelated and unaffected by this fix.
2026-09-15 18:42:08 -04:00
James Dansereau 49f18ad1e6 docs(D285): new finding - audio-thread SIGSEGV, separate from D255
Caught a real crash on the user's Steam Deck during D255 live-test
setup: a fresh ge007.crash.log from the published v0.2.1 binary,
inside the user's active testing window. Symbolized directly on-device
with addr2line against the crashing binary's own debug symbols.

FAULT ADDR 0x62 inside sndHandleEvent (src/snd.c:366), on the audio
thread (amMain/portThreadWrapper) - not D255's 0x10/vtxstore_fix_refs
on the render thread. Different bug, different subsystem, likely
conflated with D255 in reports since both surface during the same
explosion/combat testing.

Root cause (static, code-read only): sndHandleEvent's pool-exhaustion
preempt-scan walks the shared D_800243E4 ALSoundState list with no
osSetIntMask lock anywhere in the function, while the list's insert
path (sndSetupSound, reached from 15 different game files' direct
sndPlaySfx calls) does lock it - an asymmetric-locking gap matching
the established D4/D147/D152 "N64 interrupts-off isn't a real lock on
PC" race class. Fits the repro pattern (explosion SFX bursts = high
thread-contention window).

Not fixed - flagged two open questions before it should be: whether
the missing lock is a genuine decomp-fidelity gap (N64 assembly may
have had it) vs a legitimate new PC-only lock, and it needs gdb
confirmation on a live crash, not just this static read.
2026-09-15 18:12:30 -04:00
James Dansereau edcfb1f163 docs(D255): consolidate real-Deck evidence - destroy-via-explosion, not activation
Further Steam Deck testing on the published v0.2.1 release reconciles
the two competing D255 repro theories: the trigger is an object dying
via explosion, not terminal activation. Confirmed on four levels:

- Facility: every terminal crashes consistently when destroyed
- Caverns: terminals near the radio objective crash when they explode
- Archives: a similar crash when a guard's grenade detonates (no
  terminal involved at all - narrows the real trigger to "object dies
  via explosion damage" in general)
- Frigate: negative control - shooting a room of terminals there did
  not reproduce it, so it isn't simply "any destructible OBJ record
  exploding" either

Updated the D255 finding with a static lead for the next session (the
objFree destroy path never explicitly nulls ObjectRecord.model, so the
NULL likely comes from a freed-slot-reuse or list-modify-during-iterate
race rather than a direct field write - untested, needs a live Deck
capture with the GE_D255 probes already in tree). Updated
.github/release-notes.md's D255 bullet to match.
2026-09-15 17:58:25 -04:00
James Dansereau b322b55a70 docs(D255): log user's real-hardware Deck report (destroy-trigger, not just activation)
User reproduced on real Steam Deck hardware, v0.2.1 published release:
Facility crashes consistently when terminals are DESTROYED (shot), not
only on activation as the M-139 "corrected" framing concluded. Frigate
crashed once in the same pass, specifics not recorded.

Logging as a dated, evidence-stamped addition rather than overwriting
the existing activation-repro entry - both triggers are plausible
routes into the same underlying destroy-path-ordering race and are not
necessarily contradictory; neither has been cross-verified against the
other yet. Next Linux/Deck session should test both triggers with
GE_D255=1 before revising the "reliable repro" framing again.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 17:35:51 -04:00
James Dansereau 0c209dedd4 Merge diag/d255-vtxstore-null-model-probe: D255 diagnostic probes
Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 17:12:41 -04:00
James Dansereau 126fe75d7f diag(D255): env-gated NULL-model probe for the Facility terminal crash
Adds a live-repro diagnostic for D255 (Facility door-opening terminal
SIGSEGV, Linux/Steam-Deck-only): vtxstore.c's vtxstore_fix_refs
(sub_GAME_7F09BAC4) skips-and-logs instead of dereferencing when it
finds a NULL model on an active PROP_TYPE_OBJ record, logging the
record's identity/state; propobj.c's objFree logs every free call so
the two logs can be correlated by prop/obj pointer. Both gated behind
GE_D255 (off by default, getenv-cached per the D250 lesson) - zero
behavior change unless set.

Attempted a live Windows repro this session (5 terminal activations
across 3 direct -level_34 launches): zero probe hits, zero crashes.
Combined with the existing WSL-negative result, this is now two
negatives against one Linux/Steam-Deck positive - the bug does not
reproduce on Windows, likely a genuine allocator/timing difference.
Landing the probes anyway since they're inert by default and are the
fastest path to a real trace whenever a Linux/Deck session is
available; no more Windows time should go toward a live repro here.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 17:12:35 -04:00
James Dansereau 78bca455cf Merge fix/d284-prng-mips64-shift: D284 MIPS64 PRNG shift fix
Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 16:40:24 -04:00
James Dansereau 0de99b4db4 fix(prng): correct MIPS64 dsll32/dsrl32 semantics in port/src/random.c (D284)
random.c's dsll32/dsrl32 helpers modelled the real MIPS64 dsll32/dsrl32
instructions as ordinary 32-bit shift-by-n operations, masked to 32
bits. Real MIPS64 dsll32/dsrl32 shift the FULL 64-bit register by
n+32 with no masking - they exist specifically to encode a 32-63 shift
amount in a 5-bit immediate field. The bug was bit-exact-looking but
wrong: it produced a deterministic, self-consistent stream that was
never the N64's actual stream, from the very first draw.

Surfaced by M-138's security/decomp-fidelity review (a forked review
agent's numeric trace + a hand trace of random.s both found the same
divergence); fix decided over disclose-as-known-issue per M-139b.

Fix: dsll32(x,n) = x << (n+32), dsrl32(x,n) = x >> (n+32), no masking.
No other line changes - every call site (randomGetNext,
randomGetNextFrom, chrObjRandomGetNext) already mirrors random.s/
chrObjRandom.s line-by-line through these two helpers plus plain <</>>
(which already matched dsll/dsrl correctly).

Verified against a standalone C reproduction outside the game (first
five draws from random.s's real initial seed diverge completely
between the buggy and corrected helpers, confirming the fix actually
changes the output stream). 242/242 clean build; full 21-level
verify.sh sweep PASS, no crashes.

This silently changes every RNG-derived sequence (loot, AI variance,
anything seeded from randomGetNext/randomGetNextFrom) from the first
draw in every build going forward. Savegames are unaffected (seed
isn't save state); existing recorded replays will diverge against a
post-fix build. Called out in v0.3.0 release notes per M-139c.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 16:40:19 -04:00
James Dansereau 6807555c17 Merge fix/d266-d271-extraction: D266 IA4/texedge + D271 portal-culling fixes
Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 16:34:33 -04:00
James Dansereau 532092c732 fix(render): land D266 IA4/texedge decode + D271 portal-culling fixes (M-139 R2)
R2 extraction from the parked D236 investigation tree
(park/d236-probes-m139): pulls out only the two verified, behavior-real
fixes and drops every D267-D280 debug probe (census counters, PPM
dumpers, per-tri/per-portal logging) that had accumulated alongside them.

- D266: import_texture_ia4 decodes RDP IA4 as I2:A2 (was I3:A1, an
  inherited PD assumption); gfx_opengl.cpp's CVG_X_ALPHA "texedge" path
  now keys off the real alphacompare field (dither/threshold/none)
  instead of a hardcoded 0.19 cutoff, and gfx_pc.cpp excludes AC_DECAL
  tiles from the texedge path entirely (it never discards on real
  hardware).
- D271: the D106 near-plane portal-straddling guard's `lim` constant
  widens from 1e5 to 1e38 so finite out-of-range bounds flow through to
  bgRectIntersect and clamp/cull exactly as they do on N64, instead of
  being force-mapped to full-screen (was drawing whole rooms N64 culls).

Checked D271 against D249 (distant-geometry drop-out on Streets/Egyptian)
before landing per M-139's overlap concern: no overlap, different code
paths (near-plane portal guard vs. far-plane/LOD symptom); D249 stays
open.

Verified: 242/242 clean build; full 21-level verify.sh sweep clean (one
dam NO-FRAMES on the sweep pass reproduced PASS on an immediate solo
re-run - a load-sensitivity flake, not a regression).

The D267-D280 probe code and the rest of the D236 tree-wall
investigation stay parked on park/d236-probes-m139 for whenever that
thread resumes (still no root cause for the tree wall itself - see
D280's negative outcome).

Also documents a build-environment gotcha hit this session: this Bash
tool's shell reports MINGW64 (uname/$MSYSTEM) but is actually
Git-for-Windows Bash, missing cmake/ninja/gcc from PATH until
/c/msys64/mingw64/bin and /c/msys64/usr/bin are prepended explicitly
(docs/porting-notes.md SS E).

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 16:34:28 -04:00
James Dansereau e7fee0d7a4 docs: v0.2.1 known-issues sweep, pre-1.0/roadmap framing, AI disclosure
Batched pass across README/release-notes/Pages (one commit instead of the
scattered per-fix pushes earlier this session, per new commit-cadence
preference):

- New known issues surfaced that weren't documented anywhere: no real
  widescreen (16:9 stretches 4:3 rather than expanding FOV), no controller
  rebinding UI / macOS-ARM builds, culling/LOD dropout on Streets/Egyptian
  (D249), gunshot SFX oddities under sustained fire (D240/D241).
- Fixed a stale claim (in three places) that a wrong-sounding-instrument bug
  was still open; it was fixed in v0.2.0 (D230).
- Softened the D259 "All unlocked" fix to reflect reality: still breaks
  audio/aim if enabled before any save exists; flagged experimental
  (new finding D281).
- Corrected D255's repro to the precise, deterministic trigger found this
  session (Facility's door-opening computer terminals, Linux-wide not
  Deck-specific) instead of the vaguer original combat-crash description.
- Logged D282 (front-end menus need right stick not left on gamepad) and
  D283 (Steam Deck first-run preset skipped by an early Desktop Mode
  launch) with user-facing workarounds.
- Refined D263 (Ourumov/Trevelyan) internally with a position-sensitivity
  caveat; no public bullet since it doesn't block progression.
- Added pre-1.0 framing and a new README Roadmap section (known-issues
  cleanup, PAL/JP, real widescreen, rebinding, macOS/ARM, LAN multiplayer
  explicitly "under consideration, no ETA", plus an explicit "not currently
  planned" list) across README/release-notes/Pages.
- Added an AI-development disclosure (agentic development as a genuine
  study of the process, not just a port) across the same three docs.
- Pinned the release-notes hero GIF to the v0.2.1 tag instead of v0.2.0.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 12:53:57 -04:00
James Dansereau 4b51ded7d1 docs: SEO round 2 (Bing) - demote per-page h1 to h2 site-wide, re-trim meta description, bump README/Pages to v0.2.1
Bing Webmaster re-flagged the site after the first SEO pass (32f193a0):
- Every Jekyll-processed doc page (building.md, dev-process.md, internals.md,
  porting-notes.md, dev/agentic-development.md, plus index.md caught last
  time) opens with its own "# Heading" on top of the Cayman theme's masthead
  <h1 class="project-name">, so all of them emitted two h1 tags, not just the
  homepage. Demoted each page's opening heading to h2 -- anchor slugs are
  unaffected (kramdown ids don't depend on heading level).
- index.md's meta description was 161 chars (1 over Bing's 160 limit) and
  still said v0.2.0; re-trimmed to 133 chars and bumped to v0.2.1.
- README.md and docs/index.md's Status lines/comparison table bumped from
  v0.2.0 to v0.2.1 to match the actual current release.

Verified against a live, cache-busted fetch of the deployed Pages site
(the first Bing report was reading a stale CDN edge cache from before the
32f193a0 deploy, per its own Last-Modified header) that the underlying fix
was already live; this pass addresses what was still genuinely wrong.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 10:04:02 -04:00
James Dansereau ff677da74d fix(ci): regenerate findings-index.csv (stale after D216/D181 edits)
Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 09:36:44 -04:00
James Dansereau 795c8f176f release-notes: document ScreenShakeIntensity menu removal (D181)
Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 09:29:21 -04:00
James Dansereau 1e11e1a5fd v0.2.1 polish: pull Game.ScreenShakeIntensity from the F10 menu (D181)
User testing found the slider "basically useless" -- viShake() only scales
explosion/effect shake (explosion.c), never the always-on walking head-bob
or a getting-shot reaction, which is what "Screen shake" implies to a
player. Removed the menu row until it covers all screen-shake/view-bob
sources, not just explosions. Config var + fr.c hook untouched.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 09:28:06 -04:00
James Dansereau 99f4f38e1c v0.2.1 polish: pull Game.SkipIntro from the F10 menu (breaks audio)
User spot-check of the v0.2.1 draft found that enabling Skip intro breaks
audio. It was already off by default (D216); removing the menu row so
players can't enable it until the audio interaction is root-caused. The
config var and lv.c hook are untouched (dead unless an existing ini already
has it set).

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 09:18:40 -04:00
James Dansereau 06b675b5e8 fix(ci): revert GE-ENV-PROBES.md rows for uncommitted D236 probes
The v0.2.1 docs commit (572ab7bf) added GE-ENV-PROBES.md rows for the
D266-D280 tree-investigation probes, but that probe code itself was
deliberately left uncommitted (still mid-investigation, unswept). CI's
tools_pc/gen_env_probes.py --check caught the mismatch: 13 documented
vars with no live getenv() site in the committed tree. Reverting the
doc rows to match what's actually committed; they'll return together
with the probe code once the D236 investigation lands.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 08:59:51 -04:00
James Dansereau 572ab7bf5b docs: findings/backlog through D280 (v0.2.1 batch + D236 tree-wall passes 2-9)
Records the v0.2.1 hotfix batch (D259-D264, this commit's code) and the
ongoing D236 Surface-1 tree-wall investigation (D265-D280): still open as of
D280 -- fog math verified N64-faithful, the noise class confirmed as the
weave's painter, the portal-culling over-draw fix (D271) real but
insufficient, no quick win identified. The D266-D280 probe code and the
D266 IA4 decode / portal-culling fixes themselves stay uncommitted pending
the game-wide regression sweep GRAPHICS-BACKLOG.md flags.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 08:21:56 -04:00
James Dansereau 32f193a02f docs: trim GitHub Pages descriptions for SEO, drop duplicate h1
_config.yml's description was ~230 chars once jekyll-seo-tag spliced it
into the masthead tagline and home <title>; index.md's page description
was ~277 chars for the same og/meta tag. Also removed index.md's manual
"# GoldenEye 007 PC Port" h1 -- the Cayman theme already renders the site
title as an h1, so the page was emitting two.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 08:21:42 -04:00
James Dansereau ffb786bad8 v0.2.1 hotfix batch: D259 audio silence, D260 wheel direction, D261 watch-nav repeat, D263 script order, D264 watch preview model
User playtest reports against v0.2.0:
- D259: geEepromPatchAllCheats left an all-zero fresh save at volume 0
  (silence) instead of seeding max volume like fileResetSave would.
- D260: swap wheel-up/wheel-down to match the PC wheel-menu's on-screen
  order (was inverted vs. user expectation).
- D261: watch-menu stick-held navigation had no repeat past the smooth-scroll
  band, so holding W/S or a pegged stick crawled one item per tap; added a
  bounded auto-repeat.
- D263: GE_INPUTSCRIPT stick tokens were applied after the stick was already
  written out and silently discarded (test-harness fix, kept from the D263
  Facility/Ourumov investigation, which itself found no port bug).
- D264: watch item-preview ModelRenderData template read across two globals
  that no longer sit adjacent on PC, zeroing renderdata.flags and gating all
  geometry emission in the preview's subdraw pass.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 08:21:32 -04:00
James Dansereau b0dc9d1257 README: absolute URLs for Releases/issue links (leading-slash forms 404 on GitHub) 2026-09-14 01:04:24 -04:00
James Dansereau de6c875bb8 docs: cut-content research — article bodies recovered, claims verified vs decomp
All six TCRF article bodies were extracted (backlog/cut-content/articles/,
gitignored) and cross-referenced against src/ + assets/obseg/. Full verified
findings with file:line refs land in backlog/cut-content/RESEARCH.md.

Verified 1:1: the 11 unused cheats (CHEAT_ID enum vs cheat.c grant table),
music track index (MUSIC_TRACKS enum vs Notes-page GS index), MP character
table (~40 entries, 1.0446 eye height), all unused text strings (LmiscE/LgunE/
LpropobjE/LtitleE), build dates (compiletime.c), Spectrum emulator plumbing
(front.c MENU_SPECTRUM_EMU — only the transition is missing).

Corrections to TCRF/first pass: bg_dest is shipped Frigate geometry (not an
early version); cryp/dish are shipped Egypt/Temple; Library/Stack share
Basement's bg_ame; 28 of 29 unused items exist in the object table.

Tracked doc updated to point at the research file; backlog/ gitignored.
2026-09-14 01:02:07 -04:00
James Dansereau 395c616fc5 docs: cut-content research backlog from TCRF export (first pass)
Cross-reference the local TCRF 'cutting room floor' export
(scratchpad/backlog-scrape/restore-cut-content-mod, gitignored) against the
decomp source and extracted asset tables. Article bodies were lost to a
Cloudflare challenge at scrape time; findings rest on the ~170 intact article
images + all image filenames, each mapped to ROM object/level evidence:

- A: full debug menu (U+D C-hold) decompiled in debugmenu_handler.c — free cam,
  level switch, ramrom record/replay, screenshot grabs; all compiled into PC build
- B: cut weapons (taser/rocket/grenade/P90/spectre/autoshot) have models + stats
  + SFX; rocket/grenade/P90 grantable via existing cheat codes (cheat.c)
- C: 'unused items' page maps 1:1 to file_resource_table entries with use-logic
- D: 8 commented-out MP map entries in front.c with dev notes (Citadel/Statue/
  Cradle/Aztec/Runway/Dam/Depot/Frigate)
- E: per-level unused props + orphaned level geometry (Crypt has bg+briefing but
  no LEVELID; len/dish/cat/oat/arec/ame/ark/ref dirs beyond shipped set)
- F/G/H: unused faces, prerelease comparison pairs, dev-build video embeds

Feasibility tiers 0-3 with the port-layer-only constraint called out.
2026-09-14 00:48:30 -04:00
James Dansereau e1cb96ef19 v0.2.0 final: AllUnlocked default OFF (D259 audio quirk); raise draw/LOD distance + MSAA defaults 2026-09-14 00:18:59 -04:00
James Dansereau 436484a926 docs: final v0.2.0 pre-tag pass
- Drop-in ROM wording everywhere (README quick start, Pages landing,
  release notes, bundled README): first launch auto-generates the
  derived assets via the frozen converter; no Python/toolchain needed.
  PAL/JP marked not-yet-supported with issue #85 pointers.
- building.md: Linux build is CI-compiled and ships in the bundle
  (was 'never built or run on Linux'); bundle note now describes the
  frozen ge007-convert auto-spawn; PAL/JP sidecar caveat (D258).
- Status rewording across README/Pages/release notes: drop the
  'first open-source port to our knowledge' claim; cutscene glitch
  description aligned to 'mostly with James Bond'; index.md status
  no longer contradicts the open D255 Deck crash.
- Em-dash cleanup across all GitHub Pages docs (index, building,
  internals, porting-notes, dev-process, agentic-development,
  release notes); internal dev logs untouched.
2026-09-13 23:51:27 -04:00
James Dansereau b2d5551ece Drop-in ROM: first-run sidecar conversion, no Python (issue #6) (#84)
* Drop-in ROM: first-run sidecar conversion, no Python (issue #6, Part A + interim)

The release bundle now turns a dropped-in NTSC-U ROM into a playable game
with zero tooling: when data/pcmodels-* / data/pccg-* are missing at boot,
the engine spawns the frozen prepare-assets converter shipped in the bundle
(prepare-assets/ge007-convert[.exe], PyInstaller --onefile), waits for it,
and re-checks before continuing (port/src/romconvert.c, called from
romdataInit after ROM validation). Failure aborts boot with a message
instead of the later D179-class crash.

- prepare-assets.py: runs in-process via runpy when frozen (no user Python
  to exec); NTSC-U-only gate with a clear message for PAL/JP ROMs; d88's
  sibling import needs an explicit sys.path entry (run_path does not set
  one). Verified: frozen exe output byte-identical to the unfrozen path and
  to the shipped sidecars.
- CI (linux + windows): PyInstaller freeze step before bundling
  (ensurepip / --break-system-packages for MSYS2's pip-less python).
- bundle scripts: copy ge007-convert into prepare-assets/ (hard-fail if not
  frozen), size guard 60 -> 120 MB.
- Release notes: US-only drop-in instructions, no Python required.
- D258: PAL/JP filelist naming defects documented; multi-region deferred to
  the Part B follow-up.

* CI: install pip via pacman on Windows (MSYS2 ensurepip is disabled); regen findings index for D258
2026-09-13 23:17:29 -04:00
James Dansereau 99bc2a3a6a D230: byte-swap RAW16 wavetable samples at cart-map time (BE ROM -> host LE) (#83)
The one broken bass instrument on Control/Cavern/Runway is a plain
byte-order bug in a corner the earlier passes never looked at. The
instrument bank's only two AL_RAW16_WAVE wavetables (instruments 2 and
51, sharing base 0x2118 / len 790) decode as a clean ~110 Hz soft-attack
bass sample when read big-endian s16 -- which is how the N64 CPU reads
them. The PC mixer's resampler consumes host-native (little-endian) s16,
so every sample arrives byte-swapped: deterministic per-instrument
corruption, exactly the M-106 repro. ADPCM nibble data is byte-order
agnostic, which is why every other instrument sounds right.

romdataFinishCartMap() now walks both bank trees (same ALBankFile layout
as romdataFixupAudioBank) and bswap16s each RAW16 [tbl+base,+len) range
in place in the cart map -- where alBnkfNew() points the mixer at it
directly. Shared ranges are deduped (swapping twice undoes the swap);
all reads bounds-checked. D55 in-place-fixup idiom; port-only, no game
logic touched.

Startup log: 'romdataInit: D230 byte-swapped 1 RAW16 wavetable range(s)
in place'. Owes a by-ear pass on the three repro tracks.

findings.md D230 -> FIXED with full root cause; release-notes.md drops
the music known-issue bullet.
2026-09-13 23:13:12 -04:00
James Dansereau 2c95fcd145 Release notes: drop mute-on-focus-loss claim (D231 ships post-v0.2.0 via #56) 2026-09-13 22:23:12 -04:00
James Dansereau d6561a15b3 README/Pages: replace Sept-1 attract shots with v0.2.0 playtest captures
The old attract-bunker1/attract-dam PNGs predate the D227 sky, D229 water
and 60 fps fixes. Point both pages at the Sept-13 playtest gallery instead
(shot-06 Bunker 1, shot-01 Dam) — same files as the gallery below, honest
captions — and drop the stale PNGs.
2026-09-13 22:20:49 -04:00
James Dansereau 37f86bae2d Media: drop the mp4 montage, use the GIF in README + Pages; remove orphan bug shots
- README and docs/index.md (Pages) now embed goldeneye-gh-preview.gif
  where the mp4 <video> tag was; release notes already used the GIF
- git rm the 27 MB mp4 (GIF stays — it is the release-notes asset, pinned
  to the v0.2.0 tag)
- remove three unreferenced docs/img/bugs screenshots left over from the
  culling/D176 A-B captures
2026-09-13 22:18:52 -04:00
James Dansereau 8c3ce1c688 Build provenance: origin URL + codename in --version and startup log; fork policy in CONTRIBUTING (#82)
- versioninfo gains GE007_VERSION_CODENAME (larkspur) and GE007_ORIGIN_URL,
  printed by --version (new 'origin' line) and the startup log. A build keeps
  pointing at this repository no matter who compiles or renames it.
- CONTRIBUTING: new 'Forks and derivative works' section — rebranding,
  localizing and extending are welcome without permission; attribution (with
  a link) and preserving/linking the docs/dev engineering record are the ask;
  stripping the attribution is what disqualifies a fork as a descendant.
2026-09-13 22:14:41 -04:00
James Dansereau 69582caeb0 Release notes: finalize for v0.2.0 final (#81)
* Release notes: finalize for v0.2.0 final (CI re-stamps the body on tag cut)

- Drop '(pre-release)' from the title; <version> stamps to 0.2.0, so the
  Downloads/Verify tables already name the final assets correctly.
- Blockquote rewritten: no more 'cut is for playtesting'. Claims what is true
  (21 missions load and run crash-free) without claiming an end-to-end
  completable campaign; adds the first-public-release note, hedged with
  'to our knowledge'.
- New What's-new bullets: Everything-unlocked-by-default (D257, ON by default,
  F10 opt-out, no active cheats) and the Steam Deck first-run preset + the two
  new F10 rows (No hit flash / Skip intro).
- Known issues unchanged — all eight walked with the user and confirmed still
  open; wording already matches the latest reports.
- Preview montage GIF at the top of the body (release pages can't play mp4);
  raw URL pinned to the v0.2.0 tag, so it resolves once PR #80 has merged and
  the tag is cut.

* Release notes: campaign-completed claim + D255 register-capture diagnosis

- Blockquote: full campaign playtested end to end (all 21 missions, Agent
  difficulty) per the v0.2.0-pre PC playtest
- D255 bullet: updated from 'narrowed to a stale model reference' to the
  pinned diagnosis (NULL ObjectRecord.model on an active record during
  mid-firefight destruction; repros on Bunker + Frigate)
2026-09-13 22:12:43 -04:00
James Dansereau 608808a212 Docs: v0.2.0 finalization — README/Pages + media pass (#80)
* README: 32 s gameplay preview montage (mp4) + preview-video runbook

* Prune: drop superseded goldeneye-demo.gif (replaced by gh-preview montage)

* Docs: v0.2.0 finalization pass — final wording, aligned known issues, Deck-verified line, social preview card

- README + pages site: 'v0.2.0 (pre-release)' -> v0.2.0 throughout; the
  stale 'not yet verified on real Deck hardware' line is replaced with the
  playtest fact (D253 fixed, D255 still open).
- Known-issues lists in README + site now match the release notes one-for-one
  (adds the F10 bottom-row duplicate D251 and the intermittent Deck SIGSEGV
  D255; D243/D230/D252/D245/D246/D75 carry their finding labels).
- Working list mentions AllUnlocked default-ON (D257) with the F10 opt-out.
- First-public-release note added to the README hero and site status line,
  hedged with 'to our knowledge'.
- social-preview.png (1280x640 GitHub link card) composed from a gameplay
  frame of the preview montage; scratchpad/make-social-preview.py is the
  local recipe (gitignored, like make-preview.py).
- Commit docs/img/shots/manifest.csv (gallery -> source recording map);
  gitignore the regenerable CONTACT-SHEET.jpg; drop scratch-gdb-cmds.txt.

* Social preview: regenerate card from updated briefing HTML (v0.2.0 final wording)

The PNG committed earlier was a screenshot of the rendered site (stale
wording, video element unrendered). The canonical source is
.github/social-preview.html per .github/social-preview.md; update its
Status/Statistics lines to the v0.2.0 final state and re-render:

- Status: RELEASED v0.2.0 — WINDOWS · LINUX · STEAM DECK
- All 21 solo missions — full campaign completed in playtesting
- ~22k lines C/C++ · ~10k Python (port layer + tools)
- ~250 bugs root-caused · two coding agents, one human, part-time

Verified by pixel-diff against the previous render: only the four text
bands changed; title/fonts/layout identical. Drop the unreferenced
root-level social-preview.png.

* docs: refresh social preview + status for v0.2.0 playtest results

- Social card: 'RELEASED v0.2.0', 21/21 missions completed in playtesting,
  ~250 bugs fixed, two agents + one human; re-rendered the PNG from the
  canonical .github/social-preview.html source (pixel-diff vs the old render
  shows only the four edited text bands changed)
- README Status: campaign now claimed as completable end to end (user
  playtest: full campaign, Agent difficulty, PC); Working list matches
- findings: D255 updated with the Deck register capture (Rdi=nil -> NULL
  ObjectRecord.model, destroy-path ordering case confirmed; truncation ruled
  out) and the rule-#2 sign-off question for a guard; D254 field-verified

* docs: regenerate findings index (D254/D255 status updates)
2026-09-13 22:12:13 -04:00
James Dansereau c62803f890 D257: Game.AllUnlocked option — all levels, 007 mode, full cheat menu (#79)
Port-layer only (no game-code edits):
- main.c seeds debug_enable_all_levels_flag + debug_007_unlock_flag
  after configLoad() when Game.AllUnlocked is set (same class as the
  existing GE_UNLOCK_ALL getenv hook).
- libultra.c eep shim patches the save block at read time (the exact
  fileValidateSaves pattern: block 4, 5x96 bytes): sets all 20
  progression-gated cheat-unlock bits per slot and fills every empty
  stage/difficulty completion time with the max (0x3FF), preserving
  existing player times, then recomputes each slot's CRC with the
  game's own fileGenerateCRC so validation passes. The completion-time
  half is required: with any SP cheat active, g_AppendCheatSinglePlayer
  makes mission select accept only STAGESTATUS_COMPLETED levels, so an
  unlock-only save locks every level once a cheat is turned on.
- optionsoverlay.c: 'All unlocked' toggle row (default ON for this cut).

Verified: fresh-eep GE_SAVELOG shows all 5 slots stored==calc; user
playtest confirms all levels at each difficulty, 007 mode, populated
cheat menu, cheats working, mission select intact with all cheats on.
2026-09-13 21:48:09 -04:00
James Dansereau c51f01ad14 F10 overlay: expose Game.NoHitFlash and Game.SkipIntro toggles (#78)
The two user-facing config keys were registered but only reachable by
hand-editing ge007.ini. Add ROW_TOGGLE rows (below Screen shake):

- No hit flash  (D232, community "no damage flash"; suppresses the
  non-black fade paths in bondview2)
- Skip intro    (D216; read at title-stage load, so it takes effect the
  next time the title screen is reached)

Video.FixMipTextures / Video.WrapFix deliberately left out: opt-in
fidelity bug-fixes, not preferences.

Verified live (isolated CWD): both rows render in the F10 panel and
toggle; clean exit persists Game.NoHitFlash=1 + Game.SkipIntro=1 to
ini; relaunch with SkipIntro=1 logs "Game.SkipIntro: booting to
file-select" and skips straight to mission select. NoHitFlash's
in-game suppression rests on its prior D232 verification (needs
combat, not automatable).
2026-09-13 21:44:56 -04:00
James Dansereau da31edaa70 Steam Deck: first-run preset (native 1280x800 fullscreen, MSAA 4, 150% draw/LOD distance) (#77)
STEAMOS detection in main() seeds video config defaults before configLoad;
first-launch-only by construction (an existing ge007.ini always wins). The
150% DrawDistance/LodDistance addresses authored N64 fade distances reading
short on the close-up Deck panel (e.g. the Dam lock fading in within sight)
while desktops keep the faithful 100% default.
2026-09-13 21:43:29 -04:00
James Dansereau f8526f66d0 CI: re-stamp release notes on every tag cut, not just at creation
The release job only set --notes-file when creating the draft; existing
drafts got 'upload --clobber' (assets only), so the body stayed frozen at
the first cut's notes. Compute the stamped notes unconditionally and add
'gh release edit --notes-file' to the update path.
2026-09-13 20:33:00 -04:00
James Dansereau 1f71bccbdb Release notes: D256 save-persistence fix, D255 status update
D256 (Linux saves/config now persist from any launch dir) is in this cut
and Deck-verified; the D255 line reflects the narrowed diagnosis and the
register-capturing crash handler. Drop the 'no known crashes' claim from
the pre-release blurb since D255 is open.
2026-09-13 20:29:38 -04:00
James Dansereau 4ca075061e D256: Linux $S (saves/config) now falls back to the exe dir, mirroring Windows
romdataInit finds the ROM in CWD data/, exe-dir data/, or ./, but the
POSIX $S branch only ever produced a CWD-relative path, so any launch
from another directory ran fine while every EEPROM and ini write failed
silently (no ./data/ to fopen into) — saves never persisted and F10
settings reset on every launch. Mirror the Windows branch via
readlink(/proc/self/exe) and mkdir the target dir best-effort. Verified
on WSL with exe+data in /tmp and CWD=/: config, eeprom, and ROM all
land in <exedir>/data/.
2026-09-13 20:17:34 -04:00
James Dansereau c7898d1620 D254/D255: stop the Linux crash handler eating its own logs; document the Facility firefight crash
The POSIX handler re-entered on SIGABRT (sysFatalError -> abort) and
truncated ge007.crash.log, so field crashes came back as bare abort
records with no faulting PC or registers. Guard against re-entry,
terminate directly with _exit(), and dump si_addr + the general register
set from the ucontext — a Deck crash now names the bad pointer on its
face.

Also documents D255 (new SIGSEGV in vtxstore's active-object walk during
a Facility firefight on the Deck; disassembly shows the fault is a
32-bit-zero-extended read of ObjectRecord.model, misnamed ChrRecord.
chrflags by the decompiler) and updates the release notes' Deck status.
2026-09-13 19:28:25 -04:00
James Dansereau f091619553 Release notes: known-issues pass from v0.2.0-pre playtest
- D197 (Silo face wrap): closed per user playtest, dropped
- D75: reworded — Nintendo logo still broken, Rareware logo filtering off;
  mission-complete/mode-select screens have no 3D models (old framing wrong)
- F10 ghost row: now deterministic bottom-row duplicate of the selected item
  (D251 addendum); earlier intermittent 4K framing superseded
2026-09-13 19:02:04 -04:00
James Dansereau 49f843a156 Regenerate findings index (D253) 2026-09-13 18:58:20 -04:00
James Dansereau 20368d1d17 Docs: drop false 'no widescreen' limitation; Deck install via SFTP or releases download 2026-09-13 18:56:40 -04:00
James Dansereau 1db6992e5e Fix D203/D253: Facility SIGSEGV on Linux — callback prototype ABI mismatch
stanCheckLinkedSpecialTile was decompiled with an all-s32 middle-arg
prototype while its callers pass it through the standTileLocusCallback_B_t
typedef (f32 f32 f32). On x86-64 SysV the int/float class split puts the
6th arg (outFlags/record) in %rdx per the typedef but %r9 per the
definition, so the FORCECROUCH/LADDER store hit a stale scratch register.
MIPS o32 passes both through GPRs a0-a5 identically, which is why the
mismatch was invisible on N64 and in the decompilation.

ABI-class edit (AGENTS.md #2 narrow exception): body never uses the three
middle params; N64 codegen unaffected. Verified under gdb on WSL:
pre-fix SIGSEGV at stan.c:2441 with r9=0x3 and a valid record in the
caller frame; post-fix the same walk route completes clean.
2026-09-13 18:55:26 -04:00
James Dansereau 060d5ac86d Docs: README + Pages landing overhaul for v0.2.0; Deck settings docs.
- README: manual Contents TOC dropped (GitHub auto-generates); Download
  table moved to the top with Quick start under it; Status compressed to
  a Working paragraph + tight known-issue list (D-numbers live in the
  release notes / finding log now); How-it-works tightened.
- Pages (docs/index.md): demo gif is the hero; download table up top;
  new How-it-works and Honest-status sections; legal footer; the 19-shot
  playtest gallery unchanged.
- Options overlay is now gamepad-driven: README Steam Deck section, the
  bundle README (new @DECK@ block on Linux, controller column added to
  the controls table), and the release-notes template all document
  Select / D-pad / A / B / Start instead of a Steam key-mapping workaround.
2026-09-13 18:40:57 -04:00
James Dansereau 72af96cf0d Options overlay: native gamepad support (Select / D-pad / A / B / Start).
Controller-only machines (Steam Deck) had no way to open or navigate the
F10 options overlay -- it was keyboard+mouse only. Now:

- Select (BACK) rising edge toggles the overlay, tracked across both the
  open and closed poll paths (padSelectPrev) so a held button cannot
  re-toggle on the transition. BACK is read nowhere else in the port.
- While open, D-pad or left stick up/down moves the selection, A/X step
  forward (the Enter equivalent), B/Y step back, Start closes. The gamepad
  bits are OR-ed into the existing keyboard edge logic, so repeat/clamp/
  scroll behaviour is identical; input.c already swallows pad 0 while the
  overlay is open, so nothing reaches the game.
- inputPadButton/inputPadAxis accessors expose raw SDL pad state to the
  overlay (unaffected by our logic-layer swallow).

Smoke-tested headless with GE_OPTIONSOVERLAY=1 and no pad attached.
2026-09-13 18:40:52 -04:00
James Dansereau 97ba1e9b07 Pages: caption remaining gallery shots (7 Silo, 9 Frigate) — all 19 now labeled. 2026-09-13 18:26:42 -04:00
James Dansereau 5768bd7f46 Pages: level captions on the playtest gallery (owner-provided).
Shots 7 and 9 remain uncaptioned pending owner.
2026-09-13 18:26:00 -04:00
James Dansereau f6ffa41c58 Pages: trim playtest gallery to 19 captures (drop 8,12,14,15,17,18,22 per owner review).
Numbering kept as-is (gaps remain) so the contact-sheet numbers stay
valid for the pending caption pass.
2026-09-13 18:24:23 -04:00
James Dansereau 998bbd5364 Pages: v0.2.0-pre playtest gallery (26 in-engine captures).
Resized to 1600px width / q85, clean names (shot-NN.jpg), in a
collapsible section so the landing page stays light. Raw captures and
the curation work-products (contact sheet, manifest) stay local under
docs/img/screenshots/ (gitignored). Per-image captions/level names and
trimming pending owner review of the contact sheet.
2026-09-13 18:22:25 -04:00
James Dansereau c5e5abea52 review pass: shoulder edge-state in menus; release-notes asset names
Two defects found in the local review of the unpushed v0.2.0-pre work:

1. input.c: padShoulderPrev was only updated outside menu mode, so a
   shoulder held across the menu->game transition fired a spurious
   weapon cycle on the first gameplay poll. Edge state is now tracked
   every poll; emission stays suppressed in menus where A means select.

2. ci.yml release job: the notes template substitutes <version> with the
   raw tag (v0.2.0-pre), but bundle-{linux,win}.sh strip the leading v
   when building asset names -- the published notes would have linked
   goldeneye-pc-port-v0.2.0-pre-* assets that don't exist. Substitute
   the stripped form (NOTES_VERSION) instead.
2026-09-13 17:16:07 -04:00
James Dansereau 3da19f7539 docs: README + Pages landing overhaul for the v0.2.0-pre push
README:
- compress the Background section (two-agent detail now one tight
  paragraph; full write-up stays in agentic-development.md)
- new 'Beyond playing' section: tweak it (ini/F10/-fresh), read it
  (internals + porting notes as the entry points, src/ vs port/ split),
  mod it (MIT port layer, CONTRIBUTING ground rules, docs/dev record)
- Quick start no longer implies NTSC-U only

Pages (docs/index.md, served from main:/docs):
- status was stale ('alpha, v0.1.0') -- now v0.2.0 pre-release with the
  honest rough-edges summary linking to README #status
- new Downloads section up top: Windows + Linux/Steam Deck bundles with
  the no-ROM note and the four-step path
- 'Play it - or take it apart' CTA mirroring the README sentiment
  (try it / read the code / mod it / research angle)
- in-engine gallery (attract views + demo clip); more captures land
  here as playtesting continues
2026-09-13 16:05:49 -04:00
James Dansereau 517754546b README: document -fresh clean-slate launch flag 2026-09-13 16:03:01 -04:00
James Dansereau 798aebcb64 main: -fresh flag wipes save + config before startup
Launch with -fresh (or --fresh) for a clean-slate run: removes
$S/ge007.eep (the file-backed EEPROM save, all five slots) and
$S/ge007.ini before configLoad / the eeprom's lazy load touch them.
The ini is re-written with defaults on exit as usual; the .eep only
reappears if a save is actually written. Verified in a scratch data
dir: both files removed at launch, ini regenerated as defaults.
2026-09-13 16:02:50 -04:00
James Dansereau 135df58e0a input: modern dual-stick controller layout (Xbox re-release scheme)
Steam Deck target. LS move/strafe + RS look + RT fire + LT aim were
already in place (D194/D238 naturalPitchMode); this completes the
scheme with shoulder weapon cycling per the console re-releases:
RB rising edge = next weapon, LB rising edge = previous weapon.

Cycling rides the game's own A-line formulas (bondview2.c
weaponForwardOffset/weaponBackOffset): a fresh A edge cycles forward;
A+Z presented on the same tick cycles backward -- the identical trick
the mouse-wheel path uses (D223), which also guarantees no stray shot
(triggerOn is gated off while invButtons is held). Edges are emitted
for exactly one poll so holding a shoulder cannot latch A and block
firing; both are suppressed in menu mode where A means select.

A/X = action/use, B/Y = crouch/cancel (unchanged); LB no longer maps
to lean. README controls table + release notes updated.
2026-09-13 15:58:03 -04:00
James Dansereau 3ed75e72f5 bundle-linux.sh: match ldd's absolute paths in the SDL2 check (CI red fix)
The $ORIGIN verification compared ldd's absolute resolved path against the
relative $OUT, so it always failed even though the bundled libSDL2 was the
copy being resolved (CI 34778363397 showed exactly that). Match both forms;
the remaining-deps listing now filters by bundle dir name for the same
reason.
2026-09-13 15:49:37 -04:00
James Dansereau 2f002a31aa D252: document the rainbow particle-palette bug (no fix for v0.2.0-pre)
User playtest report: bullet-impact sparks and lingering smoke/explosion
residue drift through the full hue wheel over time instead of holding their
intended grey/orange palette. Logged in findings §F (D252) with this
session's mechanism work: particle colour comes entirely from the 15
distinct IMAGE_SMOKE_N x IMAGE_FIRE_N (RGBA16) pairs the particles age
through (assets/oddtextures.c records, white default vertex); the M-82/D172
tile-offset fix and the 5551 decode are both verified correct for this path;
prime suspect is import_texture_rgba16 deriving width/height from the tile
line size (32x7 instead of 16x14 for the 16px-wide fire tiles), which would
scramble each aging stage into a different wrong hue -- exactly the observed
slow rainbow sweep. Per user instruction: documented only, ships as a known
issue (README + release notes + GRAPHICS-BACKLOG); index csv regenerated.
2026-09-13 15:49:34 -04:00
James Dansereau 52f79b2cb6 Credits: GEPD Edition mouse-aim reference (Carnivorous; 1964 team)
The in-game GEPD-style aim mode (D194/M-122) is modelled on the Mouse
Injector plugin for 1964 -- credit its author Carnivorous and the 1964
developers Rice & schibo in the README Credits section. Note the port's
implementation is an independent reimplementation of the behaviour, not
derived code.
2026-09-13 15:49:28 -04:00
James Dansereau 4065a0007d Bundle SDL2 in the Linux tarball (turnkey Steam Deck sideload)
bundle-linux.sh now copies the exact libSDL2 the exe was linked against next
to it and sets the rpath to $ORIGIN via patchelf, with an ldd assertion that
the bundled copy is the one resolved. zlib/libGL remain system deps
(preinstalled on desktop distros and SteamOS). CI installs patchelf; the
generated in-bundle README gains a Steam Deck sideload note and the SDL2
zlib-license copyright file is carried when the distro ships one.

Also refreshes the release-facing docs that ship with / describe the cut:
  - .github/release-notes.md: v0.1.0-alpha body -> <version> pre-release body
    (what's new + known issues incl. D243/D230/D245/D246/D197/D75, the D251
    4K ghost row, and 'not yet verified on real Deck hardware')
  - tools_pc/dist/README.md.in: alpha claims (no audio / black sky / US-only)
    -> pre-release status; ROM table now lists all three regions (the asset
    step auto-detects by SHA-1)
2026-09-13 15:39:22 -04:00
James Dansereau 6d29f0accf README for v0.2.0 pre-release: status rewrite, download emphasis, Steam Deck section
- Status: 'alpha' -> 'v0.2.0 (pre-release)'; working list now reflects the
  60 fps fix (D248), click-to-lock mouse + GEPD aim mode (D194 closed), sky
  (D227) and water green-pulse (D229) fixes, F10 overlay, mute/screenshot QoL;
  known issues updated (cutscenes D243/D160/D173, music D230, front-end models
  D75, water seam D245, edge strips D246).
- Download: leads with the two v0.2.0-pre bundles (names match bundle-*.sh
  output, which strips the leading v), notes SDL2 is bundled in the Linux
  tarball so it runs as-is on any distro or a sideloaded Steam Deck.
- New 'Steam Deck' subsection: sideload steps, CPU-bound performance note,
  D203 status (suspect fixed, unverified on real hardware).
- Quick start + comparison table + Linux build section updated to match.
2026-09-13 15:36:09 -04:00
James Dansereau cc9068bfa1 Remove the legacy always-grab mouse mode (D192 + D239)
Per the M-82/M-106 user decision: click-to-lock is now the only cursor
capture mode. Port-only deletion, no game-code touch:

  port/src/input.c       drop mouseCaptureMode + Input.MouseCaptureMode key;
                         simplify reconcileGrab/notifyClick/releaseCapture/
                         aimGepdCompute guards; delete the !haveAbs relative
                         P-controller menu path and its dead state (menuEst/Tgt,
                         menuCursorStep, MENU_P_GAIN/EST_DELTA/MOUSE_TO_PX);
                         drop the unused inputMouseCaptureActive() export
  port/include/input.h   update WI-1 comment, remove the dead declaration
  port/src/optionsoverlay.c  remove the 'Mouse capture' toggle row + labels

Existing ini files that set Input.MouseCaptureMode are ignored (config layer
skips unknown keys). Closes D192 (outer grid cells unreachable under mode 0)
and D239 (always-grab parity) by removing the bugged path rather than
patching it.
2026-09-13 15:33:50 -04:00
James Dansereau d3d968bc0e D251: F10 options overlay QoL pass -- scrollable panel, MSAA wrap, hidden auto-% rows, SensLink toggle, hover-select removed
Known issue (logged in D251): intermittent ghost repeat of a top row at the
panel bottom in true 4K fullscreen; local repro clean, MSAA-off bisection
pending with the user.
2026-09-13 15:29:23 -04:00
James Dansereau 774d398263 D194: record full closure (M-123) -- sens co-tuned, aim-mode FPS resolved by user evidence 2026-09-13 15:29:19 -04:00
James Dansereau 4b322638a1 Merge branch 'fix/d194-mouse-dt-decouple' -- D194 GEPD-mirror aim mode
RMB aim mode rewritten as a direct mirror of GEPD's mouse aiming:
crosshair-position writes with the game's own damping left running,
gun pose derived from crosshair position (no stick synthesis), camera
edge-scroll at screen edges. vv_theta [0,360) wrap dropped for GEPD
parity. User playtest: spazz accepted (95%), sensitivity calibrated
(hipfire matched to aim mode; MouseSensitivity master now scales both).
Defaults ~25% above the user-approved match point. Input.GepdSens
renamed Input.AimModeSens (old key kept as deprecated alias).
2026-09-13 14:29:41 -04:00
James Dansereau 46f83aa116 D194: rename user-facing Input.GepdSens -> Input.AimModeSens (community name); old key kept as deprecated alias 2026-09-13 14:29:35 -04:00
James Dansereau 2d5674bd8c D194/D238: defaults ~25% faster per user -- GepdSens 30->38, MouseTurnSpeed 40->50 (match point kept as the 100% master baseline) 2026-09-13 14:27:49 -04:00
James Dansereau 7410893273 D194/D238: align hipfire with GEPD aim -- MouseTurnSpeed default 100->40 (mid-speed match at GepdSens=30); master MouseSensitivity now scales aim mode too 2026-09-13 14:22:25 -04:00
James Dansereau 6d083c2c7c D194: user playtest verdict -- spazz accepted (95%), remaining work is sensitivity co-tune + FPS control 2026-09-13 14:17:17 -04:00
James Dansereau 68cb3872d0 D194: GepdSens default 20->25 (user: a bit slow); log ct=g_ClockTimer for multi-tick spazz check 2026-09-13 14:14:17 -04:00
James Dansereau e7c2ffe52b D194: drop vv_theta [0,360) wrap in GEPD aim (GEPD parity); extend gepdaim log with residue/autoaim terms for spazz diagnosis 2026-09-13 13:59:58 -04:00
James Dansereau 790fb622c5 D194: GEPD-mirror aim -- direct crosshair/gun/camera writes, game damping left running
Mirrors MouseInjectorPlugin (games/goldeneye.c) in model: mouse motion
drives a crosshair POSITION accumulator (clamped +/-5.16), written
straight into crosshair_x/y_pos + gun_azimuth_angle/turning via GEPD's
formulas; the view only scrolls past 72% toward the screen edge
((ratio-0.72)*475*dt*fov/basefov); no look stick is emitted while
aiming; on release the game's own damping eases everything to centre.

Key correction vs the earlier attempt: GEPD patches NOTHING in
bondview2's damped crosshair update (caclulate_gun_crosshair_position_
rotation) -- the per-frame overwrite simply wins over the damping, and
that function is what derives crosshair_angle (HUD crosshair + aim ray)
and field_FFC (arm/gun offset in gunUpdateAndFire). The temporary
portAimOverrideActive() guard is removed; bondview2.c carries only an
explanatory comment. No game-code behaviour change.

Knobs: Input.AimAbsolute (1=GEPD mirror, 0=legacy velocity stick),
Input.GepdSens (default 20). Field mapping vs GEPD's N64 RAM offsets
verified with offsetof (constant 0xBE0 delta = 32->64-bit pointer
expansion).

Open: crosshair/arm 'spazz' glitch, aim-mode FPS drop (perf.ps1),
sensitivity tuning -- see docs/HANDOFF.md M-122.
2026-09-13 13:53:05 -04:00
James Dansereau d956dffefc D194: GEPD-style relative aim -- crosshair locked, mouse movement rotates view
The cursor-anchored chase model swung the aim out to where the cursor
sat, which is not how GEPD feels. Replace it with the model the PD port
itself uses (mouseDX/DY + sensitivity): while RMB is held and the cursor
is free, per-poll cursor DELTA rotates the view by the same screen angle
the cursor swept (viewport width <-> full hFOV, height <-> fovy). The
crosshair stays locked at centre; stop moving and the game's own speed
decay coasts the view to rest where it is. Scope zooms compose for free
(angle-per-pixel scales with the live fovy). Dithered quantization keeps
fine sub-level movements smooth; no target, no chase, no snap-back.
2026-09-13 11:54:49 -04:00
James Dansereau d4b283899f D194: fix inverted angle conventions in absolute aim
Both axis signs were wrong (verified against bondview2.c plant code +
live GE_INPUTLOG trace): a right turn INCREASES vv_theta and looking
down DECREASES vv_verta. The anchor placed the frozen target at the
mirror image of the cursor point, so the controller steered through
the real point and ran away to the screen corner.

- anchor: tgtTheta = curT + offYawR, tgtVerta = curV - offPitchD
- stick map: errT>0 -> +stick (turn right), errV<0 -> +stick (look down)
- per-poll absaim log now includes cursor/ndc/fov for tuning

Injection test (RMB hold + 15 mouse steps): target tracks cursor,
error converges to deadband (0.7deg), stick->0, view holds.
2026-09-13 11:38:39 -04:00
James Dansereau c20a205bac D194: absolute (cursor-anchored) aim -- GEPD-style position-based aiming
Holding RMB (or the Aim bind, default Left Shift) in a stage now releases
the click-to-lock grab and drives the aim stick from a closed position
loop on the live camera (vv_theta/vv_verta read via player.h): the
crosshair chases the point under the free OS cursor and HOLDS it --
stationary cursor = stationary aim, even while walking. Release re-grabs
with no snap.

- Target derived from cursor NDC against the real render viewport rect
  (viGetView*), portScaleFovY so scope zooms compose for free; re-anchors
  on every cursor move or FOV change.
- Coast prediction (game's 0.05*mult/tick speed decay) prevents overshoot;
  first-order dithering maps continuous demand onto the game's 10 discrete
  stick levels for precision work (shoot-the-lock etc.).
- LMB fires while the cursor is out; ESC still releases capture; menus and
  MouseCaptureMode=0 unchanged. New knob: Input.AimAbsolute (default 1).

Also: -I include shadows <math.h> with the game's constants-only header,
which left fabs/pow/tan in input.c on implicit declarations -- new
port/include/port_math.h pulls the real system math.h via #include_next.
2026-09-13 11:21:13 -04:00
James Dansereau 16966df2e3 Merge branch 'main' into fix/d194-mouse-dt-decouple 2026-09-13 10:13:34 -04:00
James Dansereau 7c48f664ca D222/D218/D249: fix high-FovScale culling, draw distance, LOD scaling (#76)
* D222/D218: fix high-FovScale culling + add Video.DrawDistance

D222 (high FovScale breaks screen-edge culling): currentPlayerSetCameraScale()
derives both the frustum-cull plane normals and the fog/LOD
distance-visibility-fade scale (c_scalelod/c_lodscalez) from c_perspfovy,
which every viSetFovY/viSetAspect/viSetFov/viSetZRange call in fr.c was
setting from the nominal fovy -- never the portFovScale-widened value D211
already computes for the actual render projection two lines away. Factored
the widen-FOV math into a shared port/src/video.c helper (portScaleFovY,
same logic D211 already had inline) and fed it into all 4 currentPlayerSetPerspective
call sites. Identity at FovScale=100 by construction. Audited: the affected
fields are consumed only by render-side fog-visibility-fade and screen-space
transform code (propobj.c/bondview.c), not NPC AI visibility/behavior.

D218 (wide FOV exposes the fog/far-clip boundary): implements the
previously-parked plan. Video.DrawDistance (percent, 100-400, explicit
override) and Video.DrawDistanceAutoFov (couples the multiplier to FovScale
by default) applied in fogLoadCurrentEnvironment (bgfog.c) to both the
far-clip plane and the fog-blend distance read, so they stay consistent.
Clamped to [1.0, 2.5] per the plan's own far-field z-fighting caution.
Both new knobs added to the F10 overlay alongside FovScale.

Rebuilt clean, crash-free at FovScale=150/DrawDistanceAutoFov=1 on Dam.
bunker1 golden framediff was inconclusive in this dev box -- confirmed via
an A/B stash/rebuild that the unmodified baseline also fails it by a
similar/larger margin (pre-existing frame nondeterminism, not a regression
from this change).

D249 (default-FOV culling, sometimes) is unrelated/still open -- different
mechanism, not touched here.

Needs a human's eyes: does high-FOV screen-edge culling and the wide-FOV
fog/draw-distance artifacting actually look fixed now.

Claude-Session: https://claude.ai/code/session_016DQUKGLZxFtPy52MkKXXog

* D249 follow-up: add Video.LodDistance geometry-LOD scaling

Found a real, distinct geometry-LOD system in src/game/model.c:
MODELNODE_OPCODE_LOD nodes whose children are shown/hidden per-frame by
modelUpdateDistanceRelations() testing a view-space distance (scaled by
getPlayer_c_lodscalez(), now FOV-consistent per this branch's D222 fix)
against each node's MinDistance/MaxDistance -- separate from the
fog-visibility-fade D218 already covers. It has an existing ground-truth
multiplier hook, g_ModelDistanceScale, already used transiently by the
game itself (chr.c's 0.3125f close-up override, front.c/title.c's menu
modelSetDistanceDisabled calls).

Rather than write to that shared global (would fight those existing
overrides), compose a second multiplier at the same call site:
portLodDistanceMultiplier() (port/src/video.c), driven by new
Video.LodDistance (percent, 25-400, default 100) and
Video.LodDistanceAutoFov (default OFF, unlike DrawDistanceAutoFov --
this is meant as a standalone perf lever to pull down, not something
that silently costs more as FovScale widens). Inverse-scaled: 200%
keeps full detail twice as far (more cost), 50% drops to the lower LOD
twice as soon (less cost). Both new keys in the F10 overlay.

Identity at LodDistance=100. Rebuilt clean, crash-free at LodDistance=40
(aggressive) on Dam and at defaults on bunker1.

Claude-Session: https://claude.ai/code/session_016DQUKGLZxFtPy52MkKXXog

* D222/D218 follow-up: fix guard fade-early regression, retune draw-distance coupling

Live playtest on Dam found two issues in the D222/D218/D249 work:

1. Regression: D222's c_lodscalez widening (correct for the screen-edge
   cull planes) is also read directly by propobj.c's
   chrobjFogVisRangeRelated()/sub_GAME_7F054C58() -- the character/prop
   fog-visibility-fade cutoff. Uncompensated, this made guards fade
   sooner at wide FOV than before D222. Fixed by dividing that same
   c_lodscalez reading by portDrawDistanceMultiplier() at both call
   sites, so the "how far can you see" question stays answered
   consistently across both consumers.

2. Tuning: a straight 1:1 FovScale auto-coupling on
   portDrawDistanceMultiplier() wasn't aggressive enough -- live-tested
   up twice (1x -> 2x -> 4x FovScale, clamp ceiling raised 2.5x -> 4.0x
   -> 6.0x each time) until guards held up and the Dam tunnel's blue fog
   artifact receded. Re-verified the fog ramp itself (not just the
   far-clip cutoff) scales with the multiplier end-to-end.

User confirms this is good for now.
2026-09-13 10:13:04 -04:00
James Dansereau 70bc9d65b5 D250: getenv() caching fix -- root cause of the Dam fps drop (plus sysSleep unit fix, GE_D250 probe, perf.ps1) (#75)
* D250: fix sysSleep unit-conversion bug in frame-pacing; add GE_D250 lvlRender probe

port/fast3d/gfx_sdl2.cpp sync_framerate_with_timer() computed the
remaining frame budget in 100ns units (qpc_to_100ns) and passed it
straight into sysSleep(), which takes microseconds -- a real 10x
oversleep whenever that branch fires. Genuine bug, but only active
when target_fps is nonzero (tied to Input/Video FpsCap); confirmed via
live testing that it is NOT the active cause of the user's reported
~45-50fps with FpsCap=0/VSync=0, where this pacing path never runs.

Added GE_D250 (src/boss.c, #ifdef PORT) to isolate lvlRender()'s
DL-building cost (culling/animation/scene traversal) from gfx_run's
already-measured software-RSP interpretation cost. Live test on Dam
shows lvlRender averaging ~0.5-1.5ms -- also not the bottleneck.

Documented in docs/dev/GE-ENV-PROBES.md (GE_D250 row). findings.md
D250 update to follow after rebase.

Claude-Session: https://claude.ai/code/session_016DQUKGLZxFtPy52MkKXXog

* docs(D250): live A/B session -- MSAA/aniso/FOV/resolution/vsync all ruled out

Live session with the user driving the game directly, confirmed the
sim-healthy/render-sagging pattern from earlier idle testing reproduces
under real play on Dam. One-at-a-time A/B via the F10 overlay ruled out
MSAA, anisotropy, FOV, resolution (1280x960 fullscreen vs 640x480
windowed), and VSync (render never exceeds ~60 even uncapped) as the
cause -- the cost is resolution/GPU-setting-independent.

gfx_run (~6-10ms) + lvlRender (~0.5-1.5ms, via the new GE_D250 probe)
together don't account for the real ~20-22ms/frame. Leading remaining
suspect: the emulated N64 thread-handoff/scheduler round-trip
(gesched.c/sched.c) -- real OS context-switch/wake latency standing in
for what was free hardware handoff on N64. Needs a real profiler
(Visual Studio / WPR) attached during a live bad-fps moment to confirm;
ad-hoc per-call timers have now ruled out the two most obvious CPU call
sites without finding the cost.

Claude-Session: https://claude.ai/code/session_016DQUKGLZxFtPy52MkKXXog

* D250: fix uncached getenv() in fast3d hot paths -- root cause of the fps drop

A live WPR/xperf CPU trace (tools_pc/perf.ps1, new sister script to
debug.ps1) with Microsoft public symbols resolved found the dominant
render thread spending only 5.2% of its CPU time in the game's own
code -- 43% was in msvcrt.dll, and getenv()/_getenv_helper_nolock
alone accounted for ~50% of the thread's total CPU time.

Root cause: port/fast3d/gfx_pc.cpp's import_texture() (runs on every
texture bind) and several per-triangle/per-G_SETCOMBINE debug-probe
checks (GE_DTEX, GE_TEXDUMP, GE_TEXRAW, GE_D229 x2, GE_D172 x2, plus
GE_TEXDUMP/GE_PCDUMP in gfx_opengl.cpp) called getenv() uncached on
every call, unlike the safe static-cached pattern already used
elsewhere in this codebase. On a texture-heavy scene that's tens of
thousands of locked, locale-aware Windows CRT environment lookups per
second -- invisible to both of the timers added earlier this session
(gfx_run's own cost measurement and the new GE_D250 lvlRender probe),
since neither isolates CRT/library time from the game's own code.

Fix: cache every getenv() site in gfx_pc.cpp/gfx_opengl.cpp with the
established static-int pattern. Narrow, mechanical, no logic change.

Verified: the exact idle-Dam test that was pinned at 45-50fps before
the fix now holds a rock-steady 60-61fps, zero dips. Also explains why
the earlier MSAA/aniso/FOV/resolution/vsync A/B pass all showed "no
change" -- the getenv() cost was a large per-call constant that
swamped any real, smaller resolution-dependent GPU cost at every
setting tried.

Residual, separate issue found once this was fixed: still runs badly
at 1440p+ (640x480 now holds a clean 60fps) -- likely the real GPU
fill-rate cost the getenv() bug was masking, now exposed on its own.
Not yet investigated.

docs/dev/findings.md D250 updated with the full root-cause writeup.
docs/porting-notes.md gets a new lesson under section E (cache
getenv() in any hot-path probe). tools_pc/perf.ps1 automates the
trace-and-analyze workflow for next time.

Claude-Session: https://claude.ai/code/session_016DQUKGLZxFtPy52MkKXXog

* docs(D250): correct the 1440p+ follow-up -- it was a stale-build false alarm

User re-tested 1440p/4K on the actual getenv-fix build and confirmed
it holds up fine too. The "still runs badly at 1440p+" report was
from before the fix was live, same stale-build class as the DLL-load
false alarm earlier in the session. D250 is fully resolved across all
resolutions tested, not just 640x480.

Claude-Session: https://claude.ai/code/session_016DQUKGLZxFtPy52MkKXXog

* docs: add missing GE_D219RAW row to GE-ENV-PROBES.md (CI validate fix)

Pre-existing gap unrelated to this branch's own changes -- gen_env_probes.py
--check caught it now since it runs on every PR. GE_D219RAW has had live
getenv() sites in src/game/image.c since the D219 investigation but was
never added to the probe doc.
2026-09-13 09:33:36 -04:00
James Dansereau ef0c0900b4 docs(D250): rule out build flags + idle-load as the fps bottleneck (#74)
Build flags confirmed as -O2 RelWithDebInfo by default, not the cause.
GE_D193 idle-load probe on bunker1 and streets both hold a healthy
55-60fps in this dev box, well under the 16.7ms/60fps budget -- does
not reproduce the user's reported ~45fps at 720p on a high-end PC, so
the real cost is likely gameplay-load-dependent (AI/combat/particles)
or resolution/GPU-fill-rate-bound in a way idle testing doesn't
exercise. This session had no interactive input tooling to drive real
gameplay headlessly.

Ruled out in passing: the D51 vi-post debug log is throttled to
~once/sec, not a per-frame cost.

Next step needs a human: capture GE_D193=1 output during a live
session at the moment fps visibly tanks, to tell sim-side cost from
render/GL-side cost.

Claude-Session: https://claude.ai/code/session_016DQUKGLZxFtPy52MkKXXog
2026-09-13 08:54:08 -04:00
James Dansereau 1e9d8eb852 docs(D194): PR #62 playtest feedback -- hipfire good, aim-mode still too sensitive
User A/B playtest of PR #62 (fix/d194-mouse-dt-decouple, rebased onto
main this session) confirms hipfire/primary mouse-look feels right now
(dt-decouple + SOLITAIRE natural-pitch), but RMB/Shift aim-mode is
still too sensitive and not usable for precision aiming.

Static re-read of the aimHeld branch in port/src/input.c finds a likely
cause the M-107 curve fix didn't reach: AIM_STICK_MIN=61 is a hard
floor the stick snaps to on any motion past a 0.3px/poll threshold --
the aimCurveGamma response curve only shapes the range above that
floor, so tiny mouse nudges still jump straight to 10% of max aim-turn
speed instead of ramping from zero, unlike the (working) hipfire path.
Not build/runtime-verified -- a hypothesis, not a confirmed fix.

PR #62 stays held, unmerged, pending another aim-mode iteration.

Claude-Session: https://claude.ai/code/session_016DQUKGLZxFtPy52MkKXXog
2026-09-13 08:28:52 -04:00
James Dansereau a406a79e37 Merge origin/main into fix/d194-mouse-dt-decouple (pick up D248 60fps fix + D228/D195/D229/D245/D247 etc)
# Conflicts:
#	docs/dev/findings-index.csv
#	docs/dev/findings.md
2026-09-13 08:17:50 -04:00
James Dansereau 4ed9cf88cd docs(D230/D243): log dispatch results — one hypothesis ruled out, one build-blocked (#73)
M-120: dispatched a local-Qwen triage pass on D230 (music quality) and a
Claude subagent on D243 (Dam cutscene race), per the roadmap triage.

D230: the dispatched hypothesis (afFixupBook's BE->LE book-coefficient
conversion in port/src/romdata.c is backwards) does not survive review —
it's the same conversion pattern used for every other field in the same
function, and if it were wrong every ADPCM sound would be corrupted, not
one bass instrument on three tracks. Ruled out; the standing M-108 leads
(predictor-count truncation / order mismatch in port/src/mixer.c) remain
the live hypothesis.

D243: the subagent wrote the planned GE_D243 env-gated MENU_* transition
trace (src/game/front.c only, statically reviewed sound) but could not
build-verify it — hit a reproducible CMake/ninja-vs-bare-gcc environment
failure ("Cannot create temporary file in C:\Windows\"). Independently
reproduced the same failure in a fresh worktree this session. Diagnostic
is pushed on investigate/d243-cutscene-race (2a47f110), not merged —
needs a session with a working toolchain to build, run the -level_33
repro, and correlate against the D146 burst.

Claude-Session: https://claude.ai/code/session_016DQUKGLZxFtPy52MkKXXog
2026-09-13 08:14:45 -04:00
James Dansereau 3470b68ae9 docs: regenerate findings-index.csv, fix stale statuses; file D249/D250 (#72)
Findings-index regen (tools_pc/gen_findings_index.py) — only D194's row
had actually drifted (fix direction implemented since, on PR #62, not
"not implemented" as the cell said). GRAPHICS-BACKLOG.md's D233/D234
rows were also stale ("not investigated") despite both being fixed and
verified since M-107/M-108 per findings.md.

New findings from user QA (M-119):
- D249: LOD/culling breaks on far geometry on big open levels (Streets,
  Egyptian) at default FOV; distinct from D222/D218. Also a QoL ask for
  a togglable LOD improvement, N64-accurate LOD kept selectable.
- D250: FPS now drops to 20-40 intermittently on many levels, a likely
  direct consequence of D248 (M-118) exposing real per-frame cost that
  was invisible while the port silently ran at half rate.

Claude-Session: https://claude.ai/code/session_016DQUKGLZxFtPy52MkKXXog
2026-09-12 22:50:38 -04:00
James Dansereau c2938ca6a8 fix(D194a/D194/D238): aim curve, master sensitivity, natural-pitch scheme
D194(a): bondview2.c's aim-mode turn speed formula, (stick-60)/10 clamped
to 1.0, saturates at stick=70 -- the port's [61, 60+AimBand] mapping (up
to stick=80) was treating an effectively-capped range as proportional,
so a few px/poll already maxed out perceived sensitivity ("bang-bang").
Curve-map px/poll onto the game's real [61,70] band with a configurable
gamma (Input.MouseAimCurve, default 160) for a wider low-speed zone.

D238: added Input.MouseSensitivity as a master multiplier over both
MouseAimSpeed and MouseTurnSpeed/HipfirePitchSpeed, so one knob scales
overall feel; legacy per-axis knobs remain an independent trim.

D194/D238 vertical-vs-horizontal asymmetry: GE's default 1.1/HONEY
scheme only exposes hipfire pitch as a digital C-button, so the port
duty-cycled it (D166) to approximate continuous motion -- structurally
incapable of matching yaw's genuinely continuous analog mapping. Fix:
force GE's own 1.2/SOLITARE control style (cur_player_set_control_type,
an original player-selectable scheme, not new game logic) every poll,
which gives pitch the same continuous analog treatment as yaw
(canNaturalPitch/canNaturalTurn in bondviewProcessInput). Trade-off:
SOLITARE moves movement from the analog stick to digital step buttons,
so WASD forward/back now fires C-up/C-down instead of the stick (strafe
was already C-button-driven in both schemes, unchanged). Gamepad stick
roles swapped to match (left = digital-step movement, right =
continuous look) so the global scheme switch doesn't break pad play.
Escape hatch: Input.NaturalPitch=0 reverts to 1.1/HONEY + the D166
digital-pulse hack, same pattern as the existing MouseDtDecouple knob.

Verified: build green, tools_pc/verify.sh bunker1 PASS (worst_cell=0).
Needs a human A/B playtest for feel -- headlessly unverifiable.

Claude-Session: https://claude.ai/code/session_016DQUKGLZxFtPy52MkKXXog
2026-09-12 22:36:38 -04:00
James Dansereau 52477b44af D248: fix port-wide 30fps halving -- pointer-width scheduler ABI bug
osScAddClient() stashes a per-client "every retrace vs every other
retrace" flag in the next array slot's .next field (c[1].next = next);
__scHandleRetrace read it back via a hardcoded 8-byte offset that only
lands on the right memory when OSScClient is 8 bytes (32-bit
pointers). On this 64-bit port the struct is 16 bytes, so the read
landed inside the gfx client's own msgQ pointer (always non-NULL)
instead, silently collapsing the gfx client onto the audio client's
30Hz path even though the VI-retrace pacemaker itself was always
correctly ticking at 60Hz.

Fix indexes the struct (client[1].next) instead of hand-rolling the
byte offset, so it scales with pointer width on either platform --
same class as D122/D126/D132/D209, just in the scheduler instead of a
ROM-serialized struct. Verified via the Video.DisplayFPS overlay:
30 FPS pre-fix, 60 FPS post-fix, same build/scene (-level_09).
-level_09/-level_20 both crash-free post-fix.
2026-09-12 21:36:21 -04:00
James Dansereau dac4ae7c0c D228: fix white NPC-weapon-texture patches -- IA16 palette intensity/alpha byte-order bug
GE_TEXDUMP on Facility (-level_34) found ~14% of LOD-textured CI uploads
whose palette is IA16 (intensity-alpha, not RGBA16) rendering solid
near-white. A GE_D228 correlation probe ruled out the texpool/RC2-clip
hypothesis this row started with -- decoded size exactly matched the base
tile every time, so the data reaches import_texture fine.

The real bug: palette_to_rgba32's G_TT_IA16 branch read
intensity = palentry & 0xff (low byte), alpha = palentry >> 8 (high byte)
-- backwards relative to the already-correct direct-IA16 decode
(import_texture_ia16) a few lines above it in the same file, which reads
intensity from the first byte (high, after gfx_dp_load_tlut's PD_BE16
swap) and alpha from the second. A dim, fully-opaque palette entry (e.g.
intensity=0x58, alpha=0xff) decoded backwards as bright and
mostly-transparent -- the white-patch symptom.

Fix: swap the two lines to match the working convention.

User-verified live: AK47 and other metallic guns render correctly.
Headless -level_34 run crash-free pre-verification; a full sweep is
still owed (the user's own game instance was running concurrently this
session, so verify.sh/taskkill were avoided).

Claude-Session: https://claude.ai/code/session_016DQUKGLZxFtPy52MkKXXog
2026-09-12 21:20:51 -04:00
James Dansereau 53c9a879f3 D245: reopen -- live playtest shows the allowShift=TRUE fix did not resolve the seam
User report on Frigate post-fix: water still glitches between two patterns
and still changes with view angle, unlike the sky (which uses the same
skyPortBeginFan/skyPortEmitTileShift mechanism post-D227 and no longer has
this problem). The mechanistic writeup and GE_D227V trace from the same
session looked correct but were evidently not sufficient evidence -- a
single static frame/angle capture never exercised whatever actually causes
the live symptom. Reopening honestly rather than leaving a stale FIXED
label; the superseded analysis is kept in the row for reference but flagged
as unconfirmed. No code change in this commit (docs only).

Claude-Session: https://claude.ai/code/session_016DQUKGLZxFtPy52MkKXXog
2026-09-12 21:16:38 -04:00
James Dansereau a088bb51e7 D195: restore G_TEXTURE_GEN envmap UV generation -- fixes black reflective materials game-wide
GE uses real N64 RSP environment-mapping (G_TEXTURE_GEN) for every
shiny/reflective material: gold/silver weapon skins, chrome trim, and
Control's reflective console glass. An earlier session (D72.1) removed
the PD-inherited envmap UV-generation block wholesale from gfx_sp_vertex
to fix the Rareware logo, reasoning "GE never generates texture
coordinates from vertex normals" -- true for the logo (lit, but doesn't
set G_TEXTURE_GEN) but false in general. Every envmap draw was left
sampling whatever raw tc[] happened to be authored (near 0), landing on
a fixed corner texel of the reflection texture regardless of view angle
-- reading as solid black for a typically dark-edged reflection map.

Restored the envmap UV generation (ported from the reference Perfect
Dark PC port's already-correct implementation), gated specifically on
rsp.geometry_mode & G_TEXTURE_GEN so D72.1's actual fix is untouched
(the logo never sets this bit) while every other envmap draw gets real
reflection coordinates back.

User-verified live: console glass and gold/silver guns are now visibly
reflective instead of flat black. The remaining "not translucent"
complaint on the glass is confirmed NOT a bug -- it was never see-through
on the original N64 either (reflective by design, not transparent).

Full 21-level sweep crash-free post-fix.

Claude-Session: https://claude.ai/code/session_016DQUKGLZxFtPy52MkKXXog
2026-09-12 21:02:53 -04:00
James Dansereau 1f7f804733 D247: close as NOT A BUG -- confirmed faithful N64 TV-safe letterbox; D246 narrowed
Display pipeline pass on the two remaining M-116 QA reports.

D247 (black bars top/bottom): traced to bondviewGetCurrentPlayerViewportHeight()
returning VIEWPORT_HEIGHT_DEFAULT (220 of 240 NTSC scanlines, fr.h) for the
decomp's own default SCREEN_SIZE_FULLSCREEN option -- unmodified ground-truth
code, no #ifdef PORT anywhere near it. Verified against both the existing
save and a fresh EEPROM (rules out a stale Screen=Wide/Cinema setting from
earlier sessions) -- same inset either way. This is GoldenEye's real
TV-safe-area letterbox, present on original hardware. No code change.

D246 (left/right edge strips): GE_PCDUMP captures of the native render show
clean edge columns, ruling out an RDP/game-DL scissor bug and the D227/D245
tc-overflow class -- the defect must be introduced later, in the SDL window
presentation path GE_PCDUMP bypasses (MSAA resolve blit or a live-window
scale remainder). Still needs the user's window size/MSAA setting to chase
further; documented as a narrowed OPEN with concrete next steps.

New porting-notes.md entry: check the decomp's own constants before
assuming a rendering "defect" is a port bug.

Claude-Session: https://claude.ai/code/session_016DQUKGLZxFtPy52MkKXXog
2026-09-12 20:35:50 -04:00
James Dansereau 4806799867 D245: fix IsWater view-tracking seam -- allow the D227 tc-shift safety valve on the water quad
The water quad's fan was the one skyPortBeginFan() caller passing
allowShift=FALSE, on the theory that a matched tc-shift on both texunits
would flatten the sinf()-driven cross-fade the effect depends on. That
theory was wrong: skyPortEmitTileShift() replays both tiles' SETTILE
commands with the identical k, rescaling both texunits' decoded S/T by
the same factor before each tile's own (unshifted) uls/ult offset is
applied by the RDP -- the offset between tile 0 and tile 1's sample
points is preserved, not flattened.

Confirmed with the GE_D227V probe on Frigate (-level_26): a water fan
vertex spans S=-166..299317, T=-6451..288410 in S10.5 units (same order
of magnitude as D227's pre-fix sky overflow), now correctly getting
kS=4 kT=4 instead of being forced to 0.

verify.sh frigate and "surface 2" (the two IsWater levels) both PASS
crash-free. Live playtest of the mouse-tracking seam itself still owed.

Claude-Session: https://claude.ai/code/session_016DQUKGLZxFtPy52MkKXXog
2026-09-12 20:28:49 -04:00
James Dansereau 9b0acccd1d D229: fix green/pulsating IsWater water -- fast3d wasn't expanding CI8 indices
texSelect loads the water image as ZLIB+CI8 (raw 8-bit palette indices);
GE's custom RSP ucode expands them to RGBA16 in TMEM at load time, so a
later RGBA/16b tile re-declaration over the same slot samples real blue
pixels. fast3d has no such expansion and read the raw index byte-pairs as
16-bit texels (g dominates -> green mottle); the "pulsing" was always the
pre-existing PRIM_LOD_FRAC sinf() cross-fade between two mis-decoded tiles.

Track each TMEM load's source G_IM_FMT in LoadedTexture.src_fmt; in
import_texture, an RGBA/16b tile over a CI-sourced slot now routes through
the CI8 palette import path instead. Fires only on the genuine format
disagreement, so real RGBA16 textures are untouched.

Verified live on Frigate (-level_26): water renders blue, user confirms
"looks better". Sweep + Surface 2 by-eye still owed.

Also files D245 (water seam), D246 (edge strips), D247 (black bars) from
the same playtest session.

Claude-Session: https://claude.ai/code/session_016DQUKGLZxFtPy52MkKXXog
2026-09-12 20:16:45 -04:00
James Dansereau 9137f8f44b docs(D219): flag the unverified 16-bit texnums in porting-notes for future triage 2026-09-12 19:07:04 -04:00
James Dansereau b1cf0eac4c D219: scope the fire-particle byte-order fix by pixel format; resolved
M-115: re-apply M-113's store-time bswap scoped to the 16-bit GBI-size
family only (PORT_PIXEL16 = bswap16 under #ifdef PORT; PORT_PIXEL32 stays
identity). The fast3d importers disagree on pool byte order:
import_texture_rgba16/_ia16 read manually big-endian bytes, while
import_texture_rgba32 does PD_BE32() on a native load -- M-113's blanket
swap hit both and regressed muzzle flash/ammo HUD (M-114).

Format is the structural discriminator, not compmethod (fire frames span
LOOKUP/RLE/HUFFMANBLUR/RLELOOKUP) or texnum range. Offline census of all
2698 ROM images (scratch/d219_headers.py / d219_census.txt): 94 non-zlib
wide-pixel; the 16-bit family is IMAGE_FIRE_0..14 + texnum 1198-1201/2430/
2510-2523, every ammo/flare/crosshair/muzzle-flash image is RGBA32.

Runtime-verified on Dam (scripted fire): all 15 frames decode format=1,
pool dumps warm R-dominant (mean opaque RGB 255,238,139). RGBA32 pool
bytes bit-identical to the M-114-revert build. User playtest confirmed;
D219 closed. GE_D219RAW probes generalized (no texnum filter), inert.
2026-09-12 19:05:50 -04:00
James Dansereau f55a89515f D219: root cause found -- fire particle texture data itself is R/B-swapped, not a rendering bug (#71)
Live-tested against 4 real user playtests (not scripted) tonight, each
confirming purple/blue explosions still reproduce on current main:

1. G_LIGHTING hypothesis (previous session's lead) RULED OUT: extended the
   D172 probe to flag lit particle draws uncapped (the original 40-hit cap,
   sized for a short scripted repro, silently went dark after ~40 ordinary
   bullet-impact particles well before any real explosion -- masking the
   result, not confirming it). Fixed with an always-on periodic counter.
   Result: 5000+ particle draws logged across a real playtest with
   explosions triggered, 100% LIGHTING=off, 0 suspect hits.

2. Texture-cache-collision hypothesis RULED OUT: extended the same probe to
   log the actual bound TEXEL1 address/GL-texture-id/byte-count. 7 distinct,
   internally-consistent 448-byte (16x14 RGBA16) textures observed -- no
   aliasing, no stale-slot signature.

3. ROOT CAUSE: used the existing GE_TEXDUMP feature (extended to exempt
   16x14 RGBA images from its own 400-image cap, same masking problem as
   above) to dump all 15 IMAGE_FIRE_0..14 particle textures as PPM during a
   live playtest with confirmed-purple explosions. Every single one decodes
   blue/purple-dominant; swapping the decoded R and B channels turns all 15
   into a plausible warm fire palette, consistently. This is NOT a bug in
   the shared RGBA16 decoder (port/fast3d/gfx_pc.cpp import_texture_rgba16)
   -- that function is proven correct by every other RGBA16 texture in the
   game rendering fine, and the general C-array byte-order normalization
   already runs upstream of it. The wrongness is in the decoded bytes
   themselves: either the Globalimagetable entries for these 15 image ids
   resolve to the wrong ROM offset, or these assets use a sub-format the
   shared decoder doesn't special-case. Do NOT blanket-swap R/B in the
   shared decoder -- it would break every correctly-rendering RGBA16
   texture in the game.

Also fixes a mistake made mid-session: a follow-up raw-byte-dump probe
(GE_TEXRAW) initially keyed its own cap-exemption on the wrong field
(gsDPSetTileSize's logical 56x56 wrap size instead of the real 16x14 pixel
content), so it never matched and burned a live-playtest cycle for nothing.
Fixed to key on the real signature: a 448-byte load (16*14*2, RGBA16).

Diagnostic infra left in tree, env-gated and inert by default:
GE_D172=1 (uncapped lit-case + periodic counter in gfx_sp_tri1),
GE_TEXDUMP=1 (16x14-exempt-from-cap fire dumper in gfx_opengl_upload_texture),
GE_TEXRAW=1 (fixed 448-byte-exempt raw dump in the texture import path).
2026-09-12 17:06:59 -04:00
James Dansereau 85393fec3f ci(windows-build): default to the self-hosted runner, windows-latest only for releases/fork PRs (#70)
Previously: self-hosted for push/workflow_dispatch, windows-latest for every
pull_request (including the user's own same-repo branches). Per standing
preference (2026-09-12): route all Windows builds to the self-hosted box for
now, except when cutting a release (tag push) -- that stays on a clean
GitHub-hosted runner for the artifact that actually ships. Fork PRs also
stay on windows-latest regardless (untrusted code must never reach a box
that will later hold a ROM, docs/dev/notes/SELFHOSTED-RUNNER-PLAN.md §G2).

Adds a `windows_selfhosted` output on the `changes` job (computed once) and
threads it through windows-build's runs-on, the msys2/setup-msys2
release/update/install flags, and the self-hosted dependency check --
replacing every prior `github.event_name == 'pull_request'` check.
2026-09-12 16:20:14 -04:00
James Dansereau 81054e4c98 D219: instrument a G_LIGHTING probe on explosion particle draws (#68)
* docs+probe(D219): new lead -- explosion.c never clears G_LIGHTING before particle draws

explosion.c's 15 particle DLs (assets/oddtextures.c globalDL_0x078..0x9a8,
all G_CC_INTERFERENCE+G_CC_MODULATEIA2) never get G_LIGHTING cleared by any
of explosion.c's own gSPClearGeometryMode calls. If lighting is left on from
a prior draw (e.g. a nearby lit character model), the port's vertex path
(gfx_pc.cpp:1301-1346) reinterprets the particle's authored RGBA tint as a
normal vector, turning SHADE into a scene-light color instead of red/orange
-- state-dependent, would explain "game-wide, live-play-only" exactly as
reported, and why the M-90 scripted repro (no nearby lit models) came back
clean.

Extends the existing D172 multitex-tri probe (GE_D172=1, already proven
reliable and uniquely keyed to this particle family) to also flag
geometry_mode & G_LIGHTING. Sanity-checked against the known-clean M-90
repro: still fires 40/40, all LIGHTING=off, matching the prior clean result
-- confirms the probe works without false-positiving.

Not a fix -- needs one live GE_D172=1 capture during a real explosion to
confirm or rule out before touching any rendering logic.

* fix: regenerate findings-index.csv via tools_pc/gen_findings_index.py

Hand-edited the CSV directly in the previous commit instead of running the
generator -- CI's "validate" job caught the drift.
2026-09-12 16:16:37 -04:00
James Dansereau 6e541483a4 fix: regenerate findings-index.csv (stale since PR #66/#67 hand-edits) (#69)
Those two PRs edited the CSV by hand instead of running
tools_pc/gen_findings_index.py. Both were docs-only, so CI's "validate" job
(which only runs when non-doc files also change) never caught the drift.
2026-09-12 16:15:17 -04:00
James Dansereau 8e2213c28f docs(D219/D234): user confirms D219 game-wide + wants it fixed; D234 already closed (#67)
D219 (purple/blue explosions): user confirmed 2026-09-12 this is not
Dam-exclusive -- game-wide -- and wants an active fix, not further triage.
Static/scripted analysis (M-89/M-90) is exhausted; next step is a live
GE_PCDUMP capture during a real explosion.

D234 (Dam guard towers): already marked VISUALLY VERIFIED FIXED in both the
findings.md row and findings-index.csv -- no change needed, just confirming
consistency per the user's note that this is resolved.
2026-09-12 15:58:22 -04:00
James Dansereau df7186617b docs(D230/D235/D243): M-108/M-109 static-analysis follow-ups from 3 parallel subagent investigations (#66)
D230 (bass-instrument audio corruption): narrowed suspect to port/src/mixer.c's
hardcoded order=2/npredictors<=8 ADPCM decoder assumptions; not yet confirmed
against real ROM book data.

D235 (wrong textures after death/restart): the leading "CI-cache survives
restart" theory is likely refuted -- lvlStageLoad (the only call site for both
set_mt_tex_alloc and proplvreset2) runs on every stage transition including
restarts, so the cache reset isn't actually skipped. Real mechanism still
unknown.

D243 (Dam end-cutscene D146 corruption burst): corrects the prior write-up's
timing claim by re-reading the same session's raw logs at the frame-counter
level -- the corruption fires ~1600-3900 frames after the camera-cut, not
during it, in both runs (not just one). This also weakens the D134/pendingGfx
race citation, since osSpTaskStartGo runs gfx_run() synchronously. New lead:
the mission-complete/briefing screen chain, not the cutscene render itself.

Claude-Session: https://claude.ai/code/session_01CUFSnpgrAyQSDVHoQQ6Sx3
2026-09-12 15:53:56 -04:00
James Dansereau f26fb8f0a3 docs(D160/D219/D233/D234/D243): M-108 live-playtest findings (#64)
Live GE_D160=1 traces on Dam plus a fresh-save playthrough during this
session's interactive playtest:

- D160: corrects its old "cutscene never plays" premise (refuted by a
  live trace showing the AI trigger fires and all 3 camera_switch calls
  execute). Real defect split out to new D243: a stale/freed
  display-list-memory read (D146 guard burst, freed-memory byte pattern)
  during the cutscene's camera-angle cuts, evidenced by non-deterministic
  call ordering between two runs. User confirmed the glitch reproduces
  on real fresh-save play, not just the debug-load harness.
- D233: visually confirmed fixed (Frigate -level_26, corrected level id).
- D234: visually confirmed fixed; mechanism still unexplained (the M-107
  investigation argued D233's fix shouldn't cover this symptom).
- D219: REOPENED. Live Dam PPK firefight still shows purple/blue
  explosions, contradicting M-90's clean scripted-repro verification —
  the scripted GE_INPUTSCRIPT repro likely didn't exercise the same
  explosion DL live play hits. User's impression is this is game-wide,
  not Dam-specific; needs a second-level confirmation.

No src/ or port/ changes. findings-index.csv regenerated via
tools_pc/gen_findings_index.py.

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
2026-09-12 15:41:09 -04:00
James Dansereau 3dcceaafd9 ci: route windows-build to self-hosted runner (G2); land ROM-gated sweep workflow (G1) (#65)
* ci: route windows-build to self-hosted runner (G2); land ROM-gated sweep workflow (G1)

G2: windows-build now runs on the self-hosted box (corpyvt6agent01) for
push/workflow_dispatch, using local ccache instead of the actions/cache
round-trip and skipping msys2/setup-msys2's install once packages are
already present. pull_request always stays on windows-latest — fork PRs
must never reach a box that will hold a ROM.

G1: adds .github/workflows/selfhosted.yml, a dispatch-only workflow that
builds + runs the 21-level headless sweep (tools_pc/verify.sh sweep --json)
against a runner-cached ROM. Inert until manually dispatched.

Still pending (manual, physical steps on the box — not done by this
commit, see docs/dev/notes/SELFHOSTED-RUNNER-PLAN.md rollout table):
- register corpyvt6agent01 as a runner against this repo
- confirm MSYS2 at C:\msys64 with the required mingw64 package set
- one-time ROM seed into {runner_home}/_rom-seed/
- first workflow_dispatch sweep + parity check against a local run
- flip G1 from workflow_dispatch to push:[main] once green

Claude-Session: https://claude.ai/code/session_01J5PZ3sTHiuW6MdQnepasSU

* fix(selfhosted.yml): remove invalid concurrency.max-parallel key

Found by testing: dispatching the workflow failed with a hard parse
error (HTTP 422, "Unexpected value 'max-parallel'"). GitHub Actions'
workflow-level `concurrency` block only supports `group` and
`cancel-in-progress` — `max-parallel` is a job `strategy:` key for
matrix jobs, not a concurrency-block key. Serialization was already
achieved via group + cancel-in-progress:false; the stray key just
blocked every dispatch.

Claude-Session: https://claude.ai/code/session_01J5PZ3sTHiuW6MdQnepasSU

* fix(selfhosted.yml): register the msys2 shell wrapper (setup-msys2 step)

Found by testing: the first fixed dispatch failed every msys2-shell step
with "msys2: command not found". Unlike ci.yml's windows-build, this
workflow used `shell: msys2 {0}` without ever running msys2/setup-msys2,
which is what registers that shell alias for the runner — MSYS2 being
pre-installed on the box doesn't help if Actions doesn't know the shell
exists. Adds the same setup-msys2 step ci.yml uses, with update:false
since the packages are expected already present (fast no-op per plan).

Claude-Session: https://claude.ai/code/session_01J5PZ3sTHiuW6MdQnepasSU

* ci: stop reinstalling MSYS2 per run on the self-hosted box

The self-hosted paths (selfhosted.yml; ci.yml windows-build's push/
dispatch route) no longer call msys2/setup-msys2 or pacman -S at all.
MSYS2 + the mingw64 package set are now assumed hand-maintained on
corpyvt6agent01 (docs/dev/notes/SELFHOSTED-RUNNER-PLAN.md), invoked by
calling its bash directly (`C:\msys64\usr\bin\bash.exe -leo pipefail
"{0}"`, the same invocation setup-msys2's generated wrapper uses) with
MSYSTEM=MINGW64 so /etc/profile still puts mingw64/bin on PATH.

Adds tools_pc/check-selfhosted-deps.sh: a fast, install-free check that
exits 0 immediately when the required packages (toolchain, cmake,
SDL2, zlib, python, ccache, zip, unzip) are present, or fails with the
exact `pacman -S --needed ...` line to run otherwise — so a missing
package on the box surfaces immediately with a clear fix, instead of
as a confusing failure deeper in the build.

ci.yml's windows-latest (PR) path is unchanged: ephemeral runners still
get a full msys2/setup-msys2 install every time, since there's nothing
to persist there.

Claude-Session: https://claude.ai/code/session_01J5PZ3sTHiuW6MdQnepasSU

* fix: use msys2/setup-msys2 release:false instead of a raw bash-path shell

The previous attempt (invoking C:\msys64\usr\bin\bash.exe directly as a
custom `shell:` command) failed on the actual runner with "Second path
fragment must not be a drive or UNC name" — a real limitation, not a
typo; it failed identically in both files, including the one with a
static (non-templated) shell string, so it wasn't the ${{ }} expression
either.

Root-caused by reading msys2/setup-msys2's actual source: `release:
false` makes it reuse the existing C:\msys64 as-is (hardcoded path in
that branch, confirmed in main.js) instead of downloading+installing a
fresh copy, and — verified in the same source — omitting `update` and
`install` skips every pacman call entirely (just one `uname -a` + a
pacman.conf sed tweak, no network). This is the officially documented
mechanism for self-hosted runners with a pre-existing MSYS2 install
(see the action's README, "release" section).

ci.yml's windows-build now runs setup-msys2 unconditionally but with
release/update/install all keyed off github.event_name: full install on
windows-latest (PR, ephemeral), reuse-only on the self-hosted box
(push/dispatch). selfhosted.yml always uses release:false. Both then
run tools_pc/check-selfhosted-deps.sh to verify the box's hand-
maintained packages are actually present.

Claude-Session: https://claude.ai/code/session_01J5PZ3sTHiuW6MdQnepasSU

* fix(selfhosted.yml): cp -r for the ROM sidecar directories

Found by testing with the ROM seed actually in place: pcmodels-ntsc-final
and pccg-ntsc-final are directories (not single files, unlike
ge007.ntsc-final.z64), so a plain `cp` failed with "omitting directory"
and left the seed step failing on every run even with the seed populated.

Claude-Session: https://claude.ai/code/session_01J5PZ3sTHiuW6MdQnepasSU

* ci: drive MSYS2 MINGW64 directly via ci-shell.cmd, drop msys2 shell shim

The self-hosted runs were dying with 'msys2: command not found' because the
msys2/setup-msys2 action's shell shim was never registered on the box. Replace
the dependency on that shim with a small committed launcher (tools_pc/ci-shell.cmd)
that headlessly reproduces the local 'MSYS2 MINGW64' terminal:

  - MSYSTEM=MINGW64 + /mingw64/bin on PATH
  - NON-login bash, so it keeps the workspace cwd (a login shell cd's to $HOME
    and would break every relative path in the workflow)
  - -e -o pipefail for GitHub-style error propagation

Both workflows now set defaults.run.shell to C:\msys64\ci-shell.cmd {0}; a
bootstrap step (shell: cmd) refreshes the launcher from the repo into C:\msys64
each run. selfhosted.yml drops setup-msys2 entirely; ci.yml keeps it only for
the ephemeral windows-latest PR path (which installs MSYS2 at C:\msys64).

.gitattributes: force CRLF for *.cmd so cmd.exe parses the launcher correctly.

* Revert "ci: drive MSYS2 MINGW64 directly via ci-shell.cmd, drop msys2 shell shim"

This reverts commit a976b2589c.

* docs(D244): log self-hosted-runner link failure — undefined reference to objectiveGetStatus_WEAK

First real build attempt on the reinstalled corpyvt6agent01 box (run
34707798707) fails at link with an unresolved #pragma weak alias. A local
repro of the identical alias pattern links fine on current MSYS2
(gcc 16.2.0/binutils 2.47); the box reports gcc 16.1.0 — likely a stale
toolchain, consistent with rollout step 2's earlier package-hygiene gap.

Claude-Session: https://claude.ai/code/session_01CUFSnpgrAyQSDVHoQQ6Sx3

* fix(verify.sh): surface run.log tail on NO-FRAMES instead of silently discarding it

sweep mode's per-level run.log (the game process's own stdout/stderr, where
an SDL/GL init failure would show up) was captured into a mktemp capdir but
unconditionally rm -rf'd with no diagnostic ever printed for the NO-FRAMES
(no-crash) case -- every level failed this way on the self-hosted runner's
first real sweep attempt with zero clue why.

Claude-Session: https://claude.ai/code/session_01CUFSnpgrAyQSDVHoQQ6Sx3

* fix(selfhosted.yml): stop deleting the ROM seed dir -- cache save silently fails on this box

actions/cache/save has been failing to persist the ROM cache entry on
corpyvt6agent01 (WARNING annotation, tar.exe exit code 2) but doesn't fail
the step -- by design, a cache-save failure is non-fatal, so the workflow
carried on and the now-unconditional "Delete one-time seed dir" step wiped
the only copy of the ROM every single run. Every dispatch since has failed
at "Seed ROM cache" with an empty seed dir and an empty cache -- exactly
the user-reported "it wipes out the ROM data each run".

Fix: don't delete the seed dir. It's cheap disk (three files, a few hundred
MB) and the existing restore-then-seed-if-missing logic already treats it as
the fallback source whenever the cache comes back empty, so leaving it in
place is safe and makes the workflow tolerant of the cache-save bug until
that's root-caused separately.

Claude-Session: https://claude.ai/code/session_01CUFSnpgrAyQSDVHoQQ6Sx3

* fix(verify.sh): sweep --json never actually produced a verdict array; fix NO-FRAMES diagnostic clipping it further

Two bugs, found together while chasing the self-hosted runner's first real
sweep attempt:

1. sweep mode's `out=$(verify_level ...); echo "$out" | head -1` always
   discarded emit_verdict's second (--json) output line -- RESULTS_JSON was
   declared for assembling a verdict array but never populated or printed,
   so `sweep --json` has never produced anything but `[]` for the
   workflow's "Extract JSON verdict" step. Now builds and prints a real
   `[{...}, {...}]` array before the RC exit.

2. My own D244-session addition (printing a NO-FRAMES level's run.log tail)
   wrote to stdout, which the same `out=$(...)/head -1` capture turned into
   the *new* first line, clipping the real verdict summary. Moved to stderr,
   which bypasses that capture.

Claude-Session: https://claude.ai/code/session_01CUFSnpgrAyQSDVHoQQ6Sx3

* docs: regenerate findings-index.csv for D244

Claude-Session: https://claude.ai/code/session_01CUFSnpgrAyQSDVHoQQ6Sx3
2026-09-12 15:33:07 -04:00
James Dansereau 0b4b8141b1 Merge remote-tracking branch 'origin/main' into fix/d194-mouse-dt-decouple 2026-09-12 10:19:37 -04:00
James Dansereau 7e99344741 docs(D148/D160/D173): re-confirm propDef stride ruled-out; rule out PadRecord.stan and act_anim aliasing for puppet-position bug (#61)
Investigation pass on the cutscene-reliability cluster (D148/D160/D173),
part of the 1.0 acceptance-bar #4 push. No build/runtime toolchain was
available in this worktree (no gcc/cmake on PATH, MSYS2 MINGW64 not
active), so this is a static-analysis-only pass; no code changes.

D160 (Dam end-cutscene / D148 label): independently re-derived (not
just re-cited) that hypothesis (a), a propDef sizepropdef() stride
desync, is ruled out — CutsceneRecord (propDef type 46) is all-scalar,
28B, zero pointer/union fields, so the D122/D126/D132/D209 pointer-
width-growth class cannot apply. Confirms the existing M-31 audit
recorded in porting-notes.md. Hypotheses (b)/(c) (AI trigger flow /
invisible puppet render) remain unconfirmed — still need the shipped
GE_D160=1 trace driven live.

D173 (third-person Bond puppet floats / spins in cutscenes): checked
the two most D209-shaped candidates for a puppet Y-position aliasing
bug and ruled out both. (1) PadRecord.stan is a ROM-serialized pointer
field zeroed by tools_pc/d88_emit.py's emit_pad() with a "dead-on-load"
comment; verified against the N64 source asset tables (UsetupdamZ.c)
that every PadRecord literal initializes stan to 0 on N64 too, so the
PC zeroing is byte-identical to ground truth, not a converter bug.
(2) The anim-root-motion Y-accumulation chain was already exhaustively
audited during D193/D209 (M-80/M-81) and the padding[]-union sweep
that found D209/D210 explicitly cleared act_anim/act_dead as
layout-stable. Root cause not found; the D209-style struct-aliasing
pattern does not appear to be the shared explanation here, unlike the
hope stated in the dispatch brief. Flagged next steps: a live Cradle
capture, and locating the actual two-actor Cuba end-credits code path
(not found this session).

Added a generalizable porting-notes.md quirk: before assuming a zeroed
ROM-serialized pointer-width tail slot is a converter miss, check the
N64 source literal — some fields are legitimately always NULL on both
platforms.

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
2026-09-12 10:15:12 -04:00
James Dansereau 650ac57fc6 fix(D233): fast3d trivial-reject drops visible triangles with a behind-camera vertex (#63)
gfx_sp_tri1's CPU-side trivial-reject AND-test trusted per-vertex outcodes
(x<-w / x>w / y<-w / y>w / z>w) that are only valid half-space tests when
w>0. A vertex that has crossed behind the camera plane (w<0 -- common when
the camera is close to a large room-structure polygon, e.g. walls/ceiling
trusses near a doorway) flips the comparison sense, so its outcode can come
out wrong. When that spurious bit happens to match the other two genuinely
correct vertices' bits, the AND-reduction fired and dropped the whole
triangle before it ever reached OpenGL -- even though GL's own homogeneous
clipper (which handles w<0 correctly) would have rendered the visible part
of it.

This reads exactly like D233 (indoor geometry intermittently vanishing near
doors while props/furniture keep drawing): large near-camera polygons are
far more likely to have a vertex cross behind the eye than small
self-contained prop meshes, and the backface-cull code six lines below
already special-cases this same w-sign hazard for its own cross-product
sign -- the trivial-reject block above it never got the same treatment.

Fix: skip the trivial-reject AND-test whenever any of the triangle's three
vertices has w<0, deferring to GL's clipper instead. This can only ever
send MORE triangles to the GPU than before, never fewer, so it cannot make
anything newly invisible.

Also investigated D234 (Dam guard towers, only the first renders) under
the same M-106 hypothesis that it shares a root cause with D233/D222 --
did not find fast3d-side evidence for it; left open in findings.md with
the ruled-out mechanisms and a next-step recommendation. D222 (FovScale
breaks culling) is a separate, already-characterised game-logic bug in
src/game/fr.c/bondview2.c, out of this fast3d-only fix's scope.

Build verified green (MSYS2 mingw64 cmake+ninja, full link succeeds).
Could not visually verify -- this worktree has no ROM/data and no
interactive display; see docs/dev/findings.md D233 entry for the exact
human verification steps (GE_PCDUMP on a doorway-heavy interior).

docs/dev/findings.md: D233 (FIXED, high confidence on mechanism/unverified
visually) + D234 (OPEN, not root-caused) entries added.
docs/porting-notes.md: generalised the w<0 trivial-reject hazard as a
recurring class for any future CPU-side "skip this triangle" heuristic.
docs/dev/findings-index.csv regenerated.

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
2026-09-12 10:11:51 -04:00
James Dansereau 067c8384d0 docs(M-106): log user QA report as D233-D239 (#60)
* docs(M-106): log user QA report as D233-D239 (delegate triage)

7 new open findings from local-delegate triage of a fresh user QA pass:
- D233: indoor geometry vanishes near doors while world objects still draw
  (portal/frustum-cull suspect; screenshots committed, D106/D222 neighbours)
- D234: Dam guard towers invisible except the first (instance suspect)
- D235: Facility wrong textures, reported as a regression (bisect vs
  pre-D217-fix build first)
- D236: Surface 1 billboard trees render as a texture-sheet wall, not
  discrete camera-facing sprites
- D237: F10 overlay QoL — wheel scroll, more exposed settings incl.
  Game.NoHitFlash, category grouping
- D238: mouse sensitivity rework — separate RMB aim sensitivity, single
  linked master by default, fix up/down vs left/right asymmetry
- D239: click-to-lock is already default; drop always-grab entirely if
  parity isn't cheap (re-confirms M-82, closes D192 by deletion)

findings-index.csv regenerated via tools_pc/gen_findings_index.py.

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG

* docs(M-106): file D230 — in-level music quality (bass instrument corrupted)

Repro obtained from the user, deterministic (not intermittent): Control
(elevator + regular track, throughout), Cavern (elevator, throughout),
Runway (a bass instrument enters ~5-7s in and stays bad). User confirmed
against original N64 audio that a bass instrument belongs on Runway --
instrument selection is correct, only its sound is wrong on PC. Determinism
scopes this to a static decode/synthesis bug (csplayer.c/synthesizer.c or
bank loading), not the D147/D152/D202 live voice-stealing family.

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG

* docs(M-106): dedupe D230 -- fold M-106 repro into the original M-105 filing

The M-105 session already filed D230; the M-106 delegate triage didn't
know that (branched off a stale local main before pulling) and filed a
second D230 row with the refined repro. Merged into one entry -- kept
the M-105 filing as the base, folded in the M-106 deterministic repro
(Control/Cavern/Runway) and the bass-instrument-is-correct clarification.
findings-index.csv regenerated via tools_pc/gen_findings_index.py.

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG

* docs(D235): clarify trigger is death/restart, not a Facility-only regression

User clarification: the wrong-texture symptom isn't specific to Facility
and isn't a general build regression -- it's triggered by dying/
restarting a level (seen on other levels too). Reframes the leading
hypothesis from "bisect against #47/#49" to a texpool/CI-cache reset gap
on level re-entry (D217/D228 neighbourhood). Updated GRAPHICS-BACKLOG.md
+ findings.md; findings-index.csv regenerated.

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
2026-09-12 10:00:01 -04:00
James Dansereau e141e59316 fix(D194b): decouple mouse-look sensitivity from poll/frame-rate drift
Part (b) of D194 ("sensitivity correlates with movement/frame rate"):
input.c converted raw accumulated mouse px directly to stick counts,
treating "one inputComputePad(0) drain" as a fixed-size time unit even
though the real interval between drains drifts with render/scene load.
Same physical hand motion -> different emitted turn rate depending on
how ragged the poll cadence gets.

Fix: measure real elapsed time between drains with a monotonic clock
(SDL_GetPerformanceCounter) and rescale the accumulated px by
(1/60s / dtActual), clamped [0.25, 4.0], before MOUSE_TURN_GAIN/AIM_GAIN
apply -- so "px this poll" always means "px for a nominal 1/60s tick"
regardless of actual poll-to-poll timing. 1/60s matches D193's own
measured steady-state sim tick rate. Applied only to the aim-mode and
hipfire gameplay-look branches; menu-cursor paths (D165/D169's
poll-count-based P-controller) are untouched.

New Input.MouseDtDecouple config knob (default 1, escape hatch to the
prior frame-coupled behaviour) + a GE_INPUTLOG lookdt trace line.

Part (a) (aim-mode response curve) and the CONTROLLER_CONFIG_SOLITARE
controldef switch are deliberately NOT touched here -- both are design
decisions (D238 overlaps significantly with part (a)) that should get
the user's own interactive feel-testing rather than be baked in blind.

Build green (241/241), verify.sh bunker1 PASS (crash-free). Cannot be
feel-verified headlessly -- needs a human playtest (A/B against
Input.MouseDtDecouple=0).

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
2026-09-12 09:54:33 -04:00
James Dansereau 646798be56 docs(M-102): fix findings-index false-positive, release <version> bug, land parked site cleanup (#53)
* docs(M-102): fix findings-index false-positive, release <version> bug, land parked site cleanup

Docs triage pass (docs/dev/notes/PROMPT-docs-triage-session.md).

- gen_findings_index.py: D194's row was silently bucketed FIXED (an open
  design item) because it had no distinct status cell, so the generator's
  fallback keyword search matched "fixed" somewhere inside 6,500 chars of
  narrative -- the same misleading-index failure mode that cost a session
  on D227 (see docs/dev/findings.md SS H "D227 -- M-98"/"M-99"). Gave D194
  a real status cell. Also extended LEAD_RE/bucket() with 5 verdict tokens
  already in use in the doc (ANALYZED, GUARDED, DIAGNOSTIC SHIPPED,
  ROOT-CAUSED, NOT A BUG) and added a leading verdict word to 4 more rows
  that had a status cell but no recognized token -- fixes all 10 rows the
  generator previously bucketed "?" (D175 D174 D173 D220 D160 D133 D136
  D156 D153, plus D194). findings-index.csv regenerated; --check passes;
  diff reviewed, only these 10 rows moved.
- ci.yml: release-notes.md's <version> placeholders were never substituted
  before `gh release create --notes-file`, so every published release body
  (including the live v0.1.0 release) ships the literal text. Now sed'd to
  the tag first.
- Landed docs/dev/notes/UNLANDED-docs-site-cleanup-steps3-5.{md,patch}
  (parked M-101, never pushed, branch deleted): deletes the orphaned
  docs/StructureGuide.md (0 tracked references), adds title/description
  frontmatter to 5 published Pages docs, rewrites docs/index.md's stale
  "Phase 2 of 4 / no audio yet / 19-of-21" status text to match the
  current README. The 2 files whose hunks no longer applied via `git am`
  (docs/index.md, docs/dev/agentic-development.md) were redone by hand
  against current main.
- docs/BACKLOG.md (local, gitignored): retargeted a dead
  docs/PORT-LEARNINGS.md reference to docs/porting-notes.md.

Larger triage items (findings.md monster-row convention, full
status-accuracy sweep, duplicate-defect clustering, dev-notes plan
triage, HANDOFF rotation policy) scoped but not started -- see
docs/dev/notes/FOLLOWUP-docs-triage-session.md (gitignored).

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG

* docs/index.md: fix stale in-level-music claim before merge

This PR predates the M-102c/M-105 correction (already landed in the
README via PR #55): in-level music is NOT absent, it plays with some
tracks having wrong-sounding instruments / occasional garbling (D77
fixed, D230 filed for the residual). This branch's own rewritten Status
paragraph still said music 'doesn't yet' play -- fixing it to match
before merge so main doesn't briefly regress to a stale claim this
session already corrected elsewhere.
2026-09-12 09:33:54 -04:00
James Dansereau 8f0abae18f QoL: Game.NoHitFlash toggle (D232) (#58)
* QoL: Game.NoHitFlash toggle (D232, route-(b) in currentPlayerSetFadeColour)

The community 'no damage flash' option. Every fade routes through
currentPlayerSetFadeColour; the only non-black ones are the two damage
paths (white g_DamageTypes envelope + red 150,0,0 death overlay), so
zeroing the RGB there kills exactly the hit flash and leaves blackouts,
the colour screen and the death fade untouched. Default 0 = original.

Frame A/B verification in progress; finding entry to follow.

* D232: add GE_NOHITFLASH_TRACE diagnostic to currentPlayerSetFadeColour

Env-gated osSyncPrintf (pattern per GE_D160) logging r/g/b/frac/caller
whenever a non-black fade reaches the gate. Used to resolve the frame-A/B
anomaly from the previous session: zero trace fires during a headless
no-input BUNKER1 run confirms the gate is inert absent real damage, and a
controlled ini-toggled A/B (framediff.py, exact + tolerant modes) shows
the gate's visual effect (99/100 frames match) is far smaller than the
port's own run-to-run noise floor (0/100 match between two runs of the
identical build) — the anomaly was D117 nondeterminism, not a real bug.
Full writeup in docs/HANDOFF.md (local, gitignored).

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG

* D232 fix: zero frac too, not just RGB, in the NoHitFlash gate

User playtest found the white hit-flash was gone but a black fade-to-black
still played on every hit -- the gate zeroed r/g/b but left frac (the
overlay's alpha/opacity) untouched, so a fully-opaque BLACK rectangle drew
at the same envelope the white one would have. Same visual intrusion,
different colour, not what the toggle promises.

Fix: zero frac too when the gate fires, so the overlay doesn't draw at
all. This also drops the GE_NOHITFLASH_TRACE diagnostic added earlier
this session to hunt the original (unrelated, since-resolved) headless
frame-diff anomaly -- no longer needed now that the real bug is fixed and
user-verified in a live playtest.

User-verified: hit flash fully gone, no residual black flash, blackouts/
colour-screen/death-fade untouched.
2026-09-12 09:20:13 -04:00
James Dansereau a24495672e docs: scrub stale status claims (D77 music, D227 sky, D217 green textures) (#55)
* docs: scrub stale status claims (D77 music, D227 sky, D217 green textures)

The M-104 roadmap refresh flagged four live inaccuracies; this fixes all
four plus files the missing music-quality finding:

- README: in-level music no longer described as silent/absent (D77 fixed,
  PR #42); sky line no longer 'still being fixed' (D227 merged, PR #43) --
  D229 green water is now the named sky-path residual.
- GRAPHICS-BACKLOG.md: D77 row updated PARTIAL -> RESOLVED with root cause.
- findings.md: D217 status cell led 'OPEN -- root cause characterised
  (M-86)' although the green defect was fixed and merged (PR #47/#49, two
  human playtests); now leads FIXED with the D228 split-out noted.
- New D230: in-level music quality residual (wrong instruments on some
  tracks -- elevator named specifically -- plus occasional garbling).
  Scope pointed at the seq/instrument-bank decode path, not the proven
  mixer/trigger layers.

findings-index.csv regenerated; gen_findings_index.py --check passes.

* docs(D190): root-caused and closed — level 45 is MP-only, no solo setup in ROM

M-48's 'Linux-only crash' was a misframing. Full chain (reproduced on
Windows this session, 0xc0000005 in modelLoad via domakedefaultobj with
obj=0x784f):

- setup_text_pointers[45] = "UsetupimpZ" but that name is absent from the
  ROM file table; only the MP variant "Ump_setupimpZ" exists. BASEMENT is
  a 4P MP level (front.c multi_stage_setups[]) — its solo name was never
  shipped, like the other MP-only levels (STACK/TEMPLE/COMPLEX/CAVES/
  LIBRARY).
- A solo -level_45 boot requests the nonexistent file. On PC the inert
  rmon stubs leave fileGetIndex's dynamic-path response uninitialized ->
  size-0 table entry -> 0-byte load into a stale pool buffer -> the
  propDef walk runs over stale memory (M-48 hit add_ptr_to_objective;
  same garbage walk, different contents).
- N64 parity: file genuinely absent there too; the boot is broken in the
  original as well. No fix taken (a port guard cannot synthesize a valid
  setup; fabricating ROM content has no ground truth).
2026-09-12 09:19:04 -04:00
James Dansereau 3c11e40c13 fix(D223): directional mouse-wheel weapon cycling (#54)
* fix(D223): directional mouse-wheel weapon cycling

Wheel-up keeps the existing 2-poll A pulse (weaponForwardOffset: fresh
A edge, Z released). Wheel-down now runs a 2-poll backward sequence:
A-only poll, then A+Z_TRIG -- the fresh-Z-edge-while-A-held combo that
weaponBackOffset requires (bondview2.c:5337-5340); ordering matters
because the edge is computed against the game's own previous poll.
moveData.triggerOn (bondview2.c:5419) requires A to be released, so
the synthesized Z edge can never fire a shot.

Roles are stable: the port forces CONTROLLER_CONFIG_HONEY
(bondview.c:1484), whose scheme gives invButtons=A_BUTTON /
shootButtons=Z_TRIG -- the same roles the pre-fix forward pulse
already relied on. While RMB aim is held the game flips controldef to
KISSY, under which neither direction cycles -- identical to the old
forward-only behavior in that state (pre-existing, not a regression).

Port-only; no src/ changes.

* docs(D223/D196): mark D223 FIXED (M-105); correct stale D196 status

D223 row now records the implemented fix and its known aim-mode
limitation. D196 was already fixed and merged (a17d5466, the
prescribed 1-line applyCursorVisibility() after reconcileGrab()) but
its status cell still led OPEN -- same stale-claim class the roadmap
flags; corrected. findings-index.csv regenerated.

* D223 follow-up: fix wheel-down backstep racing the real game tick

User playtest (M-105 cont.) found the backward-cycle combo unreliable:
PP7->AK47->PP7->wheel-down sometimes correctly landed on melee, but
sometimes landed back on AK47, and separately AK47->wheel-down sometimes
skipped past PP7 straight to melee.

Root cause: the wheel-down pulse staggered the synthetic 'hold A, tap Z'
combo across two polls (A alone, then A+Z one poll later). Whenever those
landed in two SEPARATE game ticks -- which happens unpredictably depending
on real-time poll/tick alignment (same D117-class nondeterminism as
everywhere else in the port) -- the 'A alone' tick was itself a fresh A
edge with no Z held, which bondview2.c's weaponForwardOffset formula (both
control-scheme sites) reads as a genuine cycle-FORWARD request, firing a
real, visible weapon change *before* the correcting backward tick ran.
Confirmed with an instrumented trace build: every wheel-down after the
very first one in a session mis-fired as a forward step.

Fix (port/src/input.c only, no game-logic touched): present A and Z
together from the first poll instead of staggering them, so whichever
single tick samples the 0->(A|Z) transition sees them rising
simultaneously. weaponForwardOffset requires Z NOT held, so that's
unambiguous -- only the backward path can fire, regardless of tick
timing. moveData.triggerOn (actual fire) is separately gated off while
A/invButtons is held, so this doesn't introduce an accidental shot.

User-verified: repeated up/down cycling through PP7/AK47/melee/Skorpion
in both directions with no more mismatches.
2026-09-12 09:18:43 -04:00
James Dansereau 1a97895a37 docs(issue #6): resolve 3 of the drop-in-ROM plan's open questions (#57)
Research pass (local Qwen delegate, repo-mapper role, findings spot-checked
against the actual files):

- filelist.u.csv hard-coding in d43_emit.py/d69_emit.py is confirmed real
  (converts PAL/JP ROMs with NTSC byte offsets); per-region filelist.e.csv/
  .j.csv already exist and have the same shape, so the fix is narrow. Real
  remaining unknown: whether modelFileHeader.inc.c's NS/NT differ cross-
  region (needs an actual PAL/JP ROM to check).
- Native converter output path for Part B nailed down exactly:
  data/pcmodels-<region>/{pcmodels.bin,manifest.csv} and
  data/pccg-<region>/{pccg.bin,manifest.csv}, via pcmodels.c/pccg.c's
  existing $S//$E//./ + country-byte region resolution.
- Part B's CMake codegen scope narrowed to one real artifact: a 512-entry
  model-symbol -> (NS,NT) table from modelFileHeader.inc.c. Everything else
  the emitters read is either runtime-readable from disk or a small
  hand-transcribable constant.
2026-09-12 09:18:16 -04:00
James Dansereau 4bfb64093e CLAUDE.md: dispatch preflight rule -- branch delegate output off main explicitly (#59)
This session's docs/issue-6-dropin-rom-plan PR (#57) got 'git branch <name>
<sha>' where <sha> happened to be on top of an unrelated in-flight feature
branch instead of main -- the resulting PR silently carried that branch's
whole history (including code since reverted) into the diff. It only
surfaced as a CI failure (a stale env-probe-index check), not from
inspecting the PR diff, meaning it could easily have gone unnoticed.

Adds an explicit rule to the Local Qwen dispatch section: branch off
main/origin/main HEAD explicitly when landing delegate output, never off
whatever the session happens to have checked out, and sanity-check with
'git log --oneline main..<branch>' before pushing.
2026-09-12 09:17:28 -04:00
James Dansereau 741dfe78b9 fix(D227/D176a): sky texcoords are S10.5, not texels — fixes seam, over-tiling and scroll speed (#43)
* fix(D176a): sky scroll/tiling too fast -- perspective-correct tc interpolation

skyPortRenderPoly (Path B, D176(a) black-sky fix) placed sky quad verts
under a plain w=1 ortho projection, so fast3d's GPU pipeline interpolated
texture coords linearly in screen space across the primitive. The source
S/T data (unk20/unk24) is computed upstream from a world-space projection
without a perspective divide -- exactly like the real N64 RDP's
G_TRI_SHADE_TXTR coefficient block, which relies on real perspective-
correct interpolation to divide it back out per pixel.

A single sky quad's near (top) and horizon-grazing (bottom) vertices can
differ up to ~38x in camera-space w. Under linear interpolation that
smears the entire S/T delta evenly across every pixel instead of
concentrating it near the horizon, producing a uniformly dense aliased
'hatching' over the whole sky -- reported as 'scrolls too fast / tiles
too visibly'.

Fix: build a custom projection matrix that gives each vertex its real
camera-space w (already computed upstream, SkyRelated38.unk0c) while
keeping the same screen-space x,y, so the GPU's normal perspective divide
recovers the intended position but interpolates tc (and colour) with
real 1/w weighting -- the same pipeline every other textured draw in the
game already depends on. No port/fast3d changes; #ifdef PORT only, N64
path untouched.

Verified: tools_pc/verify.sh bunker1 PASS (no regression on a non-sky
level); -level_22 (Statue) and -level_41 (Cradle) GE_PCDUMP captures show
the expected before/after change (uniform parallel aliasing -> a proper
perspective-converging fan), and Cradle's sky is no longer black at
default FOV. Defect 2 (horizon-band tessellation) and the FOV-coupled
black-sky-past-frustum symptom are unchanged / still open.

docs/dev/findings.md: new D176(a) subsection + summary-row update.
docs/porting-notes.md: generalized the screen-space-overlay quirk to
call out the w=1-vs-real-w interpolation pitfall.

* chore(D176a): regen findings index

* docs(D176a/D227): M-93 - human playtest of PR #43 confirms Cradle black-sky fix + partial Statue scroll improvement; new split-sky residual filed as D227



* docs(D227): M-94 - visual evidence + a concrete mechanistic hypothesis for the sky seam (per-primitive wScale quantization), not yet fixed

Headless -level_22 capture (this shell can do offscreen GE_PCDUMP capture
via the game binary directly, run from the repo root with the MSYS2
mingw64 PATH set -- the earlier GUI-launch flakiness in this session was
specific to a live window, not this path) shows a real, static-frame
crease in the sky's cloud texture. Screenshot committed:
docs/img/bugs/d227-sky-seam-statue.png.

Hypothesis: skyPortRenderPoly (D176(a)'s fix) computes wScale per call,
and skyRenderTri/skyRenderFull issue up to 5 separate calls per frame
(one per triangle in the sky fan). The position math is exact for any
wScale (re-verified algebraically), but Vtx.ob[] is s16, so two
triangles sharing an edge can quantize a logically-shared vertex to two
slightly different values depending on which triangle's independently-
chosen wScale computed it -- crack at the shared edge, reads as "two
regions with a different pattern."

Not yet confirmed (didn't isolate camera motion from sky-scroll motion
to test the "moves at a different rate" half of the report) and not
fixed -- documented the fix direction (one wScale shared across the
whole fan, not recomputed per triangle) for whoever picks this up next.
Cleaned up capture scratch files after pulling the one evidence shot.

* docs(D227): M-94 addendum - static-camera test confirms the sky genuinely animates over time, independent of camera motion

* fix(D227): M-95 - one shared wScale across the whole sky fan instead of one per triangle, closes the visible seam

skyRenderTri/skyRenderFull draw up to 5 triangles per frame from a
shared pool of up to 5 vertices (built once, then fan-triangulated
across 1-3 calls). D176(a)'s fix had each call compute its own wScale
from only its own 2-4 verts; two triangles sharing a vertex could each
pick a different wScale, and since Vtx.ob[] is s16, quantize that
logically-shared vertex to two different values depending on which
triangle's call computed it -- a crack at the shared edge (D227, the
"two sky halves" report).

Fix: skyPortBeginFan()/skyPortEndFan() (new, #ifdef PORT) compute one
wScale across the whole fan and skyPortRenderPoly uses it when set,
falling back to the old per-call computation otherwise. N64 path
unchanged (these are PORT-only additions around existing #ifdef PORT
code, no #else branch touched).

Verified:
- Clean build, no new/duplicate symbols.
- Full verify.sh sweep: 21/21 PASS, both with and without this change.
- Same-frame before/after screenshots at -level_22 frame 345
  (docs/img/bugs/d227-sky-seam-statue{,-fixed}.png): visible kink in
  the cloud streaks in the before shot, gone in the after shot, same
  exact frame.
- bunker1's nonzero framediff worst_cell is unaffected by this change
  (reproduced identically pre-fix) -- confirmed pre-existing stale
  golden-baseline issue, unrelated to sky.c.
- Cradle's black-rectangle artifact is also confirmed pre-existing
  (reproduced identically pre-fix) -- the separate, already-documented
  FOV-coupled black-sky-past-frustum-edge symptom (D218/D222 family),
  not touched by this fix.

Not re-verified: whether the "moves at a different rate" half of the
original report is also resolved, and the aspect-ratio-artifact
alternate theory was never independently ruled out. Human eyeball
still owed before merge, same as every other rendering fix landed this
session without interactive display access.

Also generalized the quirk (per-call vs per-shared-vertex-group scale
quantization) to docs/porting-notes.md alongside the existing D176(a)
entry.

* docs(D227): M-96 - human playtest on Statue: seam/crack STILL PRESENT despite M-95 fix

The M-95 wScale-sharing fix only verified the static per-primitive
quantization theory (single-frame screenshot diff); it explicitly left
the 'two halves move at different rates' temporal theory from the
original report unverified. This playtest is that missing
verification, and it says the seam is still visible in live play.

D227 NOT resolved. PR #43 stays in draft pending investigation of the
temporal/rate theory -- do not merge on the M-95 fix alone.

* docs(D227): M-96 addendum - N64 sky should be near-static, this build scrolls extremely fast

New data point from the same Statue playtest session, separate from the
seam/crack: the user reports the intended N64 scroll rate is near-static,
stronger than M-93's 'still reads too fast'. Bears on D176(a)'s fix
magnitude itself, not just the D227 seam -- flagged as a possible shared
root cause (wrong per-frame tc delta could drive both symptoms), not yet
investigated.

* fix(D227): M-98 - share sky S/T fold across whole fan, not per-triangle

skyPortBeginFan() (M-95) shared wScale across a whole sky fan so shared
vertices quantize position consistently, but skyPortRenderPoly's S/T
fold (foldS/foldT -- floorf(minS/64)*64, keeps texel coords in s16
range) was still computed per-call, from only that triangle's own 2-4
verts. Same bug class as the wScale fix, one field over, never fixed --
which is why M-96's human playtest still saw the seam after M-95.

Two triangles in one fan can floor a shared vertex's S/T to different
multiples of 64 texels; nominally invisible (GL_REPEAT period 64), but
each triangle bakes (S-fold)*32 into its own s16 Vtx buffer, so two
folds 64 texels apart produce tc values 2048 apart -- a real crack at
the shared edge.

Fix: extend skyPortBeginFan()/skyPortEndFan() to also compute one
shared fold across the whole fan, threaded into skyPortRenderPoly the
same way as wScale. #ifdef PORT only, no game-logic change.

Also measured (per M-96/M-97's ask): cross-correlated 11 consecutive
frames at a static camera -> ~7 px/tick horizontal scroll at 640px
capture width, a full screen-width sweep every ~1.5s. Confirms
"extremely fast" quantitatively; root cause of that magnitude is not
yet found (accumulator + vertex geometry are unmodified N64 logic, so
if it's genuinely wrong the bug is in how the Path B substitution maps
texel-ticks to screen pixels, not the tick rate itself -- next step
spec'd in findings.md).

Verified: build clean; -level_22 frame 345 (same frame M-95's
before/after used) shows no visible crease vs. the pre-M-95 reference
screenshot; verify.sh bunker1 PASS, worst_cell unchanged (known stale
golden baseline, not a regression). Human eyeball still owed on both
the seam-close and the scroll-speed question before PR #43 leaves
draft.

* docs(D176a): M-98 - Cradle side of PR #43 checked; tower starburst is Defect 2

Checked the Cradle side of the D227 fold-sharing fix (M-97 left this
outstanding). No regression -- verify.sh cradle PASS -- but found a
much more visible pre-existing artifact while there: looking up the
suspension tower near spawn, roughly half the sky renders as a sharp
radiating starburst instead of clouds.

Confirmed pre-existing (pixel-identical with sky.c reverted to
pre-M-98) and confirmed static, not the D227 "moves at different
rates" symptom (pixel-identical 30 ticks apart at a fixed camera).
This is the already-known, already-open D176(a) Defect 2
(horizon-band tessellation) -- the code's own fold-logic comment
already predicts exactly this failure shape for a triangle spanning
>1000 texels near the horizon, which looking straight up the tower
produces. Not fixed (tessellation is separate, bigger work); logged
with a concrete repro + screenshot so it's no longer "inconclusive."

* docs(D227): M-99 - human playtest negative (seam persists, view-angle-dependent); headless repro blocked

Human playtest after M-98's fold-sharing fix: the seam is still present
on Statue and now confirmed to move/change with mouse/view position --
also reported still bad on Cradle and present on Surface. M-98's fix
was real but is not the dominant mechanism live play is hitting, same
pattern as M-95 before it.

Ruled out this pass: the cross-quad water/cloud texture-scale mismatch
theory, on the two IsWater==0 levels actually tested (Statue, Cradle)
-- untested on any IsWater==1 level, which is now the top ranked
candidate for next session.

Blocked on: headless camera-pan repro via GE_INPUTSCRIPT does not
work -- confirmed neither analog-stick nor C-button tokens rotate the
camera, so this needs either a human playtest or a debug hook into the
real mouselook path before it can be chased further headlessly. Full
ranked candidate list + environment notes for next session in
findings.md and (session-local) docs/HANDOFF.md.

* docs(D227): M-100 - root cause PROVEN: s16 texture-coordinate overflow

The sky seam is not a crack, not a quantization mismatch, and not
view-dependence. One sky quad spans ~30,000 texels in S and T, but
Vtx.tc is S10.5 in an s16 = +/-1024 texels, so 7 of its 8 texture
coordinates overflow and wrap to arbitrary values. The fold cannot help
against a span 29x the representable range.

Unblocked by a user-supplied emulator-vs-ours screenshot pair on Surface:
the real symptom is a radial starburst, not a crack, which is a different
mechanism from what M-94..M-98 were chasing. M-95 (shared wScale) and M-98
(shared S/T fold) each fixed a real second-order bug; neither could touch
this one. Both stay - per-fan consistency is exactly what will make
subdivision seamless.

Measured with a new env-gated GE_D227V probe (GE_D176 style) on the Cradle
frame-350 camera. All four w are positive, so no projective pole. The quad
is split gSP2Triangles(0,1,3, 0,3,2) on the 0-3 diagonal, and vertices 0
and 3 are exactly the two that keep one coordinate in range - hence "two
halves with different patterns, split by a line", the original D227 report
verbatim. Apparent view-dependence needs no view-dependent mechanism: S/T
depend on view, so the wrap point moves.

Confirmed by a diagnostic build: baking tc at 1-texel instead of
1/32-texel granularity (32x range, deliberately wrong scale) makes the
starburst and the split completely disappear. No second defect behind it.

D227 == D176(a) "Defect 2" == M-98's "Cradle tower starburst". M-98
misfiled that capture as unrelated, which is what made D227 look
headlessly-unreproducible in M-99 - it reproduces statically.

Fix not implemented: subdivide until span < ~960 texels/primitive. The
subdivision math and cut placement are worked out in the finding (S is
linear in w, so cut uniformly in w), but the vertex budget is an open
decision - dynAllocateVertices is an unchecked 50 KB/frame pool and pure
tessellation wants ~40% of it.

* docs(D227): M-100 addendum - RDP tile shift (option D) dominates; fast3d already implements it

shifts/shiftt is the RDP's native precision-vs-range knob and gfx_pc.cpp
implements it fully including the left-shift case. Emitting tc at 32/2^k
units with shifts = 16-k buys 2^k range with no tessellation, no fast3d
change and no vertex-budget cost. k=5 covers the measured 29,830-texel
span outright.

* fix(D227): M-100b - sky texcoords are S10.5, not texels; drop M-82's stray *32

Fixes the sky seam/starburst, the over-tiling, and the "scrolls too fast"
complaint - all three were the same bug.

M-82 ("D176(a) Defect 1") read unk20/unk24 as texel counts and baked tc as
(S - fold) * 32 to convert texels into tc's S10.5 units. They are not texel
counts. skyRender sets them as worldpos * 0.1f and the N64 path hands them
to the RDP coefficient block unscaled (skyRenderTri: S * unk34 * sp368), so
they are already in the RDP's native S10.5 1/32-texel units.

That stray *32 made every sky texture coordinate 32x too large, producing
three symptoms previously tracked as separate bugs:

  1. D227 - spans of ~30,000 texels against an s16 tc holding +/-1024, so 7
     of a quad's 8 coordinates overflowed and wrapped to arbitrary values.
     The quad splits on the 0-3 diagonal and verts 0 and 3 are exactly the
     two that keep one coordinate in range, hence "two halves with different
     patterns" - the original D227 report verbatim.
  2. Over-tiling - ~466 repeats of the 64-texel cloud per quad, not ~15.
  3. M-96's "the N64 sky is nearly static, ours races", measured at ~7
     px/tick in M-98. g_SkyCloudOffset advances 1 unit/tick = 1/32 texel,
     not 1 texel. This is why M-98's re-derivation of the tick path kept
     coming back clean: the accumulator was never wrong, its unit was.

Fix: tc = (S - fold) 1:1, with the fold rebased to a whole tile period
(SKY_TC_WRAP = 64 texels * 32 = 2048 units). The ~30,000-unit span then fits
an s16 unaided - which is also why the original N64 code needs no
tessellation here: it never had a range problem to solve.

Kept: M-95's shared wScale and M-98's shared fold (both real bugs; per-fan
consistency keeps a fan's shared vertices bit-identical). Also kept as a
safety valve, since only ~4% of s16 headroom remains on the observed span:
skyPortPickShift/skyPortCaptureTile/skyPortEmitTileShift, which trade tc
precision for range via the RDP's own tile shifts/shiftt (port/fast3d
already implements it, so no fast3d change). skyPortCaptureTile replays the
exact G_SETTILE texSelect emitted with only the shift fields patched, so it
cannot drift from texSelect's format/line/mask derivation.

Also adds the env-gated GE_D227V per-vertex probe (GE_D176 style) that
measured all of this.

Verified: -level_41 Cradle frame 350 (the M-98 starburst repro) now shows
soft cloud bands at the correct scale with no starburst, diagonal split or
moire; GE_D227V confirms kS=kT=0 and max tc 31,513 of 32,767. -level_22
Statue frame 345 clean. verify.sh bunker1 PASS (worst_cell 10.32, the known
stale-golden-baseline number). Human play-test still owed.

* D229: file green/pulsating water; decouple D227's tc safety valve from it

New finding D229: on IsWater levels (Frigate, Surface 2) the water renders
green and pulses blue<->green on an interval.

Pre-existing, proven by A/B: built plain main with none of the D227 work and
captured -level_26 frame 340 - same green, only the tiling density differs.
D229 is independent of D227.

The gun half of the same report was NOT a bug: the branch was 2 commits
behind main and those 2 commits were exactly the D217 fixes (#47 texpool
alignment, #49 CI cache palette-content key). After merging main the user
confirms the gun is good on Frigate.

Mechanism so far: the water quad is sky.c's IsWater block1 (Frigate confirmed
IsWater=1 via GE_D176, which also settles M-99's ranked item 1).
sub_GAME_7F09343C binds tile 0 and tile 1 to the SAME tmem, offsets tile 1 via
gDPSetTileSize(1, 90, 150, 0, 0), and cross-fades TEXEL0/TEXEL1 with a
PRIM_LOD_FRAC driven by sinf(). That sine is the reported pulsing, so one
texunit resolves blue and the other green. Ruled out the shade path by
measurement (it spans dark blue to pale yellow, never green; sampled pixels
have R and B crushed). Not yet root-caused.

Code change here is only the decoupling: skyPortBeginFan takes allowShift and
the water quad passes FALSE, leaving that path byte-identical to its pre-D227
behaviour. The water quad is a two-texunit draw sharing one set of vertex tc,
so a tc rescale needs a matching shift on both tiles - and shifting both makes
them sample identically, which flattens the cross-fade. An intermediate build
that did so turned the water from pulsating into constantly green, observed
live. Also fixes skyPortCaptureTile to capture after all tile setup (not just
after texSelect) and to handle both tiles, so a future shift cannot clobber a
later re-declaration.

Cradle sky re-verified unaffected (frame 350 still correct). bunker1 golden
framediff reports REGRESSION at 55.02, but plain main scores WORSE at 73.24 -
the baseline is stale relative to main's D217 texture fixes, the re-baseline
owed since M-83/M-84. Not caused by this branch.

* docs(D227/D229): regenerate findings index; mark D227 FIXED

The generated findings-index.csv was hand-edited during the main merge and
when D229 was filed, so CI's 'Check generated dev indexes are current' failed
on PR #43. Regenerated with tools_pc/gen_findings_index.py.

Also updates the D227 row's status to lead with FIXED (M-100b) rather than
the M-94 'not yet fixed' text, so the index reflects the user-confirmed fix.

* docs: register GE_D227V in the env-probe index

CI's 'Check generated dev indexes are current' also runs
gen_env_probes.py --check, which flags any GE_* probe with live sites that
has no row in docs/dev/GE-ENV-PROBES.md. GE_D227V (added in M-100 to measure
the sky texcoord overflow) had none.
2026-09-11 20:45:08 -04:00
James Dansereau f800275247 fast3d/D217: key CI texture cache on palette content, not just TLUT address (#49)
GE assembles weapon and character model display lists in scratch arena RAM
and reissues gDPLoadTLUT from a repeated source address with different
palette *content* between materials and frames. The CI TextureCacheKey
keyed only on {texel addr, palette_addrs[0/1], palette_index, size_bytes}
with no content check, so gfx_texture_cache_lookup() (a plain map.find)
could return a stale HIT whose GL texture was decoded against a different
palette. Proven with an env-gated probe on -level_34: the same
{addr, paladdr} pair re-imported many times per frame with materially
different rdp.palette content, all taking HITs.

Fix: FNV-1a hash of the 512-byte rdp.palette, recomputed only in
gfx_dp_load_tlut() (rare vs draws -> no per-texel or per-draw cost), added
as a trailing field of the CI cache key. Non-CI keys carry 0. Cannot
produce a worse decode than the stale HIT it replaces; worst case is a few
extra texture uploads per frame (~2 measured on -level_34).

Partial for D217: this removes a real stale-palette-HIT class (the D217
finding's own "test a palette-content hash" next-step) but does NOT resolve
the visible green PP7 grip. That has an upstream cause - the grip's count=1
model TLUT is fed different source bytes per level (Bunker 0x0001 -> black,
Facility 0x1A00 -> green) for the same tile; fast3d faithfully stores what
it is given. See scratchpad D217-report.md. Full 21-level world-CI
regression sweep still owed before merge.

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
2026-09-11 18:17:29 -04:00
James Dansereau 50d73c519c D217 (M-90): align texpool arena bases to 8 bytes on PC — fixes solid wrong-colour weapon/model CI tiles (#47)
* D217 (M-90): align texpool arena bases to 8 bytes on PC — fixes solid wrong-colour weapon/model CI tiles

Root cause: texAlignIndices pads CI index rows to ABSOLUTE 8-byte
boundaries, and model-DL G_LOADTLUT palette offsets are a formula that
only matches the real layout when the texpool base is 8-aligned. On N64
the memp bump allocations feeding these pools happened to be 8-multiples;
PC pointer bloat (D36/D37-class struct growth) made some STAGE-bank sizes
not, so weapon-pool bases land %8==4 (e.g. FACILITY). Every palette in a
misaligned pool then sits 4 bytes early inside its allocation while the
draw reads the N64 offset: the PP7 grip's count=1 TLUT lands on the next
tenant's 8-byte header slot (texnum=26 -> LE s16 -> 0x1A00 = the green),
and larger loads read a 2-entry zero pad then the real palette (the M-86
'leading zero pad' tell). BUNKER1 was clean only because its bases
happened to be aligned.

Fix: texInitPool rounds the arena base up to 8 under #ifdef PORT; end
unchanged (pool loses <=7 bytes). No-op on N64 where bases were already
aligned. ABI/layout-class edit per AGENTS.md rule 2 narrow exception —
no game logic touched.

Verified: FACILITY 188/188 decodes %8==0, draw reads pal0=0001 (was
1a00), 0 green pixels in frames 250-450 (pre-fix ~1300); BUNKER1 + DEPOT
still clean; full verify.sh sweep 21/21 PASS. Pending: human playtest
eyeball of the M-87 weapon matrix (closes defect 1). Defect 2 (CI cache
content-hash key, fix/d217-model-texture-palette) remains separate/parked.

findings.md: new section H 'D217 - M-90' + D217 row update; GRAPHICS-BACKLOG
D217 status.

* docs(D217): M-92 - human playtest of PR #47 confirms PP7/PPK/watch fixed; AK47 + NPC-held world-model weapons are a distinct, still-open white-texture residual

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG

* docs(D217/D228): M-97 human playtest - green FIXED, AK47 white patches split out as D228

Second independent playtest (after M-92) of PR #47 on FACILITY, this
time with the branch merged up to date with main. Confirms the same
split the M-90 write-up predicted: PP7/PPK grip+hammer, watch face and
world-model green are all fixed by the texpool alignment; NPC-held
AK47 white missing-texture patches are untouched.

White is a different signature from wrong-colour -- a wrong palette
binds a real TLUT with wrong contents, white means the texture was
never decoded/uploaded at all -- so this is a separate mechanism, not
an incomplete fix. Split out as D228 so D217 can close on the green
mechanism it actually describes instead of carrying an unrelated open
failure mode indefinitely.
2026-09-11 18:06:03 -04:00
James Dansereau 6c49c25083 Merge pull request #50 from jkdansereau/fix/d221-fileselect-hitbox
front/D221: file-select mouse hit-box — pass explicit {min,max} pairs (stack-layout dependency)
2026-09-11 17:47:07 -04:00
James Dansereau 9d45c6aae6 chore(D221): regen findings-index.csv (CI validate) 2026-09-11 17:43:52 -04:00
James Dansereau 4dd596a570 docs(D221): record human mouse-playtest verification (M-91)
Clicking directly over each visible file wallet selects it immediately
across all slots and the Copy/Erase icons; no crash. Closes the last
open item on PR #50.
2026-09-11 17:41:42 -04:00
James Dansereau cc332d7287 merge main into fix/d221-fileselect-hitbox
Resolves the D219/D220 findings.md conflict: main had the M-90 D219
runtime-recheck update (from merged PR #48), this branch had the M-91
D220 analysis (its own second commit) -- kept both.
2026-09-11 17:39:48 -04:00
James Dansereau 972b7e69bf docs: README update - trim repeated framing, fix stale status claims, drop going-stale badges (#52)
* docs: step 2 of the docs/site cleanup - README rewrite + agentic-development.md numbers refresh

README: trim the repeated AI-collaboration pitch to one mention with a
link (was stated near-verbatim 3x in this file alone); drop the status/
tested_on/built_with badges (CI + license only - the others just go
stale); cut the duplicate 'by the numbers' table and mermaid diagram
(both live, and are current, in docs/dev/agentic-development.md); tighten
the comparison-table Why-it-exists/Status cells; update the Documentation
table for the site-config change in the prior PR. Fixes the contradictory
status claims: audio (in-level music does not play yet, D77 -- was
claimed both silent and working in different sections) and Linux (not
actively tested since v0.1.0 -- was claimed both untested and level-swept
in different sections).

docs/dev/agentic-development.md: refreshed the 'By the numbers' table
with git-verified figures (calendar range, commit count, bug count,
ABI-edit file/line counts) -- the old numbers were from an earlier
session and had drifted well behind main. Trimmed the italic pitch
block under the H1 to one plain paragraph; left the timeline, milestone
table, and effort-split estimate untouched (maintainer's own record, not
independently re-derivable from git).

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG

* docs: README fixup pass - trim bold/em-dash density, drop redundant WARNING callout

Benchmarked against comparable projects (Perfect Dark PC port, the
GoldenEye-Recomp competitor, ripgrep as a general engineering-README
gold standard): this file was still running ~42 bold spans and 48 em
dashes across ~2,600 words, well above what any of those use, and the
alpha-status callout box duplicated the Status section's own opening
paragraph.

- Bold audit for real this time: kept proper nouns on first mention,
  table row labels, and one bullet-lead-phrase convention used
  consistently in three lists; cut everything that was just mid-sentence
  emphasis or a second bold of something already established (the ROM
  phrase, Windows/Linux, v0.1.0, a bolded URL). 42 spans -> 33.
- Un-bolded and merged the duplicate audio/music caveat (was bolded once
  in Working and again in Not-yet-working for the same fact).
- Dropped the alpha WARNING callout; folded its one load-bearing
  sentence into the Status section's existing opening paragraph instead
  of stating it twice in two different box styles. Kept the ROM-supply
  IMPORTANT callout - that one's legally load-bearing and not
  duplicated elsewhere.
- Varied em-dash-as-everything-connector into periods/colons/semicolons/
  parens where a flowing sentence called for it (not touched: the
  term-dash-description bulleted lists in Credits/How-it-works/Not-yet-
  working, which are a legitimate, consistent convention, not filler).
  48 -> 33, and the worst offender (a double-em-dash-wrapped parenthetical
  in Background) is gone.
- Reworded the intro's research-angle sentence away from "doubles as a
  research project" to plain prose.

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
2026-09-11 17:37:52 -04:00
James Dansereau 4d45e78176 docs: Pages site cleanup step 1 - curated publish set + cross-link fixes (#51)
* docs: step 1 of the docs/site cleanup - whitelist Pages site config, fix cross-links to excluded docs, drop game-behavior-reference index bullet

Publish only the curated set (index/building/internals/porting-notes/
dev-process/agentic-development); everything else under docs/ stays in
the repo but out of the Pages build and sitemap. Cross-links from the
published pages to excluded docs now point at GitHub blob URLs so they
still resolve. No content rewrite yet -- steps 2-5 (README update, per-
page descriptions, light copy passes, repo hygiene) remain.

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG

* docs(dev-process): add §6 -- keep PR/git-history noise low

Squash-merge single-purpose/docs PRs, batch same-session docs updates
into one branch instead of one-PR-per-finding, keep unrelated
housekeeping off active branches, declare stacked-PR dependencies, and
triage the open-PR list every session -- close stale PRs (superseded
by newer main content) rather than force-merging or endlessly
rebasing them.

Prompted by this session's PR #26: real conflicts against main because
D191/D193/D149 were all fixed/closed since it was opened; closed
rather than rebased.
2026-09-11 17:31:36 -04:00
James Dansereau b577008543 Merge pull request #48 from jkdansereau/docs/d219-runtime-recheck
docs(D219): M-90 runtime re-check — D172's fix covers Dam's particle DLs
2026-09-11 17:19:56 -04:00
James Dansereau fd0f6eb749 docs/D220: analyze muzzle-flash FovScale drift -- correct perspective behavior, not an unscaled-viewmodel bug
The flash nodes share the gun DL and the (scaled) per-frame projection, so
flash-to-muzzle attachment is geometrically preserved; the whole camera-
anchored viewmodel migrates toward screen center as FOV widens. Disposition:
human call -- QoL viewmodel re-anchor if it bothers, not a fidelity bug.
2026-09-11 01:39:50 -04:00
James Dansereau 257e9c3fc1 front/D221: pass explicit {min,max} coord2d pairs to projectRectCornersTo2D in file select
interface_menu05_fileselect passed four separate f32 locals (&xmin, &ymin)
to a function that takes {min,max} coord2d pairs, relying on the N64
compiler laying each max adjacent to its min. The PC toolchain lays the
locals out in declaration order, so arg1->f[1] read ymax (as an X coord)
and arg2->f[1] read an unrelated stack float (sometimes NaN) -- the file-
select hit band collapsed to a ~14px strip at each wallet's bottom edge:
'click below the file to select it' (D221, M-87 user QA).

Pass explicit pairs under #ifdef PORT; identical values to what the N64
binary used, N64 call untouched. ABI/layout exception per AGENTS.md rule 2.

Verified headless: same cursor position + A press -- pre-fix never
selected folder 0, post-fix selects and advances (findings.md §H D221).
2026-09-11 01:36:12 -04:00
James Dansereau 0cb38c6bdc docs(D219): M-90 runtime re-check — D172's fix demonstrably covers Dam's particle DLs
Scripted-fire repro (GE_INPUTSCRIPT, 64 Z-pulses) on -level_33 with
GE_D172=1: 40/40 particle multitex tris sample SAMPLED1=tile1 (tmem=392),
the correct RGBA fire tile (pre-fix behaviour was tile0/smoke); zero
BUG/unknown-GBI/D146-abort lines; 0 purple/magenta pixels across 28
captured frames. Row can close on a human by-eye pass (folds into the
D217 PR #47 playtest).
2026-09-11 01:18:41 -04:00
James Dansereau f0b87ae0f5 Merge pull request #46 from jkdansereau/docs/d219-triage-note
docs(D219): triage note — likely already covered by D172's fix
2026-09-11 00:09:09 -04:00
James Dansereau 2ccc1af51e Merge pull request #45 from jkdansereau/probe/d217-texload-invariance
probe(D217): decode is proven correct — parked defect ② fix does NOT fix the visible symptom
2026-09-11 00:08:58 -04:00
James Dansereau a6b26f430f docs(D219): M-89 local-Qwen triage note - likely already covered by D172's fix, needs a runtime re-check
Static analysis found no separate/unfixed code path -- the explosion
DLs use the identical G_CC_INTERFERENCE/G_CC_MODULATEIA2 two-tile
mechanism D172 already fixed in gfx_lod_tile_offset. Not yet runtime-
confirmed (needs a scripted explosion repro on Dam with GE_D172=1).

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
2026-09-11 00:07:37 -04:00
James Dansereau 3d3044a469 docs(GE-ENV-PROBES): add GE_D217TEX row (validate CI gate) 2026-09-11 00:06:25 -04:00
James Dansereau 78f002e5cf probe+docs(D217): M-89 cont. - decode proven correct+identical; parked defect 2 fix does NOT fix this; new stale-arena-pointer theory
Added a GE_D217TEX env-gated probe (src/game/image.c, texInflateZlib)
logging texnum/numcolours/palette[0..1] straight off the decode
bitstream -- the most direct check of whether the SAME texture number
decodes to the SAME bytes across levels.

Result: texnum=1608 (the PP7 grip, confirmed by its pal0=0001 matching
the previously-documented BUNKER1-correct value) decodes IDENTICALLY on
both BUNKER1 and FACILITY. This disproves the 'different ROM bytes per
level' theory and confirms the bug is strictly downstream of a provably
correct decode.

Then built and tested the parked fix/d217-model-texture-palette
(f6fc0399, the CI-texture-cache content-hash fix, defect (2)) directly
against the FACILITY repro -- still renders green. That fix does not
close this row; defect (1) and (2) are confirmed separate bugs.

New leading theory: a stale/dangling tex->data pointer into the
texpool arena, re-resolved once and not re-checked at draw time, so a
later texture decode reusing the same arena address on a texture-heavy
level silently overwrites the grip's palette bytes. Documented as the
concrete next probe for whoever picks this up.

Probe left in tree, env-gated (GE_D217TEX), inert by default.
tools_pc/verify.sh bunker1: PASS, no regression.

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
2026-09-11 00:01:57 -04:00
James Dansereau 14bf7e59f2 TEMP probe(D217): log decode-time palette[0] keyed by texnum (GE_D217TEX) 2026-09-11 00:00:43 -04:00
James Dansereau 80a3ccea5e Merge pull request #44 from jkdansereau/docs/d217-m89-pitch-hypothesis-refuted
docs(D217): M-89 — refute the gfx_dp_load_tlut pitch/width=1 hypothesis
2026-09-11 00:00:32 -04:00
James Dansereau aa6733436a docs(D217): M-89 - refute the gfx_dp_load_tlut pitch/width=1 hypothesis; new leads
Local-Qwen triage proposed hardcoding pitch=1 in gfx_dp_load_tlut
(port/fast3d/gfx_pc.cpp), on the theory that N64 LOADTLUT addressing
never strides by the SetTextureImage width. Verified by hand before
applying it, and refuted:

- gDPSetTextureImage/gSetImage (include/PR/gbi.h) pack width-1 into the
  raw command field, and texWriteLoadToTmemAddr always passes width=1,
  so pitch = (1-1)+1 = 1 ALREADY for every model-DL TLUT load. The
  proposed patch is a no-op for this call site.
- Continuing the derivation confirms base = tex->data + len (the
  documented pixel-data-then-palette layout from texInflateZlib) for
  every call, regardless of the uls/ult 10-bit-field split -- the
  addressing math is correct by inspection.
- tex->unk0a = numcolours - 1 exactly explains gfx_dp_load_tlut's
  count = unk0a+1 term, matching the M-85 GE_D217Z count observations
  precisely -- the grip tile's count=1 is a genuine 1-color palette,
  not a truncated load.

New leads (neither probed this session): (a) does g_Textures[texturenum]
/_imagesSegmentRomStart resolve to identical ROM bytes across levels for
this weapon (a different image-table path than the already-ruled-out
per-model TextureID/d43_emit.py/Globalimagetable/D68 path), or (b) does
the shared texpool arena (image.c texLoad, two-ended leftpos/rightpos
allocator) serve a stale/aliased tex->data pointer for what should be a
fresh per-level decode.

Docs-only. No code changes -- the proposed patch was not applied.

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
2026-09-10 23:53:12 -04:00
James Dansereau 272b453545 Merge pull request #42 from jkdansereau/fix/d77-inlevel-music-missing-return
fix(D77): in-level music silent — missing return in sub_GAME_7F0C0BF0
2026-09-10 23:41:26 -04:00
James Dansereau 97946633dd chore(D77): regen findings index 2026-09-10 23:38:26 -04:00
James Dansereau 199182d1d9 fix(D77): in-level music silent — missing return in sub_GAME_7F0C0BF0 (D6-class UB)
Root cause: src/game/mp_music.c's sub_GAME_7F0C0BF0() is a one-line wrapper
around get_mTrack2Vol() with no return statement. On N64/IDO the value
happened to already sit in $v0; at the PC release build's -O2 the return
register is genuinely undefined (docs/porting-notes.md D6 class, same as
D187). Every in-level music trigger goes through set_missionstate()'s
MISSION_STATE_1/4 cases (the only path that starts a level's background
track), which feeds this garbage/zero value straight into
musicTrack1ApplySeqpVol()/musicTrack3ApplySeqpVol() right before
musicTrack1Play()/musicTrack3Play() — silencing the whole track for its
entire duration while note-on processing continues normally. Front-end/menu
music bypasses set_missionstate() entirely (front.c calls musicTrack1Play()
directly), which is why it was never affected.

Fix: return the tail call under #ifdef AVOID_UB, #else keeps the N64 body
verbatim. One line, no game-logic change.

Diagnostic: added a temporary, cheap (~1/s, gated on GE_AUDIOTRACE)
[AUDIOLVL] peak/RMS probe of the exact buffer audioSetNextBuffer() hands to
SDL (port/src/audio.c), interleaved into the same audiotrace.log as the
existing csplayer.c [MUSICNOTE] probe via the shared geTracePrintf lock.
Before the fix: 173 [MUSICNOTE] events over a 28s BUNKER1 capture, but every
single [AUDIOLVL] reading (29 samples spanning the full run) read
peak=0 rms=0.0. After the fix: same note count, every [AUDIOLVL] reading
sustained non-zero (peak ~2700-10300, rms ~800-2900). tools_pc/verify.sh
bunker1: PASS, no visual regression.

Docs: docs/dev/findings.md D77 updated from stale "nothing reaches PC
speakers" framing to the actual root cause + verification; new D6 instance
logged in docs/porting-notes.md.

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
2026-09-10 23:34:40 -04:00
James Dansereau 7c437c0ed6 Merge pull request #41 from jkdansereau/docs/d217-defect1-not-sidecar
docs(D217): defect 1 is not a tools_pc/d43_emit.py sidecar bug
2026-09-10 23:29:14 -04:00
James Dansereau d285a30725 docs(D217): M-88 - defect 1 is NOT a tools_pc/d43_emit.py sidecar bug
Dispatched to root-cause + fix D217 defect (1) (wrong palette bytes on
model-DL CI textures) confined to tools_pc/d43_*.py, working theory: a
4-byte/2-entry model-TLUT source-pointer offset in the sidecar.

Investigation refutes that theory: traced the full texture reference
chain (ModelFileTextures TextureID -> Globalimagetable -> runtime
texInflateZlib/NonZlib decode -> texWriteLoadToTmemAddr -> fast3d
gfx_dp_load_tlut) and confirmed tools_pc/d43_emit.py never touches
texture or palette content for model files - it only converts
geometry/node/GDL structure and passes TextureID through unchanged.

Reproduced the exact reported bytes with the existing GE_TEXDUMP env
probe (no code changes): BUNKER1 pal[0]=0001 (black, correct) vs
FACILITY pal[0]=1a00 (green, matches the originally reported defect)
for the same tile=1x1 CI4 grip texture.

Narrowed the likely real mechanism to port/fast3d/gfx_pc.cpp
(gfx_dp_load_tlut palette-address pitch math) interacting with
src/game/tex.c (texWriteLoadToTmemAddr's literal width=1
SetTextureImage) - both out of this dispatch's file scope, so no fix
applied per the brief's own instruction to stop and write up rather
than edit out-of-scope code.

No tools_pc/*.py files modified. docs-only commit.

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
2026-09-10 23:27:36 -04:00
James Dansereau 117cc6f053 Merge pull request #40 from jkdansereau/chore/verify-sh-fast-sweep
tools_pc/verify.sh: kill each level as soon as its last GE_PCDUMP frame lands
2026-09-10 23:09:06 -04:00
James Dansereau 14adbd3cad tools_pc/verify.sh: kill each level as soon as its last GE_PCDUMP frame lands
run_one always idled out the full watchdog (45s/level sweep, 90s/level
single) even though the game only needs to render its intro pan +
spawn-in once -- everything after the last requested frame was wasted
wall-clock. Poll for the last frame's PPM (or a crash log) every 0.2s and
kill immediately; the watchdog stays as the hang/load-flake safety net
for a level that never gets there. Also trims the pre-launch settle
1s -> 0.3s.

Falls back to the old timeout-wrapped launch if the --dump spec doesn't
parse as lo-hi[:step] integers.

Measured: bunker1 (9 frames, 80-400:40) 45s -> 18s; dam (3 frames, default
200-440:120) 90s -> 17s. A full 21-level sweep should drop from ~16 min
toward ~6 min.

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
2026-09-10 23:05:22 -04:00
James Dansereau 4121a9426a Merge pull request #39 from jkdansereau/chore/golden-bunker1-rebaseline-d215
Golden bunker1 re-baseline (D215 viewmodel) + M-85 D217/D211 triage
2026-09-10 23:04:16 -04:00
James Dansereau f19022d2e1 docs(WIDESCREEN-FOV-PLAN): correct overstated claim about SCREEN_RATIO_16_9
User pushed back asking whether the two PD-survey divergence notes were
architecturally sound, not just factually accurate. On review, the
SCREEN_RATIO_16_9 note overstated the plan: it's a live code path but an
old anamorphic-stretch toggle, not true wide-FOV rendering, and this
doc's own pre-existing Open Questions section already leans toward a
separate new Video.Aspect config as Option B's real trigger, leaving the
legacy flag dormant - the same separation-of-concerns choice PD itself
made, not the opposite. Not yet decided (Phase 1). Overscan note (full
frame, no crop) reviewed and confirmed sound as-is - standard modern-
remaster convention, matches Nightdive's own approach.

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
2026-09-10 22:59:00 -04:00
James Dansereau 037f71dff8 docs: fold PD-legacy-survey findings into D224/QOL-INVENTORY/widescreen+FPS plans
Reviewed docs/dev/notes/PD-LEGACY-SURVEY.md (local Qwen agent's read-only
survey of the PD PC port for N64-legacy concepts worth modernizing/
noting) and folded its 8 candidates in:

- D224 (rumble): the stated fix is NOT sufficient alone - two upstream
  blockers (CONT_CARD_ON never set; osPfsInit returns PFS_ERR_NOPACK
  instead of PFS_ERR_DEVICE) sit before osMotorInit is ever reached.
  Both must land alongside the SDL_GameControllerRumble wiring.
- WIDESCREEN-FOV-PLAN: recorded two deliberate GE-vs-PD divergences so
  they're never mistaken for gaps - full-frame render (no PD-style
  overscan crop) and keeping the SCREEN_RATIO_16_9 path GE already ships
  (PD deleted its equivalent) as the widescreen mechanism.
- QOL-INVENTORY per-pad tuning row: PD's incremental hot-plug with
  remembered seat is the natural implementation shape, sidesteps GE's
  boot-latch controller-mask merge.
- D223 note: PD's per-player PC-controls-mode (save-compat shim) filed
  for later, only relevant if P2+ keyboard input is ever a real ask.
- UNLOCKED-FPS-PLAN: PD's TickRateDivisor/ExtraSleep noted as a fallback
  lever only if this plan stalls and GE stays fixed-tick.

Candidates 6/7 (MP-setup persistence, PD's PC-only gameplay options)
correctly judged net-new / rule-#2-cost-too-high by the survey - no
action, not re-noted elsewhere.

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
2026-09-10 22:55:05 -04:00
James Dansereau 1351e9db34 docs(D194): user confirms aim-mode over-sensitivity, suggests unifying sensitivity base
User independently confirms D194 part (a) and suggests the fix: aim
sensitivity should scale off the same base per-pixel sensitivity as
normal hipfire-turn mouse movement, not its own disconnected AIM_GAIN/
AimBand floor+clamp curve. Noted this is orthogonal to the SOLITAIRE
scheme change (aim mode is a separate insightaimmode-gated codepath,
untouched by natural-pitch/turn) - needs its own fix either way.

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
2026-09-10 22:52:10 -04:00
James Dansereau c729fe4750 docs(D194/D166/D223): SOLITAIRE control scheme gives free continuous mouselook
User correction (they were right): the digital-C-pitch-vs-analog-yaw
asymmetry only applies to the 1.1 HONEY scheme the port currently
forces. User recalled that 1.2 SOLITAIRE uses C-buttons for movement
(fwd/back/strafe) and gives the analog stick full continuous pitch+yaw
('natural pitch/turn') - verified in code (bondview2.c:5195-5253 digital
step movement; :5950/6008 canNaturalPitch/Turn drive
analogPitch//70.0f/analogTurn/70.0f directly, gated only by
!insightaimmode, no threshold/clamp; :4881-4882 these default to the
primary stick's raw X/Y for every 1.x scheme).

This is architecturally identical to modern FPS mouselook. Recommended
fix: force controldef=SOLITAIRE instead of HONEY, WASD -> digital step
buttons, mouse X/Y -> stick X/Y directly and continuously. Resolves the
vertical/horizontal asymmetry with zero digital-pulse emulation (D166's
hack becomes largely moot) and likely much of 'feels unresponsive' too.
Bonus: SOLITAIRE keeps the same shootButtons/invButtons roles as the
current default (not grouped with KISSY/GOODNIGHT's different button
roles), so D223's wheel-cycle plan needs no changes if SOLITAIRE is the
target. GOODNIGHT also gets natural pitch/turn but would need D223 rederived.

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
2026-09-10 22:50:06 -04:00
James Dansereau 59cd0e5785 docs(D224): QOL-INVENTORY - deprioritized, gamepad-only
Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
2026-09-10 22:43:05 -04:00
James Dansereau bc287cd97d docs(D224): deprioritized — gamepad-only, this release targets mouse/keyboard
Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
2026-09-10 22:42:30 -04:00
James Dansereau fad4703bc3 docs(D176): sky partially works but scrolls/repeats too fast; FOV-black is a 3rd wide-FOV-edge case
Sky isn't just solid black - where it renders, it scrolls too fast and
repeats too visibly (likely the M-82 phase-fold's 64-texel wrap period).
Cradle (and others) still black specifically at higher FovScale - a
third instance of the 'wide FOV exposes an edge nothing was built past'
family alongside D218 (fog/draw-distance) and D222 (culling); ties to
D176(a)'s already-logged horizon-band coverage/tessellation gap.

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
2026-09-10 22:41:41 -04:00
James Dansereau c7e9e490f2 docs(D217): note severity/type varies per level, not uniform
Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
2026-09-10 22:40:48 -04:00
James Dansereau a3b9eeabe5 docs(D226): new — player HUD scale for ammo/pickup/dialogue text
User ask: adjustable HUD scale for the ammo counter, bottom-left pickup/
item status text, and top-of-screen dialogue. All three route through
textRenderOutlined() (textrelated.c:688, doc comment confirms ammo
counter + bottom-left HUD messages), called from exactly two in-game
sites in bondview2.c (:10005 ammo/pickup, :10163/10165 dialogue) -
distinct from the many other callers in front.c/options.c/mpmenu.c
(menu/UI text), which must NOT be scaled by this feature (own layout,
D211 already established HUD anchors stay put). Underlying mechanism:
plain RDP gSPTextureRectangle at native glyph size, dsdx/dsdy=0x400
(1:1 texel:pixel), no scale factor in the chain. Two implementation
directions logged (surgical wrapper at the 2-3 call sites vs. a scale
param threaded through the shared function). No PD precedent. Not
designed or implemented - research only.

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
2026-09-10 22:39:14 -04:00
James Dansereau ebbd743232 docs(D225): new — one-function debug unlock-all for testing (all missions + all cheats)
User ask: a debug toggle that always enables all level access and all
cheats, for easier testing. Root-caused a single choke point:
fileGetIsCheatUnlocked() (file2.c:391) is a pure, side-effect-free
save-bitfield read that BOTH the mission-select lock (file.c:59) and the
cheat-menu availability check (frontCheckIfCheatIsUnlocked, front.c:1009)
route through. Short-circuiting it to always-true under an env flag
unlocks both surfaces in one change. Bonus: exposes leftover Rare dev
cheats otherwise unreachable (CHEAT_LINEMODE wireframe - useful for
D222-style culling bugs, CHEAT_BONDPHASE, CHEAT_DEBUG_POS/UNK5). Plan:
#ifdef PORT env-gated (GE_DEBUG_UNLOCKALL), default off = zero behavior
change, no save-data mutation. Not implemented yet.

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
2026-09-10 22:36:01 -04:00
James Dansereau f5af9f3b05 docs(D224): new — rumble fully dropped, PD wires it to real gamepad rumble
Survey of the PD PC port for other N64-legacy things worth dropping/
modernizing (per user ask, following D223's control-preset finding).

D224: GE genuinely has a rumble subsystem (motor.c, joy.c) but the
port's osMotorInit/Start/Stop are hard stubs returning -1 / NOPACK
unconditionally - every rumble event (recoil, explosions) computes and
calls into a dead path. PD's PC port wires the equivalent straight
through to SDL_GameControllerRumble via inputRumbleSupported/inputRumble
+ a per-pad RumbleScale config + options-menu slider - same 'pretend the
N64 accessory is there' pattern this port already uses for osMemSize/
Expansion Pak, just applied to a real capability instead of faked.
Straightforward port-only fix, no game-logic touch.

Also checked and found NOT applicable here: PD's Controller Pak
formatting UI has no GE equivalent (front.c has no PFS/pak menu code),
so there's no dead UI to hide there.

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
2026-09-10 22:24:57 -04:00
James Dansereau c3a548a7be docs(D223): PD-PC-port precedent — decouple from N64 control presets entirely
User asked what the PD PC port did about N64 controller presets for
mouse/keyboard. Checked: PD doesn't route through the N64 controller-
config enum at all - it has its own logical CK_* control-key layer fully
decoupled from it, and treats mouse-wheel-up/-down as two independent
bindable virtual keys (not a single 'wheel = cycle' event). PD's
controldef is hardcoded, not left to the in-game control-style picker.

This GE port already has the equivalent layer: D214's Input.Bind system.
Recommendation logged: add IA_WEAPON_NEXT/IA_WEAPON_PREV bindable
actions that synthesize the invButtons/shootButtons combo, default-bound
to wheel up/down and open for a future gamepad-bumper default; force a
single fixed controldef so the synthesis target is stable. Matches the
user's broader direction (N64 presets aren't meaningful for mouse/kb;
modern Xbox/PS support should be first-class, not routed through N64
presets). Not implemented - design note only.

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
2026-09-10 22:22:25 -04:00
James Dansereau ce69b8456b docs(D217): scope clarified — NPC-held weapon world models affected too, not just Bond
User confirms the glitched-color/missing-texture symptom also shows on
NPC-held weapon models (other characters' guns in third person), not
just Bond's 1P viewmodel / 3P world model. Points at the shared
weapon-model-DL/TLUT loading path in general rather than anything
Bond-model-specific.

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
2026-09-10 22:20:07 -04:00
James Dansereau 8f615bde14 docs(D223): new — directional mouse-wheel weapon cycling, root-caused
QoL ask: wheel up = next weapon, wheel down = previous, instead of both
directions cycling forward. Root-caused: GE already computes separate
weaponForwardOffset/weaponBackOffset signals from
invButtons(A)+shootButtons(Z) held/edge combos (bondview2.c:5338-5347,
the N64 'hold A tap Z' backward-cycle trick); triggerOn is suppressed
while invButtons is held so this never accidentally fires. Port's
inputPostWheel() discards wheel direction and only ever synthesizes the
forward (bare A pulse) case. Fix is port-only (input.c), synthesize the
A-held+G-edge combo for wheel-down instead - no game-logic change.
Open question: which N64 controldef preset is actually active in port
saves, since invButtons/shootButtons differ per preset.

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
2026-09-10 22:18:48 -04:00
James Dansereau 9016cf183c docs(D222): new — high FovScale breaks culling, NPCs/objects vanish near screen edges
Split out of D211's 'cosmetic edge-cull-early' caveat - user confirms
this is a real functional bug, not cosmetic: NPCs and some world objects
stop rendering near screen edges at high FovScale. Root cause: frustum
cull / visibility math still runs at nominal unscaled fovy while the
render projection is portFovScale-widened. Fix direction + a real risk
flagged: some of these cull call sites may double as AI
awareness/visibility checks, not just render culling - must audit before
changing so portFovScale never leaks into NPC behavior (non-negotiable
#2, game logic unmodified).

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
2026-09-10 22:14:37 -04:00
James Dansereau 4e96a60b3f docs(D211/D218): M-87 QoL asks — FOV in real degrees, draw-distance/LOD wanted
D211/WIDESCREEN-FOV-PLAN: user wants Video.FovScale exposed as a real
horizontal-or-vertical degree value instead of the current
percent-of-original-vertical-FOV scale. Not critical, backlogged. Noted
the cleaner path (relabel as degrees, drop the percent indirection) vs
keeping percent internally and just displaying the computed degrees.

D218: user confirms the high-FOV draw-distance/fog artifact still
occurs, and now wants Video.DrawDistance implemented (previously
declined) plus broadened to draw-distance/LOD in general, not just the
fog-boundary case this row root-caused. Open question logged: does GE
have a separate object/geometry LOD-swap distance mechanism worth
exposing, or is Visibility.FarFog the whole story.

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
2026-09-10 22:13:07 -04:00
James Dansereau d3e3819561 docs(D194): M-87 design-goal note — mouse feel should target Quake/PD-remaster responsiveness without losing GE aim nuance
User wants overall mouse feel closer to Quake Remaster / the Perfect Dark
PC port (raw, direct, responsive) while explicitly preserving GE's
aim-mode vs hipfire split (the reason D118/D194 exist). Reframes the fix
order: normalize by a fixed reference dt first (D194 part b, the
frame-rate coupling - likely the dominant cause of 'feels
unresponsive'), then re-derive the aim-mode curve, and evaluate raw
PD/Quake-style hipfire mouse-look as a real option outside aim mode
rather than assuming N64 stick-emulation is mandatory everywhere.

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
2026-09-10 22:11:08 -04:00
James Dansereau 60a31cbc2e docs: M-87 playtest results — D178/D149 fixed, D182 still broken, D77 rescoped, D221 new
D178 (briefing objectives blank) and D149 (front-end 3D models garbled) -
user-verified fixed, closed.
D182 (file-select round-trip degradation) - still broken, confirmed.
D77 (no music) - rescoped: music plays on intro/cast-roll/main menu, only
missing in-level. Points at the level-start music cue, not the
mixer/playback layer.
D221 (new): SELECT FILE mouse hit-box vertically offset from the visible
box (click below a file to select it) under the shipped default
MouseCaptureMode=1 - distinct from D192's clamp-range bug under legacy
mode 0, likely same front-end grid-coordinate family.

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
2026-09-10 22:10:30 -04:00
James Dansereau c82430abcf docs: M-87 playtest notes — D176/D173/D148 still current
D176: skybox still glitchy broadly (not just Surface); Cradle named as
still showing solid black where sky/backdrop should be. Confirms (a)
never landed a verified fix.
D173: Bond hover/float cutscene glitch still present.
D148/D160: new specific instance - Bond spins in circles in some
cutscenes (end-of-Cradle Trevelyan-falls cutscene named) - likely same
actor-position/anim-root cause as D173, manifesting as bad rotation.

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
2026-09-10 22:07:11 -04:00
James Dansereau ad82c0cac6 docs: M-87 playtest notes — D211 good, D217 matrix, D219/D220 logged
D211: user-verified good at big FovScale.
D217: playtest matrix across weapons/levels (PPK/AK47/M16, Dam/Facility/
Jungle) - green, blue, white (missing), orange, all consistent with the
per-weapon-material upstream theory, not one fixed offset. Watch still
green.
D219 (new): PPK explosions render purple on Dam - likely D172 particle-
color family, different DL.
D220 (new): 1P muzzle flash shifts upward at higher FovScale (Jungle
M16) - likely same unscaled-viewmodel-fovy root as D211's residual
caveat.

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
2026-09-10 22:04:13 -04:00
James Dansereau 6b66c9ddc9 docs(D217/D211): M-86 — D217 root cause characterised; D211 aim-zoom verified
D217: two defects found. (1) The visible green grip/hand is upstream of
fast3d — the grip's count=1 model G_LOADTLUT is fed different source bytes
per level (Facility 1a 00 -> 0x1A00 green; Bunker 00 01 -> black); every
larger model TLUT carries a 2-entry leading zero pad, so the port's
model-TLUT source pointer is likely 4 bytes low (or a d43_emit.py blob
offset). fast3d byteswap is correct (proven by Bunker). (2) A real CI
texture-cache collision (no palette-content key) — fix landed on branch
fix/d217-model-texture-palette f6fc0399 (palette FNV-1a hash in the CI
key); owes a 21-level sweep; does not fix (1). porting-notes + graphics-
backlog updated.

D211: aim-zoom compose verified correct by construction (single post-lerp
multiply at the fr.c chokepoint; zoom ratio preserved). Only a low-risk
cosmetic eyeball remains (viewmodel/frustum-cull use unscaled fovy).

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
2026-09-10 18:40:48 -04:00
James Dansereau 2cdecd3aa6 docs(D217): rule out stale-tail; best theory = CI-cache/palette collision
GE_D217Z experiment (zero rdp.palette[count..255] after each partial
load) did NOT change the green grip -> bad texels index within [0..count),
so the wrong palette is *bound* to the grip tile, not tail overflow.
Best remaining theory: TextureCacheKey collision / missing CI-cache
invalidation on palette change (key has no palette-content hash). Facility
renders green-uniformed guards right before the viewmodel -> a stale
cache HIT / palette_addrs collision makes the grip decode against the
guard-fatigue palette. Next probes + the all-levels regression
requirement noted in the finding.

Claude-Session: https://claude.ai/code/session_01U7gyYC3pcatXHhD1kPCgsR
2026-09-09 23:43:38 -04:00
James Dansereau 2e3d8be7e6 docs(D217): M-85 probe results — partial-count TLUT + stale upper palette
User confirms D217 is widespread (many levels, multiple weapons), not
Facility-specific. GE_D217 temp probe (reverted) shows GE weapon/character
model DLs issue partial-count G_LOADTLUT commands (count 1/28/125/172/193/
...); count=125 writes rdp.palette[0..124] and leaves [125..255] holding
the previous TLUT's entries, which differ per level. Weapon TLUT sequence
is identical BUNKER1 vs FACILITY (same PP7) + tiles import palidx=0
cache=HIT, so the delta is purely which stale upper-palette entry a weapon
CI8 texel lands in when it indexes >= its palette count. Root cause open:
CI8 decode reading past the base image (RC2/D183) vs GE relying on N64
TMEM-TLUT leftover semantics. Concrete next probes + a zero-the-tail
experiment noted in the finding. Wants a dedicated debug session.

Claude-Session: https://claude.ai/code/session_01U7gyYC3pcatXHhD1kPCgsR
2026-09-09 23:40:50 -04:00
James Dansereau 717a5210d3 docs(D217): repro pinned to FACILITY post-cutscene
BUNKER1 is clean in any config; -level_34 FACILITY reproduces at ~frame
300 (just after the vent-drop cutscene). PP7 grip = solid green block,
green ring at the barrel joint, hand has a green cast; slide/barrel upper
is correct black. GE_TEXI confirms the weapon CI tiles import with a
loaded TLUT (fmt=2 palfmt=0x8000 palidx=0) -> gDPLoadTLUT does reach
gfx_pc.cpp for model DLs. Green-tinted hand => wrong TLUT bound, not
corrupt index data. Working theory: FACILITY pre-level cutscene leaves
fast3d palette/tile state (or a TextureCacheKey collision on
rdp.palette_addrs[] at gfx_pc.cpp:990) that the cutsceneless BUNKER1
never hits. Next steps + suggested subagent scope in the finding.

Claude-Session: https://claude.ai/code/session_01U7gyYC3pcatXHhD1kPCgsR
2026-09-09 23:33:34 -04:00
James Dansereau f9ff24101a docs(D217/D211): M-85 triage results
D217: did not reproduce headlessly on main @ 3fa1ba53. GE_DTEX/GE_TEXI/
GE_TEXDUMP on -level_09 across FovScale 100/120/150, TextureFilter 1&2,
FixMipTextures 1&0, Anisotropy 4&16 -> PP7 + hand + guard models all
render clean. gDPLoadTLUT DOES reach gfx_pc.cpp for model DLs (weapon CI
tiles import palfmt=0x8000 with non-stale palettes) -> the stale-rdp.palette
hypothesis is disproven for the BUNKER1 start pose. Row updated with the
remaining leads; blocked on a user screenshot of the actual defect.

D211: headless FovScale 100-vs-120 diff confirms HUD-static (ammo counter
screen position identical, no 4:3 stretch, viewmodel composes). aim-zoom
eyeball still owed.

Claude-Session: https://claude.ai/code/session_01U7gyYC3pcatXHhD1kPCgsR
2026-09-09 23:22:44 -04:00
James Dansereau 40e875bcbb tools_pc/golden: re-baseline bunker1 frames for D215 viewmodel
D215 (#37, merged 73768a03) un-gated the 1P weapon-model DL nodes, so the
PP7 + hand now render in the -level_09 intro-pan capture window. The
committed golden predated that, making `verify.sh bunker1` report
REGRESSION (worst_cell ~28) on every run.

Captured on main @ 3fa1ba53, clean default ini pinned to 640x480,
GE_PCDUMP=200-440:120. Gun eyeball-confirmed visible + correctly posed +
clean (no D217 garble in headless capture). Post-rebaseline
`verify.sh bunker1` = PASS worst_cell 12.3.

Claude-Session: https://claude.ai/code/session_01U7gyYC3pcatXHhD1kPCgsR
2026-09-09 23:20:05 -04:00
James Dansereau 3fa1ba534d Merge pull request #38 from jkdansereau/feat/d216-skip-intro
feat(D216): Game.SkipIntro — boot straight to the file-select menu
2026-09-09 20:57:09 -04:00
James Dansereau 208fcfe27d Merge main into feat/d216-skip-intro (M-84 + D215 findings rows)
# Conflicts:
#	docs/dev/QOL-INVENTORY.md
#	docs/dev/findings-index.csv
#	docs/dev/findings.md
#	port/src/video.c
2026-09-09 20:54:05 -04:00
James Dansereau 73768a0357 Merge pull request #37 from jkdansereau/fix/d215-viewmodel-renderdata-flags
fix(D215): 1P weapon viewmodel never drew in-level — renderdata.flags read 0 on x86-64
2026-09-09 20:52:40 -04:00
James Dansereau 9c623858c1 regen findings-index (validate gate); M-84 verdict tokens on D211/D214/D217 status cells 2026-09-09 20:49:57 -04:00
James Dansereau 6e5a4e4e7f Merge main into fix/d215-viewmodel-renderdata-flags (M-84 findings rows)
# Conflicts:
#	docs/dev/findings-index.csv
#	docs/dev/findings.md
2026-09-09 20:37:22 -04:00
James Dansereau 9a667bd0b0 Merge pull request #36 from jkdansereau/feat/pc-qol-fov-aniso-fps
feat(D211-D214): PC QoL wave — FOV slider, anisotropic, on-screen FPS, keyboard rebinding
2026-09-09 20:35:03 -04:00
James Dansereau b3bf7786ff M-84: fold D211 FOV refix + user-test findings into the QoL wave
D211 didn't affect the in-level view (user test): the fast3d
gfx_apply_fov_scale hook only recognised *pure* perspective matrix loads,
but GE draws the world through g_CurrentPlayer->field_10E0 -- a
projection x view matrix pre-combined on the CPU (bondview2.c:8224) --
which failed the guard. Only the pause/watch model + sky FOV changed.

Fix: scale fovy game-side at the guPerspectiveF chokepoint
(src/fr.c, #ifdef PORT, frFovY *= portFovScale, clamp 160deg), which
lands before the CPU combine. Removed the fast3d hook + gfx_api.h decl;
port/src/video.c now sets f32 portFovScale. Gated to
lvlGetCurrentStageToLoad() != LEVELID_TITLE so the front end (which
renders through the same path) keeps its authored FOV.

User-verified: menu unaffected, in-level view widens (Dam tunnel).
Caveat: currentPlayerSetCameraScale keeps nominal FOV for frustum-cull
/ screen<->world math (edge geometry culls slightly early at high scale).

Adds src/fr.c as a #ifdef PORT game-code touch (D181 policy class).

Docs (this test pass):
- D214: config.c sections by the last dot, so binds live in [Input.Bind]
  not [Bind]. Doc-only fix (finding, index, QOL-INVENTORY); code unchanged.
- D217 (OPEN): viewmodel + 3rd-person Bond model textures render garbled
  -- stale-palette CI-texture bug (D161 family), surfaced by D215.
- D218 (LOGGED, parked): wide FOV exposes the per-level FarFog
  draw-distance boundary; Video.DrawDistance fix proposed.
2026-09-09 20:30:21 -04:00
James Dansereau da25c4c507 feat(D216): Game.SkipIntro — boot straight to the file-select menu
New s32 portSkipIntro (port/src/video.c, configRegisterInt Game.SkipIntro),
read once in src/game/lv.c at LEVELID_TITLE stage load, in the else branch of
the existing GE_STARTMENU #ifdef PORT block. When set it performs the same
state change every intro stage's own !is_first_time_on_main_menu branch does
(front.c): is_first_time_on_main_menu=FALSE; prev_keypresses=TRUE;
maybe_is_in_menu=TRUE; menu_update=MENU_FILE_SELECT. So it reuses the game's
own post-intro route rather than inventing one; the legal screen + logo
attract loop is skipped. GE_STARTMENU still wins when both are set.

Default 0 => unchanged. Verified headless: default boot frame 90 = the
TWYCROSS classification screen; SkipIntro=1 frame 90 = the SELECT FILE menu
(folders/photos/cursor render, no crash across repeated boots). Screenshots
eyeballed.

findings.md D216 + index; QOL-INVENTORY row marked done.
2026-09-09 01:07:05 -04:00
James Dansereau 152b70a760 docs(D215): add GE_DVM to GE-ENV-PROBES.md (validate gate) 2026-09-09 00:58:29 -04:00
James Dansereau f602b1738f fix(D215): 1P weapon viewmodel never drew in-level — renderdata.flags read 0
gunRenderFirstPersonGunModels built its ModelRenderData with
  renderdata = *(ModelRenderData *)&D_80035CC0;
a reinterpret that reads the lone `u32 D_80035CC0 = 0` and continues into the
next declared global `u32 D_80035CC4[] = {1, 3, 0, ...}` (gun.c). On N64 that
yields { basemtx=0, zbufferenabled=1, flags=3, rest=0 } because the linker
lays the two globals out contiguously in declaration order and pointers are
4 bytes. On x86-64 it breaks twice: ModelRenderData's pointer members widen
4->8 B (so `flags` is no longer at struct offset 8) and C guarantees nothing
about the two globals' placement. Measured (GE_DVM=1, BUNKER1): flags == 0,
which gates every weapon-model DL node off in modelRenderNodeGundl
((renderdata->flags & 1)) -> subdraw emits 1 gfx command instead of ~150 ->
no gun visible while playing.

All three VIEWMODEL-RESEARCH.md ranked suspects were ruled out first by the
same probe: field_87F==1, RootNode valid, numRecords 22 < pool 192, node walk
visits 24+ valid DL/SWITCH/BSP nodes.

Fix: #ifdef PORT -> set the fields explicitly (zbufferenabled=TRUE; flags=3);
N64 line verbatim under #else. ABI/layout only (D3x class), no logic change.

Verified (headless): GE_DVM=1 post-fix shows flags=0x3 and the weapon subdraw
now emits 153 gfx commands (was 1); bunker1 framediff worst_cell 12.2 -> 28.5
(gun renders in-frame); Dam/Facility/Bunker1/Archives/Cradle/Aztec sweep
crash-free. Owed: one BUNKER1 eyeball; the bunker1 golden needs re-baselining.

GE_DVM gate+render probes left in, env-gated (cf. GE_D193*).
findings.md D215 + index; GRAPHICS-BACKLOG + VIEWMODEL-RESEARCH marked resolved.
2026-09-09 00:56:19 -04:00
James Dansereau de31f2874b feat(D214): keyboard rebinding via [Bind] config section
port/src/input.c: 12 gameplay actions (Forward/Back/StrafeLeft/StrafeRight/
TurnLeft/TurnRight/Fire/Aim/Action/Cancel/LeanLeft/Start), each bound to a
comma-separated list of up to 4 SDL scancode names via Input.Bind.* keys.
inputRebuildBinds() parses them in inputInit() after configLoad();
SDL_GetScancodeFromName resolves names, unknowns warn + skip. The hardcoded
keyDown(ks, SDL_SCANCODE_*) block in inputComputePad is replaced with
actHeld(ks, IA_*). Mouse buttons (fire/aim) stay hardwired.

File-driven, no UI -- matches the PD port. A fresh ge007.ini gets the full
[Bind] block with defaults visible.

Defaults reproduce the previous FPS layout exactly: all 20 default key names
round-trip to the identical SDL_Scancode the old code used (0 mismatches in a
SDL_GetScancodeFromName test). Build clean; verify.sh dam/facility/bunker1
PASS crash-free.

findings.md D214 + index; QOL-INVENTORY row marked done.
2026-09-09 00:09:26 -04:00
James Dansereau 7bcfbf313c feat(D211-D213): PC QoL image options — FOV slider, anisotropic filtering, on-screen FPS
Three port-only knobs, each defaulting to byte-identical behaviour:

- Video.FovScale (D211): multiplicative half-angle scale on every perspective
  projection matrix as it is loaded (gfx_apply_fov_scale, gfx_pc.cpp). Columns
  0+1 scaled together so H/V FOV widen uniformly and aspect is preserved; ortho
  (HUD/menu) matrices untouched. Composes with the game's per-frame aim-zoom
  rewrites for free. Percent of vertical FOV, 50-150, default 100 => guarded
  early-return (zero FP ops, golden dumps byte-identical). WIDESCREEN-FOV-PLAN
  Phase 4, landed standalone.

- Video.Anisotropy (D212): expose fast3d's already-implemented GL anisotropy
  hook as config (1-16). New gfx_set_anisotropy_level() extern-C wrapper clamps
  to the GL max. Default 4 = the value fast3d already hardcoded => no delta.

- Video.DisplayFPS (D213): PD-parity top-right FPS readout, sampled once per
  emitted frame (optionsoverlay.c fpsTick). Config-only; default 0 => emit
  still returns NULL with the panel closed.

F10 overlay gains 'Anisotropic' and 'FOV scale %' rows (DisplayFPS is file-only
- the panel is at its 13-row layout limit, matching PD).

Build clean (ntsc-final); bunker1 framediff worst_cell 12.219 vs 12.221
baseline = render noise, no-op confirmed. Interactive eyeballs owed (see
docs/HANDOFF.md M-83).

findings.md: D211-D213 rows + index regen. WIDESCREEN-FOV-PLAN Phase 4 and
QOL-INVENTORY rows marked done.
2026-09-09 00:02:47 -04:00
James Dansereau 8742b93f29 Merge pull request #34 from jkdansereau/docs/d172-0xb9-withdrawn
fix(D172): particle magenta/cyan — gfx_lod_tile_offset collapses non-LOD 2-texture combines
2026-09-08 23:40:52 -04:00
James Dansereau fb8557b2da docs(D172): add GE_D172 to GE-ENV-PROBES.md (validate gate) 2026-09-08 23:37:49 -04:00
James Dansereau 9b649b025d docs(D172): ROOT CAUSE FOUND + FIXED — gfx_lod_tile_offset collapse; findings D172 row + index 2026-09-08 23:31:47 -04:00
James Dansereau 157a8de59a fix(D172): stop gfx_lod_tile_offset collapsing non-LOD 2-texture combines
ROOT CAUSE FOUND (GE_D172 probe, user Silo firefight capture): particle
draws (G_CC_INTERFERENCE = TEXEL0*TEXEL1) configure tile 0 = IA8 smoke @
tmem 0 and tile 1 = RGBA16 fire @ tmem 0x188, but gfx_lod_tile_offset()
returns 0 unconditionally on the !gfx_detail_textures_enabled path (a D107
blurry-ceiling fix), so texunit 1 samples the smoke texture too. smoke *
smoke -> the magenta/cyan blood/spark colour.

Probe evidence, 40/40 particle tris:
  cfg1[tile=1 tmem=392 fmt=RGBA siz=16b]  SAMPLED1=tile0(tmem=0 fmt=IA)

Fix: only fold texunit i to the base tile when LOD is actually active
(rdp.tex_lod). D107's mip case (tex_lod=1) is unchanged; genuine non-LOD
multitexture now gets tile i. bunker1 verify PASS (worst_cell 11.3, the
D107 repro level, no regression).

Needs an in-game eyeball: blood/sparks should read dark red, not magenta.
2026-09-08 23:30:45 -04:00
James Dansereau 9ad69679b4 diag(D172): probe only non-LOD multitex (tex_lod=0) — the particle case 2026-09-08 23:25:26 -04:00
James Dansereau e65830d980 diag(D172): sharper probe — filter to real multitex, log tex_lod + sampled vs configured tile
The 16-line budget filled during the Silo intro with LOD-bilerp tris
(TEXEL1 = mip level, T0==T1 is correct there). New probe: only fires when
the DL configured tile first+1 at a DISTINCT tmem (genuine 2nd material),
logs tex_lod + which tile each texunit actually SAMPLES vs what was
configured, flags TEXEL1==TEXEL0. Budget 40.
2026-09-08 23:22:32 -04:00
James Dansereau 0c6173f1bd diag(D172): replace weak tri probe with a decisive TEXEL1-bind dump
The old 'cyc2 combine at cycletype=1CYC' probe over-fired on trivial
passthrough cyc2 terms. Replace with one that fires only when a 2-cycle
triangle actually consumes TEXEL1 (the G_CC_INTERFERENCE particle case) and
dumps both texunits' tile/tmem/fmt/siz. One Silo capture then shows whether
TEXEL1 resolves to the fire tile (tmem 392, RGBA16) or something wrong.

Build clean; bunker1 verify PASS.
2026-09-08 23:12:09 -04:00
James Dansereau 9215fde4fb diag(D172): rule out cycle-type too; ship GE_D172 probes
Real particle records (assets/oddtextures.c globalDL_0x078..) DO set
gsDPSetCycleType(G_CYC_2CYCLE); GE_D172=1 probe confirms fast3d applies it
(particle combine fc26a004 logged at cycletype=2CYC). Both the 0xB9-abort
and the cycle-type hypotheses are now dead. Bug is the G_CC_INTERFERENCE
two-tile combine / TEXEL1 path (IA8 smoke x RGBA16 fire @ tmem 0x188).

Probes (#ifdef PORT, GE_D172=1, inert otherwise):
- gfx_pc.cpp G_SETCOMBINE: distinct combine + active cycletype
- gfx_pc.cpp gfx_sp_tri1: tris with cyc2 combine drawn at cycletype!=2CYC

Needs a display capture to go further (headless can't see particle colour).
2026-09-08 23:12:09 -04:00
James Dansereau 809560ca1e docs(D172): withdraw 0xB9 root cause — it's G_SETOTHERMODE_L, already handled
0xB9 == G_SETOTHERMODE_L in this non-F3DEX2 build (G_IMMFIRST-6); fast3d
handles it as gsDPSetRenderMode. -level_09 sustained-fire run: zero D146
aborts, zero warnings. Re-scoped to the 2-cycle/cycletype hypothesis
(records never set G_MDSFT_CYCLETYPE -> use_2cyc==0 on PC -> TEXEL1
garbage -> magenta/cyan).
2026-09-08 23:12:09 -04:00
James Dansereau 90641f6305 Merge pull request #18 from jkdansereau/fix/d176a-sky-path-b
D176(a): render the sky on PC (Path B — screen-space emit)
2026-09-08 22:34:27 -04:00
James Dansereau b2efc0af9d fix(D176a): phase-fold sky texcoords to kill the horizon streak (Defect 1)
Cloud tile is s_skywaterimages[0] = 64x64 IA8, G_TX_WRAP -> it tiles.
GE's screen-space textured-quad idiom is tc = width<<5 (glass2.c:678), so
skyPortRenderPoly's *32.0f scale was correct; the bug was (s16) overflow on
the +/-20k-texel horizon S/T (unk20/unk24 are texel coords, not fractions).

Fix: fold every vertex of one primitive by the same multiple of the 64-texel
tile period before scaling. Inter-vertex deltas (hence the interpolated
cloud texture) are preserved; magnitudes stay in s16 range. Exact for any
triangle whose S/T span is < ~1000 texels -- everything except tris
straddling the horizon line, which are Defect 2 (adaptive tessellation).

sky.c #ifdef PORT only; N64 path unchanged. Builds clean; bunker1 + statue
verify PASS, no crash. UNVERIFIED VISUALLY -- owed: one -level_22 screenshot
(D176a-SKY-NOTES.md 'Verification ask').

Also: rebased onto main (porting-notes.md conflict resolved).
2026-09-08 22:29:28 -04:00
James Dansereau 9a6017a784 D176(a): emit screen-space sky geometry on PC (Path B)
skyRenderTri/skyRenderFull emit the sky as a verbatim N64 RDP triangle
command chopped into G_RDPHALF_* immediates, which fast3d deliberately
no-ops -> Surface/Statue/Streets skies render solid black.

Behind #ifdef PORT, substitute a screen-space gSPVertex + gSP1Triangle/
gSP2Triangles emit built from the already-projected SkyRelated38 verts
(new helper skyPortRenderPoly, ortho matrices via dynAllocate*). N64 path
unchanged under #else. Pure RSP-idiom substitution, no behavioural change.

Links 243/243. Render check owed on a display: -level_22 frame ~360.

findings.md D176(a) M-46; porting-notes.mdD.
2026-09-08 22:29:28 -04:00
James Dansereau b7bb08db65 Merge pull request #35 from jkdansereau/fix/d210-patrol-lastvisible-alias
fix(D210): chrToPatrol leaves act_patrol.lastvisible60 uninitialised on PC
2026-09-08 22:28:31 -04:00
James Dansereau 1a07c8201a Merge pull request #33 from jkdansereau/fix/d196-overlay-cursor
fix(D196): re-hide the OS cursor after the F10 options overlay closes
2026-09-08 22:28:28 -04:00
James Dansereau b47eea65e3 fix(D210): chrToPatrol leaves act_patrol.lastvisible60 uninitialised on PC
Sibling of D209 (A1 raw-byte union-alias class). chrToPatrol writes
act_init.padding[0x13] (union byte 0x4c) = -1, which is act_patrol.lastvisible60
on N64. act_patrol starts with a patrol_path* that widens 4->8 on x86-64, so
0x4c now lands in waydata and lastvisible60 (@0x50) stays uninitialised. The
patrol 'haven't seen the player recently' gate (chraction.c:~9347) then reads
garbage on the first ticks after entering PATROL.

Fix: write the named field under #ifdef PORT; N64 line verbatim under #else.
ABI/pointer-width only, no game-logic change.

Found by the post-D209 A1 sweep; the only new instance (act_anim/act_dead
padding aliases are pointer-free -> layout-stable).

Build clean; bunker1 + facility verify PASS (both patrol-heavy), no crash.
findings D210 + porting-notes A1 + findings-index regen.
2026-09-08 20:51:18 -04:00
James Dansereau a17d5466ae fix(D196): re-hide the OS cursor after the F10 options overlay closes
inputSuspendForOverlay() forces the OS cursor visible while the overlay is
open; on close nothing re-hid it unless reconcileGrab() happened to re-grab
(only true if the user had already clicked-to-lock in a stage). Opening the
overlay from a menu, or from a stage not yet click-locked, left the cursor
stuck visible over the game's own crosshair.

Fix: re-assert applyCursorVisibility() once per controller-0 poll, right
after reconcileGrab(). Unreachable while the overlay is open (early return
above), so it only fires once the overlay has closed and self-heals every
poll thereafter. Port-only, no render change; verify.sh bunker1 PASS.

Owed: 5-second visual check - open F10 from a menu, close it, cursor gone.
2026-09-08 20:24:35 -04:00
James Dansereau 97a09665a1 Merge pull request #32 from jkdansereau/diag/d193-timing-keystone-measured
fix(D209): AI locomotion locked to walk animation — union byte alias shifts at 64-bit (fixes D193)
2026-09-08 20:00:18 -04:00
James Dansereau b0b5f948f1 chore(D209): regen findings index
gen_findings_index.py --check gate was red on the new D209 entry.
2026-09-08 19:57:22 -04:00
James Dansereau a63efe184b fix(D209): AI locomotion locked to walk anim — union byte alias shifts at 64-bit
Root cause of D193 ("NPC AI locomotion is too slow", the user's biggest
playtest complaint; broke Cradle completability).

get_sound_at_range() (chraction.c:3573) is the locomotion-animation
selector: it picks ANIM_DATA_sprinting/running/walking from an arg1 speed
tier. Its only caller passed that tier as self->act_ubytes.padding[45] — a
raw byte alias into the ChrRecord action union rather than the named field.

On N64 offset 45 is exactly act_gopos.unk59:
  act_gopos = {coord3d targetpos@0, StandTile *target@12,
               waypoint *target_path@16, waypoint *waypoints[6]@20,
               u8 curindex@44, u8 unk59@45}
At 64-bit the three pointer members widen 4->8B (target@16, target_path@24,
waypoints@32), pushing unk59 to 81 while the literal 45 lands inside
waypoints[1] (40..47) — byte 5 of a heap pointer. The arena is low-4GB
(0x00000000_70xxxxxx), so that byte is always 0x00: arg1 reads 0 forever
and every AI chr in the game was bound to ANIM_DATA_walking /
ANIM_DATA_walking_unarmed regardless of what the ailist commanded. GE
character travel is animation-root-motion driven, so all AI moved at walk
pace on every level. Bond is unaffected (explicit physics, no root motion),
which is the reported "player fine / all AI slow" split.

Fix: PORT-only read of the named field; N64 line kept verbatim under #else.
ABI/pointer-width only, no game-logic change (rule-2 narrow exception, D3x).
Both callers reach the site with actiontype == ACT_GOPOS, so it is the same
byte N64 reads.

Evidence (Cradle -level_41, GE_D193A extended with tier/aidx): all six chrs
report ACT_GOPOS tier=1 SPEED_RUN on 420/440 samples, while the live
model->anim reverse-resolves to animation_table_ptrs1 index 40
(PTR_ANIM_walking) for Trevelyan and 107 (PTR_ANIM_walking_unarmed) for the
guards on every sample; 42 (running) and 41 (sprinting) never appear.
Travel sat in a flat 140-155 u/s band for all of them.

User playtest-verified on the fix build: Trevelyan and the guards run.

Also:
- GE_D193A extended with tier / gspd / typ / tick / move / frz / hid / aidx
  (aidx reverse-resolves model->anim against animation_table_ptrs1).
- bondtypes.h: unk59 was commented "guess: room" — it is the SPEED tier.
- porting-notes.md A1: new bug class — raw-byte aliases into a union read a
  plausible constant rather than garbage, so they misbehave silently.
- findings.md: D209 added; D193 closed against it.
2026-09-08 19:50:45 -04:00
James Dansereau 47d6dfa9c3 docs(D193): anim-data endianness (D33) audited complete; blocked on N64 reference
Static review of the full anim-root-motion path finds no port-side bug:
frame clock ruled out (measured), all model/chr scalars nominal,
ModelSkeleton ruled out (static C not serialized), and the D33
romdataFixupAnimationData byte-swap covers every field the locomotion
decode reads -- walk/run anim records ARE in animation_table_ptrs1,
_ptrs2 is 3 non-overlapping anims (no double-swap), no fixup errors,
sizeof(ModelAnimation)=64 and sizeof(ModelAnimBitField)=6 both platforms,
bitstream walked byte-MSB-first (endianness neutral, matches PD).

A magnitude error, if present, is only visible against an N64 reference.
Two paths documented (both need a fresh session; no MIPS toolchain /
emulator in this env): (A) build ROM with matching logging + diff,
(B) frame-count N64 reference footage of a measured guard traversal.

Claude-Session: https://claude.ai/code/session_01U7gyYC3pcatXHhD1kPCgsR
2026-09-08 10:03:16 -04:00
James Dansereau 33667c72d7 docs(D193): rule out ModelSkeleton (static C, not serialized) — bitstream/anim-table is the remaining surface
Claude-Session: https://claude.ai/code/session_01U7gyYC3pcatXHhD1kPCgsR
2026-09-08 09:51:14 -04:00
James Dansereau 25b46e020a diag(D193): instrument the locomotion / anim-root-motion path
Two #ifdef PORT env-gated probes (default-inert, no golden regression):

- GE_D193A (chr.c chrTick): per-wall-second per-chr travel speed +
  the root-motion scalars (scale / anim_translation_scale / animrate /
  playspeed / model->speed / g_GlobalTimerDelta). GE_D193A_CHR=<n>
  narrows to one chrnum.
- GE_D193B (model.c sub_GAME_7F06D3F4): rate-limited raw anim-bitstream
  root-motion decode (joint / mtx base channel / decoded s16 triple /
  angle / the D32 bitDescriptors+bitStream addrs).

Silo + Facility captures: every port-side input to the locomotion math
reads nominal -- g_GlobalTimerDelta=2.0, playspeed=1.0, scale=0.1,
anim_translation_scale=1.0, and the C in this path is identical to N64
(only the non-firing D156 NaN guards are #ifdef PORT). animrate=0.0 is
the default (only set on blended transitions). Patrolling guards travel
~60-99 u/s. GE_D193B shows base=0 / angle=0 on every call.

So the divergence is either subtle wrong magnitudes in the decoded
bitstream values, or in the pos34 accumulation -- neither visible
without an N64-decomp reference trace. Findings D193 + GE-ENV-PROBES
updated with the numbers and the next step (build n64decomp/007 with
matching logging and diff, or frame-count reference footage). One
explicit structural check flagged: ModelSkeleton.Joints (ModelJoint*,
4->8 B) reached via model->obj->Skeleton from D43/D45-converted data.

Claude-Session: https://claude.ai/code/session_01U7gyYC3pcatXHhD1kPCgsR
2026-09-08 09:50:06 -04:00
James Dansereau 7206bf8cc1 docs(D193): live-load confirms the pivot — locomotion/anim bug, not the frame clock
User ran Cradle (-level_41; the "-level_51" in the docs was wrong -> that
id is LEVELID_EAR, an unused slot that SIGSEGVs in stanBuildRoomData)
with GE_D193=1 and drove the Trevelyan sequence while Trevelyan walked
visibly slow. Probe held render=30 / sim=60 / clamped=0 / maxraw=2 dead
steady, no [D156] fires. So under the exact failing scene the sim clock
is perfect -> D193 is in the character locomotion/animation path.

Best hypothesis: GE character travel is anim-root-motion driven
(modelTickAnim -> modelSetAnimFrame2WithChrStuff, translation scaled by
model->scale * model->anim_translation_scale); the player uses explicit
physics instead, which explains "player fine / all AI slow". Next:
instrument sub_GAME_7F06D3F4 pos magnitude + scale + playspeed/speed per
chrTick for one walking NPC. Findings D193 + HANDOFF updated; Cradle
level-id corrected to 41.

Claude-Session: https://claude.ai/code/session_01U7gyYC3pcatXHhD1kPCgsR
2026-09-08 09:38:07 -04:00
James Dansereau c723207d3a diag(D193): measure the timing keystone — "sim sums low" hypothesis contradicted
Add GE_D193=1 per-wall-second probe in waitForNextFrame (#ifdef PORT,
env-gated, default path byte-identical): render frames, sim ticks
delivered (Σ deltaFrames == Σ g_ClockTimer), catch-up clamp hits, max
raw pre-clamp delta.

Measured -level_09/_20/_27/_34 idle: rock-steady render=30 fps /
sim=60 ticks/s / clamped=0 / maxraw=2 on every level, VSync on or off.
The per-second sim sum is exactly the N64's ~60 and FRAMETIMING_PORT_
MAX_CATCHUP never bites in normal play, so B1's premise
(g_GlobalTimerDelta / g_ClockTimer sum low over a real second) is wrong
for the steady-state case. Findings D193 updated with the measurement,
the residual quantization risk (no remainder carry in the per-frame
round; only matters if a real AI scene drops into the 20-30 fps band),
and the revised next step (instrument under real AI load, then pivot to
the locomotion/anim path). GE_D193 catalogued in GE-ENV-PROBES.md.

No game-logic change; no golden regression (level_09 f203 91.52% vs
~91.6% ref).

Claude-Session: https://claude.ai/code/session_01U7gyYC3pcatXHhD1kPCgsR
2026-09-08 09:30:49 -04:00
James Dansereau 3eca8ba79b Merge pull request #31 from jkdansereau/fix/d191-bondview-cuff-switch-stride
fix(model): D191 — Bunker ii / Statue mid-mission crash (bondviewSelectCuff Switches stride)
2026-09-08 09:15:25 -04:00
James Dansereau cf477c20cf docs(D191): findings + porting-notes + regen findings-index
Playtest-verified (Bunker ii + Statue). 21/21 solo levels crash-free.
Regenerated findings-index.csv (the validate CI check requires it current).
2026-09-08 09:12:12 -04:00
James Dansereau 1ad03b3d53 fix(model): D191 — Bunker ii / Statue crash: bondviewSelectCuff Switches stride
bondviewSelectCuff byte-indexed header->Switches (a ModelNode* array,
8-byte stride on PC per D43/D45) with `offset = switchindex << 2`, the
N64 4-byte-pointer stride. Every base[N] deref then landed mid-slot and
produced a half-word-swapped pointer (0x70267a90 -> 0x70267a9000000000),
non-NULL by luck, handed straight into modelGetNodeRwData -> SIGSEGV on
the frame the player fires (gunUpdateAndFire -> bondviewSelectCuff,
switchindex=29). Confirmed by a gdb backtrace from the shipped v0.1.0
bundle on the Bunker ii first-guard-kill repro.

Identical to D141, which fixed the same Switches byte-index bug in
gunfire.c; this call site was missed. Fix is PORT-only:
`offset = switchindex * sizeof(ModelNode *)`. The downstream
`base = (u8*)switches + offset` math and the #else N64 line are unchanged.
ABI / pointer-width only (D3x class) — no game-logic change.

Verified: build clean; -level_09 golden framediff 3/3; -level_27 +
scripted-fire crash-free; user playtest of Bunker ii + Statue both
crash-free -> 21/21 solo levels crash-free (v0.2.0 gate).
2026-09-08 09:12:12 -04:00
James Dansereau aecad2ac91 Merge pull request #30 from jkdansereau/docs/d208-verified
docs: D208 FIXED + playtest-verified
2026-09-08 08:13:54 -04:00
James Dansereau 15c374813e docs(audio): D208 FIXED + playtest-verified (M-78d)
Claude-Session: https://claude.ai/code/session_01U7gyYC3pcatXHhD1kPCgsR
2026-09-08 08:13:01 -04:00
James Dansereau 15b32d73d9 Merge pull request #29 from jkdansereau/audio/d207-drop-probe
D205 closed; D208 player-fire-sound fix (field_A48 pointer width); D207 [D207-DROP] probe re-armed.

https://claude.ai/code/session_01U7gyYC3pcatXHhD1kPCgsR
2026-09-08 08:10:17 -04:00
James Dansereau 76910cbf19 fix(audio): D208 — player fire sound goes permanently silent (field_A48 ptr width)
Root cause of the "doesn't recover after the alarm stops" half of the user's
D207 report (distinct bug from D207's voice-pool question).

struct field bondview.h:200 `s32 field_A48` is used as a SECOND
ALSoundState* sound-handle slot for the player's double-buffered per-hand
fire sound (gunfire.c:3186-3200). sndPlaySfx stores the new state through
`&field_A48` as an 8-byte `pendingState->link.next = nextState`; as s32 that
write tears into the adjacent field_A4C (the auto-fire cadence timer, rewritten
every frame). After enough double-buffered fire cycles field_A48 holds a value
that is neither 0 nor a valid pointer, so the `else if (field_A48 == 0)`
re-arm gate never fires again and no further player gunshot is issued.

Evidence: user Bunker audiotrace.log — silenced-PP7 idx-46 sndPlaySfx requests
stop dead at log line 18930/25086 while all other SFX continue; the player-gun
slot 0x70076630 is then caught holding a torn value 0x000001db00000000
(field_A48=0 + field_A4C=0x1db read as one word).

Fix (D3x ABI/layout, #ifdef PORT): declare field_A48 as ALSoundState* under
PORT, s32 verbatim under #else. hands[] is runtime-only state (g_CurrentPlayer),
never ROM-serialized, and all downstream fields are accessed by member name —
widening 4 bytes is layout-safe. The (struct ALSoundState *) casts at the use
sites become no-ops and stay for a minimal diff.

Build clean (gunfire.c + full link). Runtime verification owed: replay the
Bunker firefight, confirm idx-46 requests continue for its full duration.

Claude-Session: https://claude.ai/code/session_01U7gyYC3pcatXHhD1kPCgsR
2026-09-08 08:06:28 -04:00
James Dansereau 0f7fbcecdf docs(audio): D207 M-78b — user Bunker repro; player-gun double-buffer lead
User repro: player gunshots specifically go permanently muted mid-firefight
and stay muted after the alarm ends; all other SFX fine. Not generic pool
starvation — a stuck-state bug in the gunfire.c:3178 two-slot double buffer
(audioHandle / field_A48). Leading mechanism: asynchronous sndDeactivate
posts to the 64-slot SFX event queue; under firefight+alarm load the queue
saturates, alEvtqPostEvent drops the deactivate, the state never disposes,
the slot never clears — once both slots stick, no player gunshot can play.
All three pool sizes are byte-match stock GE => port timing bug, not a value
to bump. Next: read the user's audiotrace.log to confirm evtq saturation +
uncleared slots.

Claude-Session: https://claude.ai/code/session_01U7gyYC3pcatXHhD1kPCgsR
2026-09-08 07:56:02 -04:00
James Dansereau 2f503f007d diag(audio): D207 — re-arm [D207-DROP] counter; close D205
D205: CLOSED (M-78). User re-tested explosion->scream on Bunker after the
PR #28 merge — the explosion SFX is correct by ear now. All four D205
symptoms were D206 (the +1 bank-slot alias); the "scream" was a coincident
guard death-yelp (faithful N64 behaviour). M-74's pan audit already ruled
out a presentation-layer bug.

D207: re-add ONLY the [D207-DROP] trace line (not the reverted last-resort
LOOPED-preemption pass) at both sndDisposeSound no-voice sites in snd.c,
gated on GE_AUDIOTRACE, logging state/prio/flags/count=N/max — so a user
capture on the real failing level is decisive between (a) heavy-level 8/8
starvation, (b) a leak on an alarm-stop path, (c) perceptual. Static
re-check noted in findings D207: ALARM3 slot 162 sampleVolume=80 (below
gunfire), keyMax=8 => not RETRIGGER => continuous-loop by design on N64 too.

findings-index.csv regenerated.

Claude-Session: https://claude.ai/code/session_01U7gyYC3pcatXHhD1kPCgsR
2026-09-08 07:44:29 -04:00
James Dansereau e3bebfc454 Merge pull request #28 from jkdansereau/feat/phase3-audio
Merge Phase 3 audio: software mixer + SFX fidelity (D198–D206), D202 closed.

https://claude.ai/code/session_01U7gyYC3pcatXHhD1kPCgsR
2026-09-08 07:32:23 -04:00
James Dansereau 1dcc518e75 docs(dev): GE-ENV-PROBES.md — add GE_FORCEALARM (D207) row
Fixes the validate CI job (gen_env_probes.py --check drift).
2026-09-07 22:35:57 -04:00
James Dansereau c855625643 docs(dev): D202 closed, D206 by-ear PASS, D207 monitoring (M-77c)
User by-ear A/B vs N64 confirms D206: armour pickup, unarmed melee whiff
and silenced PP7 all correct. D202's silenced-PPK 'slap' was D206 (SFX
bank index +1) — CLOSED; M-65/M-66b ownerless-loop mitigations stay.
D205 narrows to symptom (2) explosion->scream only. D207 (alarm klaxon)
acceptable in play per user; voice-starvation hypothesis didn't hold up
(M-77b), no fix shipped, GE_FORCEALARM probe kept for follow-up.

Regenerated findings-index.csv.
2026-09-07 22:30:54 -04:00
James Dansereau 0845d70953 port(audio): D207 diag probe — GE_FORCEALARM
Temporary env-gated probe in handle_alarm_gas_timer_calldamage to force
and trace the alarm klaxon while investigating D207 (user report: alarm
SFX overrides other audio). #ifdef PORT + getenv, inert unless set.
Remove once D207 is resolved (see findings.md D207 / HANDOFF).
2026-09-07 22:23:01 -04:00
James Dansereau 9bf0c49151 fix(audio): D206 — every SFX resolved one bank slot too high
GE's sndPlaySfx reads the on-disk sound bank through the cast alias
struct ALInstrumentAlt_s { s32 unk0, unk4, unk8; ALSound *soundArray[1]; }.
On N64 (4-byte pointers) soundArray sits at struct offset 12 and
deliberately aliases the on-disk ALInstrument's bendRange/soundCount
words, so soundArray[N] == on-disk table entry [N-1] — GE's SFX_IDs are
1-based into the table by design (ID 0 = NOTHING_SFX, short-circuited).
On PC the 8-byte pointer + 8-byte alignment push soundArray to offset 16,
and the converted bank (romdata.c afFixupInst) is packed to match, so
every SFX_ID resolved one bank slot too high.

Root cause of D202's silenced-PPK 'slap' (id 46 -> PUNCH1 instead of
GUN_SILPPK_A) and D205 (3) armour + (4) melee->Klobb. M-61's rejected
'index 45, +0.999' was correct all along.

Fix: src/snd.c:1001, #ifndef PORT / #else -> soundArray[soundIndex - 1],
N64 line kept verbatim. ABI/layout only (D3x class), no game-logic change.
Also serves the retrigger chain (same 1-based space).

Verified: build clean; BUNKER1 GE_AUDIOTRACE 60s crash-free, all ~90
distinct requested indices resolve to rom_sfx_decode.py slot N-1 (0/90 at
N); -level_09 golden framediff 3/3. User playtest: 'most audio is good now.'

findings.md D206 + D205 (M-73/M-74) + D207; porting-notes.md §E.
2026-09-07 22:22:55 -04:00
James Dansereau 044d24a15f docs(dev): D205 M-73 correction — Klobb is a specific in-game SMG (Skorpion, idx 106); the user's sound identifications stand; capture shows no 106 while user was punching (47-49/105 all voiced), so melee->Klobb identification is OPEN (H3 vs H4), not resolved by the 109 barrage 2026-09-07 15:23:50 -04:00
James Dansereau fb037b3c0c docs(dev): D205 M-73 — user combat capture root-causes all three wrong-sound moments to guards stuck in sustained fire-sound re-triggering (idx 109 barrage, no bullets); LOS/seen_bond_time on converted geometry is the leading suspect 2026-09-07 15:17:03 -04:00
James Dansereau 087c60d05f docs(dev): D205 M-72 — 4th symptom (melee→Klobb idx 106); controlled GE_AUDIOTRACE runs exonerate all request paths; mechanism (b) extra-events leading suspect 2026-09-07 14:47:25 -04:00
James Dansereau c0047f76f7 docs(dev): D202 M-71 by-ear PASS + close-ready; new D205 wrong/extra SFX indices (bank exonerated); [VOL]/[DISTVOL] probes 2026-09-07 14:18:39 -04:00
James Dansereau c511b9060b docs(dev): D202 M-67/M-68/M-69 findings writeups + probe catalogue + audio-plan status
- findings.md: D202 §M-67 (static analysis exhausted, GE_VOICEDUMP probe
  motivation), §M-68 (decisive runtime A/B — all three user complaint
  classes play the exact ROM samples at the exact ROM pitches) and new
  §M-69 (320-request full-corpus exact match: 286/320 CC >= 0.85; the
  initial 44 'BAD' were tooling artifacts — voicedump stamps are block-END
  times, coarse-slide missed sharp onsets, refs unclipped at voice death;
  remaining failures = dropped allocations / shared-wavetable wire
  attribution, not corruption) + index row.
- GE-ENV-PROBES.md: GE_VOICEDUMP entry (incl. block-END-stamp gotcha);
  [WIRE]/[VOICEMAP]/[EVT]/[STOP-EVT] documented under GE_AUDIOTRACE.
- AUDIO-PLAN.md: status banner — Phases 0-2 done (D199/D200/D201/D204),
  Phase 3 verification is the active track.
2026-09-06 08:21:20 -04:00
James Dansereau 4ceb917ad8 port(audio): D202 M-67-M-69 runtime probes — GE_VOICEDUMP, [VOICEMAP], timestamped [WIRE]/[EVT]/[STOP-EVT]
Env-gated diagnostics for the D202 per-voice sample-correctness work
(M-67/M-68/M-69), all inert unless the env var is set:

- GE_VOICEDUMP (mixer.c aEnvMixerImpl): per-voice resampled mono stream
  to voicedump.raw, records [stateAddr][nSamples][us][s16 x n]. NB the
  us stamp is mix time — records render ahead, samples END at the stamp.
- [VOICEMAP] (synthesizer.c, under GE_AUDIOTRACE): full PVoice ->
  decoder/resampler/env-state address map at synth init, tying voicedump
  slot addresses to WIRE filter addresses.
- [WIRE] (load.c) now carries a µs timestamp; sndPlaySfx trace gains
  t=µs + wavetable pointer.
- [EVT]/[STOP-EVT] (snd.c): every ALSndpEvent the player processes, plus
  the release ramp a STOP/DEACTIVATE computes (deltaUs).
- audiodebug.ps1: -VoiceDump switch.

With scratchpad/exact_match.py these prove every allocated voice plays
the exact ROM sample at the exact ROM pitch (286/320 CC >= 0.85 on the
M-69 full-corpus run; remaining failures are dropped allocations + wire
attribution, not corruption). Remove once D202 closes.
2026-09-06 08:21:14 -04:00
James Dansereau 9b31f864d3 tools(pc): audiodebug.ps1 audio-probe driver; gitignore run artifacts
audiodebug.ps1 drives the D202/D204 audio probes (health monitor,
GE_AUDIOTRACE/GE_AUDIODUMP, soundIndex histogram, -AB before/after runs —
how D204 was measured); companion to debug.ps1 (crashes). README table row
added. .gitignore: scratchpad/ + root-level audiodump/audiotrace/mixertrace
artifacts.
2026-09-05 19:07:43 -04:00
James Dansereau 6736d6ee16 docs(dev): add live research/plan docs (D172, D176a, viewmodel, QOL, widescreen, unlocked-fps)
All referenced from HANDOFF.md / GRAPHICS-BACKLOG.md but untracked:
- D172-PARTICLE-COLOR-RESEARCH.md: magenta/cyan blood root-cause candidate
  (unhandled GBI opcode 0xB9 in ROM particle records + D146 guard)
- D176a-SKY-NOTES.md: RDPHALF sky-tri wire format decoded; Path B draft state
- VIEWMODEL-RESEARCH.md: 1P weapon model static audit, three ranked suspects
- QOL-INVENTORY.md / WIDESCREEN-FOV-PLAN.md / UNLOCKED-FPS-PLAN.md: parked
  Phase-4 proposals
2026-09-05 19:07:43 -04:00
James Dansereau efe477033f docs: D202 M-63/M-65/M-66/M-66b + D204 writeups; audio status in README/AGENTS
- findings.md: D202 full session history (M-63 live repro, M-65 voice-leak
  guard + sndCreatePostEvent unstub, M-66 root cause — sound 203 infinite
  ADPCM loop played fire-and-forget = faithful N64 quirk; M-66b option-C
  expiration implemented + measured) and D204 (AI-queue starvation, fixed
  + measured). Index row updated; findings-index.csv regenerated.
- GE-ENV-PROBES.md: D202 probe set documented ([EXPIRE] marked permanent).
- GRAPHICS-BACKLOG.md: D172 / D176 / 1P-viewmodel rows point at the new
  research docs.
- README + AGENTS.md: audio status corrected (mixer landed post-v0.1.0;
  Phase 3 in progress, not 'no audio').
2026-09-05 19:07:39 -04:00
James Dansereau 54c5095c23 port(audio): D202 env-gated diagnostics probes (removable on close)
Env-gated trace probes added while root-causing D202 (stuck door loop +
voice leak). All off by default; catalogue: docs/dev/GE-ENV-PROBES.md.
Remove this commit's additions when D202 closes after the user by-ear pass:

- GE_AUDIOTRACE: [VOICE+]/[VOICE-] voice acquire/release pairing,
  [EXPIRE] (permanent — part of the M-66b fix), sndPlaySfx resolution
  lines (src/snd.c)
- [DOORSND] + [SLOTWRITE] door sound state/slot ownership (propobj.c)
- [WAVELOOP] per-sound ALADPCMloop presence (libultra/audio/load.c)
- GE_PULLTRACE mixer pull diagnostics (port/src/mixer.c)
- GE_BANKDUMP / GE_KEYMAPDUMP ROM audio-table dumps (port/src/romdata.c)
- GE_MIXERTRACE [DMAHIT]/[DMAMISS] AI DMA callback trace (src/audi.c,
  __x86_64__-only)
2026-09-05 19:07:38 -04:00
James Dansereau 388d932df7 port(audio): D202 sndp fixes — ownerless-loop guard (M-65), sndCreatePostEvent unstub, option-C loop expiration (M-66b)
Three #ifdef PORT changes in src/snd.c (N64 build zero-diff), all
documented in findings.md D202:

1. M-65 voice-leak guard: the preemption scan reclaims provably-ownerless
   SOUND_FLAG_LOOPED SFX voices only, and only under pool pressure.
   Sound 203 (METAL_SLIDE_CLOSE_SFX) is played fire-and-forget by
   doorPlayCloseSound0/1 with a NULL owner; stock GE never preempts
   flag-0x12 voices, so leaked loops accumulate until new SFX are dropped.

2. sndCreatePostEvent un-stub (D138 stub removed): the stub had deleted ALL
   distance attenuation from SFX volume/pan. Verified rt=0.996, drop 0.

3. M-66b option-C expiration (user-chosen disposition for D202): new PC-only
   AL_SNDP_PORT_EXPIRE_EVT (reuses the unused 1<<13 slot in snd.h). After a
   voice starts, if it is provably ownerless (LOOPED + FINAL_IN_SEQUENCE +
   not RETRIGGER + state->state == NULL) and its wave carries an ADPCM loop
   with count == -1, post the expire at D202_EXPIRE_DELAY_US (2 s); the
   handler re-validates the predicate, fades volume to 0 over
   D202_EXPIRE_FADE_US (500 ms), and posts END_EVT. Not a reused STOP_EVT:
   its ramp uses the envelope releaseTime (~2.7 ms for sound 203) and would
   hard-cut/click. Stale-event safe because sndDisposeSound removes ALL
   pending events for a state; owned loops and retrigger sounds untouched.
   Placed in sndp (not port/src/mixer.c) because ownership is only visible
   there. Measured: pre-fix capture had the 256.7 Hz loop line flat at max
   t=39->361 s; post-fix run shows transient bursts with full silence
   between and ends allocated=0/8.
2026-09-05 19:07:32 -04:00
James Dansereau 1087820bc3 port(audio): D204 AI-queue starvation fix + GE_D204 health monitor
GE's amMain feedback loop (src/audi.c:531) has a ~3 ms setpoint that PC
scheduling jitter clears, so the SDL queue starves and the device pads
playback with silence -> audio runs permanently ~2 % below real time.

- F5 (the fix): audioGetAiLengthBytes() subtracts a 1024-frame (~46 ms)
  cushion before reporting, so the loop tops up while slack remains;
  audi.c's control law untouched (rule #2 clean).
- F1: osAiGetLength reports AI_LEN_REG single-buffer semantics (bytes left
  in the buffer currently playing), not full SDL queue depth.
- F3: Audio.QueueLimit 8192->2880 + non-silent drops on overflow.
- F4: oversize-block invariant guard.
- GE_D204=1 cheap audio-health monitor (rt/q/drop/max), safe to leave on;
  GE_D204_OLD restores pre-fix behaviour for A/B measurement.

Measured A/B in one binary: rt 0.980 -> 1.000, queue min 0 -> 368,
drop 0. Findings: docs/dev/findings.md D204.
2026-09-05 19:07:32 -04:00
James Dansereau c0ae2cf295 chore(ci): regenerate findings-index.csv for D195/D197-201
tools_pc/gen_findings_index.py output after the audio-findings commit.
2026-09-05 07:22:49 -04:00
James Dansereau a9cc629de6 port(audio): Phase-3 software mixer + K0_TO_PHYS/bnkf pointer fixes (D198-201)
Software RSP-audio-ucode mixer (macro-swap aXxx -> Impl functions, PD-port
pattern) makes libaudio output audible on PC. Alongside it, two pointer-width
bugs surfaced by exercising the audio path for the first time:

- D198: alAdpcmPull/_decodeChunk used K0_TO_PHYS (N64 kseg0 mask) on 64-bit
  heap pointers; switched to osVirtualToPhysical like every sibling call site.
- D201: bnkf.c bank/inst/sound/wavetable relocation offsets were s32,
  truncating the real 64-bit base pointer for ~50% of allocations; widened
  to uintptr_t.
- D200 (already committed-pending from a prior session): reverb's ALDelay
  u32 negative indices zero-extended into +4GB offsets on 64-bit; (s32)/(s64)
  casts match PD ground truth.
- D199: documents the mixer's aSetBuffer persistent-DMEM-context design,
  reconstructed from call-site pairs (no RSP disassembly to check against)
  since there's no N64 audio ucode source to reference.
- D195/D197 (playtest reports referenced by an earlier HANDOFF note but never
  written into findings.md): backfilled as OPEN with next steps.

Verified: ./build-pc.sh ntsc-final links clean; -level_09 runs 600+ frames
crash-free with music + SFX audible.
2026-09-05 07:22:21 -04:00
James Dansereau 6d6609a550 Merge pull request #27 from jkdansereau/chore/agentic-speedups-s1-4-6
chore(agentic): speed-ups plan Steps 1, 4 (N2/R8/R9), 6
2026-09-04 22:28:03 -04:00
James Dansereau a5e6cd443a fix(ci): regenerate findings-index.csv, document GE_DETERM_TRACE
The D117 findings.md addenda in the prior commit changed D117's status
text without regenerating findings-index.csv, and the new GE_DETERM_TRACE
env var wasn't yet mentioned in GE-ENV-PROBES.md -- both fail their
--check gate in CI's validate job. Caught by a real CI run, not predicted.

Claude-Session: https://claude.ai/code/session_01P3WrUHAFQZUuhrHhiJgTUr
2026-09-04 22:20:44 -04:00
James Dansereau 70208feb64 docs(claude.md): correct wrong local-Qwen-worker shell claim
The "worker's shell is cmd.exe, no grep/cat" note was wrong -- verified
directly against the delegate-local harness's own _run_bash: grep, sed,
cat, ls, head, pipes, and grep -rn all work (Git-for-Windows/MSYS2 on
PATH). The stale claim, baked into 4 of 5 local agent .md files, caused a
real dispatch to burn its whole turn budget full-read_file-ing files
instead of grepping them, hit the turn limit, and returned nothing.

Claude-Session: https://claude.ai/code/session_01P3WrUHAFQZUuhrHhiJgTUr
2026-09-04 22:11:07 -04:00
James Dansereau bf195d3eb5 port(determ): experimental GE_DETERM=1 fixed-tick mode, not yet achieved (D117)
Adds an env-gated (GE_DETERM=1, default OFF), test-only deterministic-clock
mode to port/src/libultra.c per the design in docs/dev/findings.md section D117:
osGetCount() returns a virtual clock advanced synchronously in osRecvMesg()
when __scMain (the sole consumer of the scheduler's retrace queue) asks for
the next message, instead of tracking real elapsed time. Goal: two runs of
the same build take the same simulation path, for exact frame-diff
regression testing and reproducible crash repro.

Four passes this session, each is a real, verified result, not a guess:
1. Fixed the predicted pre-first-frame deadlock (a poll-driven fallback
   caused it in the first cut).
2. Replaced the poll-driven fallback with synchronous per-ask generation
   in osRecvMesg -- fixed the deadlock cleanly, but two-run divergence was
   still 33-38%.
3. Capped the pre-first-task tick backlog to a fixed constant -- divergence
   dropped to 17-20%, still not the ~0% target.
4. Added GE_DETERM_TRACE=1 instrumented tracing and diffed two runs' clock
   event logs directly. Proved the tick mechanism is now fully
   deterministic (byte-identical clock values through the first real task).
   Root-caused the actual remaining divergence: every synthesized retrace
   increments the scheduler's frameCount, which gates a half-rate client
   (the audio client) by parity -- since the pre-task spin count is still
   real-time-scheduling-dependent, that parity isn't guaranteed to match
   between runs.

GE_DETERM is NOT achieving its determinism goal yet and is not meant to be
used for anything today -- it ships here purely as a well-documented,
zero-risk-when-off checkpoint (verify.sh bunker1 PASS, worst_cell unchanged,
confirmed after every change this session) so the next attempt starts from
proof instead of re-deriving four sessions of dead ends. Full trace numbers,
exact line citations, and the mechanism: docs/dev/findings.md section D117
(four M-52 addenda).

Claude-Session: https://claude.ai/code/session_01P3WrUHAFQZUuhrHhiJgTUr
2026-09-04 22:10:58 -04:00
James Dansereau 226f0490cb ci: add ccache to Linux + Windows build jobs (speed-ups plan Step 5)
CMakeLists.txt already opt-in enables CMAKE_{C,CXX}_COMPILER_LAUNCHER=
ccache when found (b49907de); CI just never installed ccache or persisted
its cache dir across runs. Wires actions/cache + ccache install into both
linux-build and windows-build, keyed on CMakeLists.txt's hash. Game
sources are frozen, so this should give a near-total hit rate after the
first run on each job.

YAML-validated; the actual hit-rate payoff needs a real CI run (cold, then
warm) to confirm -- not verifiable locally.

Claude-Session: https://claude.ai/code/session_01P3WrUHAFQZUuhrHhiJgTUr
2026-09-04 22:10:41 -04:00
James Dansereau 884cfdb9b7 tools(verify): add verify.sh + crash_brief.py (speed-ups plan Step 2, 9)
verify.sh wraps the manual render-verify recipe (build-if-stale, 640x480
ini pin/restore, headless GE_PCDUMP capture, pixcount, framediff, crash
symbolication) into one command with a machine-readable PASS/CRASH/
NO-FRAMES/REGRESSION verdict + --json, for a single level or a sweep
subset. Live-tested on Windows: single-level and subset-sweep runs both
produce correct verdicts and leave the tree clean.

crash_brief.py turns a ge007.crash.log into a symbolicated, dispatch-ready
investigation brief: greps porting-notes.md/findings-index.csv for
sections covering the crashing function/file, and flags known
parked-crash signatures (D188 va_list, D189 stack overrun) so agents stop
re-investigating them. verify.sh calls it automatically on a CRASH
verdict. Live-tested against a synthetic-but-real crash log built from an
actual built symbol; caught and fixed 3 bugs before trusting it (addr2line
unresolvable under native Windows Python's PATH handling, a substring
keyword-match false-positive, a Windows drive-letter path-split bug).

Claude-Session: https://claude.ai/code/session_01P3WrUHAFQZUuhrHhiJgTUr
2026-09-04 22:09:58 -04:00
James Dansereau dbea5a30fe chore(agentic): speed-ups plan Steps 1, 4 (N2/R8/R9), 6
Static/tooling slice of docs/dev/notes/AGENTIC-SPEEDUPS-PLAN.md — no build or
runtime environment needed to verify any of it.

- Step 6 / N7: add a VERIFY: line to the investigation-brief template
  (docs/dev-process.md) and the CLAUDE.md dispatch-preflight checklist — the
  brief must now name the exact probe/verify invocation that proves the task
  done, or state that runtime verification is impossible here.
- Step 4 / N2: tools_pc/README.md — indexes the ~60 scripts with a
  living vs artifact marker so closed-finding artifacts can be archived.
- Step 4 / R8: tools_pc/gen_findings_index.py -> docs/dev/findings-index.csv
  (label, bucket, one_liner, status_raw) — grep-before-you-read aid for the
  ~200 KB finding log. Heuristic parser of the §F/§H tables.
- Step 4 / R9: tools_pc/gen_env_probes.py — re-greps live getenv("GE_*")
  sites and reports NEW/GONE drift vs docs/dev/GE-ENV-PROBES.md (keeps the
  curated prose, only checks the site map). Doc reconciled: 7 new probes
  added, GE_D54 kept as a tombstone.
- CI: validate job now runs both generators with --check.

Step 1 / N1 (HANDOFF preflight) applied in the local-only docs/HANDOFF.md.
Step 2 (verify.sh) and the HANDOFF/HANDOFF-ARCHIVE trim are left for a
session with a build environment.
2026-09-04 19:21:35 -04:00
James Dansereau 5c3944582b docs: v0.1.0 alpha framing + hygiene pass (#25)
Update the public-facing docs to the M-49 state (v0.1.0 alpha, first
full-campaign playtest: 19/21 missions completable, D191 crashes Bunker ii
+ Statue, D193 AI pacing is the top flaw).

- README: lead with what it is + status, research angle second; new
  Quick start section; Status/Download rewritten for the alpha + dual
  platform; "By the numbers" refreshed (~3 weeks, ~320 commits, ~190
  findings, ~49 sessions, 69 PORT files).
- .github/release-notes.md: v0.1.0 title + known-issues rewrite.
- .github/social-preview.html: status/stat lines (regen + re-upload the
  PNG separately).
- docs/index.md, docs/dev/LEVEL-STATUS.md: M-49 status.
- docs/dev/findings.md, docs/dev/GRAPHICS-BACKLOG.md: D191-D197 +
  D148/D170/D173 M-49 escalations.

Hygiene:
- Fix the `&amp;` literal in internals.md H1.
- Repoint two ../README.md links that 404 on the Pages site.
- Standardise British -> American spelling across the public docs
  (behaviour->behavior, colour->color, catalogue->catalog,
  centre->center, etc.). Deep internal logs left untouched.
- .gitignore: document the local-notes convention (docs/HANDOFF.md,
  docs/BACKLOG.md, and a new docs/dev/notes/ drop for roadmaps and
  session plans) so it survives a fresh clone.
2026-09-04 00:13:43 -04:00
244 changed files with 476534 additions and 2118 deletions

No files matched your search

+8 -3
View File
@@ -13,9 +13,14 @@ rules there are non-negotiable.
## Scope check
- [ ] No changes under `src/` or `include/` — **or** the only changes are the
narrow `#ifdef PORT` ABI exception (CONTRIBUTING.md rule 2), and each is
documented in `docs/porting-notes.md` / `docs/dev/findings.md`.
- [ ] No changes under `src/` or `include/` — **or** the only changes are
the narrow `#ifdef PORT` ABI exception (CONTRIBUTING.md rule 2),
checked against `docs/porting-notes.md` §A1, and documented in
`docs/dev/findings.md` (§F/D3x).
- [ ] If this PR contains a genuine `src/game` behavior change (not an
ABI/layout fix): an explicit rule-2 sign-off was granted
(`docs/dev-process.md` §7) and is recorded in a
`RULE-2-SIGNOFF`-tagged `docs/dev/findings.md` entry linked here.
- [ ] `Makefile`, `tools/`, `rsp/`, `ld/` untouched (N64 build).
- [ ] If `CMakeLists.txt` `REGION_DEFS` changed, it still matches the N64
`Makefile` per-region macro set exactly.
+312 -39
View File
@@ -1,60 +1,333 @@
## GoldenEye 007 PC Port — alpha
# GoldenEye 007 PC Port — v0.4.0
> ⚠️ **Very early research alpha. Not a playable game.**
> The intro and menus render and all 21 solo missions load, but:
> - **no audio** (Phase 3, not started);
> - some front-end 3D models are broken (the spinning Nintendo logo, the
> MISSION COMPLETE / mode-select models);
> - outdoor levels render with a **black sky**;
> - expect crashes and glitches once past the level intro.
>
> This is a technical demo of the porting work. If you want to *play* GoldenEye
> on PC, use one of the Xbox 360 recompilation projects instead — see the
> [project README](https://github.com/jkdansereau/goldeneye-pc-port#how-this-differs-from-the-other-goldeneye-pc-projects).
<p align="center">
<img src="https://github.com/jkdansereau/goldeneye-pc-port/raw/v0.4.0/docs/media/goldeneye-gh-preview.gif" width="480"
alt="~24 s gameplay montage from live v0.4.0 play sessions, opening on the Runway tank (no audio track)">
</p>
### Downloads
Platforms: **Windows x86-64** and **Linux x86-64 (including Steam Deck)**.
Region: **NTSC-U (US) only** — see [Things to know](#things-to-know).
v0.4.0 is a large feature and fidelity release. The full campaign runs at a
steady 60 fps with music and SFX throughout, and has been playtested end to
end (all 20 missions, Agent difficulty) on Windows and on Steam Deck with a
physical controller. This is a pre-1.0 release: the known issues below are
real, and some areas (multiplayer, PAL/JP, HD assets) are still ahead of us.
---
## New features
### Native widescreen *(on by default)*
The world now renders natively at your display's aspect ratio instead of
stretched 4:3: geometry is undistorted (wider view, same vertical FOV), the
in-level HUD keeps its shape and anchors to the screen edges, and the 4:3
front-end menus and ending-credits sequence are pillarboxed. Turn it off
with the **`Native widescreen`** toggle (F10 → Display) to restore the old
stretched frame. (D334/D335)
### A true, stable 60 fps
v0.3.0 could settle at 30 fps game-wide on some setups. The cause was a
pointer-width bug in the game's frame scheduler: a "present every frame"
flag was read back at a hardcoded byte offset that the 32→64-bit transition
silently moved, so the render path ran on the audio client's every-other-
frame cadence. Fixed at the root; the game now holds a rock-stable 60 fps
(measured with the in-game FPS readout) on both platforms. (D248)
### A complete aim system for mouse *and* controller
- **`Aim style`** — **N64** (default): the crosshair deflects and the
camera follows once it reaches the screen edge. **Centred (PC)**: the
crosshair is fixed at screen center and your mouse — or your right stick —
moves the camera directly. In v0.3.0 the centred mode only worked with a
mouse; it now works with a controller too, using the N64 original's own
stick-response curve so full stick matches the original's top turn rate.
(D332/D333/D337/D404)
- **`Aim range`** (shown while Centred is selected) — **PC** (default,
full-screen feel) or **N64** (the original 65% stick limits). (D338)
- **Reticule jitter in aim mode is gone** — mouse aim now runs on the
Perfect Dark PC port's aim model. (D332)
- Per-device sensitivity: `Mouse horizontal/vertical sensitivity`, and for
controllers `X axis / Y axis look sensitivity (controller)` plus
`Look smoothing (controller)`.
### Rebuilt options (F10 overlay + front-end PC Options)
- Both options screens are reorganized into functional sections — **Input,
Gameplay, HUD, Graphics, Audio, Display** — each with its own *Reset to
defaults*; per-profile settings scoping; and the old "save file" wording
is now **profile**. (D353–D356)
- The **F10 in-game overlay** was refreshed GE-style: category headers,
full controller support (value-adjust with hold-to-repeat), fixed pointer/
keyboard navigation, and new rows: **Show FPS**, **Skip intro
(experimental)**, pad
**Invert look / Southpaw / Deadzone / Trigger threshold**. (D237/D345–
D347/D360–D370)
- The front end gained a **PC Options screen** beside the file-select bar,
and save-file **Copy/Erase** moved into the bottom bar. Long sections
(Input) page within the screen -- a "Page 1/2" marker plus a bottom hint
on every page -- so every row stays on screen and stays reachable; the
mouse wheel and W/S also page the list, and the selection clamps at page
edges. (D343/D406/D407)
- Watch-only settings — **Auto-aim**, **Look ahead** and the rest — are now
editable from the PC options and **persist correctly** (this closes the
"auto-aim won't stay on" report, issue #103). (D330/D350/D352/D354)
- **`HUD scale`** (75–150%, default 100%): scales the ammo counter, pickup
text and subtitle bars for smaller or larger displays. (D226)
### Crosshair customization *(defaults leave the N64 crosshair untouched)*
**Show crosshair** (on/off), **Crosshair colour** (the original red,
or green / blue / yellow / cyan / magenta / white, or custom RGB), **Crosshair size**
(default 100% of the original drawing), and **Crosshair style** (Original /
Thin cross). (D373/D379/D381/D382)
### In-game key rebinding + new default layout
A two-group key editor (**Movement**, **Actions**, each with its own *Reset
to defaults*) lives in both options screens. The new default **Actions**
layout is the GEPD-style preset (e.g. Action/next-weapon on the keys you'd
expect from a modern layout); existing INI bindings are honored and older
ones migrate automatically. (D371/D374/D383/D385)
### Controller improvements
- **Sane pad defaults**: **A = use/interact, X = reload, Y = weapon cycle**
(v0.3.0 mapped two buttons to crouch and had no in-game use button). (D393)
- Crouch uses the engine's real crouch input, with a **`Crouch mode`**
toggle (hold / latched). (D375)
- Xbox-variant controller parity, **Southpaw**, and per-stick
**deadzone** + **trigger threshold** rows. (D237/D394)
- **The N64 Rumble Pak now drives real gamepad haptics** (Steam Deck
included), scaled by a global **`Vibration`** slider (default 50%). (D401)
- The F10 overlay is fully gamepad-driven. (D347/D395/D396)
### `All unlocked` ships as a documented EXPERIMENTAL feature
F10 → Gameplay → **`All unlocked (EXPERIMENTAL)`** opens every mission, 007
mode and the full cheat menu. On a fresh install it now yields the game's
own fresh-slot layout instead of corrupting state (D281) — but enabling it
can still bake synthetic cheat/completion data into your save that switching
it off does not undo: **complete a level normally first, and back up
`data/ge007.eep` before enabling it.** (D387)
### Steam Deck / Linux
The first clean SteamOS/glibc build, plus a first-launch preset for decks
without a config (native 1280×800, 2× MSAA, vsync, 250% draw/LOD
distances). (D402/D283)
---
## Bug fixes
### Fidelity (verified against the N64 original / era-correct references)
- **Water** on the `IsWater` levels (Dam, Frigate, Surface 2, …) matches
the original: the moving seam between two patterns and the pattern
"resetting" as you moved are gone. (D245)
- The **particle "rainbow" effect** (intermittently recoloured sparks and
explosion residue) no longer occurs. (D252)
- **Frigate: "every polygon breaks except the gun"** after turning — and
the rare "Bond briefly out of place" quirk — were the same never-written
fields read by the head-bob animation; both are fixed. (D336/D311)
- Surface 1/2: the tree backdrop that rendered as a solid wall of texture
now renders as proper camera-facing tree cards. (D236)
- Ejected **shell casings** render again. (D331)
- The intro **gun-barrel blood drip** draws correctly. (D341)
- The extra erroneous **long muzzle flash** on some guns is gone. (D303)
- Occasional **z-fighting** fixed; **security-camera props** no longer face
backwards; the front-end **Nintendo logo and copyright page** render
correctly. (D308/D307/D403)
- The **train-intro soldier pose** is correct. (D392)
- **Tanks** (Runway, Streets) can be boarded and exited again — the new
use/reload button split had removed the B-button tap the engine's tank
handlers use; B is presented again in tank states, and use keeps its
no-reload-fallback semantics everywhere else. A short input lockout during
the sit-down animation means a double-tap can't cancel boarding, and in-tank
mouse aim has its own **`Tank aim speed`** setting (`Input.TankAimScale`,
default 100) so the turret matches your on-foot feel. (D407)
- File-select background / gun-barrel comb rendering fixed. (D397)
- Stale-texture artifacts after level transitions fixed. (D235)
- Distant-geometry dropout on the biggest open levels (Streets, Egyptian)
substantially improved at default settings. (D249)
- The **Facility execution-scene watchdog** from v0.3.0 stays enabled as a
safeguard; the animation-pinning that motivated it is root-caused (a tick
granularity divergence) and fixed, so the scene plays out as authored.
(D329/D318)
### Front end & UI
- Randomly **flickering F10 menu items** are gone. (D314)
- The watch's **controller-page graphic** renders again. (D290)
- File-select **folders and Bond photos** no longer vanish after backing
out of a file. (D342)
- File-select bottom row (SELECT FILE / Copy / Erase / PC Options) aligned.
(D398–D400)
### Audio
- **Gunshot SFX** cadence matches the N64 original's rate, and the
intermittent silence of the PP7/AK47 under sustained fire near a looping
sound is gone. (D240/D241)
- The silenced **PPK "slap"** and the broader real-time mixer corruption
behind it are fixed. (D202)
- Windows audio now prefers the **DirectSound** backend over WASAPI (lower,
more robust latency). (D322)
- In-stage music **re-synchronizes** correctly at stage transitions and
level entry. (D355)
- Long-session audio degradation (issue #87): pool hardening shipped, plus
an optional `GE_D322=1` telemetry probe if it ever recurs.
### Stability & renderer
- **Orderly game exit**: quitting no longer lands inside the graphics
driver (a class of Windows driver bugchecks seen during testing). (D344)
- A **non-self-recovering Windows freeze** after the window sat idle and
lost focus is fixed (mouse-mode changes now applied on the window
thread). (D287)
- **MSAA above your driver's maximum no longer black-screens** (the request
is clamped to `GL_MAX_SAMPLES`), and two framebuffer-state bugs in the
render/resolve paths were fixed — community contribution, PR #106,
security-reviewed. (D405)
- Port-wide code audit: missing returns in front-end menus, missing
prototypes, undefined-behaviour shifts.
---
## Options reference
All settings available in v0.4.0, in both options screens (front-end
**PC Options** and the in-game **F10 overlay**). Defaults in parentheses.
| Setting | Section | Default |
|---|---|---|
| `Native widescreen` | Display | On |
| `Frame rate cap` | Display | 60 (30 / 60) |
| `Show FPS` | Display | Off |
| `MSAA` | Display | 2× (1/2/4/8, clamped to driver max) |
| `Aim style` | Input | N64 (N64 / Centred (PC)) |
| `Aim range` | Input | PC (shown while Centred) |
| `Mouse horizontal/vertical sensitivity` | Input | calibrated midpoint |
| `Invert look (mouse)` / `Invert look (controller)` / `Southpaw` | Input | Off |
| `X / Y axis look sensitivity (controller)` | Input | 100% (native) |
| `Look smoothing (controller)` | Input | 0 |
| `Tank aim speed` | Input | 100% (`Input.TankAimScale`) |
| `Deadzone (left/right stick)` | Input | 70% |
| `Trigger threshold` | Input | 23% |
| `Vibration` | Input | 50% |
| `Crouch mode` | Input | Hold |
| `Bindings…` (Movement / Actions editors) | Input | GEPD-style preset |
| `Auto-aim` / `Look ahead` | Gameplay | Off (watch-backed) |
| `Skip intro` / `No hit flash` | Gameplay | Off |
| `All unlocked (EXPERIMENTAL)` | Gameplay | Off |
| `HUD scale` | HUD | 100% (75–150%) |
| `Show crosshair` / `Crosshair colour` / `Crosshair size` / `Crosshair style` | HUD | N64-intact (size 100%) |
---
## Things to know
- **NTSC-U (US) ROMs only.** PAL and JP ROMs are not supported in this
version; asset repair for those regions is on the post-release roadmap.
(D258)
- **The default Actions key layout is new** (GEPD-style). If you preferred
the v0.3.0 defaults, rebind them in *Input → Bindings…* or reset the
group. Existing INI bindings are auto-migrated and still honored.
- **Back up `data/ge007.eep` before using `All unlocked`** (see above).
- Steam Deck: the first launch in Game Mode applies the Deck preset only if
no `ge007.ini` exists yet; F10 changes always win afterwards.
## Known issues
- On Facility, if gas leaks during Ourumov's monologue he can pause for up
to ~10 s before resuming the scripted shootout — a latent race that exists
in the N64 original too (where it softlocks permanently); the port detects
and auto-recovers it. (D318)
- With *Native widescreen* on, the F10 options overlay stretches with the
window instead of pillarboxing like the front-end menus (legible;
cosmetic). (D335b)
- The front-end Rareware logo shows a subtle texture-filtering artifact
(the Nintendo logo and legal page are clean). Cosmetic only. (D75)
- **`All unlocked` is experimental** (see above); the fresh-install
corruption case is fixed, but saves made while it is on are not
guaranteed recoverable by switching it off. (D387)
- **`Skip intro` is experimental — not recommended for regular use yet.**
With it on, a failed or aborted mission skips the post-mission failure
report screen and returns straight to the menus (the mission still records
correctly). An older audio quirk on some saves is also still being chased.
(D408/D216)
## Roadmap since v0.3.0 (research, not yet features)
- ROM-level **mod support** investigated and found viable (the game's file
table lives in the ROM); HD texture packs and the drop-in-ROM flow are
tracked post-1.0. (D325–D328)
- A measured CPU budget on low-end hardware (issue #92) reshaped the
performance investigation toward the GPU. (D339)
- Controller options wave 2 (presets, pad reassignment UI) and PAL/JP
support are planned post-v0.4.0.
---
## Downloads
| File | Platform |
|---|---|
| `goldeneye-pc-port-<version>-win64.zip` | Windows x86-64 |
| `goldeneye-pc-port-<version>-linux-x86_64.tar.gz` | Linux x86-64 |
| `goldeneye-pc-port-0.4.0-win64.zip` | Windows x86-64 |
| `goldeneye-pc-port-0.4.0-linux-x86_64.tar.gz` | Linux x86-64 (incl. Steam Deck) |
Each contains the engine executable, a README, license texts, and the
`prepare-assets` tool. **No ROM, no game assets.** The Windows bundle also
carries its runtime DLLs; the Linux bundle links against your distro's SDL2 /
zlib / libGL (`sudo apt install libsdl2-2.0-0 zlib1g libgl1`, or the equivalent).
`prepare-assets` tool. **No ROM, no game assets.** The Windows bundle
carries its runtime DLLs; the Linux bundle carries SDL2, so on both
platforms nothing needs to be installed first.
Windows development and playtesting is the primary path; the Linux build boots
and renders (tested on WSLg) but has had far less exercise.
## Running it
### Running it
You supply your own **NTSC-U GoldenEye 007 N64 ROM** (`.z64`, big-endian,
`SHA-1 abe01e4aeb033b6c0836819f549c791b26cfde83`). Only the US ROM is supported
in this alpha.
You supply your own **GoldenEye 007 N64 ROM** (`.z64`, big-endian) that you
legally own (NTSC-U only, as noted above). No Python, no toolchain, nothing
to install.
1. Unpack the archive.
2. Make a `data/` folder next to the executable and put the ROM in it as
2. Make a `data/` folder next to the executable and put the ROM in it, named
`ge007.ntsc-final.z64`.
3. Run the one-time asset step (needs Python 3.8+):
`python3 prepare-assets/prepare-assets.py`
— it reads your ROM and writes the two `data/pc*-ntsc-final/` folders the
engine needs. Standard library only; a few seconds.
4. Run the executable **from that folder**.
3. Run the executable **from that folder**. The first run takes a few extra
seconds: it detects your ROM, generates the two derived asset folders
(`data/pcmodels-ntsc-final/`, `data/pccg-ntsc-final/`) once, and saves
them for every future run. (The generator is
`prepare-assets/ge007-convert` inside the bundle; you can also run it
manually; it prints what it's doing.)
**Steam Deck:** sideload the unpacked folder (SFTP, USB or a file manager), do
steps 2–3, then add the executable to Games → *Add Game* as a non-Steam
game. The first launch in Game Mode picks up the Deck preset (native
1280×800, 2× MSAA, vsync) automatically if no config exists yet.
**In-game settings on the Deck:** the options overlay is fully
gamepad-driven: it opens with **Select**, the D-pad or left stick (up/down)
moves between options, **A** steps the selected option forward, **B** steps
it back, and **Start** (or Select again) closes. No keyboard needed.
Full steps are in the bundled `README.md`.
### Verify the download
## Verify the download
```
sha256sum -c goldeneye-pc-port-<version>-win64.zip.sha256
sha256sum -c goldeneye-pc-port-<version>-linux-x86_64.tar.gz.sha256
sha256sum -c goldeneye-pc-port-0.4.0-win64.zip.sha256
sha256sum -c goldeneye-pc-port-0.4.0-linux-x86_64.tar.gz.sha256
```
### Source & docs
## Source & docs
<https://github.com/jkdansereau/goldeneye-pc-port> — built on the
[GoldenEye 007 decompilation](https://github.com/n64decomp/007), architecture
after the [Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark).
<https://github.com/jkdansereau/goldeneye-pc-port>, built on the
[GoldenEye 007 decompilation](https://github.com/n64decomp/007),
architecture after the [Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark).
Non-commercial fan preservation/research project; not affiliated with any
rights holder.
rights holder. **AI disclosure:** built through agentic AI coding (Claude
Code + a local open-weight model), directed by one person in their spare
time — as much a study of what agentic development gets wrong on a
game-sized codebase as it is a port. See the README's
[Background section](https://github.com/jkdansereau/goldeneye-pc-port#background)
for the full account.
+5 -5
View File
@@ -118,18 +118,18 @@
<div class="h1">GOLDENEYE <span class="r">007</span><br><small>PC&nbsp;PORT</small></div>
<div class="meta">
Mission: Native PC port of GoldenEye, compiled from the N64 decomp project
Mission: Native PC port of GoldenEye 007, compiled from the N64 decomp project
</div>
<div class="rep">REPORT:</div>
<div class="rows">
<div class="statusline">Status: <span class="status">PHASE 2 OF 4 &mdash; RENDERING</span></div>
<div class="statusline">Status: <span class="status">RELEASED v0.3.0 &mdash; WINDOWS &middot; LINUX &middot; STEAM DECK</span></div>
<div class="stats">
<span class="k">Statistics:</span><span>21 / 21 solo missions load &amp; render</span>
<span class="k">Statistics:</span><span>All 20 solo missions &mdash; full campaign completed in playtesting</span>
<span></span><span>~230 game-code units, compiled unmodified</span>
<span></span><span>~17k lines C/C++ &nbsp;&middot;&nbsp; ~6k Python (port layer + tools)</span>
<span></span><span>162 bugs root-caused &nbsp;&middot;&nbsp; built in ~2 weeks, part-time</span>
<span></span><span>~24k lines C/C++ &nbsp;&middot;&nbsp; ~7k Python (port layer + tools)</span>
<span></span><span>199 findings root-caused &nbsp;&middot;&nbsp; two coding agents, one human, part-time</span>
</div>
</div>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 583 KiB

After

Width:  |  Height:  |  Size: 597 KiB

+149 -15
View File
@@ -66,12 +66,31 @@ jobs:
runs-on: ubuntu-22.04
outputs:
code: ${{ steps.filter.outputs.code }}
# Windows builds default to the self-hosted box (fast, local ccache;
# docs/dev/notes/SELFHOSTED-RUNNER-PLAN.md §G2) for everything EXCEPT:
# (a) a release build (tag push) -- keep that on a clean, reproducible
# GitHub-hosted runner for the artifact that actually ships, and
# (b) a fork PR -- untrusted PR code must never reach a box that will
# later hold a ROM. Same-repo (non-fork) PRs now also get routed to
# self-hosted per the 2026-09-12 standing preference.
windows_selfhosted: ${{ steps.runner.outputs.selfhosted }}
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
fetch-depth: 0
- id: runner
run: |
if [ "${GITHUB_REF_TYPE}" = "tag" ]; then
echo "selfhosted=false" >> "$GITHUB_OUTPUT"
elif [ "${{ github.event_name }}" = "pull_request" ] && \
[ "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]; then
echo "selfhosted=false" >> "$GITHUB_OUTPUT"
else
echo "selfhosted=true" >> "$GITHUB_OUTPUT"
fi
- id: filter
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
@@ -113,6 +132,26 @@ jobs:
- name: Byte-compile Python tooling
run: python3 -m compileall -q scripts tools_pc
# Generated dev-docs stay in sync with the tree (speed-ups plan Step 4).
- name: Check generated dev indexes are current
run: |
python3 tools_pc/gen_findings_index.py --check
python3 tools_pc/gen_env_probes.py --check
# Docs budget guardrail (ctx-opt P2): warn, don't fail. The §F index
# table should stay one-liners; fat cell content belongs in `## Dxx`
# sections at the file tail (lookup path: docs/dev/findings-index.csv).
- name: Check dev-docs size budgets (warn only)
run: |
# Budget = ~55 KB of table rows + ~110 KB of legacy D24–D33 / M-xxx
# narrative that predates the ctx-opt migration. The signal is fat
# ROWS re-accumulating, so the threshold sits ~15 KB above today's
# 166 KB total.
f=$(awk '/^### F\./{f=1;next} /^### G\./{f=0} f' docs/dev/findings.md | wc -c)
[ "$f" -le 184320 ] || echo "WARNING: findings.md §F region is $f bytes (> 180 KB) — fat rows accumulating; migrate cell content to ## Dxx sections at the file tail"
p=$(wc -c < docs/porting-notes.md)
[ "$p" -le 122880 ] || echo "WARNING: porting-notes.md is $p bytes (> 120 KB) — consider splitting or adding a TOC block"
# US only. gen_romassets.py resolves every symbol for the US region from
# the committed CSVs with no ROM present; PAL/JP each need a handful of
# region-specific prop/font offsets that only come from that region's ROM
@@ -141,6 +180,12 @@ jobs:
env:
CC: gcc-14
CXX: g++-14
# CMakeLists.txt opt-in enables CMAKE_{C,CXX}_COMPILER_LAUNCHER=ccache
# when ccache is on PATH (b4de1c68, speed-ups plan Step 5). Game
# sources are frozen (1:1 with the decomp), so after the first run only
# touched port/ files miss cache -- near-total hit rate.
CCACHE_DIR: ${{ github.workspace }}/.ccache
CCACHE_MAXSIZE: 500M
steps:
- uses: actions/checkout@v7
with:
@@ -150,11 +195,36 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install -y build-essential gcc-14 g++-14 cmake python3 \
libsdl2-dev zlib1g-dev libgl1-mesa-dev
python3-pip libsdl2-dev zlib1g-dev libgl1-mesa-dev ccache patchelf
- name: Restore ccache
uses: actions/cache@v4
with:
path: ${{ env.CCACHE_DIR }}
key: ccache-linux-${{ hashFiles('CMakeLists.txt') }}-${{ github.sha }}
restore-keys: |
ccache-linux-${{ hashFiles('CMakeLists.txt') }}-
ccache-linux-
- name: Regenerate ROM-asset symbol file (US)
run: python3 scripts/gen_romassets.py u
# Drop-in ROM support: freeze the asset-prep tool into a single
# ge007-convert binary the engine spawns on first run when the
# pcmodels/pccg sidecars are missing (port/src/romconvert.c). Pure-
# stdlib scripts, so PyInstaller --onefile is clean; lands in
# tools_pc/dist/prepare-assets/ where bundle-linux.sh picks it up.
- name: Freeze asset converter (PyInstaller)
run: |
python3 -c "import PyInstaller" 2>/dev/null \
|| python3 -m pip install --quiet --break-system-packages pyinstaller
python3 -m PyInstaller --onefile --noconfirm --clean \
--name ge007-convert \
--distpath tools_pc/dist/prepare-assets \
--workpath build-pyinst --specpath build-pyinst \
tools_pc/dist/prepare-assets/prepare-assets.py
ls -la tools_pc/dist/prepare-assets/ge007-convert
- name: Configure (ntsc-final)
run: cmake -S . -B build-linux -DROMID=ntsc-final
-DCMAKE_C_COMPILER=gcc-14 -DCMAKE_CXX_COMPILER=g++-14
@@ -162,15 +232,18 @@ jobs:
- name: Build (ntsc-final)
run: cmake --build build-linux -j"$(nproc)"
- name: ccache stats
run: ccache -s
- name: Show binary
run: |
ls -la build-linux/ge007* || true
file build-linux/ge007* || true
# Package the Linux bundle on every run so the release path stays
# exercised. No shared libs are bundled — the README tells the user to
# install SDL2 / zlib / libGL from their distro. Same ROM / size guards
# as the Windows bundle.
# exercised. SDL2 is bundled (patchelf sets the exe's rpath to $ORIGIN)
# so the tarball runs as-is on any distro / a sideloaded Steam Deck;
# zlib + libGL stay system deps. Same ROM / size guards as Windows.
- name: Package Linux bundle
run: |
if [ "${GITHUB_REF_TYPE}" = "tag" ]; then
@@ -200,34 +273,85 @@ jobs:
windows-build:
needs: changes
if: needs.changes.outputs.code == 'true'
runs-on: windows-latest
# Self-hosted by default (fast, local ccache); windows-latest only for a
# release (tag) build or a fork PR — see the `changes` job's `runner`
# step for the exact rule (docs/dev/notes/SELFHOSTED-RUNNER-PLAN.md §G2).
runs-on: ${{ needs.changes.outputs.windows_selfhosted == 'true' && fromJSON('["self-hosted", "windows"]') || 'windows-latest' }}
defaults:
run:
shell: msys2 {0}
env:
# Same opt-in ccache CMakeLists.txt already wires up (Step 5). Note:
# this cache path is read/written by the JS actions/cache step outside
# the msys2 shell too, so it stays a plain workspace-relative path
# rather than an msys /c/... form.
CCACHE_DIR: ${{ github.workspace }}/.ccache
CCACHE_MAXSIZE: 500M
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
# windows-latest (release tag / fork PR) is ephemeral: full install +
# update every run. Self-hosted (everything else): `release: false`
# reuses the box's own C:\msys64 as-is and, with update/install both
# empty here, does no pacman -Syu / pacman -S at all (verified against
# the action's source) — it only registers the `msys2 {0}` shell
# wrapper every job needs. Packages themselves are hand-maintained on
# the box; the dependency check below (not this step) verifies they're
# present.
- uses: msys2/setup-msys2@v2
with:
msystem: MINGW64
update: true
install: >-
mingw-w64-x86_64-toolchain
mingw-w64-x86_64-cmake
mingw-w64-x86_64-SDL2
mingw-w64-x86_64-zlib
mingw-w64-x86_64-python
zip
unzip
release: ${{ needs.changes.outputs.windows_selfhosted != 'true' }}
update: ${{ needs.changes.outputs.windows_selfhosted != 'true' }}
install: ${{ needs.changes.outputs.windows_selfhosted != 'true' && 'mingw-w64-x86_64-toolchain mingw-w64-x86_64-cmake mingw-w64-x86_64-SDL2 mingw-w64-x86_64-zlib mingw-w64-x86_64-python mingw-w64-x86_64-ccache zip unzip' || '' }}
# Self-hosted only: fails fast with exact install instructions if the
# box's hand-maintained MSYS2 is missing a required package, instead of
# a confusing failure deeper in the build. windows-latest just installed
# everything above, so this would be redundant there.
- name: Check self-hosted dependencies
if: needs.changes.outputs.windows_selfhosted == 'true'
run: tools_pc/check-selfhosted-deps.sh
- name: Restore ccache
uses: actions/cache@v4
with:
path: ${{ env.CCACHE_DIR }}
key: ccache-windows-${{ hashFiles('CMakeLists.txt') }}-${{ github.sha }}
restore-keys: |
ccache-windows-${{ hashFiles('CMakeLists.txt') }}-
ccache-windows-
- name: Regenerate ROM-asset symbol file (US)
run: python scripts/gen_romassets.py u
# Drop-in ROM support: freeze the asset-prep tool into a single
# ge007-convert.exe the engine spawns on first run when the pcmodels/
# pccg sidecars are missing (port/src/romconvert.c). Lands in
# tools_pc/dist/prepare-assets/ where bundle-win.sh picks it up.
- name: Freeze asset converter (PyInstaller)
run: |
# MSYS2's mingw python ships without pip and has ensurepip
# disabled — install pip from pacman, then PyInstaller.
python -c "import PyInstaller" 2>/dev/null || {
pacman -S --noconfirm mingw-w64-x86_64-python-pip
python -m pip install --quiet --break-system-packages pyinstaller
}
python -m PyInstaller --onefile --noconfirm --clean \
--name ge007-convert \
--distpath tools_pc/dist/prepare-assets \
--workpath build-pyinst --specpath build-pyinst \
tools_pc/dist/prepare-assets/prepare-assets.py
ls -la tools_pc/dist/prepare-assets/ge007-convert.exe
- name: Build (ntsc-final)
run: ./build-pc.sh ntsc-final
- name: ccache stats
run: ccache -s
# Package the distributable bundle on every run so the release path is
# always exercised. On a tag build VERSION is the tag; otherwise it is a
# throwaway CI string. bundle-win.sh hard-fails if a ROM image or an
@@ -286,11 +410,21 @@ jobs:
run: |
TAG="${GITHUB_REF_NAME}"
ASSETS=(dist/*.zip dist/*.zip.sha256 dist/*.tar.gz dist/*.tar.gz.sha256)
NOTES="$(mktemp)"
# Asset names are built from the tag with the leading v stripped
# (bundle-{linux,win}.sh), so substitute the same form -- a raw
# TAG would make the notes reference goldeneye-pc-port-v*.* assets
# that don't exist.
NOTES_VERSION="${TAG#v}"
sed "s/<version>/${NOTES_VERSION}/g" .github/release-notes.md > "$NOTES"
if gh release view "$TAG" >/dev/null 2>&1; then
gh release upload "$TAG" "${ASSETS[@]}" --clobber
# Re-stamp the notes body too: it is only set at creation, and
# release-notes.md may have moved since the draft was first cut.
gh release edit "$TAG" --notes-file "$NOTES"
else
gh release create "$TAG" "${ASSETS[@]}" \
--draft --prerelease \
--title "GoldenEye 007 PC Port $TAG" \
--notes-file .github/release-notes.md
--notes-file "$NOTES"
fi
+234
View File
@@ -0,0 +1,234 @@
# Self-hosted runtime (ROM-gated)
#
# Runs the build + the 21-level headless sweep (tools_pc/verify.sh) on the
# self-hosted runner "corpyvt6agent01", the one machine that has a legal ROM.
# This is the job class ci.yml explicitly does NOT do: CI is build-only, no
# ROM, so runtime checks live here. Setup + rollout plan (gitignored, local):
# docs/dev/notes/SELFHOSTED-RUNNER-PLAN.md
#
# Persistence model — everything lives under the runner's own paths, managed
# by the pipeline; no hand-maintained directories:
# - ccache -> actions/cache (stored by the runner under _work/_temp)
# - ROM -> actions/cache, key ge-rom-data-ntsc-v1. Bootstrap: copy the
# three files into {runner_home}/_rom-seed/ on the box; a run
# with an empty cache copies them in and attempts to save the
# cache entry. The seed dir is kept (NOT deleted) rather than
# treated as one-time -- actions/cache/save has been silently
# failing to persist on this box (warning, not a hard step
# failure; root cause open, see SELFHOSTED-RUNNER-PLAN.md), so
# the seed is the durable fallback every run re-copies from
# until that's fixed. Integrity is re-verified every run
# against the committed ge007.u.sha1 (repo-tracked US ROM hash).
# - MSYS2 -> machine-level install at C:\msys64 (verify.sh hardcodes the
# prefix), installed and its packages maintained BY HAND on
# the box. msys2/setup-msys2 still runs every job (it's what
# registers the `msys2 {0}` shell wrapper), but with
# `release: false` and update/install both omitted — verified
# against the action's source to do no pacman -Syu / pacman -S
# / network round-trip, just a fast reuse of the existing
# C:\msys64. Package presence is checked (not installed) by
# tools_pc/check-selfhosted-deps.sh, which fails fast with
# exact install instructions if something's missing.
#
# Trigger gating (non-negotiable):
# - workflow_dispatch ONLY for now. Flip on `push: branches: [main]` once
# the first manual runs are reliably green.
# - NEVER add `pull_request`: unreviewed fork code must not be routable to
# a runner that can see the ROM.
#
# Leak rules (docs/dev/notes/AGENT-VM-FORGEJO-PLAN.md checklist):
# - The ROM + sidecars live in the runner-managed cache only; nothing in
# this workflow uploads from data/.
# - Artifacts here are console logs + a JSON verdict only. Rendered frames
# (ppm/) are ROM-derived content and are NOT uploaded.
name: Self-hosted runtime
on:
workflow_dispatch:
inputs:
levels:
description: "Sweep subset, SWEEP_LEVELS format (blank = all 21)"
required: false
default: ""
type: string
permissions:
contents: read
# One ROM job at a time per ref; never cancel an in-flight sweep out from
# under itself (the sweep is serialized by its own lockdir + watchdog). Group
# + cancel-in-progress:false is what actually serializes runs — GitHub
# Actions' workflow-level `concurrency` has no `max-parallel` key (that only
# exists under a job's `strategy:` for matrix jobs); a stray one here is a
# hard parse error that blocks every dispatch.
concurrency:
group: selfhosted-${{ github.ref }}
cancel-in-progress: false
env:
ROM_CACHE_KEY: ge-rom-data-ntsc-v1
jobs:
build-and-sweep:
runs-on: [ self-hosted, windows ]
defaults:
run:
shell: msys2 {0}
env:
CCACHE_DIR: ${{ github.workspace }}/.ccache
CCACHE_MAXSIZE: 500M
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
# `release: false` reuses the box's own C:\msys64 as-is instead of
# downloading/installing MSYS2 fresh (verified against the action's
# source: with this set, and update/install both omitted, it does
# exactly one `uname -a` + a pacman.conf sed tweak — no pacman -Syu,
# no pacman -S, no network round-trip). This step still has to run
# every time — it's what registers the `msys2 {0}` shell wrapper
# above — but does no install/update work.
- uses: msys2/setup-msys2@v2
with:
msystem: MINGW64
release: false
# Packages themselves are hand-maintained on the box (never installed
# by this workflow) — fails fast with exact install instructions if
# one's missing, instead of a confusing failure deeper in the build.
- name: Check self-hosted dependencies
run: tools_pc/check-selfhosted-deps.sh
# --- ROM + sidecars: runner-managed cache, one-time seed --------------
- name: Restore ROM + sidecars (runner cache)
uses: actions/cache/restore@v4
with:
path: |
data/ge007.ntsc-final.z64
data/pcmodels-ntsc-final
data/pccg-ntsc-final
key: ${{ env.ROM_CACHE_KEY }}
- name: Check ROM store presence
id: romcheck
run: |
present=true
for f in ge007.ntsc-final.z64 pcmodels-ntsc-final pccg-ntsc-final; do
if [ ! -f "data/$f" ]; then echo "missing from cache: $f"; present=false; fi
done
echo "present=$present" >> "$GITHUB_OUTPUT"
# {runner_home} = two levels up from RUNNER_WORKSPACE ({home}/_work/{repo}).
# NOT deleted after the cache save below (see that step's comment) --
# kept as a durable fallback source until the cache save is proven to
# actually round-trip on this box.
- name: Seed ROM cache (from durable seed dir)
if: steps.romcheck.outputs.present == 'false'
run: |
seed="$(dirname "$(dirname "$RUNNER_WORKSPACE")")/_rom-seed"
[ -d "$seed" ] || {
echo "ROM cache is empty and no seed found at $seed." >&2
echo "One-time bootstrap: copy ge007.ntsc-final.z64, pcmodels-ntsc-final," >&2
echo "and pccg-ntsc-final into that directory on the runner, then re-run." >&2
exit 2
}
mkdir -p data
# -r: pcmodels-*/pccg-* are directories, not single files (found by
# testing — a plain cp failed with "omitting directory").
cp -r "$seed/ge007.ntsc-final.z64" "$seed/pcmodels-ntsc-final" \
"$seed/pccg-ntsc-final" data/
echo "seeded ROM store from $seed"
# Explicit save (restore alone never saves): persist the seeded store
# immediately, before the build/sweep can fail out from under it.
# KNOWN ISSUE (2026-09-12): this reliably logs a WARNING annotation
# ("Failed to save: ... tar.exe failed with exit code 2") on this box
# and never actually persists anything to the cache -- but
# actions/cache/save treats a save failure as non-fatal by design, so
# the step still reports success and the job carries on. Root cause not
# yet found (see docs/dev/notes/SELFHOSTED-RUNNER-PLAN.md). Consequence:
# do NOT delete the seed dir after this step (previously done, and it
# meant a *silent* cache-save failure combined with the delete wiped
# the only copy of the ROM every run -- exactly what broke rollout
# step 5's repeated dispatches). The seed dir now stays in place
# indefinitely as a durable fallback; "Check ROM store presence" /
# "Seed ROM cache" above already re-copy from it whenever the cache
# comes back empty, so this is safe to leave as a no-op once the cache
# save is fixed for real.
- name: Save ROM cache
if: steps.romcheck.outputs.present == 'false'
uses: actions/cache/save@v4
with:
path: |
data/ge007.ntsc-final.z64
data/pcmodels-ntsc-final
data/pccg-ntsc-final
key: ${{ env.ROM_CACHE_KEY }}
- name: Verify ROM integrity (committed US sha1)
run: |
cd data
want="$(cut -d' ' -f1 ../ge007.u.sha1)"
got="$(sha1sum ge007.ntsc-final.z64 | cut -d' ' -f1)"
[ "$want" = "$got" ] || { echo "ROM hash mismatch: want $want got $got" >&2; exit 2; }
echo "ROM sha1 ok: $got"
# --- build -------------------------------------------------------------
# Same opt-in ccache CMakeLists.txt wires up (speed-ups plan Step 5);
# persisted via actions/cache like ci.yml's windows-build.
- name: Restore ccache
uses: actions/cache/restore@v4
with:
path: ${{ env.CCACHE_DIR }}
key: ccache-selfhosted-${{ hashFiles('CMakeLists.txt') }}-${{ github.sha }}
restore-keys: |
ccache-selfhosted-${{ hashFiles('CMakeLists.txt') }}-
ccache-selfhosted-
- name: Regenerate ROM-asset symbol file (US)
run: python scripts/gen_romassets.py u
- name: Build (ntsc-final)
run: ./build-pc.sh ntsc-final
# verify.sh: build-if-stale -> headless GE_PCDUMP per level -> crash
# symbolication -> pixcount -> framediff vs tools_pc/golden/. Serialized
# + watchdog + one retry by design (D117: not frame-deterministic).
- name: Level sweep (all 21, or dispatch subset)
run: |
[ -n "${{ inputs.levels }}" ] && export SWEEP_LEVELS='${{ inputs.levels }}'
tools_pc/verify.sh sweep --json > sweep.log 2>&1
rc=$?
cat sweep.log
exit $rc
# Pull the JSON verdict array out of the mixed stdout for easy review.
- name: Extract JSON verdict
if: always()
run: |
python -c "
import json, sys
s = open('sweep.log', encoding='utf-8', errors='replace').read()
i = s.rfind('[{')
out = '[]'
if i >= 0:
d, _ = json.JSONDecoder().raw_decode(s[i:])
out = json.dumps(d, indent=1)
open('sweep-verdict.json', 'w').write(out + '\n')
print(out)
"
# Console logs + verdict only — no frames, no ROM bytes (leak rules).
- name: Upload sweep report
if: always()
uses: actions/upload-artifact@v7
with:
name: selfhosted-sweep-${{ github.run_number }}
path: |
sweep.log
sweep-verdict.json
ge007.crash.log
if-no-files-found: ignore
retention-days: 14
+49
View File
@@ -68,6 +68,14 @@ build/*
build-*/*
notes/*
# Local working / planning notes — rolling drafts, not published, not tracked.
# docs/HANDOFF.md and docs/BACKLOG.md are the long-standing pair (also in
# .git/info/exclude); docs/dev/notes/ is the drop for everything else
# (roadmaps, session plans, scratch triage).
docs/HANDOFF.md
docs/BACKLOG.md
docs/dev/notes/
# PC port (see CMakeLists.txt / port/)
build-pc/
tools_pc/*.exe
@@ -134,14 +142,38 @@ ge007.crash.log
run_out.log
ppm/
screenshots/
# tools_pc/perf.ps1 output (raw ETW traces + CSV analysis, regenerable)
perf_captures/
# local Mupen64 Plus checkout (N64 reference footage); binaries, do not commit
tools_pc/mupen64/
# local N64 emulator checkout (D205 A/B reference)
mupen64/
# PC port run/scratch logs
run_*.log
d6*.log
ge007.crash.log
# agent scratch space + audio probe artifacts (regenerable dumps/traces)
scratchpad/
# session logs / investigation scratch — local only, never in the public repo
# (untracked 2026-09-19 during the v0.3.0 release review)
/scratch/
# re-vendored GPL reference source (see reference/mouse-injector/README.md)
/reference/mouse-injector/*
!reference/mouse-injector/README.md
# local research/backlog working docs (TCRF cut-content cross-reference etc.)
backlog/
# montage work artifact: the tiled contact sheet behind docs/img/shots/ and
# the preview video (regenerable from the same session recordings)
docs/img/shots/CONTACT-SHEET.jpg
/audiodump.raw
/audiotrace.log
/mixertrace.log
# texture-dump probe artifacts (regenerable via GE_TEXDUMP)
/texdump/
/texdump_p23_old/
# Claude Code local state / agent worktrees
.claude/
@@ -151,3 +183,20 @@ nohup.out
gdb.txt
gdb-hang.txt
# frozen asset converter (built by CI via PyInstaller; local freezes land here)
tools_pc/dist/prepare-assets/ge007-convert*
build-pyinst/
# Local working junk (frame captures, timerun state, PPM dumps) - moved here
# from repo root / scratch on 2026-09-18; never commit.
/local/
build-pc-cmd.bat
# v0.4.0 modern-options wave: per-landing sweep capture dirs (capture.sh output)
tools_pc/sweep-captures/capture-*/
# Stray full CPython install that landed in repo root (2026-09-28);
# referenced by no project tooling. Never commit.
/Python/
docs/media/*.mp4
+63 -6
View File
@@ -4,14 +4,14 @@
- `n64decomp/007`: WIP decompilation of GoldenEye 007 (N64), byte-matches US/EU/JP ROMs.
- Active work: **PC port** modelled on the Perfect Dark PC port (same Rare "Indy" engine family).
- **Reference docs:** `docs/internals.md` — architecture, GE-specific RSP deltas, phased plan (§1–§10). `docs/dev/findings.md` — the `Dxx` finding log (§F + §H, indexed at the top of §F); read the entry you need, don't linear-read. `docs/porting-notes.md` — the recurring N64→PC bug classes (dense; skim the headers, read what's relevant).
- **Reference docs:** `docs/internals.md` — architecture, GE-specific RSP deltas, phased plan (§1–§10). `docs/dev/findings.md` — the `Dxx` finding log (§F + §H). **Look up findings via `docs/dev/findings-index.csv` (label, one-liner, status; regenerate with `tools_pc/gen_findings_index.py`), then read only the specific `## Dxx` entry (multi-pass labels like D202/D176(a) have large sections — grep within the section or read with offset/limit, don't slurp it whole). Never linear-read.** `docs/porting-notes.md` — the recurring N64→PC bug classes (dense; skim the headers, read what's relevant).
- **Current status:** the README "Status" section, and `docs/dev/LEVEL-STATUS.md` for the per-level sweep. Current task + environment: `docs/HANDOFF.md` (a rolling local working file — may be absent in a fresh clone; fall back to the README "Status" section).
- **Dispatching subagents?** `docs/dev-process.md` — task budgets/deadlines, file partitioning, pre-flight, the standard brief template. Every investigation subagent reads `docs/porting-notes.md` first and appends to it.
## Non-negotiables
1. **N64 build untouched.** `Makefile`, `tools/`, `rsp/`, `ld/` belong to the N64 build. Never modify them for the PC port.
2. **Game logic is unmodified.** The decomp's control flow and behavior are ground truth for 1:1 fidelity — never change them. All N64 *hardware* dependencies are satisfied by the `port/` layer; if a game file seems to need a behavioral change, stop — the fix belongs in `port/`. **Narrow exception (ABI/layout only):** the 32→64-bit transition forces a small class of mechanical, semantics-preserving edits that cannot be isolated in `port/` — chiefly pointer-width reconciliation in ROM-serialized structs (a struct with a 32-bit-pointer field misaligns when read as 64-bit). These follow the PD ground-truth pattern (store the embedded address as `u32`, cast to a real pointer at the use site), change no logic or behavior, and are each documented in `docs/dev/findings.md` §F/D3x. No other game-code edits are permitted.
2. **Game logic is unmodified.** The decomp's control flow and behavior are ground truth for 1:1 fidelity — never change them. All N64 *hardware* dependencies are satisfied by the `port/` layer; if a game file seems to need a behavioral change, stop and check the exception below before assuming the fix belongs in `port/`. **Diagnosis is never restricted — only the fix.** Trace a bug as deep into `src/game` state as the evidence leads, and name the exact struct/field/logic responsible, before deciding which of three buckets it falls in (full framing: `docs/dev-process.md`). **Narrow exception (ABI/layout only):** the 32→64-bit pointer-width transition forces a small class of mechanical, semantics-preserving edits that cannot be isolated in `port/` — **any struct-layout or pointer-width-driven misread caused by 32→64-bit widening**, whether in a ROM-serialized record (a struct with a 32-bit-pointer field misaligns when read as 64-bit) or a **live runtime struct/union** (a raw-byte offset alias into a union arm whose true field shifted because an earlier member in the same union widened — the D209/D210/D255 pattern; see `docs/porting-notes.md` §A1 for the full catalogue and its diagnostic tells). These follow the PD ground-truth pattern (store the embedded address as `u32` and cast to a real pointer at the use site; or read the correctly-named/typed field instead of a raw-offset/mistyped alias), change no logic or behavior, and are each documented in `docs/dev/findings.md` §F/D3x, cross-tagged to §A1 where that pattern applies. **A genuine behavioral difference** — the decomp's byte-identical code, given verified-correct inputs, still diverging from real N64 behavior — is not covered by this exception and requires the rule-2 sign-off procedure (`docs/dev-process.md`) before any `src/game` edit. No other game-code edits are permitted.
3. **Region macros mirror the Makefile.** `CMakeLists.txt` `REGION_DEFS` must match the N64 Makefile's per-region macro set exactly (finding A1). Divergence = silent branch divergence + link failures.
4. **`src/libultrare/Makefile.libultrare` is ground truth** for original-vs-Rare libultra files (finding B3). The PC build compiles: `libultra/audio`, `libultrare/audio` (drvrNew/env/reverb), `libultra/gu`, and `libultrare/io/vitbl.c` only. All other `io/` + `os/` files are excluded and shimmed in `port/src/libultra.c`.
5. **`rsp/graphics/gmain.s` is the RSP ground truth** — the authoritative reference for which GBI commands GE emits (modified fast3d, 1545 lines). We do not run it on PC; `port/fast3d/` replaces it. Use it to validate the software RSP's command decoding and the custom CC/RM modes.
@@ -21,11 +21,12 @@
| File | Role |
|---|---|
| `docs/internals.md` | Architecture + RSP deltas + phased plan (§1–§10). Reference, not a linear read. |
| `docs/dev/findings.md` | The `Dxx` finding log (§F/§H, indexed at top of §F). |
| `docs/dev/findings.md` | The `Dxx` finding log (§F/§H); lookups via `docs/dev/findings-index.csv`. |
| `CMakeLists.txt` | PC build (parallel to the N64 Makefile). Source list + `REGION_DEFS` live here. |
| `port/src/` | Shims: `libultra.c` (OS API), `gesched.c` (scheduler), `n64stubs.c` (boot/TLB/FPU/rmon), `random.c` (PRNG ported verbatim from `random.s`), `ucode.c` (microcode segment markers), `main.c`, `video.c`, … |
| `port/fast3d/` | Software RSP (adapted from the PD port). The main Phase 2 work. |
| `rsp/graphics/gmain.s` | GE's RSP ucode — ground truth for GBI/CC/RM. |
| `reference/mouse-injector/README.md` | **Stub only** — the vendored GEPD-Edition Mouse Injector source (GPLv2) was removed from the public repo 2026-09-19 (license hygiene: GPL code + prebuilt binary in an MIT project; it was never compiled here). The stub records provenance + how to re-vendor locally (gitignored). The ported mouse-aim model lives in `port/src/input.c` (D194 lineage); design record: `docs/dev/GEPD-INPUT-PLAN.md`. |
| A local **Perfect Dark PC port** checkout ([fgsfdsfgs/perfect_dark](https://github.com/fgsfdsfgs/perfect_dark)) | **Standing reference** — consult it whenever a work item has a PD analogue (same Rare engine family): port-layer ground truth (`port/fast3d/`, crash/system/video), plus copy candidates `port/src/preprocess/` (N64→PC asset conversion; `filemodel.c` is the D43 near-analogue) and `mixer.c`/`input.c`/`fs.c`. Port-layer files only; same family ≠ identical format — validate per field. Full audit: `docs/internals.md` §2.4. |
## Build
@@ -38,6 +39,59 @@ Needs CMake + SDL2 + zlib + OpenGL, and must run from the MSYS2 MINGW64 shell
(see `build-pc.sh` header and `docs/building.md`). ROM goes in `./data/`
(not distributed); assets must be extracted from it first (`docs/building.md`).
**Windows build environment (three recurring failure modes — diagnose in this order):**
1. **`Cannot create temporary file in C:\Windows\: Permission denied`** at the
link step. The PE toolchain (ninja → cmd → gcc/ld) needs a writable
TMP/TEMP; the msys→native env conversion drops it in non-login shells
(agent harnesses; the `C:/msys64` tree here was built for `D:/M/msys64`,
so its conversion is unreliable). `build-pc.sh` now **self-heals**: it
probes a native child's TMP (via a file — piped `cmd.exe` stdout is
unreliable under msys console emulation) and, if broken, re-runs
cmake+build under PowerShell with `TMP`/`TEMP` set natively (the
native→native boundary passes env through intact; only msys→native is
broken). If you run cmake/ninja *by hand* from a broken shell: run them
from cmd/PowerShell with `C:\msys64\mingw64\bin` on PATH.
**Guard revision (2026-09-28):** the re-exec writes a self-contained
`.ps1` to a temp file and runs `powershell -File` with plain path/word
args only (the msys→native argv conversion mangles `$`-bearing
`-Command` strings; a file write is conversion-free). Inside: PATH is
built explicitly (never trust the inherited value); TMP is chosen from
writable candidates (`%LocalAppData%\Temp` first —
`[System.IO.Path]::GetTempPath()` honours the inherited *broken* TMP,
observed as `C:\Windows\`); every native step self-logs to
`build-pc/ge007-native-reexec-{diag,cmake,build}.log`; a null
`$LASTEXITCODE` ("did not execute") is a distinct failure (exit 2/3) —
in PS 5.1 a piped native command errors out non-terminating and never
sets it, so an unguarded `if ($LASTEXITCODE -ne 0)` passes vacuously.
In `.ps1`, `-DROMID=$Var` is a literal (bare tokens don't expand) — it
must be `"-DROMID=$Var"`. Launch powershell plainly — `env -i` before it
breaks the nested cmake launch (verified 2026-09-28).
**OPEN (next session):** the guard still fails *inside* `build-pc.sh`
(cmake "did not execute": the `.ps1` runs, but `$LASTEXITCODE` stays
empty) while the *identical* standalone invocation succeeds (cmake runs
and the build completes — that is how the D401 builds were made). First
test: diff the heredoc-generated `.ps1` against a hand-extracted copy
(suspect: heredoc line-ending/whitespace corruption).
**Working workaround:** extract the heredoc `.ps1` and run
`powershell -NoProfile -ExecutionPolicy Bypass -File <ps1> "C:\msys64\usr\bin" "C:\msys64\mingw64\bin" C:/msys64 <repo-win-path> build-pc <romid>`.
Re-confirmed 2026-09-28: in-script re-exec fails again (cmake configure
dies inside the ps1; re-exec logs left stale) while the identical
standalone invocation reconfigured and built cleanly (extract with
`awk "/<<'PS1'\$/{f=1;next} /^PS1\$/{f=0} f" build-pc.sh > /tmp/ps1`
— mind the leading whitespace, and `export PATH` to include mingw/bin
first, failure mode 3).
2. **`cannot open output file ge007.x86_64.exe: Permission denied`.** A
**running** `ge007.x86_64.exe` locks the output file (Windows rule; you
can't relink over a live PE). Check with `Get-Process | Where-Object {
$_.ProcessName -like '*ge007*' }` and close the game before rebuilding.
An agent must not kill the user's game process to make a link succeed.
3. **`cc1.exe: ... libmpfr-6.dll: cannot open shared object file`.** The
calling PATH lacks `C:\msys64\mingw64\bin` (the gcc driver finds cc1 via
its own directory, but the child needs the mingw DLL dir on PATH). In an
msys shell: `export PATH="/c/msys64/mingw64/bin:/c/msys64/usr/bin:$PATH"`
before building.
## Verification ritual (after any build-affecting change)
1. **Undefined symbols.** Every symbol referenced by the compiled set (see `CMakeLists.txt`: `SRC_GAME`, `SRC_ENGINE`, `SRC_LIBAUDIO`, `SRC_LIBULTRARE_AUDIO`, `SRC_LIBULTRARE_DATA`, `SRC_GU`, `SRC_PORT*`) must be defined exactly once in the compiled set or in `port/`. Symbols that live in EXCLUDED files (`libultra/io/*`, `libultrare/io/*` except `vitbl.c`, `libultra/os/*`, `libultrare/os/*`, `sched.c`, `rmon.c`, `vi.c`, `src/*.s`) must be provided by `port/src/libultra.c`, `gesched.c`, `n64stubs.c`, `random.c`, or `ucode.c`.
@@ -50,12 +104,15 @@ Run `/linkcheck` for this sweep. Record new findings in `docs/dev/findings.md`
- **Phase 0–1.5:** done. Build system, boot chain, OS-shim layer, fast3d
integration, first frames, full intro rendering.
- **Phase 2 (rendering):** in progress. All 21 solo levels load + render +
survive an unattended window; front end (menu → mission select → briefing →
- **Phase 2 (rendering):** in progress. All 20 solo missions (plus the
ending-credits sequence, `-level_54`) load + render + survive an
unattended window; front end (menu → mission select → briefing →
start) is functional; file-backed EEPROM saves work. Cosmetic defects are
parked in `docs/dev/GRAPHICS-BACKLOG.md`.
- **Phase 3 (audio + input):** input layer done (`port/src/input.c`); polish
bugs open (D118* mouse-look residuals; interactive feel-checks owed). Audio
not started — libaudio → SDL, copy-and-adapt from the PD port's `mixer.c`.
mixer done (libaudio → SDL software mixer, D198–D201); D204 tempo drift
fixed + measured; D202 stuck door loop root-caused with a port-side
expiration (M-66b) awaiting by-ear verification.
- **Phase 4 (saves + polish):** file-backed EEPROM done; widescreen, config,
rebinding UI outstanding.
+29 -7
View File
@@ -21,9 +21,10 @@ Context is scarce. Load by tier; do not blind-read whole files.
- **Tier 2 — on demand only, do NOT read start-to-finish:**
- `docs/internals.md` — architecture / RSP deltas / phased plan (§1–§10).
Read the section you need.
- `docs/dev/findings.md` — the `Dxx` finding log (§F + §H).
**Jump to a specific `Dxx` via the index at the top of §F.** Never
linear-read.
- Findings: look up via `docs/dev/findings-index.csv` (label, one-liner,
status), then read only the specific `## Dxx` entry in
`docs/dev/findings.md` (multi-pass labels like D202/D176(a) have large
sections — grep within or offset/limit). Never linear-read.
- `docs/dev/HANDOFF-ARCHIVE.md` — prior-session narrative (M-2…). Only
when tracing the history of one fix.
- `docs/dev-process.md` — before dispatching a subagent (also Tier 1
@@ -54,6 +55,9 @@ A subagent brief is INVALID and must not be sent unless it contains all of:
write-up of a half-solved bug is a deliverable, not a failure.
- **KNOWN-GOOD / RULED OUT**, **CONSTRAINTS** (no game-logic changes;
ABI/layout/format only; `#ifdef PORT`; documented in §F), **REPORT** shape.
- **VERIFY** — the exact `tools_pc/verify.sh` / probe invocation that proves
the task done, or an explicit "runtime verification impossible here; a human
must run X".
- Instruction to read `docs/porting-notes.md` first and append any
generalisable quirk to it.
@@ -85,9 +89,27 @@ still owns judgment, planning, and the verification gate.
- **Backend reality:** ~1 inference slot, slow per token — dispatch
**sequentially**, `max_tokens` ~4–8K, treat as batch work. Needs the LiteLLM
proxy (`litellm/run.ps1`) + Unsloth Studio both up.
- **Windows quirk:** the worker's shell is `cmd.exe`, not bash — its agent
prompt tells it to use `read_file`/`findstr`, not `cat`/`grep`. Always
spot-check the returned artifact against the real files before integrating;
the 27B will confidently fabricate if a read fails.
- **Corrected 2026-09-04:** the "worker's shell is `cmd.exe`, no grep/cat"
claim that used to live here was WRONG — verified directly against the
harness's own `_run_bash` (`C:\Users\james\tools\claude-code-delegate-local`):
`grep`, `sed`, `cat`, `ls`, `head`, pipes, and `grep -rn` all work fine
(Git-for-Windows/MSYS2 toolchain on PATH). The stale claim, baked into 4 of
5 local agent `.md` files, caused a real dispatch (a multi-file
cross-referencing investigation) to burn its whole turn budget
full-`read_file`-ing files instead of grepping them, hit the turn limit, and
returned nothing. Agent files fixed. Always still spot-check the returned
artifact against the real files before integrating — the 27B will
confidently fabricate if a read fails, independent of this.
- **Landing delegate output on its own branch: branch off `main` explicitly,
never off whatever's currently checked out.** (2026-09-12: a delegate's
docs-only writeup got `git branch <name> <sha>` where `<sha>` happened to
sit on top of an unrelated in-flight feature branch — the resulting PR
silently carried that branch's whole history, including code that had
since been reverted, and it only surfaced as a CI failure (a stale
env-probe-index check), not from inspecting the diff.) Before creating the
branch, confirm you're branching from `main`/`origin/main` HEAD, not from
`HEAD` of the session's current checkout; after pushing, sanity-check with
`git log --oneline main..<branch>` that it contains only the commit(s) the
delegate's task actually produced.
- Fits the **dispatch preflight** rules above (FILES / BUDGET / ON EXPIRY /
CONSTRAINTS / REPORT) — write the `task` string to that shape.
+6
View File
@@ -145,6 +145,12 @@ execute_process(COMMAND git rev-parse --short HEAD
if(NOT VERSION_HASH)
set(VERSION_HASH "unknown")
endif()
# Provenance, printed by --version and at startup (port/src/main.c): a build
# keeps pointing at this repository no matter who compiles or renames it.
set(VERSION_CODENAME "larkspur")
set(ORIGIN_URL "https://github.com/jkdansereau/goldeneye-pc-port")
configure_file(
"${CMAKE_SOURCE_DIR}/port/include/versioninfo.h.in"
"${CMAKE_BINARY_DIR}/port/include/versioninfo.h" @ONLY)
+31 -6
View File
@@ -11,12 +11,19 @@ keep the port faithful to the original game.
belong to the N64 build and are never touched for PC work. If PC code seems
to need a behavioural change in the game, the fix belongs in `port/`.
2. **Narrow ABI exception.** The 32→64-bit transition forces a small class of
mechanical, semantics-preserving edits to ROM-serialized structs (storing an
embedded 32-bit pointer as `u32` and casting at the use site). These are
allowed, must be guarded with `#ifdef PORT`, must change no behaviour, and
must be documented. See `docs/porting-notes.md` for the catalogue of
patterns.
2. **Narrow ABI exception; diagnosis is always allowed.** Trace a bug as
deep into the decomp's state as the evidence leads — that's never
restricted. Only the *fix* is constrained: the 32→64-bit transition
forces a small class of mechanical, semantics-preserving edits — any
struct-layout or pointer-width-driven misread caused by 32→64-bit
widening, in a ROM-serialized record or a live runtime struct/union alike
(see `docs/porting-notes.md` §A1 for the pattern and its tells). These
are allowed, must be guarded with `#ifdef PORT`, must change no
behaviour, and must be documented. A genuine behavioral difference from
real N64 behavior is a separate, much rarer case that needs explicit
maintainer sign-off first — see `docs/dev-process.md`'s rule-2 sign-off
procedure; it is not something a contributor can self-approve or infer
from this exception.
3. **Region macros mirror the Makefile.** `CMakeLists.txt` `REGION_DEFS` must
match the N64 `Makefile`'s per-region macro set exactly.
@@ -32,6 +39,24 @@ keep the port faithful to the original game.
- Port-layer code is plain C11 / C++17, SDL2 + OpenGL, no extra dependencies.
- Keep commits focused and describe *why*, not just *what*.
## Forks and derivative works
Fork away — the code is MIT and the `port/` layer exists to be extended.
Rebranding, localizing and extending are all welcome, and none of them need
permission. If you carry the work forward under your own name, please keep
two things:
- **Attribution.** A clear statement, in your README and about page, that the
project derives from this one — with a link. The engine prints its origin at
startup and in its `--version` output; keeping that intact is part of the
ask.
- **The record.** `docs/dev/` (the finding log, porting notes, per-level
status) is the engineering record behind every fix. Derivative projects are
asked to preserve or link to it rather than fork-and-forget.
Stripping the attribution is the one thing that turns a fork into something
we no longer recognize as a descendant of this project.
## Where to look
- `docs/internals.md` — how the port is structured.
+354 -241
View File
@@ -1,203 +1,281 @@
# GoldenEye 007 PC Port
[![CI](https://github.com/jkdansereau/goldeneye-pc-port/actions/workflows/ci.yml/badge.svg)](https://github.com/jkdansereau/goldeneye-pc-port/actions/workflows/ci.yml)
![status](https://img.shields.io/badge/status-Phase_2_of_4_(rendering)-orange)
![tested on](https://img.shields.io/badge/tested_on-Windows_x86--64-blue)
![license](https://img.shields.io/badge/license-MIT-green)
![built with](https://img.shields.io/badge/built_with-coding_agents-8A2BE2)
![license](https://img.shields.io/badge/license-MIT-ffb454)
**A work-in-progress native PC port of _GoldenEye 007_ (Rare, 1997, Nintendo 64),
built on the [GoldenEye 007 decompilation](https://github.com/n64decomp/007).**
It is first and foremost a **research project on AI-agent collaboration in a
large, unfamiliar, low-level codebase** — how far two coding agents (a local
open-weight model and Claude), driven by one person part-time, can be pushed
through ~230 translation units of unmodified big-endian MIPS game code and made
to run on a 64-bit desktop. **Current status: Phase 2 of 4 (rendering).** The
port boots, renders the full intro and front end, and loads all 21 solo
missions; there is no audio yet and it is not a playable game. See
[Status](#status) and [Background](#background).
<p align="center"><em>GoldenEye 007 (Nintendo 64, 1997) on the PC —
decompiled, ported, and playable at 60 fps.</em></p>
Technically, it follows the architecture of the
[Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark) — the same
Rare "Indy" engine family, one hardware generation apart. The unmodified game C
sources are compiled for the host; the N64's Reality Signal Processor (RSP) is
emulated in software; every other hardware surface (video, audio, input,
timers, save storage) is shimmed in a dedicated `port/` layer.
[Download](#download) · [News](#news) · [Status](#status) · [Roadmap](#roadmap) · [Building](#building) · [Docs](#documentation) · [Legal](#legal)
A native PC port of _GoldenEye 007_ (Rare, 1997, Nintendo 64), compiled from
the [GoldenEye 007 decompilation](https://github.com/n64decomp/007): the
original N64 game running from reconstructed source, not the Xbox 360
remaster. The N64's graphics coprocessor (RSP) is emulated in software; every
other hardware surface (video, audio, input, timers, save storage) is shimmed
in a dedicated `port/` layer, following the architecture of the
[Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark), the same
Rare "Indy" engine family, one hardware generation apart.
**v0.4.0** (released 2026-09-28) is out for Windows and Linux (including
Steam Deck, where the Linux bundle sideloads as-is). It is the most complete
release to date: the full campaign runs at a steady 60 fps with the known
issues now few and mostly cosmetic ([Status](#status)). Free to download,
build on and modify (you bring the ROM).
**This is a pre-1.0 release, not a finished product.** v1.0 is the target
for a polished, feature-complete build; until then, expect missing features
and the occasional breaking change between versions. See [Status](#status)
for what works today and [Roadmap](#roadmap) for where this is headed.
**AI disclosure:** development here was agentic - Claude Pro plus a local
open-weight model on a single RTX 5090, as of August–September 2026. This
project is as much a study of *that process* as it is a port: whether
current LLMs can carry a codebase like this, and what actually goes wrong
along the way. Judge the result for yourself. I'm one person doing this in my
spare time, not a team. See [Background](#background) for the full setup,
timeline, and an honest account of what worked and what didn't.
> [!IMPORTANT]
> **You must supply your own GoldenEye 007 ROM.** This repository contains no
> Nintendo code or assets, and no ROM. Nothing here is distributable as a
> playable game — see [Requirements](#requirements) and [Legal](#legal).
> playable game: see [Requirements](#requirements) and [Legal](#legal).
<p align="center">
<img src="docs/img/attract-bunker1.png" width="32%" alt="Bunker 1 intro camera">
<img src="docs/media/goldeneye-demo.gif" width="32%" alt="~15 s of the port running: mission dossier, Facility, Silo, Jungle, Archives">
<img src="docs/img/attract-dam.png" width="32%" alt="Dam intro camera">
<br><em>In-engine, running in the port — Bunker&nbsp;1 and Dam attract views, and a
~15&nbsp;s clip (no audio yet, Phase&nbsp;3): mission dossier &rarr; Facility &rarr; Silo &rarr;
Jungle &rarr; Archives.</em>
<img src="docs/media/goldeneye-gh-preview.gif" width="64%"
alt="~24 s gameplay montage from live play sessions">
<br><em>All in-engine, running in the port — a ~24&nbsp;s gameplay montage
from live v0.4.0 play sessions, opening on the Runway tank ·
<a href="docs/index.md">12 level stills in the project index</a></em>
</p>
## Contents
---
- [Background](#background)
- [How this differs from the other GoldenEye PC projects](#how-this-differs-from-the-other-goldeneye-pc-projects)
- [Status](#status)
- [Download](#download)
- [Requirements](#requirements)
- [Building](#building) · [Windows (MSYS2)](#windows-msys2) · [Linux](#linux)
- [Running](#running) · [Default controls](#default-controls)
- [How it works](#how-it-works)
- [Project layout](#project-layout)
- [Documentation](#documentation)
- [Credits](#credits)
- [Legal](#legal)
- [License](#license)
## News
## Background
- **2026-09-28** — **v0.4.0**: native widescreen, a complete aim system for
mouse and controller, in-game key rebinding, crosshair customization,
rumble-pak haptics, a rebuilt options overlay, and a broad fidelity-fix
pass (water, particles, billboard trees, front-end logo, gunshot SFX, a
true stable 60 fps). [Release notes](https://github.com/jkdansereau/goldeneye-pc-port/releases/tag/v0.4.0) ·
[downloads](#download).
- **2026-09-20** — **v0.3.0**: the first release with the complete campaign
playable end to end at 60 fps on Windows, Linux, and Steam Deck.
[Release notes](https://github.com/jkdansereau/goldeneye-pc-port/releases/tag/v0.3.0).
- **2026-09-04 → 2026-09-16** — **v0.1.0 – v0.2.2**: the alpha and beta
cycle — build chain, software RSP, first rendered frames, front end, and
per-level stabilization across the campaign.
Beyond the port itself, this is primarily a **research project on agentic
software development** — specifically, on AI-agent collaboration in a large,
unfamiliar, low-level codebase: how far coding agents can be driven, by one
person working part-time, through ~230 translation units of unmodified
big-endian MIPS game code, made to run on a 64-bit desktop.
It used two agents:
- a **local open-weight model** —
`unsloth/Qwen3.8-27B-GGUF:UD-Q4_K_XL` on a single **RTX 5090**, driven mainly
through the **[pi](https://pi.dev/)** coding agent (Unsloth Desktop was also
trialed) — which did the groundwork: the CMake build, the boot chain, the
software-RSP integration, the offline asset-conversion pipeline, and the
first rendered frames;
- **Claude** (via **Claude Code** — mostly **Sonnet 5**, with **Opus 5** as an
escalation tier for the hardest bugs), which joined on 27 Aug 2026 for a
collaborative phase: the 21-level load/render/no-crash sweep, the ABI/layout
finding catalogue, the SDL input layer, and the front-end flow.
The two **handed work back and forth** through shared written artifacts:
```mermaid
flowchart TD
H["human<br/>direction · integration · playtest"]
C["Claude<br/>(Claude Code)"]
Q["Qwen 3.8<br/>(pi, local RTX 5090)"]
D[("shared artifacts<br/>findings · notes · handoff doc")]
H -->|"scoped task + budget"| C
H -->|"scoped task + budget"| Q
C -->|"patch + write-up"| D
Q -->|"patch + write-up"| D
C <-.->|handoff| Q
D --> H
```
The handoff document was originally a session-to-session note; it became the
**interface between the two models** — when Claude hit a usage limit
mid-problem, the local model picked the task up from that state and continued.
**By the numbers:**
| Metric | Value |
|---|---|
| Timeline | ~2 weeks, one person part-time |
| Commits | ~223 |
| Root-caused bugs logged | 162 (`D1`–`D169`; some later merged or withdrawn) |
| Handoff sessions | ~33 |
| ABI edits to game code | 63 files, all `#ifdef PORT` |
| Port layer / tooling | ~17k lines C/C++ · ~6k lines Python |
| Effort split (milestone-weighted) | ~60% Claude · ~40% local model |
The local model built the entire foundation — build, boot chain, RSP wiring,
asset converters — then both agents ran the debugging phase together:
- **Day 4** — the full ~230-unit codebase compiles and links
- **Day 8** — the whole intro renders
- **Day 13** — all 21 solo missions run crash-free
- **Day 14** — front end playable end to end
Full write-up, timeline chart, and an honest "what worked / what didn't":
[`docs/dev/agentic-development.md`](docs/dev/agentic-development.md). The
workflow itself: [`docs/dev-process.md`](docs/dev-process.md). To cite this
project or its findings, use [`CITATION.cff`](CITATION.cff) (GitHub's "Cite
this repository" menu).
## How this differs from the other GoldenEye PC projects
This is a **native port of the original 1997 Nintendo 64 game, built from its
actual reconstructed source code** — the same lineage as the Perfect Dark PC
port. The other well-known "GoldenEye on PC" projects are something different:
they machine-translate the shipped binary of the *unreleased Xbox 360 XBLA
remaster* — a different game, a different codebase, no shared code with this.
| | This project | [GoldenEye-Recomp](https://github.com/SunJaycy/GoldenEye-Recomp) / [Steam Deck build](https://github.com/couchk1ng/GoldenEye-Recomp-SteamDeck) |
|---|---|---|
| **What it ports** | The original **Nintendo 64** game (1997) | The **Xbox 360 XBLA** HD remaster (built ~2007, never released) |
| **How** | **Decompilation-based source port** — human-reconstructed C, compiled for the host; game logic runs as written | **Static binary recompilation** — the shipped machine code is auto-translated to C; no source-level understanding |
| **Lineage** | [GoldenEye 007 decompilation](https://github.com/n64decomp/007) + [Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark) engine family | Xbox 360 "…Recompiled" static-recompilation family |
| **Renderer** | Software RSP → OpenGL | Hardware (Vulkan) |
| **Status** | WIP, Phase 2 — renders and loads all 21 levels; no audio; not playable end to end | Playable full game, higher frame rates, online multiplayer |
| **Why it exists** | A [case study in AI-agent collaboration](#background) on a hard low-level codebase; the port is the target, not a product | A polished, playable PC release of the remaster |
**If you just want to play GoldenEye on PC today, use one of the recompilation
projects — they are finished and this is not.** What is interesting here is the
other half: getting the *original* game running from source, and how much of
that work was driven by AI agents.
## Status
> [!WARNING]
> **Not playable yet.** This is a work-in-progress research port. The intro and
> menus render and all 21 solo levels load, but it is **not** a complete or
> reliably playable game: there is **no audio**, the front-end 3D models and
> some text are broken, input has known rough edges, and you should expect
> crashes and glitches once you are past the level intro. Treat it as a
> technical demo of the porting work, not a way to play GoldenEye.
**Phase 2 of 4 (rendering).** The port boots, renders, and is playable through
the front end into the early game. It is not finished and it is not stable.
**Working**
- Boot → Rare/Nintendo logos → gun-barrel → cast intro, fully rendered.
- Front end: main menu → mission select → difficulty → briefing → mission start.
- **All 21 solo missions load, render, and survive an unattended play window
without crashing** (see [`docs/dev/LEVEL-STATUS.md`](docs/dev/LEVEL-STATUS.md)).
- Software RSP (fast3d): textured world geometry, skeletal characters, HUD,
the GE-specific color-combiner / render modes and `G_TRI4`.
- Input: keyboard + mouse (with mode-aware mouse-look) and SDL game
controllers, mapped onto the N64 pad. Tunable via `ge007.ini`.
- File-backed EEPROM saves.
**Not yet working**
- **Audio** — not implemented (Phase 3). The game runs silent.
- Some front-end 3D models — the spinning Nintendo logo, and the MISSION
COMPLETE / mode-select models — are mispositioned or absent. (The
gun-barrel Bond intro renders correctly.)
- Assorted cosmetic defects (some text layout, a few incomplete asset
conversions) are tracked in
[`docs/dev/GRAPHICS-BACKLOG.md`](docs/dev/GRAPHICS-BACKLOG.md) and parked
below crash/level work.
- **Platform testing is Windows-only.** All development and playtesting has
been on `x86_64` Windows (MSYS2/MINGW64). The CMake build has Linux and macOS
paths and is *intended* to be portable, but the port has had **zero
functional testing on Linux or macOS** — the Linux build is provided as-is
and unverified at runtime. No ARM testing; no controller rebinding UI; no
widescreen.
---
## Download
There are no pre-built binaries yet. The project is Phase 2 WIP with the
caveats in [Status](#status) above — nothing runs end to end — so the only way
to try it is to build it yourself; see [Building](#building). You supply your
own ROM (see [Requirements](#requirements)) and drop it in `data/` before
building. Pre-release **Windows x86-64** bundles (engine executable plus
runtime DLLs, no ROM or game assets) may be published under
[Releases](../../releases) once the port is playable.
| Platform | Bundle | Notes |
|---|---|---|
| **Windows** (x86_64) | [win64.zip](https://github.com/jkdansereau/goldeneye-pc-port/releases) | Engine + runtime DLLs + the one-time asset tool. |
| **Linux** (x86_64) / **Steam Deck** | [linux tarball](https://github.com/jkdansereau/goldeneye-pc-port/releases) | SDL2 is bundled, so it runs as-is on any distro, and sideloads onto a Deck with nothing installed. |
Both bundles contain **no ROM and no game assets**: you supply your own
(see [Requirements](#requirements)), which keeps the release legal to
distribute. Earlier builds: v0.3.0, v0.2.2, v0.2.1, v0.2.0, and v0.1.0
alpha, same page. You can also build it yourself; see [Building](#building).
### Quick start
You need a GoldenEye 007 N64 ROM (`.z64`, big-endian). This release supports
the **NTSC-U (US)** version; PAL and JP are on the roadmap ([issue
#85](https://github.com/jkdansereau/goldeneye-pc-port/issues/85)). No ROM or game asset is
included or distributed. Then:
1. Download the Windows or Linux bundle from [Releases](https://github.com/jkdansereau/goldeneye-pc-port/releases) and unpack it.
2. Make a `data/` folder next to the executable and drop the ROM in as `ge007.ntsc-final.z64`.
3. Launch the executable from that folder. The first run takes a few extra seconds: it detects the ROM and generates the derived asset folders once (no Python or other tooling needed).
Read the [Status](#status) section first: the small number of known issues
in this release are listed plainly there.
---
## Status
**v0.4.0 - fully playable, with a small set of known caveats.** The full
single-player campaign is completable end to end (all 20 missions, Agent
difficulty, playtested), at a steady 60 fps; all 20 solo missions — plus
the end-of-campaign credits sequence — load, render and run crash-free,
verified on Windows, Linux and real Steam Deck hardware. The most common
defect classes from earlier releases — particle colour drift, water seams,
z-fighting, billboard trees, muzzle flashes, the front-end Nintendo logo,
gunshot SFX — are fixed in this version; what remains is a short list,
below. Feedback is very welcome.
What a release actually installs (no networking, no telemetry, no ROM or
game assets shipped) and how faithfully the port tracks the original N64
game's logic: [Security & fidelity status](docs/security-and-fidelity-status.md).
**Working:** boot sequence and front end (menu → mission select → briefing →
start), front-end menu navigation on the left stick to match the F10 overlay
(D282); all 20 solo missions load, render and are crash-free (full campaign
playtested end to end at Agent difficulty, including the ending sequence); steady 60 fps
(software RSP off the presentation critical path); full audio: in-level music and SFX; keyboard + mouse (click-to-lock, an aim style of your
choice — N64 or centred FPS-style — with per-device sensitivity) and a modern
dual-stick controller layout (use/reload/weapon-cycle on A/X/Y, rumble-pak
vibration on supported pads); native widescreen at any aspect ratio
(undistorted world, HUD anchored to the screen edges, 4:3 menus pillarboxed);
in-game key rebinding (keyboard/mouse) with a GEPD-style default layout;
crosshair customization (opt-in); Bond is fixed in cutscenes (no more
floating or spin-glitching) and his third-person model positioning generally
is right the large majority of the time now, at most a small drift when off; file-backed saves; faithful N64 progression
by default (F10 → *All unlocked* opens every level, 007 mode and the full
cheat menu); F10 in-game options overlay (video, input, gameplay, HUD,
graphics, audio; frame cap, MSAA, filtering, FOV, sensitivity, key rebinding,
crosshair, vibration); Windows and
Linux, including Steam Deck.
**Known issues:**
- On Facility, if gas leaks during Ourumov's monologue he can pause for up to
~10 s before resuming the scripted shootout — a latent race that exists in
the N64 original too (where it softlocks permanently); the port detects and
auto-recovers it (D318).
- With native widescreen on, the F10 options overlay stretches with the
window instead of pillarboxing like the front-end menus (legible; cosmetic;
the F10 *Native widescreen* toggle restores the old stretched frame
throughout). The world/HUD widescreen rendering itself is correct (D335b).
- The Rareware front-end logo shows a subtle texture-filtering artifact
(Nintendo logo and legal page are clean). Cosmetic only (D75).
- This release ships NTSC (US) assets; PAL/JP ROMs are not supported in this
version (D258).
- **`All unlocked` is experimental: back up `data/ge007.eep` before enabling
it.** Any save while ON, even a profile-settings change, can permanently
write artificial cheat unlocks and completion times into the EEPROM;
switching OFF does not undo them (D387). Do not use it on a save whose
original progression you need to preserve.
- Assorted further cosmetic defects are tracked in
[`docs/dev/GRAPHICS-BACKLOG.md`](docs/dev/GRAPHICS-BACKLOG.md).
- No macOS or ARM support. Keyboard/mouse rebinding shipped in v0.4.0;
controller-button rebinding is not supported yet.
Root causes and fix status for every item: the [release notes](https://github.com/jkdansereau/goldeneye-pc-port/releases)
and the finding log in [`docs/dev/findings.md`](docs/dev/findings.md).
### Steam Deck
The Linux bundle is the Deck build. SFTP it over from your PC, or download
it straight from the [releases page](https://github.com/jkdansereau/goldeneye-pc-port/releases) on the Deck itself:
extract, drop your ROM in `data/`, launch it once (the first run generates the
derived assets), and add the executable as a non-Steam game. SDL2 is bundled, so no dependencies need
installing. On SteamOS the first launch seeds `ge007.ini` with Deck-friendly
defaults: native 1280×800 fullscreen, VSync, 2× MSAA, and 250% draw/LOD
distance (the N64-authored fade distances read short on the close-up 7"
panel); everything is changeable in the options overlay and persists
afterwards. **Do that first launch in Game Mode, not Desktop Mode** — an ini
created by an earlier Desktop Mode launch (e.g. while testing before adding
it as a Steam shortcut) permanently skips the Deck preset, since any
existing ini always wins over it (D283). If your resolution isn't 1280×800
on first Game Mode boot, just set it manually: F10 → *Resolution*. The
renderer is CPU-bound (software RSP); expect original N64-era performance at
60 fps rather than more. This release was playtested on real Deck hardware;
the v0.1.0-era Facility crash (D203) did not recur: its root cause was
identified and fixed (D253), and a separate intermittent SIGSEGV in heavy
firefights/terminal destruction (D255) and an audio-thread crash (D305) are
both fixed and live-verified on real hardware as of this release.
**In-game settings on the Deck.** The options overlay is fully gamepad-driven:
it opens with **Select**, the D-pad or left stick (up/down) moves between
options, **A** steps the selected option forward, **B** steps it back, and
**Start** (or Select again) closes. Toggles flip, resolution / MSAA /
filtering cycle, sliders step in increments. With a keyboard attached the same
overlay is `F10` + arrows/Enter.
---
## Roadmap
No fixed timeline or committed feature list — this is spare-time work — but
directionally, on the way to v1.0:
- Working through the [known issues](#status) above and the fuller list in
[`docs/dev/findings.md`](docs/dev/findings.md).
- **PAL and JP ROM support** ([issue #85](https://github.com/jkdansereau/goldeneye-pc-port/issues/85)); NTSC-U is the only supported region today.
- **Controller (pad) button rebinding UI** (keyboard/mouse rebinding shipped
in v0.4.0), and macOS/ARM builds.
- **LAN multiplayer**: reviving GoldenEye's original split-screen/deathmatch
netplay across multiple PCs on a local network. Genuinely under
consideration, but early and not started; no ETA.
- General polish: performance, remaining rendering/audio defects, save/config
robustness.
Not currently planned: new game modes GE never shipped (e.g. co-op), online (non-LAN)
multiplayer, ray tracing. If any of these matter to you, open an issue —
it helps prioritize.
---
## Beyond playing
- **Tweak it**: `ge007.ini` and the F10 in-game overlay expose resolution,
frame cap, MSAA, texture filtering, FOV/draw distance, mouse feel, key
rebinding, crosshair and vibration; launch with `-fresh` for a clean-slate
run.
- **Read it**: [`docs/internals.md`](docs/internals.md) maps the
architecture and the software RSP; [`docs/porting-notes.md`](docs/porting-notes.md)
is the catalogue of N64→PC bug classes hit along the way. Game logic in
`src/` is unmodified decompilation; every hardware surface lives in the
MIT-licensed `port/` layer.
- **Mod it**: the port layer, build system and `tools_pc/` are yours to
extend (see [License](#license)); [`CONTRIBUTING.md`](CONTRIBUTING.md) has
the ground rules for getting changes in, and [`docs/dev/`](docs/dev/) is
the raw engineering record behind every fix.
---
## Background
The port was built by two coding agents, a local open-weight model
(`unsloth/Qwen3.8-27B-GGUF` on one RTX 5090, via the [pi](https://pi.dev/)
agent) doing the groundwork (build, boot chain, software-RSP integration,
asset pipeline, first frames), and **Claude Code** (Sonnet 5, Opus 5 for the
hardest bugs) joining for the collaborative phase (the 21-level sweep, the
ABI finding catalog, SDL input, front end), handing work back and forth
through shared written notes, directed by one person part-time. In short:
43 days (16 Aug – 28 Sep), 1005 commits, 286 findings root-caused and logged
(`D1`–`D408`).
The full write-up (timeline, handoff mechanism, effort breakdown, an honest
"what worked / what didn't"): [`docs/dev/agentic-development.md`](docs/dev/agentic-development.md).
The workflow itself: [`docs/dev-process.md`](docs/dev-process.md). To cite the
project or its findings: [`CITATION.cff`](CITATION.cff) (GitHub's "Cite this
repository" menu).
---
## How this differs from the other GoldenEye PC projects
This is a native port of the original 1997 Nintendo 64 game, built from its
actual reconstructed source code, the same lineage as the Perfect Dark PC
port. The other well-known "GoldenEye on PC" projects are something
different: they machine-translate the shipped binary of the *unreleased Xbox
360 XBLA remaster*, a different game on a different codebase with no shared
code with this one.
| | This project | The other well-known "GoldenEye on PC" recompilation projects (and their Steam Deck builds) |
|---|---|---|
| **What it ports** | The original **Nintendo 64** game (1997) | The **Xbox 360 XBLA** HD remaster (built ~2007, never released) |
| **How** | Decompilation-based source port: human-reconstructed C, compiled for the host; game logic runs as written | Static binary recompilation: the shipped machine code is auto-translated to C; no source-level understanding |
| **Lineage** | [GoldenEye 007 decompilation](https://github.com/n64decomp/007) + [Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark) engine family | Xbox 360 "…Recompiled" static-recompilation family |
| **Renderer** | Software RSP → OpenGL | Hardware (Vulkan) |
| **Status** | v0.4.0 public release; full campaign playable at 60 fps (see [Status](#status)) | Playable full game |
| **Why it exists** | To run the *original* N64 game from source, and as a [case study in AI-agent collaboration](#background) on a hard low-level codebase | To get a playable PC release of the remaster |
They answer a different question: how to get the *remaster* onto PC by
machine translation. This project answers how to get the *original 1997
game* onto PC, running from its reconstructed source.
---
## Requirements
You need a **GoldenEye 007 (Nintendo 64) ROM** that you legally own, in
You need a GoldenEye 007 (Nintendo 64) ROM that you legally own, in
big-endian (`.z64`) format, matching one of:
| Region | ROMID | ROM filename (in `data/`) | SHA-1 |
@@ -206,12 +284,16 @@ big-endian (`.z64`) format, matching one of:
| PAL (EU) | `pal-final` | `ge007.pal-final.z64` | `167c3c433dec1f1eb921736f7d53fac8cb45ee31` |
| NTSC-J (JP) | `jpn-final` | `ge007.jpn-final.z64` | `2a5dade32f7fad6c73c659d2026994632c1b3174` |
US is the recommended and best-tested version.
This release supports the US (NTSC-U) version; PAL and JP ROMs are
recognised by region but not yet supported (on the roadmap, [issue
#85](https://github.com/jkdansereau/goldeneye-pc-port/issues/85)).
The port also relies on the decompilation's asset-extraction step, which pulls
the level, model, texture and music data out of your ROM at build time. That
step, too, requires your ROM and is part of [Building](#building).
---
## Building
Prerequisites: CMake >= 3.16, a C/C++ toolchain, SDL2, zlib, OpenGL, Python 3,
@@ -232,13 +314,13 @@ git clone https://github.com/jkdansereau/goldeneye-pc-port.git
cd goldeneye-pc-port
# 1. extract assets from your ROM (see docs/building.md)
# 2. build the port
./build-pc.sh ntsc-final # or pal-final / jpn-final
./build-pc.sh ntsc-final # NTSC-U; PAL/JP engine builds compile, but their asset sidecars are not yet producible (issue #85)
```
### Linux
> **Untested.** No one has built or run the port on Linux. The steps below are
> the intended path, not a verified one — expect to fix build issues yourself.
The Linux build is compiled on every push by CI (ubuntu-24.04); the release
bundle additionally bundles SDL2 so no system packages are needed at runtime.
```sh
sudo apt install build-essential cmake python3 libsdl2-dev zlib1g-dev libgl1-mesa-dev
@@ -251,60 +333,72 @@ cd goldeneye-pc-port
The executable is written to `build-pc/ge007.x86_64` (on Windows,
`build-pc/ge007.x86_64.exe`).
---
## Running
1. Create a `data/` directory in the repo root.
2. Put your ROM in it, named as in the table above
(e.g. `data/ge007.ntsc-final.z64`).
3. Run the executable **from the repo root**:
3. Run the executable from the repo root:
`./build-pc/ge007.x86_64`.
Configuration is written to `ge007.ini` on first run.
Configuration is written to `ge007.ini` on first run; game progress
lives in `ge007.eep`. Launch with `-fresh` to wipe both before starting
(a clean-slate run for playtesting).
### Default controls
| Action | Keyboard / mouse | Controller |
|---------------------|--------------------------|---------------|
| Move / strafe | `W` `A` `S` `D` / arrows | Left stick |
| Move / strafe | `W` `A` `S` `D` / arrows | Left stick (or D-pad) |
| Aim / look | Mouse | Right stick |
| Fire (Z) | Left mouse / `LCtrl` | Right trigger |
| Aim mode (R) | Right mouse / `LShift` | Left trigger |
| Use / accept (A) | `Space` / `E` / `X` | A / X |
| Reload / cancel (B) | `R` / `F` | B / Y / RB |
| L trigger | `Q` | LB |
| Fire | Left mouse | Right trigger |
| Aim mode | Right mouse / `LShift` | Left trigger / LB |
| Use / interact | `E` | A |
| Reload | `R` | X |
| Crouch | `LCtrl` | Left or right stick click |
| Cycle owned gadgets | Watch inventory | B |
| Next weapon | Mouse wheel down / `Q` | Y |
| Previous weapon | Mouse wheel up | — |
| Start | `Enter` / `Tab` | Start |
| Options overlay | `F10` | Select (A toggles/steps; B backs; D-pad/stick adjusts sliders; Start closes) |
The default layout is the GEPD-style preset on the keyboard and the standard
dual-stick scheme on controllers (left stick move, right stick look,
triggers fire/aim, A use, X reload, B gadget cycle, Y weapon cycle, stick
click crouch). The watch gadget cycle selects the next owned gadget in the
N64 inventory. The controller layout matches Rare's Xbox 1.1 (Jinx) button
roles (user-tested on a physical pad; D394), and the remaining Xbox schemes
(1.2 Christmas, 1.3 Frost, 1.4 Elektra) are planned as selectable presets
and not yet available. Keyboard → Bindings edits **keyboard/mouse only**; a
controller can navigate those pages and use B to go back, but
controller-button rebinding is not supported yet.
Mouse sensitivity, Y-inversion and the aim/turn split are tunable in the
`[Input]` section of `ge007.ini`.
F10 overlay (*Keyboard → Sensitivity*) or the `[Input]` section of
`ge007.ini`.
---
## How it works
The R4300 game code in `src/` is compiled **completely unmodified** — the
decompilation's control flow is treated as ground truth. Everything that would
touch N64 hardware is redirected into `port/`:
- **`port/fast3d/`** — a software RSP. It interprets the GBI display list the
game builds each frame and emits OpenGL, bypassing the RDP. Adapted from the
Perfect Dark port and extended for GoldenEye's custom GBI.
- **`port/src/gesched.c`** — replaces the RCP scheduler; instead of poking RSP
registers it drives the software RSP directly.
- **`port/src/libultra.c`** — single-threaded shims for the libultra OS API
(threads, messages, timers, PI/SI/AI/VI) over the host.
- **`port/src/{video,audio,input,fs,romdata,config}.c`** — the SDL2 / OpenGL /
filesystem backends.
The 32→64-bit transition forces a small, catalogued class of mechanical
ABI-only edits to ROM-serialized structs (pointer-width reconciliation); these
change no behaviour and are documented individually.
Where it diverges from the Perfect Dark port: GoldenEye's N64 serialized asset
formats (level setup, models, backgrounds) are converted **offline** by a set
of Python "sidecar" converters in `tools_pc/`, rather than fixed up at load
time. See [`docs/internals.md`](docs/internals.md) and
The R4300 game code in `src/` is compiled completely unmodified; the
decompilation's control flow is ground truth. Everything that would touch N64
hardware is redirected into `port/`: a **software RSP** (`port/fast3d/`,
adapted from the Perfect Dark port) that interprets the GBI display list the
game builds each frame and emits OpenGL, bypassing the RDP; a scheduler
replacement (`port/src/gesched.c`) that drives it directly; single-threaded
libultra OS shims (`port/src/libultra.c`); and SDL2/OpenGL/filesystem backends
for video, audio, input and storage. The 32→64-bit transition forces a small,
cataloged class of mechanical ABI-only edits to ROM-serialized structs
(pointer-width reconciliation); these change no behavior and are documented
individually. Where it diverges from the Perfect Dark port: GoldenEye's N64
serialized asset formats are converted offline by Python "sidecar" converters
in `tools_pc/` rather than fixed up at load time. Full detail:
[`docs/internals.md`](docs/internals.md) and
[`docs/porting-notes.md`](docs/porting-notes.md).
## Project layout
```
CMakeLists.txt PC build (parallel to the decomp's Makefile, which is untouched)
build-pc.sh configure + build helper
@@ -318,21 +412,24 @@ tools_pc/ PC-port helper + analysis scripts
docs/ see below
```
---
## Documentation
Key docs are also published as a site:
**<https://jkdansereau.github.io/goldeneye-pc-port/>**.
<https://jkdansereau.github.io/goldeneye-pc-port/>.
| Doc | What's in it |
|---|---|
| [`docs/building.md`](docs/building.md) | Full build + asset-extraction guide. |
| [`docs/internals.md`](docs/internals.md) | Architecture, the RSP-emulation approach, GE-vs-PD engine differences, the phased plan. |
| [`docs/porting-notes.md`](docs/porting-notes.md) | The recurring N64→PC bug classes hit during the port, with fixes. |
| [`docs/dev/game-behavior-reference.md`](docs/dev/game-behavior-reference.md) | How the retail N64 game is meant to behave — combat/AI model, difficulty scaling, per-level objectives, timers, weapon data, original-game quirks. Playtest reference. |
| [`docs/dev/agentic-development.md`](docs/dev/agentic-development.md) | The research angle: the two-agent setup, timeline, handoff workflow, and an assessment of what did and didn't work. |
| [`docs/dev-process.md`](docs/dev-process.md) | The investigation workflow in detail — budgets, file partitioning, the finding-log discipline. |
| [`docs/dev/`](docs/dev/) | The raw engineering record: the full finding log, per-level status, graphics backlog, playtest matrices. |
| [`docs/SetupGuide.md`](docs/SetupGuide.md), [`docs/StructureGuide.md`](docs/StructureGuide.md), [`docs/StyleGuide.md`](docs/StyleGuide.md) | Inherited from the decompilation. |
| [`docs/dev-process.md`](docs/dev-process.md) | The investigation workflow in detail: budgets, file partitioning, the finding-log discipline. |
| [`docs/dev/`](docs/dev/) | The raw engineering record: the full finding log, per-level status, graphics backlog, playtest matrices, and [`docs/dev/game-behavior-reference.md`](docs/dev/game-behavior-reference.md) (a secondary-sourced playtest reference for how the retail game is meant to behave; repo-only, code is ground truth). |
| [`docs/SetupGuide.md`](docs/SetupGuide.md), [`docs/StyleGuide.md`](docs/StyleGuide.md) | Inherited from the decompilation this repo forks. |
---
## Credits
@@ -340,24 +437,30 @@ This port is a thin layer on a large amount of other people's work.
**Prior work it is built on**
- The [GoldenEye 007 decompilation](https://github.com/n64decomp/007) — years of
- The [GoldenEye 007 decompilation](https://github.com/n64decomp/007), years of
effort by Larry Ficken ("kholdfuzion") and the project's contributors; plus
zoinkity's GoldenEye documentation, which the decomp started from. This port
is a fork of that repository.
- The [Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark)
(Ryan Dwyer and contributors) — the reference architecture for this port and
(Ryan Dwyer and contributors), the reference architecture for this port and
the source of the `fast3d` software RSP.
- The [Perfect Dark decompilation](https://github.com/n64decomp/perfect_dark) —
- The [Perfect Dark decompilation](https://github.com/n64decomp/perfect_dark),
the sibling decomp the PD port is built on.
- **Carnivorous**: author of the *Mouse Injector* input plugin for 1964 (the
"GEPD Edition" bundle). Its mouse-aim behaviour for GoldenEye/Perfect Dark is
what the in-game GEPD-style aim mode is modelled on; the implementation here
is an independent reimplementation of that behaviour, not derived code.
Thanks also to **Rice** and **schibo** of the 1964 team for the emulator it
shipped with.
**Vendored / adapted code**
- `port/fast3d/` — the software RSP, adapted from the PD port. It originates
- `port/fast3d/`: the software RSP, adapted from the PD port. It originates
with the [Ship of Harkinian](https://github.com/HarbourMasters) /
libultraship fast3d (© Emill, MaikelChan; MIT — see `port/fast3d/LICENSE.txt`),
libultraship fast3d (© Emill, MaikelChan; MIT, see `port/fast3d/LICENSE.txt`),
which itself descends from
[sm64-port](https://github.com/sm64-port/sm64-port)'s fast3d and audio mixer.
- `port/fast3d/glad/` — OpenGL loader generated by
- `port/fast3d/glad/`: OpenGL loader generated by
[glad](https://github.com/Dav1dde/glad) (David Herberth, MIT).
- The decompilation toolchain: [`ido-static-recomp`](https://github.com/decompals/ido-static-recomp)
(Emill / decompals), [`qemu-irix`](https://github.com/n64decomp/qemu-irix) and
@@ -367,26 +470,33 @@ This port is a thin layer on a large amount of other people's work.
**Methodology**
- Chris Lewis, [*"Decompiling a Nintendo 64 Game in 84 Days"*](https://blog.chrislewis.au/decompiling-a-nintendo-64-game-in-84-days/) (the Snowboard
Kids decompilation write-up) — the agent-workflow practices in
Kids decompilation write-up), the agent-workflow practices in
[`docs/dev-process.md`](docs/dev-process.md) are adapted from it.
**Consultation**
- **f1zz1ec0ke** ([GitHub](https://github.com/f1zz1ec0ke)): LLM consultation on model
tuning, agent harnesses, and agentic strategy throughout the port's development.
**Tools and models used to develop the port**
- Qwen 3.8 (Alibaba Qwen team), run locally as the
- Qwen 3.8 (Alibaba Qwen team), run locally from the
[`unsloth/Qwen3.8-27B-GGUF`](https://huggingface.co/unsloth/Qwen3.8-27B-GGUF)
`UD-Q4_K_XL` quant;
[Unsloth](https://unsloth.ai) (the GGUF quantisation and Unsloth Desktop,
used as the local model server).
- [pi](https://pi.dev/) — the local coding-agent harness.
repo — `UD-Q4_K_XL` as the primary quant, with other quants used along the
way; [Unsloth](https://unsloth.ai) (the GGUF quantisation and Unsloth
Desktop) as the local model server.
- [pi](https://pi.dev/): the local coding-agent harness.
- [Claude / Claude Code](https://claude.com/claude-code) (Anthropic).
---
## Legal
This is a non-commercial fan preservation/research project, in the same
category as the many other N64 decompilation and native-port repositories on
GitHub. It follows the same conventions they do:
- **No ROM and no game assets are distributed** — not in this repository and
- **No ROM and no game assets are distributed**: not in this repository and
not in any release. Textures, audio, models, level data and in-game text are
extracted from a ROM *you already own*, on *your* machine, at build time.
- The repository is a fork of the public
@@ -395,12 +505,11 @@ GitHub. It follows the same conventions they do:
- No official logos, box art, or marketing assets are used. "GoldenEye 007",
"007", "James Bond" and related marks belong to their respective owners
(Nintendo, Microsoft/Rare, MGM, Danjaq, EON Productions).
- **If** pre-built binaries are published under [Releases](../../releases) in
the future, they will contain only the engine — the `port/` layer plus the
compiled decompilation, with no game data of any kind — bundled with
permissively-licensed runtime libraries (SDL2, zlib, the MinGW runtime; their
licenses travel in the download). No such release exists yet, and any build —
yours or ours — is useless without a ROM you supply.
- Pre-built binaries published under [Releases](https://github.com/jkdansereau/goldeneye-pc-port/releases) contain
only the engine (the `port/` layer plus the compiled decompilation, with no
game data of any kind), bundled with permissively-licensed runtime libraries
(SDL2, zlib, the MinGW runtime; their licenses travel in the download). Any
build, yours or ours, is useless without a ROM you supply.
This project is **not affiliated with, endorsed by, or sponsored by** Nintendo,
Rare, Microsoft, MGM, Danjaq, EON Productions, or any rights holder in
@@ -409,7 +518,11 @@ issue and it will be addressed.
## License
The original work in this repository — the port layer (`port/`), the PC build
system, `tools_pc/`, and the documentation — is released under the MIT License,
The original work in this repository, the port layer (`port/`), the PC build
system, `tools_pc/`, and the documentation, is released under the MIT License;
see [`LICENSE`](LICENSE). Everything inherited from the upstream decompilation
is covered by [`NOTICE`](NOTICE), not by that license.
---
*Last updated 2026-09-28 — v0.4.0 · <a href="https://github.com/jkdansereau/goldeneye-pc-port">GitHub</a> · <a href="https://github.com/jkdansereau/goldeneye-pc-port/releases">Releases</a>*
+127 -1
View File
@@ -14,7 +14,7 @@
# Example (Linux):
# sudo apt install cmake libsdl2-dev zlib1g-dev libgl1-mesa-dev
# Example (macOS):
# brew install cmake sdl2 zlib
# brew install cmake libsdl2-dev
# Example (Windows/MSYS2):
# pacman -S mingw-w64-x86_64-toolchain mingw-w64-x86_64-SDL2 \
# mingw-w64-x86_64-zlib mingw-w64-x86_64-cmake
@@ -24,6 +24,132 @@ set -euo pipefail
ROMID="${1:-ntsc-final}"
BUILD_DIR="${BUILD_DIR:-build-pc}"
# ---------------------------------------------------------------------------
# Windows toolchain env guard (see AGENTS.md "Build" — the recurring
# "Cannot create temporary file in C:\Windows\: Permission denied" link
# failure).
#
# The PE toolchain (ninja -> cmd -> gcc/ld) creates temp files under the
# child's TMP/TEMP. A full MSYS2 MINGW64 login shell arranges that for
# native children; other shells (agent harnesses, non-login shells) do not
# -- the msys->native env conversion silently drops/breaks TMP and the
# linker falls back to C:\Windows\. Probe what a native child actually
# sees (via a file: msys console emulation makes piped cmd.exe stdout
# unreliable), and if broken, re-run the cmake+build steps under
# PowerShell. Crucially the temp vars are set INSIDE PowerShell: the broken
# boundary is msys->native, while native->native (powershell -> cmake ->
# ninja) passes env vars through intact.
# GE_PC_BUILD_VIA_NATIVE=1 prevents a re-exec loop.
# ---------------------------------------------------------------------------
if [ -n "${MSYSTEM:-}" ] && [ -z "${GE_PC_BUILD_VIA_NATIVE:-}" ]; then
_probe="$(mktemp -d "${TMPDIR:-/tmp}/gebuildprobe.XXXXXX")"
_probe_win="$(cygpath -w "${_probe}" 2>/dev/null || echo 'C:\\msys64\\tmp')"
/c/Windows/system32/cmd.exe //c "if exist \"%TMP%\" (echo ok > \"${_probe_win}/ok.txt\") else (echo bad > \"${_probe_win}/bad.txt\")" >/dev/null 2>&1 || true
if [ ! -s "${_probe}/ok.txt" ]; then
command -v powershell >/dev/null 2>&1 || {
echo "ERROR: native TMP is unusable from this shell and powershell.exe was not found for the fallback." >&2
echo "Manual fix: run cmake+ninja from cmd/PowerShell with C:\\msys64\\mingw64\\bin on PATH, or" >&2
echo "set the user env vars (setx TMP \"C:\\msys64\\tmp\"; setx TEMP \"C:\\msys64\\tmp\") and re-open the shell." >&2
rm -rf "${_probe}"; exit 1
}
echo "==> native TMP unusable from this shell; re-running cmake+build via PowerShell"
export GE_PC_BUILD_VIA_NATIVE=1
_here="$(pwd -W 2>/dev/null || cygpath -w "$(pwd)")"
# Resolve the Windows-side toolchain paths from what msys found (no
# hardcoded install dir: C:\msys64 on this box, D:\M\msys64
# elsewhere). usr/bin first: mingw gcc.exe needs msys-2.0.dll from
# there.
_cmake_root="$(dirname "$(command -v cmake)")" # .../mingw64/bin
_msys_root="$(cd "${_cmake_root}/../.." && pwd -W 2>/dev/null || cygpath -w "$(cd "${_cmake_root}/../.." && pwd)")" # msys install root (.. twice: bin -> mingw64 -> root)
_bin_w="$(cygpath -w "${_cmake_root}")"
_usrbin_w="$(cygpath -w "${_msys_root}/usr/bin")"
# The PowerShell script is written to a .ps1 FILE, not passed via
# -Command: the msys->native argv conversion mangles $-bearing
# strings (a '\$LASTEXITCODE' check arrived as an empty token and a
# failed cmake would have exited 0 silently). A file write is
# conversion-free; only plain path/word arguments cross the boundary.
_ps1="$(mktemp "${TMPDIR:-/tmp}/ge007-rerun.XXXXXX.ps1")"
cat > "${_ps1}" <<'PS1'
param([string]$UsrBin, [string]$Bin, [string]$MsysRoot, [string]$Here, [string]$BuildDir, [string]$RomId)
# Self-logging: a headless shell loses this process's console output, so
# every fact that matters goes to diag/log files in the build dir.
$diag = Join-Path $BuildDir "ge007-native-reexec-diag.log"
$cmakeLog = Join-Path $BuildDir "ge007-native-reexec-cmake.log"
$buildLog = Join-Path $BuildDir "ge007-native-reexec-build.log"
# The inherited native PATH may be mangled/dropped by the broken msys->native
# env conversion, so build it explicitly: the toolchain only needs the two
# msys bin dirs plus the usual system dirs. (usr/bin first: mingw gcc/cmake
# need msys-2.0.dll from there.)
$env:PATH = "$UsrBin;$Bin;C:\Windows\System32;C:\Windows;C:\Windows\System32\Wbem"
# Writable temp for the native toolchain. [System.IO.Path]::GetTempPath()
# CANNOT be used here: it honours the inherited (broken) TMP/TEMP env vars
# and returned C:\Windows\. Pick the first writable candidate instead.
$tmpdir = $null
foreach ($cand in @((Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'Temp'), (Join-Path $MsysRoot 'tmp'))) {
try {
if (-not (Test-Path $cand)) { New-Item -ItemType Directory -Force -Path $cand -ErrorAction Stop | Out-Null }
$probe = Join-Path $cand 'ge007-tmp-write-test.tmp'
'x' | Out-File -FilePath $probe -ErrorAction Stop
Remove-Item $probe -ErrorAction SilentlyContinue
$tmpdir = $cand
break
} catch { }
}
if ($null -eq $tmpdir) { "no writable temp dir found" | Out-File $diag -Encoding ascii; Write-Host 'no writable temp dir for the native toolchain'; exit 4 }
$env:TMP = $tmpdir
$env:TEMP = $tmpdir
"start UsrBin=[$UsrBin] Bin=[$Bin] MsysRoot=[$MsysRoot] Here=[$Here] BuildDir=[$BuildDir] RomId=[$RomId] tmp=[$tmpdir]" | Out-File $diag -Encoding ascii
$exe = "$Bin\cmake.exe"
"exe=[$exe] exists=[$(Test-Path -LiteralPath $exe)]" | Out-File $diag -Append -Encoding ascii
Set-Location $Here
"cwd=[$([System.IO.Directory]::GetCurrentDirectory())]" | Out-File $diag -Append -Encoding ascii
try {
# -DROMID must be a DOUBLE-QUOTED string: a bare `-DROMID=$RomId` token
# does NOT expand the variable in PowerShell (cmake got the literal
# "$RomId" and failed with "Unknown ROMID").
$out = & $exe -S . -B $BuildDir "-DROMID=$RomId" 2>&1
"cmake ran LASTEXITCODE=[$LASTEXITCODE]" | Out-File $diag -Append -Encoding ascii
$out | Out-File -FilePath $cmakeLog -Encoding ascii
} catch {
"cmake TERMINAL ERROR: $($_.Exception.Message)" | Out-File $diag -Append -Encoding ascii
}
if ($null -eq $LASTEXITCODE) { Write-Host "cmake.exe did not execute (diag: $diag)"; exit 2 }
if ($LASTEXITCODE -ne 0) { Write-Host "cmake re-configure failed (rc=$LASTEXITCODE; log: $cmakeLog)"; exit $LASTEXITCODE }
Write-Host "cmake reconfigured (TMP=$($env:TMP))"
try {
$out = & $exe --build $BuildDir -j 2>&1
"build ran LASTEXITCODE=[$LASTEXITCODE]" | Out-File $diag -Append -Encoding ascii
$out | Out-File -FilePath $buildLog -Encoding ascii
} catch {
"build TERMINAL ERROR: $($_.Exception.Message)" | Out-File $diag -Append -Encoding ascii
}
if ($null -eq $LASTEXITCODE) { Write-Host "cmake --build did not execute (diag: $diag)"; exit 3 }
if ($LASTEXITCODE -ne 0) { Write-Host "build failed (rc=$LASTEXITCODE; log: $buildLog)"; exit $LASTEXITCODE }
"done" | Out-File $diag -Append -Encoding ascii
Write-Host "cmake + build ok (native re-exec, TMP=$($env:TMP))"
PS1
_ps1_win="$(cygpath -w "${_ps1}")"
rc=0
# NOTE: do NOT launch powershell via `env -i` (tried 2026-09-28): a
# native child started with a scrubbed msys env cannot launch
# cmake.exe (CreateProcess fails silently; LASTEXITCODE stays null).
powershell -NoProfile -ExecutionPolicy Bypass -File "${_ps1_win}" "${_usrbin_w}" "${_bin_w}" "${_msys_root}" "${_here}" "${BUILD_DIR}" "${ROMID}" || rc=$?
rm -f "${_ps1}"
if [ "${rc}" -ne 0 ]; then
# Surface the .ps1's own diagnostics (console output is lost in a
# headless shell) plus whatever the failing step wrote.
for _lg in "${BUILD_DIR}/ge007-native-reexec-diag.log" "${BUILD_DIR}/ge007-native-reexec-cmake.log" "${BUILD_DIR}/ge007-native-reexec-build.log"; do
if [ -s "${_lg}" ]; then
echo "---- ${_lg} (last 40 lines) ----" >&2
tail -40 "${_lg}" >&2
fi
done
fi
exit "${rc}"
fi
rm -rf "${_probe}"
fi
echo "==> Configuring PC port (ROMID=${ROMID})"
cmake -S . -B "${BUILD_DIR}" -DROMID="${ROMID}"
+5
View File
@@ -1,5 +1,10 @@
# Setup Guide
> Inherited from the decompilation this repo forks. It covers building the
> **N64 ROM** with the IDO toolchain — needed only for the one-time
> asset-extraction step referenced from [`building.md`](building.md), not for
> building or running the PC port itself.
<!-- TOC -->
* [Setup Guide](#setup-guide)
* [Prerequisites](#prerequisites)
-39
View File
@@ -1,39 +0,0 @@
# Project Structure
```
goldeneye_src
|-- .github/workflows: GitHub use only
├── assets: game assets
│   ├── font: font data
│   ├── images: image data
│   │   └── split: split image data
│   ├── music: music data
│   ├── obseg: animation data
│   │   ├── bg: bg data
│   │   ├── brief: briefing data
│   │   ├── chr: c model data
│   │   ├── gun: g model data
│   │   ├── prop: p model data
│   │   ├── setup: setup data
│   │   ├── stan: stan data
│   │   └── text: text data
│   └── ramrom: demo data
├── bin: files that haven't been touched
├── build: output directory
├── include: header files
├── rsp: Custom GBI code (Assembly) (C0 and 4Tri)
├── src: C source code for game
│   ├── game: core ge specific code 0x7f000000 range
│   ├── inflate: statically linked initial decompression code
│   ├── libultra: currently used libultra.s
│   └── libultrarare: libultra modified by Rare
└── tools: build tools
```
Documentation has been moved to https://github.com/kholdfuzion/goldeneye_docs/tree/master/notes
Style Guide is https://github.com/kholdfuzion/goldeneye_src/blob/AIListLogic/notes/StyleGuide.md
This decompilation was only made possible thanks to many awesome 00 Agents who will be revealed only if they wish.
GE and PD documentation made by Zoinkity.
+49 -25
View File
@@ -1,11 +1,15 @@
# GitHub Pages config. Site is served from the /docs folder on the default
# branch (Settings -> Pages -> Deploy from a branch -> main / docs).
title: GoldenEye 007 PC Port
description: >-
A native PC port of the original Nintendo 64 GoldenEye 007, built from its
decompiled source, and a case study in AI-agent collaboration on a large
low-level codebase.
theme: jekyll-theme-cayman
# v0.2.1 (Bing SEO): keep this SHORT -- jekyll-seo-tag splices it into the
# home page <title> whenever the page title equals the site title, and the
# long version made a ~230-char title. It also renders as the masthead
# tagline on every page. Per-page meta/og descriptions come from each page's
# front matter `description:` (see index.md), not from here.
description: GoldenEye 007 (Nintendo 64, 1997) on the PC - decompiled, ported, and playable.
# No GitHub theme: the site uses a hand-rolled _layouts/default.html plus the
# design in _includes/head-custom.html (charcoal/off-white/amber, square and
# flat.)
show_downloads: false
# Canonical site location - used by jekyll-seo-tag (canonical URLs, og: tags)
@@ -26,26 +30,46 @@ plugins:
- jekyll-sitemap
# jekyll-relative-links (on by default for Pages) rewrites the .md links in
# these docs to .html so cross-links work on the published site.
# these docs to .html so cross-links work on the published site. Links that
# must reach docs kept out of the site build point at the GitHub blob URL
# instead, so they resolve from the published pages too.
# Rolling local working files - not useful as published pages.
# ---------------------------------------------------------------------------
# The site publishes a small curated set (see docs/dev/notes/DE-SLOP-DOCS-PLAN.md):
# index.md, building.md, internals.md, porting-notes.md, dev-process.md,
# dev/agentic-development.md
# Everything else under docs/ is working material - the finding log, level
# status, backlogs, plans, inherited decomp guides. It stays in the repo (and
# on GitHub) but is excluded from the site build so it never shows up as a
# published page or in sitemap.xml.
exclude:
- BACKLOG.md
- HANDOFF.md
# Keep the raw engineering record (finding log, per-level status, triage
# notes, backlogs) out of sitemap.xml so search engines aren't steered at it.
# The pages still build and stay reachable from cross-links; the two dev/
# docs that are linked from the site index are kept in.
defaults:
- scope:
path: "dev"
values:
sitemap: false
- scope:
path: "dev/agentic-development.md"
values:
sitemap: true
- scope:
path: "dev/game-behavior-reference.md"
values:
sitemap: true
- SetupGuide.md
- StructureGuide.md
- StyleGuide.md
- dev/AUDIO-PLAN.md
- dev/AUDIT-M6-player-offsets.md
- dev/D172-PARTICLE-COLOR-RESEARCH.md
- dev/D176a-SKY-NOTES.md
- dev/GE-ENV-PROBES.md
- dev/GRAPHICS-BACKLOG.md
- dev/HANDOFF-ARCHIVE.md
- dev/LEVEL-OBJECTIVES.md
- dev/LEVEL-PLAYTEST.md
- dev/LEVEL-STATUS.md
- dev/LINUX-PORT.md
- dev/M-35-PLAYTEST-CHECKLIST.md
- dev/M-35-QOL-REVIEW.md
- dev/M-36-TRIAGE.md
- dev/MODERN-PORT-FEATURES-BACKLOG.md
- dev/OPTIONS-MENU-PLAN.md
- dev/QOL-INVENTORY.md
- dev/TEXTURE-GLITCH-ANALYSIS.md
- dev/UNLOCKED-FPS-PLAN.md
- dev/VIEWMODEL-RESEARCH.md
- dev/WIDESCREEN-FOV-PLAN.md
- dev/findings-index.csv
- dev/findings.md
- dev/game-behavior-reference.md
- dev/release-dropin-rom-plan.md
+147 -173
View File
@@ -3,267 +3,242 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Libre+Baskerville:ital,wght@0,400;0,700;1,400&display=swap" rel="stylesheet">
<style>
/* ----------------------------------------------------------------------
Design pass over the stock Cayman theme.
Restrained "spy-tech" palette: near-black, slate, a single amber accent.
Loaded after the theme stylesheet, so these rules win without !important.
Hand-rolled site theme (replaces jekyll-theme-cayman, 2026-09-28).
Look: committed charcoal/off-white/amber "night desk" — flat and square,
serif display type (Libre Baskerville) over a system sans body, thin
amber rules between sections, text navigation separated by middots.
No gradients, no shadows, no rounded corners, no theme switcher.
--------------------------------------------------------------------- */
:root {
color-scheme: light dark;
--ink: #14171c;
--ink-soft: #3d444d;
--ink-mute: #626b76;
--line: #e4e6e9;
--bg: #ffffff;
--bg-soft: #f7f8fa;
--accent: #b7791f;
--accent-ink: #8a5a12;
--header-1: #0d1117;
--header-2: #1c2530;
--radius: 8px;
--measure: 44rem;
--bg: #1b1b1d; /* charcoal page */
--bg-soft: #232326; /* code / alt rows */
--bg-deep: #141416; /* code blocks, masthead */
--ink: #e8e6e3; /* off-white body text */
--ink-soft: #b5b1a9; /* secondary text */
--ink-mute: #8d897f; /* captions, footer */
--line: #3a3a3e; /* 1px borders */
--accent: #ffb454; /* amber/gold */
--accent-hi: #ffd08a; /* link hover */
--measure: 44rem;
}
@media (prefers-color-scheme: dark) {
:root {
--ink: #e7eaee;
--ink-soft: #b9c0c9;
--ink-mute: #8b939e;
--line: #2a2f37;
--bg: #0f1216;
--bg-soft: #171b21;
--accent: #e0a94a;
--accent-ink: #f0c274;
--header-1: #0a0d11;
--header-2: #161d27;
}
}
html { background: var(--bg); }
body {
font-family: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, sans-serif;
font-size: 16.5px;
line-height: 1.7;
color: var(--ink);
background: var(--bg);
font-size: 17px;
line-height: 1.7;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
/* ---- Header --------------------------------------------------------- */
.page-header {
position: relative;
isolation: isolate;
text-align: center;
padding: 4rem 1rem 3.75rem;
background: linear-gradient(135deg, var(--header-1), var(--header-2));
border-bottom: 1px solid rgba(224, 169, 74, 0.28);
/* ---- Masthead -------------------------------------------------------- */
.site-masthead {
background: var(--bg-deep);
border-bottom: 1px solid var(--accent);
}
.page-header::before {
content: "";
position: absolute;
inset: 0;
z-index: -1;
background-image: url("{{ '/img/attract-bunker1.png' | relative_url }}");
background-size: cover;
background-position: center 38%;
opacity: 0.16;
filter: grayscale(0.35) contrast(1.05);
}
.page-header::after {
content: "";
position: absolute;
inset: 0;
z-index: -1;
background: linear-gradient(180deg, rgba(13,17,23,0.35), rgba(13,17,23,0.82));
}
.project-name {
font-weight: 700;
letter-spacing: -0.02em;
font-size: 2.4rem;
margin: 0 0 0.4rem;
color: #fff;
text-shadow: 0 1px 24px rgba(0,0,0,0.5);
}
.project-tagline {
font-weight: 400;
font-size: 1.12rem;
line-height: 1.55;
color: #d3d9e0;
opacity: 1;
max-width: 42rem;
margin: 0 auto 1.6rem;
}
.page-header .btn {
display: inline-block;
padding: 0.5rem 1.1rem;
margin: 0.3rem 0.25rem 0;
font-size: 0.9rem;
font-weight: 500;
letter-spacing: 0.01em;
color: #f3f5f7;
background: rgba(255,255,255,0.06);
border: 1px solid rgba(255,255,255,0.22);
border-radius: var(--radius);
transition: background 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
}
.page-header .btn:hover {
background: rgba(224,169,74,0.16);
border-color: var(--accent);
text-decoration: none;
transform: translateY(-1px);
}
/* ---- Body ---------------------------------------------------------- */
.main-content {
.masthead-inner {
max-width: 52rem;
margin: 0 auto;
padding: 3rem 1.5rem 1rem;
color: var(--ink);
font-size: 17px;
padding: 2.6rem 1.5rem 1.6rem;
text-align: center;
}
.main-content p,
.main-content ul,
.main-content ol,
.main-content blockquote { max-width: var(--measure); }
.masthead-title {
display: inline-block;
font-family: "Libre Baskerville", Georgia, "Times New Roman", serif;
font-weight: 700;
font-size: 2.1rem;
line-height: 1.25;
color: var(--ink);
text-decoration: none;
letter-spacing: 0.01em;
}
.masthead-title:hover { color: var(--accent); }
.masthead-tagline {
margin-top: 0.5rem;
color: var(--ink-mute);
font-size: 0.95rem;
}
.masthead-nav {
margin-top: 1.3rem;
font-size: 0.9rem;
}
.masthead-nav a {
color: var(--accent);
text-decoration: none;
padding: 0.1rem 0.15rem;
}
.masthead-nav a:hover { color: var(--accent-hi); text-decoration: underline; }
.nav-sep { color: var(--ink-mute); padding: 0 0.35rem; }
.main-content h1,
.main-content h2,
.main-content h3,
.main-content h4 {
/* ---- Content ---------------------------------------------------------- */
.doc-content {
max-width: 52rem;
margin: 0 auto;
padding: 2.5rem 1.5rem 1rem;
}
.doc-content p,
.doc-content ul,
.doc-content ol,
.doc-content blockquote { max-width: var(--measure); }
.doc-content h1,
.doc-content h2,
.doc-content h3,
.doc-content h4 {
font-family: "Libre Baskerville", Georgia, "Times New Roman", serif;
font-weight: 700;
line-height: 1.35;
color: var(--ink);
font-weight: 600;
letter-spacing: -0.015em;
line-height: 1.3;
margin-top: 2.4rem;
margin-bottom: 0.9rem;
}
.main-content h1 { font-size: 1.9rem; }
.main-content h2 {
font-size: 1.4rem;
padding-bottom: 0.35rem;
border-bottom: 1px solid var(--line);
}
.main-content h3 { font-size: 1.15rem; }
.main-content > h1:first-child { margin-top: 0.5rem; }
.doc-content h1 { font-size: 1.75rem; }
.doc-content h2 { font-size: 1.35rem; }
.doc-content h3 { font-size: 1.1rem; }
.doc-content > h1:first-child { margin-top: 0.5rem; }
.main-content a {
color: var(--accent-ink);
.doc-content a {
color: var(--accent);
text-decoration: underline;
text-decoration-color: rgba(183,121,31,0.4);
text-decoration-color: rgba(255, 180, 84, 0.45);
text-underline-offset: 2px;
}
.main-content a:hover {
color: var(--accent);
.doc-content a:hover {
color: var(--accent-hi);
text-decoration-color: currentColor;
}
@media (prefers-color-scheme: dark) {
.main-content a { color: var(--accent); }
.doc-content hr {
border: 0;
height: 1px;
background: var(--accent);
opacity: 0.5;
margin: 2.5rem 0;
}
.main-content blockquote {
color: var(--ink-mute);
.doc-content blockquote {
color: var(--ink-soft);
border-left: 3px solid var(--accent);
padding: 0.2rem 0 0.2rem 1.1rem;
margin-left: 0;
font-style: normal;
}
.main-content hr {
height: 1px;
background: var(--line);
border: 0;
margin: 2.5rem 0;
}
/* Lead paragraph on doc pages (the italic summary under the H1) */
.main-content > p > em:only-child {
.doc-content > p > em:only-child {
font-style: normal;
color: var(--ink-soft);
font-size: 1.05rem;
}
/* ---- Code --------------------------------------------------------- */
.main-content code {
font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
/* ---- Code ------------------------------------------------------------ */
.doc-content code {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 0.86em;
background: var(--bg-soft);
border: 1px solid var(--line);
border-radius: 4px;
padding: 0.12em 0.4em;
color: var(--ink);
}
.main-content pre {
background: var(--header-1);
.doc-content pre {
background: var(--bg-deep);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 1rem 1.15rem;
max-width: var(--measure);
overflow-x: auto;
line-height: 1.55;
}
.main-content pre code {
.doc-content pre code {
background: transparent;
border: 0;
padding: 0;
color: #e6edf3;
color: var(--ink);
font-size: 0.84rem;
}
/* ---- Tables ------------------------------------------------------- */
.main-content table {
/* ---- Tables ----------------------------------------------------------- */
.doc-content table {
border-collapse: collapse;
display: table;
width: 100%;
max-width: var(--measure);
margin: 1.5rem 0;
font-size: 0.92rem;
}
.main-content table th,
.main-content table td {
.doc-content table th,
.doc-content table td {
border: 1px solid var(--line);
padding: 0.5rem 0.75rem;
text-align: left;
}
.main-content table th { background: var(--bg-soft); font-weight: 600; }
.main-content table tr { background: transparent; }
.main-content table tr:nth-child(2n) { background: var(--bg-soft); }
.doc-content table th { background: var(--bg-soft); font-weight: 600; }
.doc-content table tr:nth-child(2n) { background: #1f1f22; }
.main-content img {
border-radius: var(--radius);
/* ---- Images & gallery -------------------------------------------------- */
.doc-content img {
border: 1px solid var(--line);
border-radius: 0;
max-width: 100%;
}
/* ---- Footer ------------------------------------------------------- */
.doc-content details {
max-width: var(--measure);
border: 1px solid var(--line);
background: var(--bg-soft);
padding: 0.6rem 0.9rem;
margin: 1.5rem 0;
}
.doc-content summary { cursor: pointer; color: var(--accent); }
/* ---- Footer ------------------------------------------------------------- */
.site-footer {
max-width: 52rem;
margin: 3rem auto 0;
padding: 1.5rem 1.5rem 2.5rem;
border-top: 1px solid var(--line);
color: var(--ink-mute);
font-size: 0.86rem;
}
.site-footer a { color: var(--accent-ink); }
@media (prefers-color-scheme: dark) { .site-footer a { color: var(--accent); } }
.footer-inner { max-width: var(--measure); }
.footer-stamp {
color: var(--ink-soft);
font-size: 0.88rem;
margin: 0 0 0.8rem;
}
.footer-stamp a { color: var(--accent); text-decoration: none; }
.footer-stamp a:hover { color: var(--accent-hi); text-decoration: underline; }
.footer-legal {
color: var(--ink-mute);
font-size: 0.8rem;
line-height: 1.6;
margin: 0;
}
@media (max-width: 42rem) {
.project-name { font-size: 1.9rem; }
.page-header { padding: 3rem 1rem 2.75rem; }
.main-content { padding-top: 2rem; }
.masthead-title { font-size: 1.7rem; }
.masthead-inner { padding: 2rem 1rem 1.2rem; }
.doc-content { padding-top: 1.8rem; }
}
/* ---- Mermaid diagrams ------------------------------------------------ */
/* ---- Mermaid diagrams ---------------------------------------------------- */
/* Kramdown renders ```mermaid fences as <pre><code class="language-mermaid">.
The script below hands their text to mermaid and swaps in the SVG. Keep
the pre unstyled (no dark code background) while it waits to render. */
.main-content pre.mermaid,
.main-content pre > code.language-mermaid {
the pre unstyled (no code-block background) while it waits to render. */
.doc-content pre.mermaid,
.doc-content pre > code.language-mermaid {
background: transparent;
border: 0;
color: var(--ink);
padding: 0;
}
.main-content pre.mermaid { text-align: center; overflow-x: auto; }
.main-content pre.mermaid:not([data-processed]) { visibility: hidden; }
.doc-content pre.mermaid { text-align: center; overflow-x: auto; }
.doc-content pre.mermaid:not([data-processed]) { visibility: hidden; }
</style>
<script type="module">
@@ -271,7 +246,6 @@
// Move each ```mermaid code block's text into a <pre class="mermaid"> that
// mermaid.run() recognises.
const dark = window.matchMedia("(prefers-color-scheme: dark)").matches;
document.querySelectorAll("pre > code.language-mermaid").forEach((code) => {
const pre = document.createElement("pre");
pre.className = "mermaid";
@@ -282,7 +256,7 @@
mermaid.initialize({
startOnLoad: false,
securityLevel: "loose", // our own docs; needed for <br/> in node labels
theme: dark ? "dark" : "neutral",
theme: "dark",
});
mermaid.run({ querySelector: "pre.mermaid" });
</script>
+48
View File
@@ -0,0 +1,48 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/png" href="{{ '/img/attract-bunker1.png' | relative_url }}">
{%- seo -%}
{%- include head-custom.html -%}
</head>
<body>
<header class="site-masthead">
<div class="masthead-inner">
<a class="masthead-title" href="{{ '/' | relative_url }}">{{ site.title }}</a>
<div class="masthead-tagline">{{ site.description }}</div>
<nav class="masthead-nav">
<a href="{{ '/' | relative_url }}">Home</a><span class="nav-sep">·</span>
<a href="{{ '/internals' | relative_url }}">Internals</a><span class="nav-sep">·</span>
<a href="{{ '/porting-notes' | relative_url }}">Porting notes</a><span class="nav-sep">·</span>
<a href="{{ '/building' | relative_url }}">Building</a><span class="nav-sep">·</span>
<a href="{{ '/dev-process' | relative_url }}">Dev process</a><span class="nav-sep">·</span>
<a href="{{ '/dev/agentic-development' | relative_url }}">Two-agent study</a>
</nav>
</div>
</header>
<main class="doc-content">
{{ content }}
</main>
<footer class="site-footer">
<div class="footer-inner">
<p class="footer-stamp">
Last updated 2026-09-28 — v0.4.0
<span class="nav-sep">·</span>
<a href="https://github.com/jkdansereau/goldeneye-pc-port">GitHub</a>
<span class="nav-sep">·</span>
<a href="https://github.com/jkdansereau/goldeneye-pc-port/releases">Releases</a>
</p>
<p class="footer-legal">
Non-commercial fan preservation/research project. No ROM or game assets
are distributed; you supply a ROM you already own. Not affiliated with
or endorsed by Nintendo, Rare, Microsoft, MGM, Danjaq, or EON
Productions.
</p>
</div>
</footer>
</body>
</html>
+31 -20
View File
@@ -1,20 +1,25 @@
# Building the PC port
---
title: Building
description: Full build and asset-extraction guide for the GoldenEye 007 PC port, covering Windows (MSYS2) and Linux.
---
## Building the PC port
Stages:
1. **Extract assets from your ROM** (§2) — a one-time step using the
1. **Extract assets from your ROM** (§2): a one-time step using the
decompilation's own toolchain to pull levels, models, textures, fonts and
music into `assets/`. *(Only needed to regenerate the committed data files;
a plain `git clone` already has what the PC build compiles.)*
2. **Build the port** (§3) — a CMake build compiling the game sources plus the
2. **Build the port** (§3): a CMake build compiling the game sources plus the
`port/` layer into a native executable. Needs no ROM.
3. **Generate the PC asset sidecars** (§4) — two pure-Python converters turn
3. **Generate the PC asset sidecars** (§4): two pure-Python converters turn
ROM model / stage data into the PC-layout `data/pcmodels-*` / `data/pccg-*`
files the port loads at runtime. **Required to run.**
You need a GoldenEye 007 N64 ROM you legally own (`.z64`, big-endian). See the
[Requirements table in the README](../README.md#requirements) for accepted
versions and hashes.
[Requirements table in the README](https://github.com/jkdansereau/goldeneye-pc-port#requirements)
for accepted versions and hashes.
---
@@ -22,10 +27,11 @@ versions and hashes.
### Port build
> **Only the Windows (MSYS2 MINGW64) path is tested.** The Debian/Ubuntu and
> macOS columns below are best-effort guidance — the port has never been built
> or run on Linux or macOS. Expect to fix build breaks yourself on those
> platforms.
> **The Windows (MSYS2 MINGW64) path is the primary one.** The Linux build is
> compiled by CI on every push and ships in the release bundle (also
> playtested on Steam Deck hardware); the macOS column is best-effort
> guidance and has never been built or run there. Expect to fix build breaks
> yourself on untested platforms.
| Need | Windows (MSYS2 MINGW64) | Debian/Ubuntu | macOS (Homebrew) |
|------|------------------------|---------------|------------------|
@@ -40,12 +46,12 @@ versions and hashes.
The extraction scripts need `binutils-mips-linux-gnu` (or an equivalent MIPS
binutils), `make`, `git`, and `python3`. They build a small host-compiled
`tools/extractor` and slice blobs straight out of the ROM — **no IDO / IRIX
`tools/extractor` and slice blobs straight out of the ROM; **no IDO / IRIX
toolchain is involved in extraction or in the PC build.** (The IDO toolchain is
only needed to build the N64 ROM itself, and its proprietary SGI binaries are
not distributed here — see [`SetupGuide.md`](SetupGuide.md) "Recompile IDO".)
not distributed here; see [`SetupGuide.md`](https://github.com/jkdansereau/goldeneye-pc-port/blob/main/docs/SetupGuide.md) "Recompile IDO".)
On Windows this is easiest under WSL or a Linux VM. Full details and
alternatives (Docker) are in [`SetupGuide.md`](SetupGuide.md).
alternatives (Docker) are in [`SetupGuide.md`](https://github.com/jkdansereau/goldeneye-pc-port/blob/main/docs/SetupGuide.md).
---
@@ -70,7 +76,7 @@ and run:
(US extraction is a prerequisite for the others.)
This populates `assets/` with the generated `.bin` blobs the build needs. See
[`SetupGuide.md`](SetupGuide.md) for the in-depth build/asset pipeline.
[`SetupGuide.md`](https://github.com/jkdansereau/goldeneye-pc-port/blob/main/docs/SetupGuide.md) for the in-depth build/asset pipeline.
---
@@ -103,7 +109,7 @@ builds are named `ge007.pal-final.x86_64` / `ge007.jpn-final.x86_64`.
## 4. Generate the PC asset sidecars (required to run)
The port does **not** read model geometry, stage bg/stan data, or per-level
setup data from the raw ROM at runtime — it reads them from PC-layout *sidecar*
setup data from the raw ROM at runtime; it reads them from PC-layout *sidecar*
files under `data/`, produced offline by three converters. **Without them the
game shows the intro logos and then crashes** in
`modelPromoteNodeOffsetsToPointers` (finding D179) or on the first level load
@@ -125,6 +131,9 @@ python3 tools_pc/d69_emit.py ntsc-final # -> data/pccg-ntsc-final/{pccg
python3 tools_pc/d88_emit.py ntsc-final --regen # appends the 21 per-level Usetup*Z stage-setup files -> ~3.6 MB
```
**PAL / JP note:** sidecar generation for these regions is currently broken at
the source-data level (finding D258); use an NTSC-U ROM until issue #85 lands.
These are **pure-stdlib Python 3** (no MIPS toolchain, independent of the
step-2 asset extraction) and read only the ROM plus files already committed to
the repo (`scripts/filelist.u.csv`, `assets/obseg/file_resource_table.inc.c`,
@@ -133,10 +142,12 @@ deterministic function of the ROM. Re-run after any change to `d43_emit.py` /
`d69_emit.py` / `d88_emit.py` or the model/bg converters (`d43_*`, `d69_*`,
`d88_propdefs.py`).
> The release bundle ships `prepare-assets/prepare-assets.py`, which runs all
> three passes against your ROM automatically — see the bundled `README.md`.
> The release bundle ships the same converter frozen as
> `prepare-assets/ge007-convert`; the game spawns it on first launch when the
> sidecars are missing, so end users never run it by hand. See the bundled
> `README.md`.
> `data/pcmodels-*/` and `data/pccg-*/` are gitignored ROM-derived game data —
> `data/pcmodels-*/` and `data/pccg-*/` are gitignored ROM-derived game data;
> never commit or redistribute them.
---
@@ -157,5 +168,5 @@ runtime. `ge007.ini` is written under `data/` on first launch.
| `-level_NN` | boot straight into a solo level (e.g. `-level_09` = Bunker 1) |
| `GE_PCDUMP="first-last:step"` | dump rendered frames as PPM (debugging) |
More diagnostic switches are catalogued in
[`dev/GE-ENV-PROBES.md`](dev/GE-ENV-PROBES.md).
More diagnostic switches are cataloged in
[`dev/GE-ENV-PROBES.md`](https://github.com/jkdansereau/goldeneye-pc-port/blob/main/docs/dev/GE-ENV-PROBES.md).
+185 -18
View File
@@ -1,32 +1,76 @@
# How this port is developed
---
title: Development Process
description: The investigation workflow behind the port; task budgets, file partitioning, and the finding-log discipline used across sessions and agents.
---
Most of this port's work is not writing code — it is **diagnosis**: figuring
out which N64 hardware or ABI behaviour a piece of unmodified game code is
## How this port is developed
Most of this port's work is not writing code; it is **diagnosis**: figuring
out which N64 hardware or ABI behavior a piece of unmodified game code is
silently relying on, and satisfying it in the `port/` layer without touching
the game's logic. A lot of that diagnosis is done with LLM coding agents, and
the workflow below is what makes that productive rather than chaotic.
For the project context — the two-model split, the timeline, and an honest
assessment of what did and did not work — see
For the project context (the two-model split, the timeline, and an honest
assessment of what did and did not work), see
[`dev/agentic-development.md`](dev/agentic-development.md).
It is adapted from Chris Lewis's write-up
[*"Decompiling a Nintendo 64 Game in 84 Days"*](https://blog.chrislewis.au/decompiling-a-nintendo-64-game-in-84-days/)
(Snowboard Kids, 2026), an AI-assisted decompilation that ran several times
faster than its predecessor. That project bit-matches compiler output;
this one matches runtime behaviour, so the mechanics differ — but the
this one matches runtime behavior, so the mechanics differ; but the
project-structure lessons carry over directly.
### Diagnosis is unrestricted; only the fix is constrained
AGENTS.md non-negotiable #2 restricts *changing* `src/game` behavior. It
has never restricted *investigating* it. An agent that stops short of
naming the exact `src/game` struct, field, or logic responsible for a bug
— because touching that file "feels" off-limits — is doing worse
diagnosis than the rules require, not better compliance with them.
Several of this port's hardest, highest-impact bugs (D209, D210, D255)
were exactly this shape: a `src/game` state variable reading wrong,
root-caused only by tracing fully into the game file that owned it.
Every investigation should end able to say which of three buckets
applies:
- **(a) ABI/layout-class.** The true cause is a struct-layout or
pointer-width misread introduced by the 32→64-bit transition — in a
ROM-serialized record or a live runtime struct/union alike. Fixable
under the narrow exception, entirely inside the game file under
`#ifdef PORT`, no sign-off needed. Check `docs/porting-notes.md` §A1
first (the pattern's tells + worked examples) — this is the most
common shape a "`src/game` behavior looks wrong" report turns out to
have.
- **(b) Port-layer bug that merely manifests through game state.** The
game code is reading state that's correct for what N64 would have
produced; something in `port/` (a shim, fast3d, frame timing, an
uninitialized or mis-ordered port-owned value) is what actually
differs. Fixable entirely in `port/`, no `src/game` touch, no sign-off
needed.
- **(c) Genuine game-logic behavior difference.** The decomp's
byte-identical control flow, given verified-correct inputs, provably
diverges from real N64 behavior. Rare, and requires the rule-2
sign-off procedure (§7 below) before any `src/game` edit.
Bucket (c) is rare by design — most bugs that look like (c) turn out to
be (a) or (b) in disguise — but ruling it in or out requires actually
tracing the state to its source, not stopping at the file boundary.
"This state lives in `src/game`" is a reason to keep tracing, not a
reason to stop.
## 1. Every investigation task gets a visible budget
An open-ended tool — for them the permuter, for us "rebuild and run the game
and stare at a frame dump" — wrecks throughput when an agent will grind it
An open-ended tool; for them the permuter, for us "rebuild and run the game
and stare at a frame dump"; wrecks throughput when an agent will grind it
indefinitely. Giving the agent a **deadline it can see** lets it trade
tool-time against thinking and decide when to give up.
So every investigation brief states:
- a **budget** — "≤ N build → run → inspect cycles" or "~M minutes"; and
- a **budget**; "≤ N build → run → inspect cycles" or "~M minutes"; and
- the **fallback on expiry**: stop, revert any temporary probes, and write up
what was found with an explicit confidence rating. A good write-up of a
half-solved bug is a deliverable, not a failure.
@@ -40,16 +84,17 @@ more room.
Lewis's agents recorded generalisable IDO quirks in a learnings file; later
agents read it and did better. Our equivalent is
[`porting-notes.md`](porting-notes.md) — the recurring N64→PC bug classes,
each entry a terse index into the full [`dev/findings.md`](dev/findings.md)
[`porting-notes.md`](porting-notes.md); the recurring N64→PC bug classes,
each entry a terse index into the full
[`dev/findings.md`](https://github.com/jkdansereau/goldeneye-pc-port/blob/main/docs/dev/findings.md)
log. Every investigation brief links it, and every investigation ends by
appending any new generalisable quirk.
The classes it currently tracks:
- pointer-width struct growth (32→64) in ROM-serialized or pun-allocated
structs — the dominant class;
- 16-byte host `Gfx`/`Vtx` vs 8-byte N64 — reservation and copy sizing;
structs; the dominant class;
- 16-byte host `Gfx`/`Vtx` vs 8-byte N64; reservation and copy sizing;
- big-endian rodata read on a little-endian host: `f32` word-pairs, header
offset tables, packed bitfields;
- N64 hardware idioms the software RSP does not emulate (fill-rect Z clear,
@@ -57,6 +102,18 @@ The classes it currently tracks:
- whole serialized formats are converted by an **offline sidecar** rather than
patched at runtime.
**Two process lessons from the sibling GEVR VR port** (same decomp lineage,
reference-only survey in `scratchpad/GEVR-TRIAGE.md` item D — their corpus
hit both repeatedly enough to name them): (1) **a grep standing in for a
read** — a site list built by grepping one helper name or spelling misses
siblings spelled differently or living in an adjacent file (their own
missed-smoke-scissor-site example); treat a grep-built site list as a draft,
not a census, until it's been read against. (2) **write the falsifier before
the run** — state up front what result would kill the hypothesis ("if X
survives, hypothesis Y is dead"), not just what would confirm it; this fits
the existing budget/findings discipline directly and costs one sentence per
brief.
## 3. Parallelise by file, consolidate often
Lewis ran multiple git worktrees and found the real cost was
@@ -65,7 +122,7 @@ Lewis ran multiple git worktrees and found the real cost was
- **Partition tracks by the files they will touch**, so patches never collide.
A task that can't be cleanly file-partitioned shouldn't be parallelised.
- Each agent reverts its own temporary probes before reporting, leaving only
committed, env-gated, capped diagnostics — and never touches another track's
committed, env-gated, capped diagnostics; and never touches another track's
uncommitted edits.
- One integrator merges each track with its write-up, then the next round
starts from a clean tree. Investigation branches are not allowed to drift.
@@ -79,7 +136,7 @@ Before dispatching an investigation agent:
- capture a baseline frame dump;
- grep the finding log for a prior instance of the same bug class;
- check the [Perfect Dark port](https://github.com/fgsfdsfgs/perfect_dark) for
the analogous code — same engine family.
the analogous code; same engine family.
Then hand the agent the *findings*, not just the problem.
@@ -88,19 +145,129 @@ Then hand the agent the *findings*, not just the problem.
- **The match-percentage loop.** This port doesn't bit-match. "Done" is: no
fault, and visually correct against N64 reference footage.
- **"Understanding the function is the easy part."** True when decompiling;
inverted here — the hard part is identifying the implicit hardware/ABI
inverted here; the hard part is identifying the implicit hardware/ABI
contract the already-readable C depends on.
## 6. Keep PR/git-history noise low
A session produces a lot of small, independently-verifiable units of work;
one Dxx row, one docs pass, one mechanical fix. Left unmanaged that turns
into a pile of small open PRs that's harder to review than the work
justifies. Rules:
- **Squash-merge single-purpose and docs-only PRs.** The repo has squash
merge enabled; use it (`gh pr merge --squash --delete-branch`) for anything
whose in-branch commit history (iteration, probe-add, probe-strip) isn't
itself useful to keep; which is most PRs. Reserve a plain merge commit for
a PR whose individual commits are each a distinct, reviewable unit someone
might want to `git revert` independently (e.g. this session's PR #50: a
fix commit + a separate analysis-only commit).
- **Batch same-session, same-flavor docs updates into one PR/branch**
instead of opening a new PR per finding row. A runtime re-check that
updates one `findings.md` row (like #48) doesn't need its own PR if
another docs-only branch is already open in the same session; fold it in.
Only split into separate PRs when the pieces have genuinely different
review/merge timing (e.g. one needs a human playtest, the other doesn't).
- **A branch takes only the change it's named for.** Don't let unrelated
housekeeping (a stray gitignore fix, a config tweak) ride along on an
active feature/fix branch just because it's the one checked out; cut a
separate branch, even for a one-line change.
- **Stacked PRs declare the dependency in the title/body** ("step 2 of N,
step 1 is #N") and get rebased/merged in order promptly; don't let a
later step sit open so long the earlier step's content drifts under it.
- **Triage the open-PR list at least once a session**, ordered by
time-to-close (docs-only and non-draft first, human-playtest-gated last).
A PR idle long enough that `main` has moved past the finding rows it
touches is **stale, not just old**; check for real textual conflict
(`git merge-tree`), not just calendar age. If `main` already has more
current information than the PR (a bug the PR still lists `OPEN` that's
since been fixed and verified), **close it rather than force a merge or
spend a cycle rebasing it**; re-derive only whatever part of its content
is still true as a fresh, small PR against current `main`. Merging stale
content back over newer information is a regression, not a save.
## 7. The rule-2 sign-off procedure (genuine `src/game` behavior changes)
Use this only for bucket (c) from the diagnosis framing above — never for
the ABI/layout exception (§A1-class fixes need no sign-off, only
documentation) and never for anything expressible in `port/`.
**To request sign-off, the write-up must state:**
1. The exact `src/game` file:line and the specific behavior being
proposed for change.
2. **Proof of divergence.** Given verified-correct inputs — i.e. every
upstream ABI/layout suspect (§A1) and every port-layer suspect has
been checked and ruled out, with the ruling-out cited — the
byte-identical decomp code still produces behavior that differs from
real N64 reference behavior. Name the reference (a capture, documented
prior N64 behavior, a cited external source). "It looks wrong" is not
proof; "it disagrees with `<reference>`, verified by `<method>`" is.
3. Why the fix cannot be expressed in `port/`, and why it does not
qualify under the ABI/layout exception — the two questions that must
be answered "no" before this procedure applies at all.
4. The proposed fix, scoped as tightly as possible to the one named
behavior, plus a same-engine precedent if one exists — the Perfect
Dark port is the standing reference; cite the analogous PD change the
way `docs/dev/WIDESCREEN-FOV-PLAN.md`'s "Rule #2 exception framing"
section did.
**Granting.** The user reviews the above and replies with an explicit
go-ahead in-thread. There is no implicit or inferred sign-off: silence,
"seems reasonable," a prior similar grant, or approval of a *different*
behavior in the same file do not carry over to a new request.
**Recording.** Every sign-off gets its own `docs/dev/findings.md` entry
(next `Dxx` label), tagged **`RULE-2-SIGNOFF`** in its status line
alongside the normal finding classification, and recording: the request
as stated above, the grant (quoted or summarized, with date), and the
resulting diff. Add the label to `findings-index.csv` like any other row,
so `tools_pc/gen_findings_index.py` keeps it discoverable — chat history
is not the record of approval; the finding entry is.
**Constraints that still apply after approval:**
- Still `#ifdef PORT` — the N64 build is untouched, and the `#else` arm
(or the file's un-PORT-guarded default) still matches the original
decomp exactly.
- Still scoped to the one named behavior — a grant is not a standing
license; a different behavior difference in the same file needs its own
request, even if related.
- Still fully documented in the `RULE-2-SIGNOFF` finding entry — that
entry is the permanent record of *why* this specific exception to
non-negotiable #2 exists.
- If the fix would visibly change behavior at *default*
settings/difficulty/region beyond the one bug being fixed, treat that
as a signal the scope drifted mid-implementation — stop and go back for
a fresh sign-off on the wider scope rather than shipping it under the
narrower original grant.
This generalizes the ad hoc precedent in
`docs/dev/WIDESCREEN-FOV-PLAN.md`'s "Rule #2 exception framing" section (a
real, already-granted case, kept as historical record) into a reusable
process; new requests cite this section rather than re-deriving the case
for a process each time.
## Investigation-brief template
```
TASK: <one bug, one subsystem>.
READ FIRST: docs/porting-notes.md; docs/dev/findings.md <specific Dxx entries>.
FILES YOU MAY TOUCH: <disjoint from any other in-flight work>.
KNOWN-GOOD / RULED OUT: <list — do not re-investigate>.
KNOWN-GOOD / RULED OUT: <list; do not re-investigate>.
PRE-FLIGHT ATTACHED: <probe output, baseline capture, PD-port pointer>.
BUDGET: <N build->run cycles / ~M min>. On expiry: revert probes, write up with confidence.
CONSTRAINTS: no game-logic changes; ABI/layout/format only; #ifdef PORT; documented in the finding log.
CONSTRAINTS: diagnosis may trace as deep into src/game state as the evidence
needs — name the exact struct/field/logic responsible before concluding
which bucket applies (see "Diagnosis is unrestricted" above). The FIX is
still constrained: no src/game behavior changes unless (a) it's an
ABI/layout-class struct-layout or pointer-width misread — check
docs/porting-notes.md §A1 first; #ifdef PORT; documented in the finding
log under §F/D3x — or (b) it's fixable entirely in port/ (no src/game
touch). A genuine game-logic behavior difference (bucket c) is NOT
authorized by this brief — stop and write up a rule-2 sign-off request
per docs/dev-process.md §7 instead of applying it.
VERIFY: <the exact tools_pc/verify.sh (or probe) invocation that proves this done; or, if runtime verification is impossible in this environment, say so and name what a human must run>.
REPORT: (a) root cause + file:line evidence (b) fix diff, or why not
(c) probes left in tree (d) confidence.
Append any generalisable quirk to docs/porting-notes.md.
+77
View File
@@ -0,0 +1,77 @@
# Accuracy / code review — N64 fidelity sweep (2026-09-20)
A thorough pass over the PC port with one question: *where does the port diverge from the
N64 original, and what latent failure classes of the D318 kind are still unpriced?*
Method: static review against the decomp ground truth + targeted verification builds.
Findings below; new finding **D320** logged in `findings.md`.
## 1. PRNG — verified bit-exact
`port/src/random.c` re-checked line-by-line against both `src/random.s` and
`src/game/chrObjRandom.s`. The MIPS64 `dsll32`/`dsrl32` semantics (post-D284) are
emulated correctly: the 32-bit LFSR state advances identically, and
`randomGetNext()` returns the same 31-bit values in the same order. No divergence.
## 2. Known bug-class sweeps — all clean or already fixed
| Class | Result |
|---|---|
| **A1** raw-byte/padding aliases (32→64-bit layout shift) | Remaining `padding[]` uses are in pointer-free structs (layout-stable) or already fixed (D115, D209, D312). No live misreads. |
| Shifts by 32 / pointer-width arithmetic in game logic | Only port-layer code (`d318watchdog` print, `DRAM_SIZE`). Game logic clean. |
| Signed-overflow UB | `-fwrapv` enabled in `CMakeLists.txt` — the whole class is neutralized to N64-wrap semantics. |
| `#ifdef PORT` hygiene (N64 path preserved) | 307 hunks audited; 178 are pure additions (diagnostics / struct extensions). No hunk alters the non-PORT control flow. |
| **§B** Gfx/Vtx 16-byte records (raw byte indexing) | `bg.c` uses typed `.words`/`.dma` fields throughout; no remaining raw byte offsets. D312's fix pattern is the last of this class. |
| `osVirtualToPhysical` call sites (88) | All flow through fast3d's `seg_addr()` (D131 fix intact). No site assumes N64 segment RAM layout. |
| Dead code | `objecthandler_2.c` compiles but is not linked on PC (symbols absent from the binary). Harmless; noted for a future source-list prune. |
## 3. Timing layer — intentional, documented
Frame-locked virtual clock (vsync tick), `GE_DETERM` fixed-tick mode, and
`g_GlobalTimerDelta` zero-guarded at every division site. All deliberate port-layer
design, each documented in its finding. No hidden drift source found beyond the
already-fixed D204 tempo work.
## 4. D318-class latent softlock sweep → **D320** (the main new result)
D318's mechanism is generic, not Facility-specific: `actor_fire_or_aim_at_target_update`
(`chraction.c:4882`) retargets any ACT_ATTACK chr with an AIM_ONLY/DONTTURN attacktype and
only moves the model's **endframe** — it never re-inits the animation. If the chr is pinned
on a pre-shoot aim-hold pose (hold endframe H < variant shoot window) and the new endframe
lands above H, the model pins forever and the timer-gated fire events never open; if the
surrounding AI script's only escape is an `if_guard_has_stopped_moving` gate, it can never
pass (`chrHasStoppedOrPatroling` is FALSE for ACT_ATTACK). **Permanent softlock — on N64 too.**
Swept every level's AI scripts (`assets/obseg/setup/*.c`): 17 lists in 8 levels contain an
aim-hold + `_update` pair. Risk-ranked table in the D320 entry. Headlines:
- **Facility ai_19** — HIGH. Same non-self-healing shape as ai_22 (second softlock point in
the same level).
- **Control ai_9** — HIGH. Near-exact ai_22 mirror (pre-aim hold → timer → update → fire
loop gated on the target dying from this guard's shot).
- **Depot ai_12** — MED-HIGH. The kill is scripted, but the objective bit is gated behind a
stop-check; pinned guard → level-complete softlock.
- **Bond-combat loops** (Aztec/Control/Egyptian/Depot/Streets/Bunker/Surface) — likely safe:
sustained fire keeps the model inside the shoot window by design, and every playtest
exercises them without freezes. Inferred, not probe-verified per list.
Classification: **as-authored latent race(s), faithfully reproduced** — same bucket as D318,
not a port bug. Verification path (generalized timeline probe) and the
generalize-the-watchdog recommendation are in D320.
## 5. Fixed this session
- `port/fast3d/gfx_pc.cpp:2417` — the GE_D116 diagnostic printf used `%f` for `uint32_t`
args (`v->u`, `tex_width`) and `%d` for unsigned tile fields; wrong debug output only, no
runtime impact. Casts added; build now warning-free on that file.
## 6. Verification ritual
Full `./build-pc.sh ntsc-final` configure + compile + link: **PASS**, no warnings on
touched files. All new/changed symbols resolve exactly once (link is the sweep). D318
watchdog/timeline symbols confirmed present in the binary.
## 7. Deliberately not committed (release-prep WIP, separate workstream)
`.github/release-notes.md`, `README.md`, `docs/index.md`, `docs/media/goldeneye-gh-preview.gif`,
`docs/dev/agentic-development.md`, `.gitignore` (`/local/` exclude), and non-D318 scratch
artifacts — left in the working tree for their owner.
+170 -343
View File
@@ -1,375 +1,202 @@
# Audio Plan — Phase 3 (libaudio → SDL)
# AUDIO-PLAN — Phase 3 / ROADMAP B3 (libaudio → SDL)
Status: **draft for evaluation** (written read-only; no code changes made).
Supersedes the audio paragraphs of §6 in `docs/internals.md` as the plan of record
for closing **D77** (no audible music/sound on PC). Companion docs: `docs/HANDOFF.md`,
`docs/porting-notes.md`.
Status: plan of record for the audio track, 2026-09-05. Companion to
`ROADMAP-1.0.md` §B3 ("audio (Phase 3)", "audio track parallel from M-50 →
v0.4.0"). Independent of B1/B2 — can start immediately.
**Status update (M-69):** Phases 0–2 below are DONE — the mixer landed as
D199 (`port/src/mixer.c` macro-swap), D200/D201 fixed the two pointer-width
landmines it exposed, and D204 fixed a permanent ~2 % tempo drift. Phase 3
(verification) is now the active track: D202's sample-correctness question is
closed negative through M-69 (full-corpus per-voice exact match — 286/320
requests CC ≥ 0.85, every allocated voice plays the exact ROM sample at the
exact ROM pitch, no corruption). Remaining: user by-ear pass + N64 A/B on the
door-loop cap (M-66b), then D202 probe removal + close; reverb/mix-balance
tuning is still owed. See findings.md D199–D204.
---
## Contents
## Where things stand (verified 2026-09-05)
1. [Goal and scope](#1-goal-and-scope)
2. [Current state](#2-current-state-verified-by-reading-not-assumption)
3. [The GE pipeline in one paragraph](#3-the-ge-pipeline-in-one-paragraph)
4. [How the PD port does it](#4-how-the-pd-port-does-it-ground-truth-comparison)
5. [Architecture options](#5-architecture-options-evaluate-all-recommendation-marked) — options A/B/C + sub-decisions A1–A3
6. [DMEM layout](#6-dmem-layout-ido-from-srclibultraaudiosynthinternalsh5771)
7. [Shims and ABI fixes required](#7-shims-and-abi-fixes-required) — incl. [7.1 `K0_TO_PHYS` bug](#71-bug-found-k0_to_phys-is-unshimmed-and-corrupts-pointers-on-pc)
8. [Phase 0 — recon](#8-phase-0--recon-do-first-informs-everything)
9. [Open questions](#9-open-questions-decide-before-phase-1)
10. [Phases and milestones](#10-phases-and-milestones)
11. [Verification ritual (audio-specific)](#11-verification-ritual-audio-specific-on-top-of-the-standard-one-in-agentsmd)
12. [Risks / unknowns](#12-risks--unknowns)
13. [Key references](#13-key-references)
More is already in place than "audio not started" suggests:
---
- **The whole CPU-side audio engine already compiles and runs.**
`src/audi.c`, `src/snd.c`, `src/music.c` are in `SRC_ENGINE`; IDO libaudio
(`src/libultra/audio/*.c`) + Rare's naudio "New" driver
(`src/libultrare/audio/*.c`: drvrNew/env/reverb) are in the link. The
`amMain` audio thread runs per VI-retrace; D54/D54b already killed its
SIGSEGVs — so synthesis bookkeeping is live (D77: "music runs in code but
no audible output").
- **The scheduler side needs zero changes.** `osSpTaskStartGo`
(`port/src/libultra.c:1237`) already has an M_AUDTASK branch that completes
the task and posts `OS_EVENT_SP`, so `amMain`'s reply-message handshake
works. The branch is currently a no-op — exactly where the audio ucode's
job used to go.
- **The two TODO stubs are the actual gap:** `osAiSetNextBuffer()` /
`osAiGetLength()` in `port/src/libultra.c:852-862` discard everything
(`osAiGetLength` returns 0), and `port/src/audio.c` never opens an SDL
device. `main.c:141-142` already calls `audioInit()` / `mixerInit()`.
- **The PD reference is local** at `C:\Users\james\Source\Repos\pd_port`,
with the full audio stack: `port/src/audio.c` (75 lines, SDL device +
queue), `port/src/mixer.c` (~720 lines, software RSP-audio-ucode), and the
wiring trick in `include/PR/abi.h`.
## 1. Goal and scope
## Architecture — PD's macro-swap trick, adapted
Make GoldenEye's audio audible on the PC port with 1:1 fidelity to the N64 pipeline:
music (3 cseq tracks) and SFX through the real libaudio synth, out of SDL2.
On N64, `alAudioFrame()` only *builds* an acmd list; the RSP audio ucode
(`aspMain`) executes it. PD's PC port never runs that ucode: under
`#ifndef PLATFORM_N64`, `include/PR/abi.h` swaps each acmd-building macro
`aXxx(pkt, ...)` for a call to a CPU implementation (`aXxxImpl`) operating on
a software DMEM array. libaudio/naudio compile unmodified; every "command"
executes inline at build time; the M_AUDTASK that reaches the scheduler is an
empty formality (PD's `pdsched.c` audio branch is literally empty).
In scope:
- Executing the per-frame **Acmd stream** that stock IDO libaudio emits (the part that
ran on the RSP under GE's `aspMain` microcode).
- SDL audio device + queue plumbing (`port/src/audio.c`, AI shims in `libultra.c`).
- One latent ABI fix in the port shim headers (`K0_TO_PHYS`, §7.1).
**Critical catch — GE and PD use different acmd ABIs** (the AGENTS.md
"same family ≠ identical format" case):
Out of scope:
- MP3 playback (naudio-only, PD-specific; GE never emits it).
- Input (done — D118), saves (Phase 4).
- Any change to game logic. All work lives in `port/` except the narrow, documented
shim-header exception in §7.1 (same class as D78).
## 2. Current state (verified by reading, not assumption)
Working already:
- **The whole CPU side of libaudio runs.** `src/libultra/audio/*` +
`src/libultrare/audio/{drvrNew,env,reverb}.c` compile and execute; the audio thread
(`amMain`, `src/audi.c`) gets retrace messages from the real scheduler (`src/sched.c`,
compiled — Phase 2 decision), calls `alAudioFrame()`, which fills a 3000-word Acmd
list and posts an `M_AUDTASK` to the scheduler. Bank trees, cseq fixups (D54, D36/D37)
are done. Sample DMA from cart works via `amDmaCallback` → `osPiStartDma`
(synchronous ROM-image shim in `libultra.c`).
Dead (the three gaps):
1. **Acmd list never executed.** `port/src/libultra.c:1108` `osSpTaskStartGo()` has an
explicit Phase-3 no-op branch for `M_AUDTASK`. The output buffer (`info->data`) is
never filled.
2. **AI shims are stubs.** `libultra.c:794–810`: `osAiSetNextBuffer` discards the
buffer, `osAiGetLength` returns 0 (so the per-frame sample-count feedback loop in
`amHandleFrameMessage`, `audi.c:527`, runs blind).
3. **No SDL device.** `port/src/audio.c` `audioInit()` is a TODO stub; and nothing
calls `audioEndFrame()` (zero callers repo-wide).
Existing PORT guards to revisit when audio lands:
- **C7 / D127** (`src/snd.c:859`): skips SFX whose resolved `ALSound*` pointer is out of
range — added because the converted bank tree (`romdata.c afFixupInst`) yields fewer/
rearranged `soundArray` slots than the N64 image on some levels (Surface1). May be
masking a real bank-tree defect. See **Q1**.
- `src/audi.c:365`: `#if defined(PORT)` memcpy of `CUSTOM_FX_PARAMS_N` (C++-style
initializer workaround) — harmless, keep.
## 3. The GE pipeline in one paragraph
Audio thread on each retrace: `amHandleFrameMessage()` (`src/audi.c:497`) hands the
*previous* frame's output to AI via `osAiSetNextBuffer(lastInfo->data, …)`, computes
this frame's sample count from `g_FrameSize - osAiGetLength()>>2` (clamped to
`g_MinFrameSize`), calls `alAudioFrame()` which walks the filter chains (decoder →
resampler → envmixer → [FX: GE's custom chorus/reverb] → aux bus → main bus → save)
emitting **Acmd words** into a 3000-word list, then posts an `M_AUDTASK` (ucode =
`aspMain`). The real scheduler (`sched.c:439,485`) queues it in the audio list and runs
it through `osSpTaskLoad/osSpTaskStartGo`. On the N64 the RSP executes the Acmd stream
against 4 KB of DMEM, writing interleaved s16 stereo to `info->data`; AI DMA then plays
it at 22050 Hz. On PC we must replace "RSP executes Acmd stream" — that is the entire
problem.
**Opcode inventory** (grep-verified across `src/libultra/audio/*.c` +
`src/libultrare/audio/*.c` — 15 ops):
| Op | Emitted by | PD reference code? |
| | GE (`include/PR/abi.h`) | PD (naudio "New") |
|---|---|---|
| `aSetBuffer` (×20) | load, mainbus, resample, save, reverb | yes (`a*Impl` pattern; different arg ABI) |
| `aMix` (×7) | mainbus, reverb | yes — `aMixImpl` |
| `aLoadBuffer` (×7) | load, reverb | yes — `aLoadBufferImpl` |
| `aClearBuffer` (×7) | auxbus, mainbus, load, reverb | yes — `aClearBufferImpl` |
| `aSetVolume` (×5) | env.c (Rare envelope) | yes — `aSetVolumeImpl` |
| `aSaveBuffer` (×4) | save, reverb | yes — `aSaveBufferImpl` |
| `aDMEMMove` (×4) | load, resample, reverb | yes — `aDMEMMoveImpl` |
| `aResample` (×2) | resample.c, reverb.c | yes — `aResampleImpl` (SSE4.1/NEON/scalar) |
| `aLoadADPCM` (×2) | load.c, reverb.c | yes — `aLoadADPCMImpl` |
| `aSetLoop` | load.c:479 | yes — `aSetLoopImpl` |
| `aSegment` | synthesizer.c:231 | n/a (no-op on CPU) |
| `aPoleFilter` | **reverb.c:402 (GE custom FX)** | **NO — PD's stub is empty ("never gets called?")** |
| `aInterleave` | save.c | yes — `aInterleaveImpl` (naudio variant takes no args) |
| `aEnvMixer` | env.c:410/414 | yes — `aEnvMixerImpl` |
| `aADPCMdec` | load.c:483 | yes — `aADPCMdecImpl` (SSE4.1/NEON/scalar) |
| `aADPCMdec` | `(pkt, flags, statePtr)` — params from an `aSetBuffer` context + `ADPCM_STATE[16]` | `(pkt, state, flags, count, inofs, outofs)` — inline args |
| `aLoadBuffer` / `aSaveBuffer` | `(pkt, dramAddr)` only — dest/count from the `aSetBuffer` context | explicit dest + count args |
| `aInterleave` | `(pkt, lDmem, rDmem)` | no args |
| Opcodes | 16: adds `aSetBuffer`, `aPan`, `aSegment`; no `aDisable` / `aPlayMP3` | 15 incl. MP3 (PD-specific) |
PD reference = the Perfect Dark port `port/src/mixer.c` +
`port/include/mixer.h`. Reusable: the DSP *math* (ADPCM 8-coefficient loop filter,
resample table + interpolation, envelope ramping, clamps). **Not** reusable as-is:
signatures and argument conventions (naudio ABI ≠ IDO ABI, §5.3).
GE's context model (from the call sites in `src/libultra/audio/`):
`aSetBuffer(flags, dmemin, dmemout, count)` sets a working-buffer context;
subsequent `aLoadBuffer(dramAddr)` / `aADPCMdec(flags, statePtr)` /
`aResample(flags, pitch, statePtr)` operate within it; the tail is
`aInterleave(AL_MAIN_L_OUT, AL_MAIN_R_OUT)` → `aSaveBuffer(f->dramout)`.
## 4. How the PD port does it (ground-truth comparison)
So this is **not** "copy `mixer.c`": port PD's mixer as the *DSP algorithm
reference* (ADPCM decode, linear resample, mix math, env-mixer / pole-filter
are the same Rare/Nintendo DSP blocks) and write GE-ABI wrappers that
maintain the `aSetBuffer` context and read params from the state structs.
1. naudio is Rare's own library; its `n_abi.h` has a `#ifndef PLATFORM_N64` branch that
redefines every `n_a*` macro as a **direct C call** into `port/src/mixer.c`
(`mixer.h` `#undef`s the macros and redirects to `a*Impl`). All DSP therefore runs
**inline on the audio thread at task-build time**, inside `amgrFrame()`.
2. The `M_AUDTASK` is an empty shell: `pdsched.c:233` receives it with the comment
`// send this into the mixer` and an empty body.
3. PD **replaced the scheduler** (`pdsched.c`) and drives audio from retrace:
`schedAudioFrame()` → `amgrFrame(); audioEndFrame();` (×`diffframe60`). Same thread
for build and flush ⇒ no locking in their SDL handoff.
4. SDL device layer: queue-based, exactly what our scaffold already copied —
`osAiGetLength` = queued **bytes** (`audioGetBytesBuffered()`),
`osAiSetNextBuffer` = stash pointer, `audioEndFrame` = `SDL_QueueAudio` with a
`queueLimit` backpressure drop. Device opened at 22020 Hz (their TODO admits this
"might cause trouble" — it is a silent −9-cent detune vs their 22050 synth rate).
5. DMEM: ~3 KB static buffer, **naudio-specific** offsets (main L/R @0x4E0/0x650,
aux @0x7C0/0x930) — different from IDO's layout (§6).
6. PD-only extras: `aPlayMP3Impl` (minimp3; GE never emits), `aPoleFilterImpl` empty
stub (GE *does* emit it).
**GE's full emittable opcode set** (static audit of every call site in
`src/libultra/audio/*.c` + `src/libultrare/audio/*.c`): ClearBuffer,
LoadADPCM, DMEMMove, SetBuffer, LoadBuffer, SetLoop, ADPCMdec, Resample,
Interleave, SaveBuffer, Mix, SetVolume, EnvMixer, PoleFilter, Segment — plus
`aPan`, defined in abi.h but apparently uncalled. PD's mixer covers the DSP
core for all of them except the trivial bookkeeping ones: SetBuffer /
LoadBuffer / SaveBuffer are context tracking + memcpys; Segment is a no-op on
PC (`amDmaCallback` already resolves ROM addresses against the mapped image —
`osVirtualToPhysical` is identity, `libultra.c:1295`).
**Why we can't copy it wholesale:** GE compiles the real `src/sched.c` and stock IDO
libaudio, whose macros only *write words* into the Acmd list — there is no build-time
hook unless we shadow `PR/abi.h`. Something must execute the stream. Two further ABI
facts:
## Phases
- **IDO's implicit "current buffer":** e.g. `aADPCMdec(pkt, f, s)` carries only flags +
a state pointer; in/out DMEM offsets and sample count come from the preceding
`aSetBuffer` (`load.c:482–483`, `resample.c:90–91`). naudio spells everything out per
call. Whatever option we pick, the implementation must model current-buffer state.
- **Addresses in the stream are RAM pointers** (`K0_TO_PHYS(f->state)`,
`osVirtualToPhysical(e->state)`, `f->dramout`) — resolvable on PC *except* for the
`K0_TO_PHYS` bug in §7.1.
### Phase 0 — Ground truth (research, ~half day, delegable)
## 5. Architecture options (evaluate all; recommendation marked)
1. **Disassemble GE's `aspMain` microcode from the ROM** and document each
opcode's exact semantics in findings.md §F (new Dxx): flag-bit meanings
(`A_FIRST`, `A_CONTINUE`, …), count units (bytes vs samples), where the
interleaved output lands before `aSaveBuffer`, how `aSetLoop` state gets
restored, `ENVMIX_STATE[40]` layout. This is the audio analogue of using
`rsp/graphics/gmain.s` as GBI ground truth — we don't run the ucode, but
it is the spec. (`port/src/ucode.c`'s header already flags this "DECISION
NEEDED in Phase 3"; CPU-bypass means the dummy `aspMainTextStart` /
`aspMainDataStart` markers stay forever — document that decision there.)
2. **Opcode inventory probe** (port-only, zero game-code change): a
`GE_AUDIOTRACE` env var in the existing M_AUDTASK branch of
`osSpTaskStartGo` that walks `t->t.data_ptr` for `data_size/8` words and
histograms `w0 >> 24`. Confirms the static call-site audit and catches
anything emitted dynamically.
### Option A — CPU-side Acmd interpreter at task time 【RECOMMENDED】
### Phase 1 — Device plumbing (small, lead work)
Decode the 64-bit command words from `t->t.data_ptr` inside the existing `M_AUDTASK`
branch of `osSpTaskStartGo()` (`libultra.c:1108`), dispatching each op to an
implementation in `port/src/mixer.c` operating on a static 4 KB "DMEM".
- Implement `port/src/audio.c::audioInit`: SDL device at **22050 Hz** (GE's
`OUTPUT_RATE = 0x5622`, not PD's 22020), `AUDIO_S16SYS`, stereo. Keep the
existing `Audio.BufferSize` / `Audio.QueueLimit` config registrations.
- Wire `osAiSetNextBuffer` → `SDL_QueueAudio` guarded by `queueLimit`;
`osAiGetLength` → `SDL_GetQueuedAudioSize()`. Faithful mapping: N64's
`AI_LEN_REG` ("bytes synthesized but not yet played") ≙ SDL queued-bytes,
and the game's self-tuning `frameSamples = g_FrameSize −
osAiGetLength()>>2` math (`audi.c:527`) keeps the virtual FIFO at ~736
samples (NTSC) with no game-code changes. Queue inline in
`osAiSetNextBuffer` (same thread, once per frame) rather than PD's deferred
`audioEndFrame`. Overflow safety is already self-limiting: a full queue
makes `frameSamples` clamp to `g_MinFrameSize`, and the `queueLimit` check
drops rather than crashes.
- Add a `GE_AUDIODEBUG` probe logging queue depth + synthesized samples every
60 frames; catalog it in `docs/GE-ENV-PROBES.md`.
- **Exit:** links 243/243, `/linkcheck` clean; headless `-level_09` 60 s
crash-free; debug probe shows the queue filling while intro music "plays"
(still silent — acmds don't execute yet).
- Pros: zero game/header/build changes; treats the stream as data exactly like fast3d
does with GBI (project-consistent); natural logging hook (`GE_DAUDIO=1` dump of op +
args per frame) for debugging and for validating against the microcode disassembly;
execution point matches N64 timing (DSP runs when the RSP would have run it).
- Cons: must write the word decoder (~50 lines, trivial — op is bits 24–31 of w0 per
`include/PR/abi.h`) and the current-buffer state machine; one scheduler tick of extra
latency vs build-time execution (inaudible).
### Phase 2 — The mixer (core work, ~1–3 days)
### Option B — Shadow `PR/abi.h`, redirect macros to direct C calls (the PD trick)
- Port PD's DSP cores into `port/src/mixer.c`: ADPCM decode (coefficient-book
handling via `aLoadADPCM`), linear resampler, stereo mix with `0x8000`
scaling, env-mixer + pole-filter (reverb/chorus driven by
`CUSTOM_FX_PARAMS_N`, `audi.c:169`), set-volume ramps. Add the GE-ABI
context layer: a static `{dmemin, dmemout, count}` updated by
`aSetBuffer`, consumed by LoadBuffer / SaveBuffer / ADPCMdec / Resample; a
shared software DMEM array across Impls. Replace the current 31-line stub
interface (`port/include/mixer.h`) with PD-style `aXxxImpl` declarations.
- Edit `include/PR/abi.h` with an `#ifdef PORT` split (N64 path
byte-identical): PC includes a port `mixer.h` whose macros match **GE's**
arg arities. This is the one non-`port/` edit — same documented
ABI/layout-exception class as D78 / D135, logged in §F with its own label.
- Unknown / unused opcodes (`aPan`, `aSegment`): log-and-no-op under
`GE_AUDIOTRACE`; implement only if Phase 0 shows them live.
- All DSP runs on the existing `amMain` thread inside `alAudioFrame` — no new
locks, no scheduler changes.
- **Exit:** opcode histogram fully covered; headless run clean; queue probe
non-zero.
Add `port/shim/PR/abi.h`: `#include "include/PR/abi.h"` then `#undef` the 15 `a*`
macros and `#define` them as direct calls into `mixer.c` (PD's `mixer.h` does exactly
this for naudio). DSP then runs inline during `alAudioFrame()` on the audio thread; the
`M_AUDTASK` branch stays a no-op, exactly like PD.
### Phase 3 — Verification & tuning (user-gated listen tests)
- Pros: battle-tested pattern (shipped in PD); no decoder; slightly less latency.
- Cons: shadows a core PR header for every TU that includes it (only libaudio uses these
macros on PC, so blast radius is small but nonzero — must verify include-path
ordering in `CMakeLists.txt`); stream becomes invisible at runtime (no logging hook —
you'd instrument inside each Impl); the shim must exactly mirror IDO arg order, which
differs from PD's naudio redirects line-by-line.
- Listen checklist: menu music → briefing → in-level music transitions →
gunfire / explosions → **positional SFX** (walk around a source) → weapon
one-shots → level changes → **mission-fail fade** (the D152 `s_imLock`
scenario — retest killing Trevelyan in Facility with live audio, since the
fade hammers the event queue; the D152 self-healing lock mitigation is
already in place but must be re-proven under load).
- Compare reverb / chorus character against N64 reference footage (that's
what `CUSTOM_FX_PARAMS_N` is for).
- Regression: `GE_PCDUMP` framediff vs goldens unchanged (audio must not
touch rendering) + 60 s `-level_09` soak.
- Docs: findings §F entry, porting-notes quirk ("classic-libultra vs
naudio-New acmd ABI delta"), ROADMAP B3 status, HANDOFF update.
### Option C — Run the real `aspMain` microcode 【NOT RECOMMENDED】
## Risks
Point `aspMainTextStart/DataStart` (`port/src/ucode.c`) at the ROM microcode and execute
it with an RSP emulator. Maximum fidelity by construction, but requires a 16-bit-MIPS
RSP core + DMEM + interrupt model for one component whose entire job is the 15 ops we
can implement directly. Keep the `ucode.c` dummies permanently (their header comment
anticipates this decision). The microcode is still *used* — as a **reference to
disassemble**, not to run (§8, Phase 0).
1. **Ucode-semantics mismatch** (count units, flag bits) → garbled or
wrong-tinted audio, not crashes; mitigated by Phase 0 disassembly +
iterative listen checks. Main schedule risk.
2. **Timing coupling with B1** (D193 family): low — audio frame size is
feedback-driven off `osAiGetLength`, robust to scheduler jitter; re-verify
after B1 lands.
3. **Performance:** ~736 samples/frame of CPU DSP is trivial on x86-64 (PD
shipped exactly this).
4. **D152 lock storm under live audio** — covered by the explicit Phase 3
retest.
### Sub-decision A1 — where samples hit the SDL queue 【RECOMMENDED: immediate】
## Delegation shape & effort
- **Immediate:** `osAiSetNextBuffer()` shim calls into `audio.c` and `SDL_QueueAudio`s
right there (on the audio thread), with the existing `queueLimit` backpressure drop.
Lock-free (single writer); matches the moment N64 AI DMA starts; `audioEndFrame()`
disappears entirely. Safe because `NUMBER_OUTPUT_BUFFERS=3` rotation guarantees the
handed-off buffer isn't rewritten before it is queued.
- **Deferred (PD-style):** stash in `osAiSetNextBuffer`, flush from a frame-end hook.
In GE this crosses threads (audio thread sets; scheduler/gfx path would flush) ⇒ needs
a mutex or accepts PD's unlocked pattern, and needs a new `audioEndFrame()` call site
that doesn't exist today. Only worth it if immediate queueing shows a problem.
Per `docs/dev-process.md`:
### Sub-decision A2 — device sample rate 【RECOMMENDED: 22050】
- Phase 0 disassembly → Claude research subagent (judgment-heavy, read-only +
findings write-up).
- Phase 1 → lead or mechanical worker (build-verifiable).
- Phase 2 DSP-core port → candidate for the local Qwen once the semantics doc
exists (mechanical, diff-checkable against PD), with the ABI mapping and
review kept by the lead.
- Phase 3 listen tests → user.
Request exactly `OUTPUT_RATE` (0x5622 = 22050 Hz), stereo s16, as PD's `audio.c`
scaffold already comments. Do **not** copy PD's shipped 22020 (detune). If a platform
rejects 22050, let SDL convert (`SDL_OpenAudioDevice` with allowed-conversion) rather
than changing the requested rate.
Total ~3–6 focused sessions. Milestone: ROADMAP "audio track (parallel from
M-50) → v0.4.0".
### Sub-decision A3 — timing and thread priorities 【RECOMMENDED: inline + host priorities; no worker thread】
## Key file map
Execution stays on the scheduler thread (Option A's execution point). If Phase 2/3
milestones show underruns (`audio: ai out of samples` spam — verification step 3) or
frame-time spikes attributable to synthesis cost, the **first lever is host thread
priorities**: `SetThreadPriority` / `pthread_attr_setschedparam` with the scheduler
thread time-critical (it now owns both fast3d and the mixer) and the tick thread
normal — a ~10-line fix in `libultra.c`. A dedicated audio worker thread consuming
`M_AUDTASK`s is explicitly **not** the next step: it adds a cross-thread handoff for a
problem priorities may fully solve, and the kernel's priority semantics are already
fake (D24 — see the backlogged bug-class entry in `docs/BACKLOG.md`), so any deeper
redesign starts there. Do **not** consider rewriting toward PD's single-threaded
inline-`amgrFrame()` model: it works for them only because they adapted game-side lib
code (direct `schedSubmitTask` calls, gutted `pdsched.c`); for GE it means editing
`src/audi.c` / scheduler submission points — a non-negotiable #2 violation. Rationale
recorded in `docs/BACKLOG.md` (Architecture decision).
## 6. DMEM layout (IDO, from `src/libultra/audio/synthInternals.h:57–71`)
```
AL_MAX_RSP_SAMPLES = 160 (per-chunk; alAudioFrame builds the list in ≤160-sample chunks)
offset 0 AL_DECODER_IN / AL_RESAMPLER_OUT / AL_TEMP_0 (320 B)
offset 320 AL_DECODER_OUT / AL_TEMP_1 (320 B)
offset 640 AL_TEMP_2 (320 B)
offset 1088 AL_MAIN_L_OUT (320 B)
offset 1408 AL_MAIN_R_OUT (320 B)
offset 1728 AL_AUX_L_OUT (320 B)
offset 2048 AL_AUX_R_OUT (320 B) → max extent 2368 B
```
Static 4 KB buffer in `mixer.c` (round number, headroom). **Not** PD's naudio offsets.
GE's custom FX (`reverb.c`) uses `AL_TEMP_0/1/2` as scratch plus RAM-backed delay lines
via `aLoadBuffer`/`aSaveBuffer` to `r->base` — all plain host pointers on PC.
## 7. Shims and ABI fixes required
### 7.1 【BUG FOUND】 `K0_TO_PHYS` is unshimmed and corrupts pointers on PC
`port/shim/PR/R4300.h` redefines `PHYS_TO_K0` (identity) but **not** `K0_TO_PHYS`.
Stock: `#define K0_TO_PHYS(x) ((x)&0x1FFFFFFF)` (`include/PR/R4300.h:65`). Game RAM
lives at the V1 view `0x70xxxxxx` (`port/src/dram.c`); masking gives `0x10xxxxxx` — an
invalid host pointer. Hit sites: `load.c:483` (`aADPCMdec(..., K0_TO_PHYS(f->state))`)
and `load.c:479` (`aSetLoop(ptr++, K0_TO_PHYS(f->lstate))`) — the mixer would receive
garbage state addresses for every ADPCM voice. Fix: extend the existing shim with
`#undef K0_TO_PHYS / #define K0_TO_PHYS(x) ((u32)(x))` (identity; V1 addresses are live
host pointers). Inert in the N64 build (no `-DPORT`) — same class as the D78 precedent.
Record as a new Dxx finding when implemented (next free label after D127).
### 7.2 AI shims (`libultra.c:794–810`)
- `osAiSetFrequency(hz)` → return `hz` (already does; 22050 flows into
`alconf->outputRate` and the frame-size math — correct).
- `osAiSetNextBuffer(buf, size_bytes)` → queue to SDL per A1.
- `osAiGetLength()` → **bytes** currently queued (`SDL_GetQueuedAudioSize`); the game
does `>> 2` for frames (`audi.c:527`). PD returns bytes too. This closes the
feedback loop that sizes each frame's synthesis.
- `osAiSetConvert` — not called by GE; leave stub.
### 7.3 Misc
- `port/src/audio.c`: fill in `audioInit()` (SDL_InitSubSystem(AUDIO), open device per
A2, unpause); add the queue function per A1; keep `Audio.BufferSize`/`Audio.QueueLimit`
config entries; add an `Audio.Enabled` toggle (PD's `g_SndDisabled`) so the level
sweep can run silent.
- `port/src/mixer.c`: rewrite as the op implementations + (for Option A) the decoder /
current-buffer state machine. `mixerInit/Destroy` reset DMEM + op state.
- Threading note: mixer runs on the scheduler thread (`sched.c __scMain` →
`osSpTaskStartGo` inline); with A1 there is exactly one shared object (the SDL queue,
internally locked) — no port-side locks needed.
## 8. Phase 0 — recon (do first; informs everything)
1. **Disassemble `bin/aspboot.text.bin`** (+ `.data.bin`; markers in `src/aspboot.s`) —
GE's real ASP microcode, 16-bit MIPS RSP. This is the ground truth for:
- exact `aSetBuffer` current-buffer semantics (which subsequent ops consume it, and
how),
- `aADPCMdec` state layout (what the pointer in w1 points at — IDO `ADPCM_STATE` +
data addressing),
- **`aPoleFilter`** — the one op with no PD reference code; implement from this,
- any DMEM convention not derivable from libaudio usage.
Methodology mirrors how `rsp/graphics/gmain.s` is used for graphics. A small
16-bit-MIPS disassembler script (`tools_pc/`) is sufficient — we decode, not execute.
2. **Acmd stream logger** (`GE_DAUDIO=1`, in the Option-A decoder or a temporary probe
in the `M_AUDTASK` branch): dump op + decoded args per frame for the first N frames.
Confirms the 15-op inventory at runtime, catches surprises (e.g. ops we didn't grep),
and gives before/after artifacts for validating the implementation.
## 9. Open questions (decide before Phase 1)
- **Q1 — C7/D127 bank-tree guard (`src/snd.c:859`):** root-cause the bogus
`ALSound*` resolutions now (likely an `afFixupInst` layout/stride defect in
`port/src/romdata.c`, same family as D37's bank-tree re-layout), or keep the guard as
a documented stopgap and file a separate finding? Keeping it means some SFX stay
silently missing on affected levels (Surface1) even after audio lands.
- **Q2 — Option A vs B** (§5): A is recommended (no header shadowing, stream logging,
project-consistent with fast3d); B is the PD-proven fallback if current-buffer
semantics prove fiddly at task time. Note for the evaluator: this is a **low-stakes,
cheap-to-revisit decision** — both options share 100% of the DSP implementation code
(§6 layout, all op bodies); only the ~50-line front end (word decoder vs macro
redirects) and the execution point differ. If Phase 0's disassembly surfaces anything
that makes B look safer, switching costs an afternoon, not a re-architecture.
- **Q3 — Pole-filter fidelity bar:** implement `aPoleFilter` strictly from the
microcode disassembly (recommended), or accept a devkit-docs approximation and verify
by ear? The chorus is audible in most music; a wrong filter coefficient shows as tonal
difference, not crash.
## 10. Phases and milestones
- **Phase 0 — Recon** (§8): microcode disassembly notes + Acmd log artifacts committed
to `docs/` (or `tools_pc/`). Exit: current-buffer semantics + pole filter documented;
op inventory confirmed at runtime.
- **Phase 1 — Plumbing.** SDL device opens @22050 stereo s16 (A2); A1 queue path live;
real AI shims (§7.2); `K0_TO_PHYS` shim fix (§7.1); `Audio.Enabled` toggle. Mixer still
a no-op is fine. **Milestone: device drains, `osAiGetLength` feedback loop visible in
logs, clean start/stop, silence (not crash).**
- **Phase 2 — Core ops.** Implement `aSetBuffer/aLoadBuffer/aClearBuffer/aDMEMMove/
aADPCMdec/aResample/aEnvMixer/aMix/aSetVolume/aLoadADPCM/aSetLoop/aInterleave/
aSaveBuffer` (+`aSegment` no-op) in `mixer.c`, reusing PD's DSP math where the ABI
maps. **Milestone: SFX audible (gunshots, doors); dry music plays.** Expect at least
one Dxx-class finding here (state-layout or endianness surprise is the usual bug
class — see `docs/porting-notes.md`).
- **Phase 3 — FX.** `aPoleFilter` + validate the full `reverb.c` chorus path against
the disassembly. **Milestone: music matches N64 character (chorus/reverb present, no
pumping); close D77.**
- **Phase 4 — Polish & bookkeeping.** Resolve Q1; remove/adjust temporary probes and
`GE_DAUDIO` defaults; latency/buffer tuning (`Audio.BufferSize`, `QueueLimit`);
host thread-priority tuning per A3 *only if* underruns were observed in Phases 2–3;
record findings in `docs/internals.md` §F/§H (next Dxx after D127) + update the
§F index, `AGENTS.md` Phase 3 status, `docs/HANDOFF.md`; append recurring bug classes
to `docs/porting-notes.md`.
## 11. Verification ritual (audio-specific, on top of the standard one in AGENTS.md)
1. Boot → title: menu music audible within a couple of seconds; no click at start.
2. Enter BUNKER1 (`-level_09`): gunfire/door SFX present and roughly level-correct;
level music plays.
3. No `audio: ai out of samples` spam from `amHandleDoneMessage` (`audi.c:596`) — that
print is the built-in underrun detector.
4. Run 2–3 levels incl. one with heavy SFX overlap; watch `SDL_GetQueuedAudioSize`
(log it) staying below `queueLimit` in steady state.
5. PAL build (`pal-final`) spot-check: 50 Hz retrace drives the same path; frame-size
math uses `MAYBE_FRAME_RATE` — confirm no underruns there too.
6. `Audio.Enabled=0` → fully silent, zero audio CPU cost, nothing else affected.
## 12. Risks / unknowns
| Risk | Mitigation |
| File | Role in this plan |
|---|---|
| IDO current-buffer semantics misread → subtle wrong output (no crash) | Phase 0 disassembly is the arbiter; Acmd logger + by-ear checks per phase |
| `aPoleFilter` has no PD code | Disassembly (Q3); it's one small IIR — bounded effort |
| ADPCM state layout differs from assumption → garbage/cratchy samples | Phase 0 documents the struct from `load.c` usage + microcode; scalar fallback path makes debugging easy |
| C7/D127 hides a bank-tree bug that audio makes *audible* (wrong SFX, not just missing) | Q1; keep the guard until root-caused |
| SDL queue backpressure drops under load (slow machines) | `queueLimit` drop is graceful (silence gap, no crash); A3: raise scheduler-thread host priority before buffer tuning; PD shipped the same design |
| Synthesis cost on the scheduler thread starves gfx frame time / causes underruns | A3: host thread priorities first; a dedicated audio worker thread only if priorities provably insufficient (and revisit D24's fake-priority semantics before designing one) |
| Cross-region: PAL sample-count math differs | Verification step 5 |
## 13. Key references
| File | What it gives us |
|---|---|
| `src/audi.c` | audio thread, frame-size feedback loop, task construction (`OUTPUT_RATE` @22) |
| `src/sched.c:439,465,485` | real scheduler's M_AUDTASK path → `osSpTaskStartGo` interception point |
| `include/PR/abi.h:260–407` | IDO Acmd word formats (the decoder's spec) + A_* opcodes |
| `src/libultra/audio/synthInternals.h:57–71` | DMEM layout constants |
| `src/libultrare/audio/reverb.c`, `env.c`, `drvrNew.c` | GE custom FX (`alFxNew`), pole filter + chorus, envelope ops |
| `bin/aspboot.text.bin` / `.data.bin` (+ `src/aspboot.s`) | **ground-truth ASP microcode to disassemble** |
| `pd_port/port/src/mixer.c`, `port/include/mixer.h` | DSP math reference + the Option-B pattern |
| `pd_port/port/src/audio.c`, `pdsched.c:233–260`, `libultra.c:150–175` | PD wiring (queue design, empty AUDTASK branch, AI shims) |
| `port/src/libultra.c:794–810, 1108` | the three dead seams this plan fills |
| `port/src/dram.c`, `port/shim/PR/R4300.h` | address model (V1/V2 views) + the K0_TO_PHYS gap |
| `src/audi.c` | Game audio manager — compiles unmodified; source of `OUTPUT_RATE`, frame-size math, `CUSTOM_FX_PARAMS_N` |
| `include/PR/abi.h` | GE's acmd accessors (16 opcodes, context ABI) — the one non-`port/` edit (`#ifdef PORT` split) |
| `src/libultra/audio/*.c`, `src/libultrare/audio/*.c` | CPU synth + naudio New driver — compile unmodified; call sites define the opcode set |
| `port/src/audio.c` | SDL device + queue (implement Phase 1) |
| `port/src/mixer.c` | Software acmd interpreter + software DMEM (implement Phase 2) |
| `port/include/mixer.h` | Stub interface → replace with `aXxxImpl` declarations |
| `port/src/libultra.c:848-863` | `osAi*` shims — wire to audio.c (Phase 1) |
| `port/src/libultra.c:1237` | M_AUDTASK branch — stays a completing no-op; hosts `GE_AUDIOTRACE` |
| `port/src/ucode.c` | Dummy `aspMain*` markers stay forever under CPU-bypass — document decision |
| `C:\Users\james\Source\Repos\pd_port\port\src\{audio,mixer}.c`, `include\PR\abi.h` | PD ground truth: device glue + DSP cores + the macro-swap pattern |
+246
View File
@@ -0,0 +1,246 @@
# Controller & input options wave (port plan)
Status: **Wave A IMPLEMENTED** (code complete, clean Windows/MSYS2 build; pending
user by-ear/by-eye feel-check + Deck tuning of item 5) — the "low-hanging
fruit" from the Nightdive *Turok* PC-port options menu that is cheap, well-
scoped, and
**port-layer only** (zero `src/` / `src/game` edits → rule 2 and the ABI
exception do not apply). Sequenced after v0.4.0 ships. This is the *input*
wave; the sibling *video/audio/graphics* Tier-A items (borderless window,
primary display, brightness, master volume, button glyphs, uncapped-FPS) are a
separate follow-on wave — see §Out of scope.
Reference: `docs/dev/RUMBLE-PLAN.md` (D401) is the closest precedent — a
controller option that lives entirely in `port/src/input.c` +
`port/src/optionsoverlay.c` + a `configRegister*` key + a `kResetDefaults`
entry, with the game side (`src/joy.c`, `src/` stick consumers) untouched.
`docs/dev/GEPD-INPUT-PLAN.md` documents the pad poll path this wave extends.
---
## Ground truth (current state, verified 2026-09-27)
**Pad poll = `inputComputePad()`** (`port/src/input.c:1375`), runs on the game
thread (`osContStartReadData`). It reads the SDL axes/buttons for pad `idx`,
writes out a 16-bit button mask plus `*stick_x` / `*stick_y` (the N64
`signed char` stick, clamped to `STICK_MAX`). All the knobs in this wave are
applied **inside** `inputComputePad`, before the two `*stick_x`/`*stick_y`
writes at `:2063-2064` — i.e. the port re-shapes the *input the game already
consumes*, never the game's stick→turn math.
**Two pitch modes drive the look stick** (selected by `Input.NaturalPitch`,
the default-on "SOLITARE" mode, D194/D238):
- **Continuous (natural-pitch, the default):** the **right stick** is the
continuous look axis — `input.c:1927-1933`:
`rxs = scaleAxis(rx);` … `rys = -scaleAxis(ry); if (padLookInvertY)
rys = -rys; sx = rxs; sy = rys;`. The left stick becomes digital-step
movement. **This is the path the X/Y look-sensitivity + look-smoothing +
right-stick deadzone knobs target.**
- **Digital (native, natural-pitch off):** the left stick is analog
movement/turn and the right stick is digitized into the C-button look
(`:1935-1947`, thresholds at `RSTICK_THRESHOLD`). Look here is the faithful
N64 *digital* C-button turn — **not sens-scalable** (there is no analog look
to scale), so the new look-sens/smoothing knobs are inert in this mode,
exactly as `Input.AimMode`/`Input.AimRange` already are per mode.
**Existing pad knobs** (all `configRegister*` in `input.c`, rows in
`optionsoverlay.c` INPUT section): `Input.PadDeadzone` (single,
`padDeadzone = STICK_DEADZONE`, `:524`), `Input.PadTriggerPct` (`:525`),
`Input.PadLookInvertY` (`:526`), `Input.RumbleScale` (D401). `scaleAxis()`
applies the shared deadzone + `STICK_MAX` scale to a raw `int16` axis.
**Already covered elsewhere** (do not re-plan): mouse sens/invert/smoothing
(`Input.MouseSensitivity`/`MouseInvertY`/`MouseSmoothing`), **uncapped FPS**
(`docs/dev/UNLOCKED-FPS-PLAN.md` — parked; has a real perf prerequisite, D250),
**controller rebinding** (PR #109 / D383 — the hard, open gap, out of scope).
---
## Items (all port-layer)
### 1. `Input.PadLookSensX` — right-stick look **horizontal** sensitivity *(S)*
New int key, default **100** (= 1×, current behaviour), range ~**25–200 %**.
Applied in the continuous-look branch only: after `rxs = scaleAxis(rx)`,
`rxs = clamp(rxs * padLookSensX / 100, ±STICK_MAX)` before `sx = rxs`
(`input.c:1928/1932`). Pure gain on the emitted look X. In digital mode: inert.
- Row: INPUT, "Look sensitivity (left/right)", slider, after "Rumble strength".
- Risk: none (monotonic gain; 100% = byte-identical to today). Deck-testable.
### 2. `Input.PadLookSensY` — right-stick look **vertical (pitch)** sensitivity *(S)*
Same shape, applied to `rys` (`input.c:1930/1933`): `rys = clamp(rys *
padLookSensY / 100, ±STICK_MAX)`. Default **100**, range ~**25–200 %**.
Kept separate from X (Turok splits them; pitch and yaw feel different on a
stick). Independent of the existing `Input.HipfirePitchSpeed` (that one is the
hipfire/mouse pitch rate, a different path). In digital mode: inert.
- Row: INPUT, "Look sensitivity (up/down)", slider.
- Risk: none (same as item 1).
### 3. `Input.PadSouthpaw` — swap L/R triggers *(S)*
New toggle, default **off**. Today (`input.c:1954-1958`): `TRIGGERRIGHT >
trigPt → GE_CONT_G`; `TRIGGERLEFT > trigPt → GE_CONT_R`. Under southpaw, swap
the two `GE_CONT_G`/`GE_CONT_R` assignments (left trigger becomes the
right-shoulder action and vice-versa). Two lines. Lets left-handed /
southpaw players put their index finger on the primary fire trigger.
- Row: INPUT, "Southpaw", toggle.
- Risk: none at default off; trivial to verify by re-aiming.
### 4. `Input.PadDeadzoneL` / `Input.PadDeadzoneR` — per-stick deadzone *(S–M)*
Split the single `Input.PadDeadzone` into a **left (movement)** and a
**right (look)** deadzone. Thread the deadzone through `scaleAxis()` (it is
currently the global `padDeadzone`) as a per-call argument: left-stick calls
use `padDeadzoneL`, right-stick calls use `padDeadzoneR`.
- **Back-compat/migration:** on load, if `Input.PadDeadzoneL`/`R` are absent
but legacy `Input.PadDeadzone` is set, seed both from it (one-time, like the
`AimStyle`→`AimMode` migration at `input.c:449-454`). Keep `Input.PadDeadzone`
registered so old inis keep loading; it becomes hidden/superseded (D337/`D333`
precedent for a superseded key).
- Rows: INPUT, "Deadzone (left stick)" + "Deadzone (right stick)", sliders.
- Risk: low — a deadzone is a per-axis floor; splitting it cannot change
behaviour when both equal the current single value. Watch the menu-navigation
path (`:1918-1922`, left stick in menus) to use the **left** deadzone there.
### 5. `Input.PadLookSmoothing` — right-stick look low-pass *(M)*
New key, default **0 (off)**, range 0–10 (0 = off = today). When >0, low-pass
filter the *emitted* right-stick look deflection so the look axis eases rather
than tracking the stick 1:1 — the pad analogue of `Input.MouseSmoothing`
(which smooths the *relative* mouse delta). Because the continuous stick is
**absolute** (not a delta), implement as a per-pad exponential moving average
on the emitted `rxs`/`rys` (state kept per `idx`, reset on pad hot-unplug and
on aim-mode exit so releasing the stick recentres cleanly). The higher the
value, the heavier the easing.
- Row: INPUT, "Look smoothing", slider (0 = off).
- Risk: **medium** — an EMA on an absolute stick can feel laggy or fail to
recentre if the filter isn't driven back to 0 when the stick is centred;
needs hands-on tuning on the Deck (the natural test device). Keep default off
so it ships inert until tuned.
### 6. Mouse **vertical** sensitivity row *(trivial — folds in)*
`Input.MouseYScale` already exists as a registered key; it just has no F10
row. Add one slider row (INPUT) so the asymmetric X/Y mouse sensitivity is
user-adjustable without editing the ini. No `input.c` change — row only.
---
## Shared plumbing (do once, not per item)
- **`configRegisterInt`/`Float`** for each new key in `input.c` next to the
existing pad-key registrations (~`:2536-2538`).
- **Static state** next to `padDeadzone`/`padLookInvertY` (`:524-526`):
`padLookSensX/Y`, `padSouthpaw`, `padDeadzoneL/R`, `padLookSmoothing` + the
per-pad EMA state for item 5.
- **F10 rows** in `optionsoverlay.c` `rows[]` INPUT section (after
"Rumble strength", with the other controller rows) + matching **front options**
rows via the shared `optionsRow*` accessors (both UIs read `rows[]` — D401
precedent, so one definition covers F10 *and* the front options screen).
- **`kResetDefaults`** entries for each new key (neutral: sens 100, smoothing
0, southpaw 0, deadzones = `STICK_DEADZONE`) so INPUT "Reset to defaults"
covers them (D401 item-3 precedent).
## Ground rules
1. **Port-layer only** — every edit is in `port/src/input.c`,
`port/src/optionsoverlay.c`, `port/src/frontoptions.c`, and `CMakeLists.txt`
(nothing new to compile — no new files). Zero `src/` / `src/game` edits →
rule 2 and the pointer-width ABI exception are not engaged.
2. **Off-by-default = today's behaviour.** Every new key defaults to a neutral
value that leaves the emitted stick/buttons byte-identical to the current
build, so the golden gate (D117/`golden_gate.sh`) is unaffected at defaults
and the v0.4.0 playtest baseline is preserved.
3. **Per-mode honesty.** The continuous-look knobs (1, 2, 5) apply only in
natural-pitch mode; in digital mode they are inert — document this on the
rows (mirrors how `Input.AimRange` is hidden in Centred mode, D338), rather
than pretending they affect the digital C-button look.
4. **Deck is the reference device** for the pad wave (native Steam Controller,
the real-world target); Windows + a plugged gamepad as the second check.
## Verification
1. `./build-pc.sh ntsc-final` clean (no new symbols/dupes — the `configRegister*`
+ `kResetDefaults` additions are the only new references).
2. `/linkcheck` + the golden gate (`tools_pc/golden_gate.sh`) — must pass
**unchanged at defaults** (proves the neutral-default invariant).
3. Hands-on, on a pad / the Steam Deck: each knob at a non-neutral value
changes feel as expected and at default leaves it identical; southpaw
re-maps the triggers; per-stick deadzones floor the correct stick; look
smoothing eases and **recentres on stick release** (item 5's known risk).
4. New finding (D4xx, next free label) recording the wave + per-item
Deck-test result, cross-ref'd to D401 (precedent), D337/D338 (per-mode
gating pattern), D333 (`AimStyle` migration pattern), PR #109 (out-of-scope
rebind).
## Out of scope for this wave (see the assessment above)
- **Sibling Tier-A, other sections (separate follow-on wave):** borderless
windowed, primary-display selection, brightness slider, master audio volume,
button-prompt glyphs.
- **Tier B (medium, contained):** radial fog toggle, HUD opacity, flash-
intensity slider (promote `Game.NoHitFlash`), show-HUD master toggle, audio
output device, low-pass filter, SMAA/FXAA.
- **Uncapped FPS** — `docs/dev/UNLOCKED-FPS-PLAN.md` (parked; D250 perf
prerequisite means it is not quick).
- **Tier C (rule-2 / big-feature / out-of-scope):** head-bobbing sliders,
death-cinematic / switch-on-pickup / walk toggles (all `src/game` flow),
bloom / light-scattering / water refraction-reflection / simple shadows,
violence / blood colour, multiple graphics APIs, and **full controller
rebinding** (PR #109 — the genuinely hard input item).
## Suggested sequencing (v0.5.0)
1. **Wave A (this doc):** items 1–4 + 6 (sens X/Y, southpaw, per-stick
deadzone, mouse-Y row) — all low-risk, shippable behind neutral defaults.
2. **Item 5 (look smoothing)** lands in the same wave but may ship default-off
pending Deck feel-tuning.
3. **Wave B:** the sibling video/audio/graphics Tier-A items.
4. Revisit Tier C only with explicit sign-off (rule-2 items need the
`docs/dev-process.md` procedure).
---
## Implementation status (2026-09-28)
Wave A code is complete and compiles clean (Windows/MSYS2 `ninja` incremental,
exit 0, no new warnings — only the pre-existing `ramrom.c` / `gfx_pc.cpp` ones).
Every edit is in `port/src/input.c` + `port/src/optionsoverlay.c`;
`frontoptions.c` needed **no** change (both UIs read the shared `rows[]` via the
`optionsRow*` API). Zero `src/` edits, no new files/symbols — only file-local
statics + `configRegisterInt` calls — so the link is unchanged.
Implemented (all off/neutral-by-default → byte-identical at defaults):
- **1/2. `Input.PadLookSensX` / `Input.PadLookSensY`** (25–200 %, dflt 100):
right-stick look, applied in the natural-pitch branch of `inputComputePad()`
after `scaleAxis()`, re-clamped to `STICK_MAX`. Inert in digital mode (the
right stick is digitized there, no analog look to scale).
- **3. `Input.PadSouthpaw`** (toggle, dflt 0): swaps the fire (`GE_CONT_G`)
and grenade (`GE_CONT_R`) trigger actions *after* the raw edges are captured,
so edge-detection stays consistent.
- **4. `Input.PadDeadzoneL` / `Input.PadDeadzoneR`** (0–30000, dflt 7000):
`scaleAxis()` now takes a per-stick deadzone param; left-stick (movement)
calls pass `L`, right-stick (look) calls pass `R`. The legacy
`Input.PadDeadzone` key stays registered (no row) so old inis load; a
one-time migration in `inputInit()` seeds any unset L/R (C-init sentinel
`PAD_DZ_UNSET = -1`) from the legacy value, before any `configSave`. The
single "Stick deadzone" row is replaced by "Deadzone (left stick)" /
"(right stick)".
- **5. `Input.PadLookSmooth`** (0–10, dflt 0 = off, shown "N/10"): per-pad EMA
low-pass on the natural-pitch look output (`padSmSX/padSmSY[MAX_PADS]`), reset
on hot-unplug (`inputRescanPads`) so a snapped-back stick can't lag from a
stale value; otherwise the EMA decays to 0 as the stick recentres. Turok has
this as on/off; GE ships a graduated 0-10 knob instead. Ships **default-off**
pending Deck feel-tuning.
- **6. `Input.MouseYScale`** row (dflt 100): the pre-existing registered key
finally gets an INPUT slider row ("Mouse vertical sensitivity") — row only,
no `input.c` change.
**Verbiage + display baseline (Turok PC, 2026-09-28):** row labels now match the
Turok options menu verbatim where GE has an analogue — *Mouse horizontal /
vertical sensitivity*, *X axis / Y axis look sensitivity*, *Look smoothing*,
*Southpaw*, *Vibration* (was "Rumble strength"), *Deadzone (left/right stick)*.
The display is baselined to Turok's scales: sensitivity + deadzone knobs read
**0-100 with 50 = the default** (the existing `calibratedDefault` "N/100" path),
and look smoothing reads **0-10** ("N/10"). (Turok's Mouse/Controller subsection
split is not mirrored — GE keeps one flat INPUT section per D356, so the two
invert rows retain their "(mouse)"/"(controller)" suffixes to stay unambiguous.)
`kResetDefaults` covers all six so INPUT "Reset to defaults" is complete;
`calibratedDefault()` gives the sensitivity + deadzone rows their 50/100 default
midpoint, and `dispMax` renders the smoothing knob as "N/10".
**Still owed (verification, not code):** user by-ear/by-eye feel-check of the
new knobs on a real pad (esp. item 5 smoothing strength + item 1/2 sens range),
the Deck cross-check, and confirming the per-stick deadzone migration on an
old-ini user (L/R seed from `Input.PadDeadzone`).
+360
View File
@@ -0,0 +1,360 @@
# CUT-CONTENT-BACKLOG — TCRF "cutting room floor" findings vs decomp/port
Research branch: `research/cut-content-backlog`. Source of truth: the local TCRF
export at `scratchpad/backlog-scrape/restore-cut-content-mod/` (gitignored).
> **Superseded by the verified cross-reference.** All six article *bodies* were
> recovered and every major claim was checked against the decomp source and asset
> tables. The full verified findings (with file:line references, corrections to
> TCRF, and updated mod tiers) live in `backlog/cut-content/RESEARCH.md`
> (gitignored; article markdowns in `backlog/cut-content/articles/`). This doc is
> the first pass (image-filename based) — read it for the per-item imagery index,
> but trust RESEARCH.md where they disagree. Key corrections: (1) "dest" is the
> *shipped* Frigate geometry, not an early version (`bg.c` levelinfotable);
> `cryp`/`dish` are shipped Egypt/Temple; (2) Library/Stack share Basement's bg
> file — they have geometry; (3) all 29 "unused items" exist in the ROM object
> table as pickup+prop pairs except heroine; (4) TCRF's 11 unused cheats, music
> track index, MP character table and build dates all verify 1:1 against
> `bondconstants.h`/`cheat.c`/`front.c`/`compiletime.c`.
>
> **Round 2 (decomp-only discoveries — not in TCRF's documentation):**
>
> 1. **The shipped game still calls the M16 an "AR33"** — pickup text
> `"an AR33 assault rifle."` (`LpropobjE.c` PROPOBJ_STR_2A, used at
> `propobj.c:10471`), plus unused strings `"US AR33 Assault Rifle"`/
> `"US AR33"` (`LgunE.c` GUN_STR_74/75) and `PROP_CHRM16 /* AR33 Assault
> Rifle */` comments. TCRF's prerelease page mentions the AR33 name in early
> footage but not that it survived into retail text.
> 2. **Klobb rename trace verified**: weapon-name strings in `LgunE.c` are
> alphabetical except GUN_STR_6B `"Klobb"` (between "Spy File" and "Staff
> List") — exactly the last-minute Skorpion→Klobb scar TCRF describes.
> 3. **Two cut weapons TCRF's unused-items page doesn't list**: `cartrifle` and
> `wristdart` — full pickup+prop resources (`GcartrifleZ`, `GwristdartZ`,
> `PchrwristdartZ /* (BETA) */`) but no `ITEM_*` entry and no WeaponStats,
> i.e. unusable as weapons. The wrist dart is the hardware behind TCRF's
> `AMMO_DARTS` ($0E).
> 4. **All four Bonds, fully mapped**: `CUFF_CONNERY/DALTON/MOORE` +
> `BOND_BROSNAN/CONNERY/DALTON/MOORE` enums; the actor-switch code
> (`bondview2.c:391-460`) survives with every branch flattened to Brosnan;
> the save struct keeps the per-dossier actor field. **No Connery/Moore/
> Dalton models exist in the retail ROM** (80 `C*Z` chr models enumerated —
> none for the former actors); only enum slots + the `SW_CONNERY`/`SW_DINNER`
> switches on the watch-arm model remain. TCRF's "four dossiers, four
> actors" was pre-release state.
> 5. **Cuba is the one orphan level with recoverable content**: `LlenE.c` holds a
> cut Natalya dialogue exchange, the "C U B A N J U N G L E" title card, and a
> full alternate credits cast. The other 12 unused level text files are empty
> stubs.
> 6. **Orphan-level geometry IS in the ROM**: `assets/obseg/obseg.h` declares all
> nine text-only orphans' bg segments (`bg_sho_all_p_seg[]` …) — a
> byte-matching decomp can't fabricate them. What's missing is mission setup
> (briefings exist only for the 21 solo levels), not geometry.
> 7. **Moneypenny's two risqué Frigate-briefing lines are gone from the ROM**
> (final `LdestE.c` keeps only the toned-down "thighs of steel" paragraph);
> recoverable only from TCRF's transcription. "Destroyer"→"Frigate" rename is
> complete; the Manticore yacht mission has zero ROM trace.
> 8. **Music enum ↔ Notes-page GS index fully aligned**: indices 0–48 match
> slot-for-slot; four local mismatches past 0x2C (49 `M_CUBA` vs "Ending
> theme", 52 `M_MPTHEME3` vs "Surface 2 X", 54 `M_GUITARGLISS` vs "MP Death
> (Alt)", 59 `M_SURFACE2X` vs "Surface 1 X") — in-game `DEB_MUSIC` audition
> needed to resolve.
## 1. Findings
### A. Debug menu & dev tools — fully in the ROM, fully compiled into the PC build
TCRF main page images: `GEDebugMenu`, `GECheatOptions`, `GoldenEye-displayspeed`,
`GoldenEye-moveview`, `GoldenEye-LineMode`, `GoldenEye-testingmanpos`,
`GoldenEye-gunwatchpos`.
All of these exist in decompiled code:
- **Debug menu** — `src/game/debugmenu_handler.c` (1202 lines). Opened by holding
**U + D C-buttons** (`debug_menu_processor`, ~line 451; wired into `boss.c:551`
and the main game loop). Full option list at lines 116–185: `move view`,
`stan view`, `bond view`, `level` / `region` / `scale` (level switching),
`play title`, `bond die`, `select anim`, `gun pos`, `flash colour`,
`hit colour`, `music`, `sfx`, `invincible`, `visible`, `collisions`,
`all guns`, `max ammo`, `display speed`, `background`, `props`,
`stan hit` / `stan region` / `stan problems`, `print man pos`,
`port close` / `port inf` / `port approx`, `pr room loads`,
`show mem use` / `show mem bars`, `grab rgb` / `grab jpeg` / `grab task`
(screenshots), `rnd walk`, **`record ramrom` / `replay ramrom` /
`save ramrom` / `load ramrom`** (input recording/replay!), `auto y aim`,
`auto x aim`, `agent` / `all` / `fast` (unlock flags), `objectives`,
`marg top/bot/left/right/reset`, `screen size`, `screen pos`,
`show patrols`, `intro` / `intro edit` / `intro pos`, `world pos`,
`gun key pos`, `vis cvg`.
- **Free camera** — `src/game/debug_camera.c` (the "move view" option).
- **Load-all-models tool** — `src/game/deb_loadallmodels.c` (used by
`initguards.c`; dumps every model — how the TCRF screenshots of unused
models were likely made).
- **Line mode** — `CHEAT_LINEMODE` in `src/game/cheat.c:685` (wireframe toggle,
cheat-menu unlockable; `front.c:1017`).
- Screenshot options (`grab rgb`/`grab jpeg`) call `indyGrabRgb32bit()` /
`indyGrabJpg32bit()` — N64 VI grabs; on PC these are the natural hook for the
port's existing screenshot QoL (see `feat/qol-mute-screenshot` lineage).
**Status: Tier 0.** Everything compiles into the PC build (`CMakeLists.txt`
`GLOB_RECURSE src/game/*.c`). The debug menu should already work in the PC port
if U+D C-buttons can be produced by `port/src/input.c`. *Verify in-game* (open
menu, toggle `display speed`, use free cam). Mod value: document it; consider a
config/keybind that synthesizes the U+D c-button hold, and mapping `grab rgb`
to the port's screenshot path.
### B. Cut weapons — fully functional in the ROM, grantable via existing cheat codes
TCRF images (main + prerelease pages): `GoldenEye-Taser[PR]`, `GoldenEye-KF7[PR]`
(+`KF7Watch`), `GoldenEye-GrenadeLauncher[PR]`, `GoldenEye-RocketLauncher[PR]`
(+`RocketLauncherWatch`), `GoldenEye-RifleLauncher` (+`RifleLauncherCheat`),
`GE64_Shotgun1`, watch variants `MagnumWatchPR`, `PP7WatchPR`, `KlobbWatchPR`,
`TaserWatch`.
Evidence in code/assets:
- Model + stat files exist for every cut gun under `assets/obseg/gun/`:
`taser/`, `rocketlaunch/`, `grenadelaunch/`, `fnp90/`, `spectre/`,
`autoshot/`, `ak47/`, `m16/`, `skorpion/` … each with
`gunWeaponStat.inc.c` + `ModelFileHeader.inc.c`.
- `assets/obseg/gun/gunWeaponStats.inc.c`: complete `WeaponStats` for
`taser_stats` (line ~365), `rocketlaunch_stats` (~305),
`grenadelaunch_stats` (~295), plus `watchlaser_stats`, `tank_stats`,
`bombcase_stats`, etc. These are data-driven — the weapon engine
(`src/game/gun.c`, `gunfire.c`) will run them.
- First-person view cases exist: `src/game/bondview_r.c:38` handles
`ITEM_GRENADELAUNCH`, `:41` `ITEM_ROCKETLAUNCH`.
- **Cheat codes grant them**: `src/game/cheat.c` — `CHEAT_2X_ROCKET_LAUNCHER`
(line 711), `CHEAT_2X_GRENADE_LAUNCHER` (712), `CHEAT_2X_RCP90` (713),
plus unlock sequences (`CHEAT_UNLOCK_2XGL/2XRL/2XFNP0`, lines ~695–734) and
the grant logic at lines 1277–1310: `bondinvAddInvItem(ITEM_ROCKETLAUNCH)` /
`give_cur_player_ammo(AMMO_ROCKETS, …)` etc. Note the `#if defined(BUGFIX_R1)`
guards — check which region macros the PC build defines (non-negotiable #3).
- Taser SFX exist: `GUN_TASER_SFX` / `GUN_TASER_LOOP_SFX` (`src/bondconstants.h:2271`).
- `ITEM_TASER` is in the item enum and used as an inventory bound in
`bondinv.c:854` (JP-text-trigger branch) — the taser was closest to shipping.
**Status: Tier 0/1.** Rocket launcher, grenade launcher and FN P90 are
playable today via the game's own cheat codes (single-player). Taser/Spectre/
AutoShot have no cheat path but everything needed is in the binary; a
port-layer toggle calling the same `bondinvAddInvItem` +
`give_cur_player_ammo` pair the cheats use would be a clean mod (no game-logic
change — it replicates an existing code path). Watch-mounted variants
(KF7/Magnum/PP7/Taser watch) are *not* in the item enum — likely cut before
data landed; **unverified** whether any model exists beyond what TCRF shows.
### C. "Unused items" page (intact imagery) — objects with full logic, never placed
The 20 intact images correspond exactly to ROM object entries
(`assets/obseg/file_resource_table.inc.c`, 781 entries):
| Image | Resource ID | Notes |
|---|---|---|
| `GE007-AudioTape` | `AUDIOTAPE` / `CHRAUDIOTAPE` (lines 125, 265) | pickup + prop variants |
| `GE007-BombCase` | `BOMBCASE` (129) | has full `bombcase_stats` (throwable explosive) |
| `GE007-Controller` | N64 controller model (`joypad/` dir in gun table) | desk prop |
| `GE007-WatchMagnetRepel` | `ITEM_WATCHMAGNETREPEL` (`bondconstants.h:4190`) | watch function, use-logic present |
| `GEMicroCamera` | `MICROCAMERA` (176) + `AMMO_MICRO_CAMERA` | |
| `GoldenEye-Weapon_Case` | `ITEM_WEAPONCASE` (`gunfire.c:3066` use-case) | |
| `GE007-PolarizedGlasses` | `POLARIZEDGLASSES` (185) + `ITEM_POLARIZEDGLASSES` | `darkglasses/` also present |
| `GoldenEye-Plans` | `PLANS` (183) | `blueprints/` variant too |
| `GoldenEye-Watch_Identifier` | `WATCHIDENTIFIER` (208) + `ITEM_WATCHIDENTIFIER` (`gunfire.c:3076`) | 4th watch function with use-anim, never spawned |
| `GoldenEye-Safecracker` | `SAFECRACKERCASE` (190) | |
| `GoldenEye-Watch_Communicator` / `Watch_Geiger_Counter` | `ITEM_WATCHCOMMUNICATOR` / `ITEM_WATCHGEIGERCOUNTER` (`bondconstants.h:4188-89`) | shown for contrast — these *are* used (Aztec) |
| `GEGasKeyring` | `GASKEYRING` (157) + `ITEM_GASKEYRING` | |
All have `GUN_ANIM_STATE_USE_ITEM` handling in `gunfire.c:3060-3084` — i.e. the
game can *use* them; level data just never places them.
**Status: Tier 1/2.** Granting any of these to Bond is a port-layer item-spawn
(same mechanism as §B). Actually *placing* them in levels requires ROM/level-data
editing (a level editor or hand-patched object tables) — Tier 2, out of scope
for this repo but viable as a data mod.
### D. Cut multiplayer maps — 8 commented-out menu entries with dev notes
`src/game/front.c:556-573` `multi_stage_setups[]`: the shipped MP list is
Random/Temple/Complex/Caves/Library/Basement/Stack (default) + Facility/Bunker2/
Archives/Caverns/Egypt (progression-unlocked). Immediately below, **commented
out with in-source dev annotations**:
```c
//{... LEVELID_CITADEL ...}, //Citadel (old format setup)
//{... LEVELID_FRIGATE ...}, //dest (has xbla setup)
//{... LEVELID_STATUE ...}, //stat (works)
//{... LEVELID_CRADLE ...}, //crad (works)
//{... LEVELID_AZTEC ...}, //azt (needs setup)
//{... LEVELID_RUNWAY ...}, //runway (xbla setup)
//{... LEVELID_DAM ...}, //dam (xbla setup)
//{... LEVELID_DEPOT ...}, //depot (xbla setup)
```
TCRF imagery matches: `GECitadel*` (5 screenshots, main page),
`GEStatueMulti`, `GECradleMulti`, the whole Depot image set (`DepotCrateStack`,
`DepotRockets`, `DepotSafeTable`, `DepotSiloPipe[Ed]`, `DepotTrailers`,
`DepotPhoto`), and `GoldenEye-FrigateMap`.
The port already boots every one of these standalone (`-level_XX`; see
`docs/dev/LEVEL-STATUS.md` — Statue `-level_22`, Train `-level_25`, etc. all
PASS). So the geometry, AI setup and briefings
(`assets/obseg/brief/UbriefdepoZ.c`, `UbrieftraZ.c`, …) are intact; only the MP
menu entry was removed.
**Status: Tier 3 (mod fork).** Re-enabling = editing a game data table in
`src/game/front.c` — that's a game-logic change, disallowed in this repo
(non-negotiable #2). Viable as a separate mod branch/fork ("8 more MP maps"),
or as a documented cheat-menu route (the debug menu's `level`/`region` options
may already jump to any level ID — verify).
### E. Per-level unused props & geometry ("Unused stuff by level" page)
~150 intact screenshots, grouped by level. Cross-referenced against the object
table and level IDs:
- **Streets** (campaign stage 12; `LEVELID_STREETS`): an entire prop kit was
never placed — `StreetsCars`, `StreetsLamp[Base/Big]`, `StreetsSign[2/3]`,
`StreetsTrafficLight`, `StreetsShovel`, `StreetsCrossing`, plus two cut set
pieces `GE64-StreetsArea1/2` and Ourumov's car driving/crashing
(`StreetsOuruGo`, `StreetsOuruCrash`, `GEOurumovsCar`). Suggests a cut
street-chase scene.
- **Silo**: `SiloVentShaft`, `SiloLadder`, `SiloLaunchDoors`,
`SiloWalkway2Door/Guard`, `SiloMissingHandrail` (a bug/oddity screenshot),
`GoldenEye_UnusedStartingPoint`, and prerelease-only `PRSiloElevator`,
`PRSiloLamps`, `SiloCutWalkway`, **`ThirdSiloEditor`** (editor view of a
third silo that didn't ship).
- **Facility**: PR-vs-final wall sets (`FacilityWalls[Final]`,
`PRFacilityWalls`, `ArchivesWalls`), plus cut rooms: `FacilityBathroom`,
`FacilityConveyor`, `FacilityGasTanks`, `FacilityLabTanks`,
`FacilityWalkway`, `FacilityArmedScientistPR`, `FacilitySatBoxTex`,
`GoldenEye007-FacilityPhoto`.
- **Archives**: `ArchivesOverview`, `ArchivesLamp[Scan]`, `ArchivesPhoto`,
`PRArchivesNoWalls` (wall-less build view).
- **Bunker**: `BunkerExtraMons[Side]` (extra monster/guard spawns),
`BunkerPIP`, PR `BunkerLamp`.
- **Caverns**: `CavernsPursueObj` (a cut "pursue" objective marker), PR
`CavernsWalkway`.
- **Dam**: `DamIsland`, `DamPlatform`, `DamDockDoors`, `DamEndDuct`,
`DamTruckDoor`; region-diff shots `DamCompUS/EU`.
- **Frigate**: `FrigateHarpoonScan` (scan of a harpoon model),
`PRTrainTargetGlass`, `Pdest_harpoonZ[2]`, `Ptorpedo_rack`,
`FrigateMoneypennyPR` (a Moneypenny model in the Frigate PR build!),
region-diff `FrigateIntroUS/JP`.
- **Aztec/Temple**: `AztecConsolePads1-4`, `GE-AztScreen2781/2789`,
`AztExtraMainframe`, `AztDronePadsLabel`, `unused-Aztec-Path` (a whole
unplaced path), `EgyptianRed/WhiteClouds` (two skybox variants),
`TempleSkySolo/Multi`, PR: `AztBlueSuitPR` (cut enemy suit),
`AztHeavyFogPR`, `AztTableScreensPR`.
- **Surface**: `SurfaceBookCrate`, `SurfaceObsPad`, `SurfaceTower`,
`SurfaceJerryCan`, `SurfaceHutTable`, `SurfacePropHut`, PR
`SurfaceOverviewPR`, `SurfaceVent[PR]`, `SurfaceDish[PR]`.
- **Train** (campaign stage 14): `GE007-TrainSecretRoom` (a hidden room),
`TrainTargetGlass`.
- **Misc props/vehicles**: `GEMotorbike`, `GERedTruck`, `GESovietBoat`,
`GEHelicopterPilot`, `GEBiker`, `GoldenEye-moonfemale` (`moonfemale/` chr
model exists), `OurumovKey/Briefcase`, `GEDeskMat`, `GoldenEye-Crypt`
(confirmed orphaned map — see note below),
`GE64-BridgeRooms`, `GEJunglePicture`, `Runwaypicture`,
`Controlicon`, `Cactus`.
- **Orphaned level geometry** — `assets/obseg/bg/` holds per-level dirs for
`ame arch arec ark azt cat cave crad cryp dam depo dest dish len oat ref run
sev sevb sevx silo stat tra`. Notable: `cryp` (Crypt) has full geometry +
briefing (`UbriefcrypZ.c`) but **no `LEVELID_CRYPT` entry** in
`bondconstants.h` — fully orphaned. `len` (Lenin statue area, cf.
`StatueLeninPR`) and `dish`/`cat`/`oat`/`arec`/`ame`/`ark`/`ref` are further
level dirs beyond the shipped set. No `citadel` dir — its geometry was
either removed or shared (TCRF's 5 Citadel screenshots show a finished map,
so it existed in some build).
**Status: Tier 2.** All models are in the ROM's object table (spot-checked:
`PchrtesttubeZ`, `Pdest_harpoonZ`, `Pdoor_roller1-4Z`, `PhatfurryZ`,
`Pletter_tray1Z`, `Pmissile_rackZ`, `PsilencerZ`, `Psteel_door2[ b]Z`,
`Ptorpedo_rack`). Placing them needs level-data editing (object spawn tables) —
a data-mod / level-editor project, not a port change. The PR-vs-final texture
pairs (`*PR__1vt.jpg`) are interesting for a "pre-release textures" cosmetic mod
via the `tools_pc` asset pipeline — but the PR ROM itself isn't available here,
so source assets would have to come from TCRF's screenshots (low-res) or a
donated PR dump.
### F. Unused characters & faces
TCRF main-page imagery: `UnusedFace1/2/3` (each with F/S1/S2 views),
`GE64-FemHead1-4`, `KenFaceUnused/Used`, `Rosika[Compare]`, `SciCompare`,
`FemaleSci`, `GEBonds`, `Terrorists`, `TerroristBiker`, `StPetersburgGuard`,
Bond-suit sets named after actors: `Connery*`, `Dalton*`, `Moore*` (each with
HeadFront/Back/Left/Right + Jacket pieces), `GEArmor[our]`.
Cross-reference:
- The MP character table (`front.c:601+` `mp_chr_setup[]`) is huge — ~40 named
entries beyond the 8 selectable, including `ROSIKA`, `KARL`, `MARTIN`,
`MARK`, `DAVE`, `DUNCAN`, … `TERRORIST`, `BIKER`, `STPETERSBURGGUARD`,
`HELICOPTERPILOT` — Brosnan-body + swapped-head combos. The "unused faces"
are almost certainly extra head models in this pool that no body/entry uses.
- Head model dirs exist under `assets/obseg/chr/`: `headken` (the Ken face),
`headbrosnan*` (4 costume variants), `moonfemale`, plus ~30 named heads.
The actor-named suits (Connery/Dalton/Moore) do **not** appear as chr dirs —
**resolved in round 2**: the ROM holds exactly 80 `C*Z` character models and
none is a former-Bond actor; only the enum slots + the flattened actor-switch
code (`bondview2.c:391-460`) survive (see banner, item 4).
**Status: Tier 2/3.** Cosmetic mod (extra MP faces) = asset-level work if the
models are in the ROM; verify by dumping `assets/obseg/chr/*/Model.c` headers.
### G. Prerelease / dev-build evidence
Prerelease page (body blocked; filenames + embeds only):
- PR-vs-final comparison pairs: Taser, KF7, GrenadeLauncher, RocketLauncher
(+watch versions), SurfaceVent/Dish, `StatueLeninPR`, Facility walls/rooms
(§E), ArchivesWalls, CavernsWalkway, SiloCutWalkway, ThirdSiloEditor,
PRSiloElevator, BunkerLamp, OutletConsoles. Pattern: the PR build had more
weapons (watch-mounted guns!) and different Facility/Silo/Aztec geometry.
- **Dev-build videos** (YouTube embeds captured in the export):
- "Goldeneye 007 - Shoshinkai/Space World 1996"
- "Nintendo of America E3 1996 Take Away Video (VHS Rip)"
- "Goldeneye Beta WalMart Video"
- "Goldeneye 007 Promotional Trailer 1996"
- "N64 vous n'en reviendrez pas (VHS RIP 1997)"
- "64History - N64 B-Roll (1995)"
- "Escape (Banjo-Kazooie, GoldenEye 007)" — engine sibling footage.
Useful for dating PR features; not actionable in-repo.
### H. Misc oddities (main page, unverified)
- `GoldenEye007MickeyMouse` (by-level page, 2 KB image) — no "mickey" object in
the resource table; likely a texture/model easter egg spotted in a level.
- `Unused_bond_walk.gif` — an unused Bond walk animation (check
`assets/animationtable_entries.c` for orphaned anim IDs).
- `GoldenEye007-MovieLogoUnused` / `-MovieNoLogoUnused` — alternate intro logo
frames (title sequence data; `src/game/brief*`? verify).
- `GE_UnusedAlarm`, `GoldenEye-BloodImpact` / `BoxBlood` / PR `OpaqueBlood` —
blood-effect variants (cf. `src/game/blood_animation.c`).
- `GoldenEye007-BarrelUnused` / `-SmallBarrelUnused` — barrel props never placed.
- `GoldenEye-OddDay`, `Sight[Alt]`, `MountainTex`, `background`, `Pbin1Z`,
`PchrtesttubeZ[Explode]`, `PitonGunBungee`, `Psev_door[3_wind]Z` — props in
the table, placement unverified.
- Bugs & Notes articles: **nothing recoverable from this export** — re-scrape.
## 2. Feasibility summary
| Tier | Item | Effort | Where it lands |
|---|---|---|---|
| **0 — already works** | Debug menu (U+D C-hold), free cam, line mode, ramrom record/replay, cheat-grantable rocket/grenade/P90 | verify in-game; document keybinds | docs + maybe `input.c` keybind for U+D c-buttons |
| **1 — port-layer hook** | Grant taser/spectre/autoshot + unused items (watch identifier, bomb case, …) via config toggle replicating the cheat grant path (`bondinvAddInvItem` + `give_cur_player_ammo`) | small; follow F10-toggles pattern (`feat/f10-game-toggles`) | `port/src/` only |
| **2 — data/asset mod** | Place unused props per level; extra MP faces; PR texture swap | needs level-editor or object-table patching; separate project | data mod, not this repo's code |
| **3 — game-logic change (mod fork)** | Re-enable 8 cut MP maps (`front.c` table), campaign reordering | one-line-per-map table edits + setup fixes ("xbla setup" = unknown editor flag) | separate mod branch/fork only |
## 3. Next steps
1. **In-game verification pass** (PC build): open debug menu; confirm free cam,
`display speed`, level switch; enter the 2X rocket/grenade/P90 cheat codes;
screenshot via `grab rgb`. Record results here + findings.md if anything new
breaks (Dxx).
2. Confirm which region macros the PC build defines vs the `BUGFIX_R1` guards in
the weapon-grant cheats (non-negotiable #3) — the grants may be dead code in
our build.
3. Dump `assets/obseg/chr/` model headers to identify the UnusedFace1-3 /
FemHead1-4 models concretely.
4. ~~Re-scrape TCRF article bodies~~ — **done**; all six bodies recovered into
`backlog/cut-content/articles/` and cross-referenced (see RESEARCH.md §0).
5. If Tier-1 item granting is wanted: spec it as an F10 toggle set, port-layer
only, each entry citing the cheat-code precedent in `cheat.c`.
+170
View File
@@ -0,0 +1,170 @@
# D172 — particle/flares wrong color (magenta/cyan blood): ROOT CAUSE FOUND + FIXED (M-82)
Symptom (user-reported, GRAPHICS-BACKLOG line 31): bullet impacts / flares / blood spatter
render magenta/cyan instead of dark red.
## TL;DR (M-82)
**Root cause:** `gfx_lod_tile_offset()` (`gfx_pc.cpp`) returns `0` unconditionally on
the `!gfx_detail_textures_enabled` path (a D107 blurry-ceiling fix — and
`port/src/video.c:225` sets that flag `false` at startup). Particle draws use
`G_CC_INTERFERENCE` (`TEXEL0 * TEXEL1`) with tile 0 = IA8 smoke @ TMEM 0 and
tile 1 = RGBA16 fire @ TMEM 0x188 — but with the offset pinned to 0, **texunit 1
samples the smoke texture too**. `smoke * smoke` through the rest of the combine
= the magenta/cyan. Confirmed by the `GE_D172` probe on a user Silo firefight:
40/40 particle tris logged `cfg1[tile=1 tmem=392 fmt=RGBA]` but
`SAMPLED1=tile0(tmem=0 fmt=IA)`.
**Fix (`gfx_pc.cpp` `gfx_lod_tile_offset`):** `return rdp.tex_lod ? 0 : i;` —
fold to the base tile only when LOD is actually active (D107's mip case,
`tex_lod=1`, unchanged); a genuine non-LOD 2-texture combine now gets tile `i`.
`bunker1` verify PASS (the D107 repro level). Committed on PR #34. **Owed: an
in-game eyeball that blood/sparks now read dark red.**
The 0xB9 / cycle-type false trails and the probe design are kept below for the
record.
---
Original note (superseded): Static audit done; one strong root-cause candidate
with a cheap in-game verification. **No code changed.**
## 1. How particles get their color state (mechanism)
Particle triangles (`explosion.c`, `gSPVertex` + `G_TRI_FILL`/`G_TRI_SHADE_TXTR`) carry
**no per-primitive material command**. Their CC/tile state comes from replaying ROM-resident
state records via `gSPDisplayList(gdl++, g_ExplosionDisplayLists[var_s2])`
(`explosion.c:894`, 15-record loop, one record + its particles per iteration).
- Records: `globalDL_0x078 … globalDL_0x9a8`, a 17-entry table at image-bank offset
0x28 (`g_pc_gimg_off_globalDL_*`), bank base cart 0x1029D160 (see `image_bank.c:322`).
- Each record is also scanned at load by `texLoadFromDisplayList` (`src/game/image.c:475`)
for its `G_SETTIMG` + ABCD marker word — that part works (textures upload fine).
## 2. Record layout (decoded from ROM)
```
+0x00 E7 C3 FF FE G_RDPPIPESYNC
+0x08 B9 00 03 1D ??? <-- UNHANDLED by fast3d (see §4)
+0x10 <varies> second word of the ??? command
+0x18 FC ... G_SETCOMBINE w0 (rgb c0 | alpha c0 <<16)
+0x20 FF ... G_SETCOMBINE w1 (rgb c1 | alpha c1 <<16)
+0x28 FD ... G_SETTIMG w0 (timg pointer)
+0x30 .. 50 50 50 50 G_SETTIMG w1 (ABCD marker)
```
Dominant particle combine mode (14 of 17 records), decoded with the standard libultra
bit layout:
- cycle 0 RGB: `TEXEL0, 0, TEXEL1, COMB_A`
- cycle 0 ALP: `TEXEL0A, COMB_A, SHADE, COMB_A`
- cycle 1 RGB: `COMBINED, 0, SHADE, COMB_A` ← **2-cycle** modulate
- cycle 1 ALP: same as cycle 0
## 3. fast3d audit (what is right)
- `G_SETCOMBINE` bit extraction (`gfx_pc.cpp:2786`) matches the standard libultra layout
exactly (verified field-by-field against the ROM words). Not the bug.
- Store/decode of `rdp.combine_mode` (`gfx_dp_set_combine_mode` vs `gfx_generate_cc`
key shifts) are consistent. Not the bug.
- The generated GLSL combiner implements the RDP formula with the usual special cases;
`SHADER_COMBINED` correctly resolves to the previous cycle's running value.
## 4. The 0xB9 root-cause candidate is WITHDRAWN (M-82, 2026-09-08)
The earlier audit claimed fast3d has "no case for opcode 0xB9" and that the D146
`default:` guard therefore aborts every particle sub-DL. **That is wrong.**
This build does **not** define `F3DEX_GBI_2` (verified: no `-DF3DEX_GBI_2` in
`CMakeLists.txt`, no `#define` in `include/PR/gbi.h` or anywhere in tree). In the
non-F3DEX2 branch of `gbi.h`, `G_SETOTHERMODE_L = G_IMMFIRST-6 = -71`, and
`(uint8_t)(-71) == 0xB9`. So `gfx_pc.cpp:2731` `case (uint8_t)G_SETOTHERMODE_L:`
**is** `case 0xB9:`.
Decode of the record word `w0 = 0xB900031D`, `w1 = <varies>`:
`gfx_sp_set_other_mode(C0(8,8)=3, C0(0,8)=29, w1)` → sets `other_mode_l` bits
3..31. `sft=3 == G_MDSFT_RENDERMODE`, `len=29` — this is exactly
`gsDPSetRenderMode(w1)`. `w1` values (`0x0C184B50`, `0x00504B50`, …) are
render-mode / blender words. fast3d applies it correctly.
So the particle sub-DL runs to completion on PC: `E7` RDPPIPESYNC (handled),
`B9` SetRenderMode (handled), `FC` SETCOMBINE (handled), `FD` SETTIMG (handled).
**No D146 abort. No stale-state fallthrough.** The wrong-colour bug is elsewhere.
## 5. Cycle-type is ALSO ruled out — the real record, and where the bug is (M-82)
The actual particle records are in **`assets/oddtextures.c`**
(`globalDL_0x078` … , 17 of them). One record verbatim:
```c
gsDPPipeSync(),
gsDPSetCycleType(G_CYC_2CYCLE), // <-- record DOES set 2-cycle
gsDPSetRenderMode(G_RM_PASS, G_RM_ZB_CLD_SURF2),
gsDPSetTextureLOD(G_TL_TILE),
gsDPSetCombineMode(G_CC_INTERFERENCE, G_CC_MODULATEIA2),
gsSPTexture(..., G_TX_RENDERTILE, G_ON),
// tile 0: IMAGE_SMOKE_0 IA16 image -> loaded/rendered as IA8, 56x56
// tile 1: IMAGE_FIRE_0 RGBA16, 16x14, loaded to tmem 0x188
gsDPSetTextureLUT(G_TT_NONE),
gsDPPipeSync(),
gsSPEndDisplayList(),
```
So the record **explicitly sets `G_CYC_2CYCLE`** (opcode `0xBA` =
`G_SETOTHERMODE_H` in this build), and fast3d handles `0xBA` correctly:
`gfx_pc.cpp:2734` → `gfx_sp_set_other_mode(52, 2, w1<<32)` sets `other_mode_h`
cycletype = 2CYC. **Confirmed by probe (M-82):** the `GE_D172=1` SETCOMBINE
probe logged the particle combine `w0=fc26a004` (= `G_CC_INTERFERENCE`/
`G_CC_MODULATEIA2`) with **`cycletype=1 (2CYC)`** every time. The cycle-type
hypothesis is dead too.
**The bug is the `G_CC_INTERFERENCE` two-tile combine / TEXEL1 path.**
`G_CC_INTERFERENCE` cycle-0 = `TEXEL0 * TEXEL1`; the record binds **two
different tiles of two different formats** (tile 0 IA8 smoke, tile 1 RGBA16
fire at tmem 0x188). Magenta/cyan = a TEXEL1 sampling/decode fault: wrong tmem
offset for tile 1, RGBA16 (5551) channel/endian misread, or fast3d treating
tile 1 as an LOD mip of tile 0 under `G_TL_TILE`. This needs a visual
render-iterate pass (headless can't see particle colour) — see §6.
### Probes shipped (M-82, `#ifdef PORT`, `GE_D172=1`, inert when unset)
- `gfx_pc.cpp` G_SETCOMBINE case — logs each distinct combine + active
cycletype (dedup, 64 max). Confirmed the particle combine `fc26a004` runs
at `cycletype=2CYC`.
- `gfx_pc.cpp` `gfx_sp_tri1` (after the texunit loop) — whenever a **2-cycle
triangle actually consumes TEXEL1**, dumps `combine_mode` + both texunits'
`tile / tmem / fmt / siz`. This is the decisive probe: a Silo close-range
guard kill with `GE_D172=1` should log
`T0[... fmt=3(IA) siz=1(8b)] T1[tmem=392 fmt=0(RGBA) siz=2(16b)]`.
If `T1.tmem != 392`, or `T1.fmt/siz` are wrong, the TEXEL1 bind is the bug.
If they're all correct, the fault is in `import_texture_rgba16` for the
second texunit or the `G_CC_INTERFERENCE` CC generation — pair with
`GE_DTEX=1` / `GE_TEXRAW=1` to dump the fire tile's bytes.
## 6. Fix path (next session — needs a display)
1. Get a `-level_XX` capture with visible particle spray (Silo guard kill at
close range per the original report, or any wall-impact spark). Note the
colour.
2. Env-gated probe in `gfx_pc.cpp` `import_texture` / the TEXEL1 bind path:
for tile 1 when combine == the particle key, log format/size/tmem-addr/
line_size and the first few decoded texels. Compare against the RGBA16
`IMAGE_FIRE_0` bytes in the extracted asset.
3. Likely fixes, in order of suspicion: (a) tile-1 tmem address 0x188 not
honoured → TEXEL1 reads tile-0 bytes; (b) RGBA16→RGBA32 decode for the
second texunit; (c) `G_TL_TILE` LOD path picking the wrong tile for TEXEL1.
4. Re-decode `G_CC_INTERFERENCE` / `G_CC_MODULATEIA2` as fast3d generates them
(`gfx_cc.cpp`) and confirm the GLSL matches `TEXEL0*TEXEL1` then
`COMBINED*SHADE`.
## Files
- `src/game/explosion.c` — record table (165), replay loop (894)
- `src/game/image_bank.c:322`, `src/game/image.c:475` — record offsets / load-time scan
- `port/fast3d/gfx_pc.cpp` — SP walker cases (~2600–2930), D146 default (2920),
SETCOMBINE decode (2786), combine store (2270), cc generation (312+), 2CYCLE flag (1523)
- `rsp/graphics/gmain.s` — N64 ucode ground truth for 0xB9 (dispatch table at dmem 0xbc;
ginit.s missing from repo)
Note (M-82): 0xB9 is `G_SETOTHERMODE_L` in this non-F3DEX2 build and is already
handled by fast3d — `gmain.s` / `ginit.s` are NOT needed to resolve D172.
+127
View File
@@ -0,0 +1,127 @@
# D176(a) Sky — consolidated research notes (read-only)
Companion to `GRAPHICS-BACKLOG.md` D176(a) and findings.md M-37/M-47.
Purpose: make the next implementation session mechanical. No code changed by
this doc.
## State of play
- **Root cause (M-37):** `skyRenderTri`/`skyRenderFull` emit geometry only as
`G_RDPHALF_*` immediates; `gfx_pc.cpp:2901-2903` deliberately no-ops them
(same in the PD port) → solid black sky on every cloud-sky level. Clean
headless repro: `-level_22` (Statue), frame ~360.
- **M-47 draft (Path B)** — `scratchpad/sky.patch`: `#ifdef PORT` hooks in
both functions re-emit the sky as ordinary screen-space GBI via
`skyPortRenderPoly` (ortho pixel projection + `gSPVertex`/triangles).
Geometry reaches GL and textures (top-half luma 0.1 → ~174 on `-level_22`).
Two open defects (below). Draft, not shippable.
- **Path A** (decode RDPHALF in fast3d) is the backlog's original suggestion;
the wire format is now fully decoded (next section), so it is a viable
fallback if Path B's units can't be pinned.
## RDPHALF wire format (decoded from `src/game/sky.c` + `include/PR/gbi.h`)
- Opcodes (`gbi.h:102-164,216-223`): `G_RDPHALF_1 = 0xe1`,
`G_RDPHALF_2 = 0xf1` (final word of a stream), `G_RDPHALF_CONT`
(= `G_IMMFIRST-13`). Tri ops: `G_TRI_FILL 0xc8`, `G_TRI_FILL_ZBUFF 0xc9`,
`G_TRI_SHADE_TXTR 0xce`, `G_TRI_SHADE_TXTR_ZBUFF 0xcf`.
- **All payload values are S15.1 fixed point** — converter is
`sub_GAME_7F094298` (`sky.c:218`): `clamp(x, ±32767.9) × 65536`, u32.
- **Packing:** each `(G_RDPHALF_1, G_RDPHALF_CONT)` pair carries four
halfwords = **two S15.1 values**: `valueA = (w1.hi16, w2.lo16)`,
`valueB = (w1.lo16, w2.hi16)`. Multi-value groups are streamed in two
passes — all high halves first, then all low halves (see the explicit
`(x & 0xffff0000) | (y & 0xffff0000) >> 16` packing at `sky.c:1854-1870`).
- **`skyRenderTri` header** (`sky.c:1725-1728`): first word =
`(op << 24) | (winding/sign flag bit23) | lo16(first value)`; the stream
then carries screen-space positions (`unk28×0.25` = pixel X, `unk2c` = Y×4
family) plus per-vertex attributes. The RSP ucode reassembles and DMAs a
real edge-walked RDP tri command (per the M-46 comment in sky.patch).
- **Vertex attribute vectors are 8 components** (`sky.c:1759-1776`):
`(r, g, b, a, S·w′, T·w′, 32767·w′, z)` with perspective weight
`w′ = unk34 × min(unk0c)/2` — i.e. color + perspective-corrected S/T +
depth. `SkyRelated38` fields: `unk28 = screenX×4`, `unk2c = screenY×4`,
`unk30 = z (0..0x7fff)`, `unk34 = 1/w`, `unk20/unk24 = S/T`.
- GE emits RDPHALF **nowhere except sky.c** — the command surface is closed
and small.
## Defect 1 — texcoord shear (clouds smeared into streaks)
`skyPortRenderPoly` uses `tc = unk20 × 32.0f` — a magic number.
### M-82 update — the scale is right, the magnitude is the bug
- **Cloud texture is `s_skywaterimages[0]` = `IMAGE_CLOUDS_GRAYSCALE`,
`0x40 × 0x40` (64×64), `G_IM_FMT_IA` / `G_IM_SIZ_8b`, `G_TX_WRAP`/`G_TX_WRAP`**
(`assets/oddtextures.c:646`). Not a fraction-indexed texture — it *tiles*.
- **GE's own screen-space textured-quad idiom is `tc = width << 5`**
(`src/game/glass2.c:678,689` — `sImageTableEntry.width << 5` /
`.height << 5`). `<< 5` = ×32 = the S10.5 texel fixed-point fast3d expects,
and it divides `tc` by `tex_width` before GL_REPEAT. So the patch's `× 32.0f`
**is the correct scale** — provided `unk20`/`unk24` are already in *texel*
units (they are: the cloud-tiling math in `sub_GAME_7F097388` /
`CloudRepeat(skyheight)=10000` produces texel coords in the thousands, and
M-47 measured `unk20` reaching ±20 000 near the horizon).
- **The bug: `(s16)(20000 * 32)` overflows.** `Vtx.tc` is `s16` (±32767); a
horizon vertex needs `tc` ≈ 640 000. It wraps/saturates → the streak.
- **Fix (no fast3d change, no grid needed for the near/mid sky):** phase-fold
every vertex of *one primitive* by the **same** multiple of the tile period
(64 texels), so inter-vertex deltas are preserved but magnitudes stay in
s16 range:
```c
s32 foldS = (s32)floorf(vmin_unk20 / 64.0f) * 64; // vmin over this prim's verts
s32 foldT = (s32)floorf(vmin_unk24 / 64.0f) * 64;
tc[0] = (s16)((v[i]->unk20 - foldS) * 32.0f);
tc[1] = (s16)((v[i]->unk24 - foldT) * 32.0f);
```
This is exact wherever the S/T *span across a single triangle* is
< ~1000 texels (s16 range ÷ 32). It holds for the corner-quad tessellation
the current patch emits everywhere except triangles that straddle the
horizon line, where the span diverges — those still need Defect-2's
finer/adaptive tessellation. Land the fold first (fixes most of the frame),
keep the grid as the horizon-band follow-up.
## Defect 2 — vertical coverage (band fills only top ~25%)
Leading hypothesis, from reading the N64 path: **the quad is not a plain
corner quad.** The N64 stream's four vertices are *perspective-interpolated*
points computed in the big interpolation block (`sky.c:1797-1813`, the
`sp210/sp290/sp2b0/sp230` vectors), not the raw TL/TR/BL/BR corners that
Path B's hook passes through. If those interpolated y-values differ from the
corner ys (they will, wherever w varies across the quad), the re-emitted
quad lands short.
Next steps (one focused session):
1. Per-call probe on `skyPortRenderPoly`: log nverts + each vertex's
(x, y) + the tri op-header word; A/B against N64 reference footage of
`-level_22`/`-level_36`.
2. If confirmed, either (a) replicate the interpolation block under PORT and
emit the 4 computed vertices, or (b) drop to Path A with the decoder spec
above (the ucode's own reassembly is then irrelevant — we decode the same
values directly).
## Recommendation
Finish **Path B** first — geometry already works; both defects are unit/
vertex-set questions answerable in one render-iterate session. Keep Path A
as fallback; it is now fully specified by this doc if B stalls.
## Verification ask (M-82 — for the next session with a display)
The Defect-1 phase-fold is committed to PR #18 but **unverified visually**
(this session had no display). One check settles it:
1. `./build-pc/ge007.x86_64.exe -level_22` (Statue, night). At the intro the
camera faces the horizon within a few seconds.
2. Look at the upper half of the frame:
- **Black** → Path B still not emitting (regression from the rebase —
check `skyPortRenderPoly` is reached; `GE_D176=1`).
- **Clouds, but smeared into horizontal streaks** → the fold didn't take;
re-check `unk20`/`unk24` really are texels (probe their range).
- **Recognisable tiled cloud texture, correct near/mid, ragged or missing
right at the horizon line** → fold works; that ragged band is Defect 2
→ adaptive tessellation is the next task.
- **Looks right** → close Defect 1, mark PR #18 ready pending a
`-level_36`/`-29`/Dam-exterior spot check.
3. A `GE_PCDUMP="330-390:30"` capture of that run attached to PR #18 is enough
for a remote reviewer to make the same call.
+369
View File
@@ -0,0 +1,369 @@
# D356 — Settings regroup: functional sections, save-file scoping, per-section reset
Status: **LANDED (D356, 2026-09-27, port-only)** — signed off and implemented
this session; verification in `docs/dev/findings.md` D356 (GE_WSPROBE_RESET
front + in-stage probes, regressions, build). Supersedes the "BOND FILE"
section introduced in D353. Baselines: Turok PC port options (Nightdive, list
provided by the maintainer), the PD port's `port/src/optionsmenu.c`
(`C:\Users\james\Source\Repos\pd_port`), and plan §6 of
`docs/dev/OPTIONS-MENU-PLAN.md` (the original "align with Turok" note).
## 1. Goal
The D353 "BOND FILE" section is the one section named after *where a value is
saved* — an implementation detail — while every other section is functional
(DISPLAY, GRAPHICS, MOUSE/AIM, CONTROLLER, GAME). Its contents are also a
mishmash (aim/look toggles + HUD toggles + audio), and it exposes N64-watch
rows that **collide with or duplicate** the PC-native rows the port added
(watch look-invert vs `Input.MouseInvertY`/`Input.PadLookInvertY`; watch aim
control vs `Input.AimMode`). The baseline ports (Turok, PD) never name a
section after the save and never re-expose legacy overrides.
D356 rewrites the row table so that:
1. Every section is functional, using the baseline names:
**INPUT, GAMEPLAY, GRAPHICS, AUDIO, VIDEO**.
2. Save-file scope is communicated the PD way — *in the section title*
("GAMEPLAY (File 1)"), generalized from the D353 BOND-FILE annotation —
plus a single **Save file: File N [change]** row at the top of the
options screen (PD "Player N" pattern; D354's default resolution applies).
"Bond file" appears nowhere in the UI.
3. Redundant/colliding watch rows are removed from the **menu surface only**
(the established D181/D216/D304 pattern): keys, in-game watch behavior,
and ini hand-editing all remain untouched. No game-code change (rule 2).
4. A **Reset to defaults** action row per section (Turok's per-category
reset; plan §6 Gate E, made concrete) with a **confirmation contract**:
edge-triggered activation (no auto-repeat), two-step arm→confirm, never
repeatable (WATCH-SETTINGS-PLAN §6 gate: "separate confirmation and
persistence policy before wiring a button" — §5.4 below is that policy).
5. A **context-aware file accessor** (front-end target vs active stage
target) with the "none" chooser position retired, so "Save file: File N"
is always a true statement (§5.5).
6. Section resets iterate the section's **declared** rows (including
conditionally hidden ones, §5.6) and reset verification exercises the UI
action itself, headlessly (§5.8).
## 2. Non-goals
- No `src/game` changes; the N64 watch menu keeps all its options and its
N64 wording (ground truth).
- No key rebinding (Phase 4) — but the section layout leaves room for a
"KEY BINDINGS" page exactly where Turok/PD have one.
- Master volume stays M3-backlog; the AUDIO section is shaped for it
(Turok's Master/Sound/Music trio).
- N64 build untouched.
## 3. Exposure filter (menu surface only)
| Row (key) | Verdict | Reason |
|---|---|---|
| `Bond.Look` ("Look up/down (watch; stacks)") | **Hide** | Duplicates `Input.MouseInvertY` + `Input.PadLookInvertY`; stacking both produces the double-negation trap. |
| `Bond.AimControl` ("Aim control") | **Hide** | Collides with `Input.AimMode` (N64 vs PC aim model) — two rows fighting over the same domain. |
| `Bond.AutoAim` | Keep | Unique gameplay assist; orthogonal to aim style/range. |
| `Bond.LookAhead` | Keep | Unique; no PC counterpart, no conflict. |
| `Bond.Sight`, `Bond.Ammo` | Keep | Unique HUD-visibility options; compose with `Game.HudScale`. |
| `Bond.Music`, `Bond.FX` | Keep | Core of D350–D355. |
Hidden rows are commented out in `rows[]` with a D356 note (D181/D216 style:
config keys + watch path stay live; ini power users can still set them).
## 4. New layout (5 pages + save-file row)
`MAX_PAGES=8`, `MAX_PROWS=15` — all pages below the caps (D353 overflow log
still guards).
Top row (drawn by `frontoptions.c` above the paged content, not paged):
**`Save file: File 1 [change]`** — cycles files via `watchSettingsChooseFile`
(D354 default resolution: explicit pick > viewed folder > first valid;
auto-init fallback). Hidden in F10 (existing `optionsRowIsBondChooser`
mechanism, generalized). F10 targets the active file, as today.
| Page | Rows (key · kind) | Title annotation |
|---|---|---|
| **INPUT** | `Input.MouseSensitivity` · `Input.MouseInvertY` · `Input.AimMode` · `Input.AimRange` · `Input.PadLookInvertY` · `Input.PadDeadzone` · `Input.PadTriggerPct` · *Reset to defaults* | |
| **GAMEPLAY** (File N) | `Bond.AutoAim` · `Bond.LookAhead` · `Bond.Sight` · `Bond.Ammo` *(save, dim "(save)" tag)* · `Game.SkipIntro` · `Game.NoHitFlash` · `Game.AllUnlocked` · `Game.HudScale` · *Reset to defaults* · `__QuitToDesktop` | (File 1) |
| **GRAPHICS** | 11 rows unchanged from D353 · *Reset to defaults* | |
| **AUDIO** (File N) | `Bond.Music` · `Bond.FX` *(save; title annotation only — homogeneous section, PD-style)* · (master volume, M3) · *Reset to defaults* | (File 1) |
| **VIDEO** | `Video.Fullscreen` · `__Resolution` · `Video.VSync` · `Video.FpsCap` · `Video.DisplayFPS` · *Reset to defaults* | |
Moves from D353 layout: mouse/aim + controller rows merge into **INPUT**
(the two old headers `__HdrMouse`/`__HdrPad` are dropped); DISPLAY → **VIDEO**
(rename, `Video.DisplayFPS` moves from GAME); GAME section is renamed
**GAMEPLAY** and absorbs the surviving watch toggles; the `__HdrBond` header
and `__BondFile` chooser row are retired (replaced by the top save-file row);
`Bond.Look`/`Bond.AimControl` rows are commented out (§3).
Scope annotation: the D353 "(File N)"/"(no file)" title annotation — today
special-cased to the BOND FILE page via `optionsRowIsBondChooser()` — is
generalized: **any page containing per-file rows annotates**. Sections with
per-file rows: GAMEPLAY, AUDIO.
F10: same table (F10 already shows all rows, chooser hidden). The
(Gameplay/Audio) annotation in-stage shows the active file number (the
context-aware accessor, §5.8 — *not* the raw chooser value). "Reset to
defaults" rows stay visible in F10 (O2) under the same confirmation
contract (§5.4).
**(save) tag (O1):** the four mixed-scope GAMEPLAY save rows carry a subtle
dim "(save)" suffix — the only place a section mixes scopes. AUDIO is
homogeneously per-file, so its title annotation is sufficient (no tags).
Implementation: one `int saveScoped` field on `struct Row` + dim-ink suffix in
the row-draw path (frontoptions.c already draws the dim annotation).
## 5. Code changes
All in `port/` (D350-family); files: `port/src/optionsoverlay.c`,
`port/src/frontoptions.c`, `port/include/optionsoverlay.h`,
`port/src/watchsettings.c` (only if reset needs a new enqueue helper —
expected not to, see below).
### 5.1 Row table (`optionsoverlay.c`)
- Rename headers: `__HdrDisplay`→VIDEO (last page), add `__HdrInput`
(INPUT), `__HdrGameplay` (GAMEPLAY); delete `__HdrBond`, `__HdrMouse`,
`__HdrPad`, and rename `__HdrGame`→GAMEPLAY. New section order in the
table: INPUT, GAMEPLAY, GRAPHICS, AUDIO, VIDEO (Turok's order: Input,
Gameplay, Graphics, Audio, Video).
- Move the 7 mouse/aim + 3 controller rows under INPUT; move the 4 surviving
`Bond.*` toggles + music/FX into GAMEPLAY/AUDIO; comment out
`Bond.Look`/`Bond.AimControl` (§3).
- New `ROW_ACTION` rows, one per section: `.key="__Reset<Section>"`,
`.label="Reset to defaults"`. All five use one shared handler
(`rowResetSection(section)`).
### 5.2 Save-file top row (`frontoptions.c`)
- New non-paged line above the page content: "Save file: File N" + a
"change" control (left/right or Enter cycles, reusing
`watchSettingsChooseFile(dir)`; D354 lazy default fills N). Dim ink, same
visual language as the D353 annotation. `selected_folder_num` unchanged —
the row never touches the game's active file (plan Gate B invariant).
**Display value reads through the context-aware accessor (§5.5), never
the raw chooser value.**
- **Unavailable state (maintainer consistency fix):** the D354 auto-init
fallback creates the blank file on a *later game tick*, so at an
empty/corrupt-save startup the accessor can return -1 for a few ticks.
The row then shows the unavailable state ("(no file)"), never a file
number the write path cannot use. Once creation succeeds the same tick
the row shows "File 1".
- F10 context: row suppressed (extend the D353 accessor to cover it).
### 5.3 Reset to defaults (Gate E)
- **Per-file rows** (GAMEPLAY/AUDIO save rows): defaults = **BLANKSAVEDATA**
(`src/game/file2.c` — the game's own factory save; per WATCH-SETTINGS-PLAN
§6, watch rows reset to BLANKSAVEDATA/`DEFAULT_OPTIONS` for the selected
file, *not* from a global ini snapshot; new saves start Music/FX 0xFF,
auto-aim/sight/look-ahead/ammo enabled — read the fields from
`BLANKSAVEDATA` at reset time, no hardcoded table). Reset re-applies live
via the existing `watchsettings` apply path **and** persists field-scoped
to the selected file (`watchSettingsPersistField`), exactly like a normal
commit. In-stage: enqueued through the D352 command queue (N field commits
with `commit=1`) — no new threading; game thread applies + persists.
- **Ini rows** (INPUT/GRAPHICS/VIDEO/GAMEPLAY-ini): `config.c` has no
central default table (first-write "defaults" = current values,
config.c:280), so the section handler uses an explicit
`static const struct { const char *key; double def; }` reset table in
`optionsoverlay.c`, mirroring the port's C initializers (each entry
commented with its source variable; **verified against the initializer at
implementation time** — e.g. `Game.AllUnlocked` = **0**,
`port/src/video.c:116,348`; the D257 comment in optionsoverlay.c that
says "default ON" is stale and is corrected as part of D356). Front-end:
write live + `configSave()` (existing). In-stage ini resets: applied
**immediately** (O3 — consistent with how every other ini row already
behaves when changed in F10; video.c applies most display changes on the
fly).
- **Resolution is excluded from the VIDEO reset (maintainer item 2).**
`__Resolution` is an action backed by `s_resSel` +
`videoRequestWindowSize()` — not a registered config row, so the numeric
default table cannot reset it, and a "default window size" is a
display-capability choice, not a tunable. The D356 findings entry
documents the exception explicitly: VIDEO reset covers Fullscreen, VSync,
FpsCap, DisplayFPS — Resolution keeps the player's current choice.
- **Scope-aware by construction:** a section's reset touches each row
**by its own scope** (file rows → that file's BLANKSAVEDATA values;
ini rows → ini defaults). No cross-scope surprise (Gate E: "do not make
a global reset wipe per-file watch settings" — a reset of the selected
file never touches other files; §5.8 verifies this).
- **Activation contract (§5.4) applies to every section's reset row.**
### 5.4 Reset activation contract (maintainer item 1)
A reset is a *destructive bulk write* — GAMEPLAY's in particular changes
both ini values **and** the selected save in one action. WATCH-SETTINGS-PLAN
§6 gates confirmation + persistence policy; this is that policy:
- **Edge-triggered:** reset actions fire only on a *fresh* event. The
frontoptions left/right key repeat (held keys re-issue the action) is
suppressed for `ROW_ACTION` reset rows — the arming/confirm events must
carry a no-repeat flag (SDL keydown with `repeat==0` / a fresh click),
so a held key or held mouse button can never arm *and* confirm, nor
re-trigger.
- **Two-step confirm:** first activation **arms** the row (label swaps to
"Confirm reset…", armed highlight); a second activation within **3 s**
commits. Timeout, or navigating to another row, disarms (label
restores). Arm state is per-row, per-open; opening/closing F10 or the
options screen clears all armed resets.
- **One commit per confirmed activation.** No auto-repeat, no re-arming on
the same event. F10 and the options screen share the identical contract
(O2: rows stay visible in F10; confirmation makes accidental in-game
resets unlikely).
- The confirmed action then dispatches the section handler (§5.3) exactly
once.
### 5.5 Context-aware file accessor; retire "none" (maintainer item 3)
`watchSettingsFolder()` returns the raw `chosen`, which may be -1 before
`frontFolder()` lazily resolves, and in-stage `chosen` can differ from the
active file (`snapFolder`). The generalized title/top-row accessor must be
**context-aware**, not a repurposed chooser-value read:
- New `watchSettingsActiveFolder(void)` (name tentative):
- **Front-end contexts** (options screen, F10-on-file-select, title):
the *resolved front target* — explicit `chosen` if set, else
`selected_folder_num` if valid, else first valid folder (i.e. the
`frontFolder()` resolution; the accessor is pure-read and does not
mutate `chosen` — lazy mutation stays in `frontFolder()`/the
auto-init fallback).
- **In-stage (F10):** the *active stage target* — `selected_folder_num`
(the file the stage loaded, `snapFolder` at apply time).
- The D353 title annotation and the §5.2 top row both read through this
accessor, in both surfaces.
- **The chooser's "none" position is retired.** `watchSettingsChooseFile`
cycles FOLDER1..MAX only (dropping the -1 stop). Rationale: D354's
default resolution + boot-time BLANKSAVEDATA slots + the auto-init
fallback guarantee a valid target, so "none" was only reachable by
deliberate cycling and serves no remaining purpose — and "Save file:
File N" (the §5.2 invariant) is not true when `chosen == -1`.
`watchSettingsFolder()`'s "-1 = none" contract is updated/superseded by
the new accessor.
### 5.6 Reset iterates declared rows (maintainer item 4)
The section handler iterates the section's **declared row range in
`rows[]`** (from its header to the next header), *not* `s_visIdx`/the
currently visible list — conditionally hidden rows (GRAPHICS draw/LOD
sliders while their auto-FOV toggles are on) must be reset too, or they
silently survive a purported section reset. Hidden-row defaults come from
the same default table; their *live* values are written directly (the
`ptr`/`rowSet` path), visibility is irrelevant. The `(save)` tag, hidden-row
resets, and the reset table are all keyed off `rows[]` indices, never the
visible list.
### 5.7 F10
Unchanged machinery: shared table, save-file row suppressed (the D353
accessor pattern extended to the §5.2 top row). Annotation shows the active
file via §5.5. "Reset to defaults" rows stay **visible** in F10 (O2) under
the §5.4 confirmation contract.
### 5.8 Reset verification probe (maintainer item 5)
New env-gated probe **`GE_WSPROBE_RESET=<tick>`** (GE_D314/GE_WSPROBE
pattern): at tick N of a *front-end* boot it drives the **UI action path**
through the same handler the confirm step calls (arm → confirm → dispatch,
no bypass). **Every section being verified is itself dispatched:** the
probe dispatches all five section resets (INPUT, GAMEPLAY, GRAPHICS, AUDIO,
VIDEO) via arm → confirm, and after *each* dispatch verifies that
section's rows:
- **INPUT / GRAPHICS / VIDEO (ini rows):** values == the default table.
GRAPHICS specifically covers the **hidden** draw/LOD sliders (hidden
while their auto-FOV toggles are on) — a section reset must land them
even while they are not visible; VIDEO's `__Resolution` is the documented
exclusion (unchanged, logged).
- **GAMEPLAY (mixed scope):** the four save rows == BLANKSAVEDATA
(auto-aim/sight/look-ahead/ammo) *and* the ini rows (SkipIntro/
NoHitFlash/AllUnlocked/HudScale) == the ini default table — the two
scopes land independently in one action.
- **AUDIO (save rows):** Music/FX == BLANKSAVEDATA (0xFF raw = 32767,
the `VOLUME_MAX` endpoint), not the probe's pre-dirtied 4096.
- **Scope isolation (after all dispatches):** a *second* folder's
`save_data` is byte-identical to its pre-reset snapshot (a reset of the
selected file never touches other files).
Front-end prep (tick N) first dirties live values (Music/FX → 4096,
VSync/HudScale → non-defaults) and snapshots the second folder, so the
verifications at tick N+1 prove the resets actually moved values back.
In-stage dispatch is covered by the same env var on a `-level_XX` run:
arm → confirm at tick N (file rows enqueue through the D352 queue — the
probe logs the enqueued command count), drain at N+1, then the same
per-section value verification against the post-apply state.
### 5.9 No changes
- `buildPages` in `frontoptions.c` (generic; 5 pages < MAX_PAGES),
`watchSettingsGameTick`/queue (D352/D354/D355 logic untouched), the D355
track-1 resync, `BLANKSAVEDATA` itself.
## 6. Threading / persistence gates (unchanged, restated)
- D350: no GE writes from the F10 input/render thread except the sanctioned
front-end commit path; in-stage edits queue to the game thread.
- D352: queue holds across transitions; per-field coalescing.
- D354: front contexts (including F10-on-file-select) route through the
chooser path; `frontFolder()` default resolution; auto-init fallback.
- D355: Music commit + level-entry resync of the BGM (track 1) player.
- D356 adds no new write path: reset = N existing commits (file rows) +
existing ini writes (ini rows).
## 7. Docs
- `docs/dev/findings.md` D356 entry + §F index row; regenerate
`findings-index.csv`.
- `docs/dev/WATCH-SETTINGS-PLAN.md`: status + D353/D354 "Bond File" section
references updated; Gate E marked landed (subset: per-section rows).
- `docs/dev/OPTIONS-MENU-PLAN.md` (tracked) + local notes: §6 "align with
Turok" annotated as landed via D356 (M3 master volume + key bindings
remain).
## 8. Verification
1. Clean NTSC build (`./build-pc.sh` / `cmake --build build-pc`).
2. Headless smoke: boots to front end; log page count (5, under MAX_PAGES),
no `buildPages` overflow warning, save-file row resolves to File 1 by
default (D354), annotation on GAMEPLAY/AUDIO pages, no "none" reachable
(chooser cycles files only, §5.5).
3. `GE_WSPROBE` / `GE_WSPROBE_FRONT` regression: identical pass as D355
(queue/commit paths untouched).
4. **`GE_WSPROBE_RESET` (§5.8) exercises the UI action, not just direct
commits:** per-section arm → confirm → dispatch of all five sections,
with each section's rows verified right after its own dispatch
(ini == table, save == BLANKSAVEDATA, hidden GRAPHICS rows included,
VIDEO resolution untouched), then the second-folder byte-identity
check. In-stage dispatch: `GE_WSPROBE`-style queue check on a
`-level_XX` run (enqueued command count + post-apply state).
5. **Confirmation contract check (§5.4):** a held key/click arms but never
confirms (edge-triggered); second activation within 3 s commits exactly
once; timeout/navigate disarms. Headlessly assertable via the probe
(arm-then-timeout leaves values untouched).
6. Manual visual pass (owed, same list as D353): arrows through 5 pages,
save-file cycling updates the annotation live, `(save)` tag on the four
GAMEPLAY save rows, F10 in-stage, reset in stage vs front (including the
armed/confirm visual state), rapid slider drag.
## 9. Open questions — RESOLVED (maintainer sign-off, 2026-09-27)
- **O1:** **yes** — subtle dim "(save)" tag on the four mixed-scope
GAMEPLAY rows; AUDIO's title annotation alone is sufficient (homogeneous
section).
- **O2:** **visible in F10**, provided resets require the §5.4 confirmation
step (which they do).
- **O3:** **immediate** in-stage ini resets, consistent with existing ini
edits in F10.
- **O4:** `__QuitToDesktop` stays in GAMEPLAY, **literally last — after the
Reset row** (section order: rows…, *Reset to defaults*, *Quit to desktop*;
the §4 page table is corrected accordingly).
Layout itself is signed off by the maintainer (2026-09-27): five-page
regroup + menu-only exposure filter, with the §5.4–§5.8 contracts as
specified above.
## 10. Estimate
One commit. ~5 files (`optionsoverlay.c`, `frontoptions.c`,
`watchsettings.c`, `port/include/optionsoverlay.h`,
`port/include/watchsettings.h`), net +400/−140 (table reshuffle + reset
contract + context-aware accessor + `GE_WSPROBE_RESET` are the new code).
Findings/csv/plan updates ride along.
+160
View File
@@ -0,0 +1,160 @@
# D357 — Settings thresholds & values: align display units/defaults with the PD port + Turok PC
Status: **PROPOSAL — awaiting sign-off** (2026-09-27). Baselines: the PD port
checkout (`C:\Users\james\Source\Repos\pd_port`, `port/src/optionsmenu.c` +
`input.c`/`video.c` config registrations), the Turok PC (Nightdive) list from
the maintainer's notes (`docs/dev/notes/OPTIONS-MENU-PLAN.md` §6 — *taken from
user notes, not checked independently*), and the N64 watch (ground truth for
the `Bond.*` rows only). Triggered by maintainer report: the current values
are "a bit confusing".
Companion change landed with this doc's review: the terminology settled on
**"Profile"** as the end-user term for a per-player game file -- the
front top row reads **Profile**, the per-file row tag reads
**"(per profile)"** and the F10 unavailable value reads **"Select a
profile"** (§4).
## 1. Problem
The D356 regroup fixed the *layout*; the *values* still mix three unit
families with no consistent rule:
- **raw internal units shown as-is**: `Input.MouseSensitivity` (1–500, 100 =
1× — the "100" is a % of a 1.0 multiplier, not a count),
`Input.PadDeadzone` (raw 0–30000, `dispDiv=300` → "23%" but the step of 500
raw is 1.67 display-%, so the slider lands on 23, 25, 26…),
`Bond.Music`/`Bond.FX` (0–32767, `dispDiv=328`, step 128 = 0.39 display-%);
- **N64-era vocabulary on PC rows**: texture filter "Nearest / Bilinear /
**3-Point**" (the N64 watch term for trilinear), "FOV scale %" instead of
degrees;
- **defaults that don't read as defaults**: MSAA shows "4x" fine, but
`FpsCap` 0–1000 with step 1 invites 117 FPS; `Video.LodDistance` default
150% vs `DrawDistance` 150% with a 25-step grid (100/125/150/175…).
The PD port is the house style to copy: per-stick sensitivity as a **±10
multiplier (default 1.0, negatives invert)** (`input.c:1537`), mouse speed
**-30..30 (default 2.5)**, deadzone **raw 0–32767 per stick+axis**, rumble
**0–1**, MSAA as a **2x/4x/8x/16x dropdown**, frame cap as **"N FPS"**,
volumes as 0–100 %. Turok's (user's list) conventions: single mouse
sensitivity knob, per-section resets (landed, D356), master/sound/music
volume trio (master still M3-backlog).
## 2. Principles
1. **Display, don't rescale the world.** Every change is port-only (row
table, `dispDiv`/step/`unit` fields, value-text, `kResetDefaults`): no
`src/game` change, and config *storage units* are kept wherever a change
would migrate existing ini files (principle 3).
2. **Human units in the value column**: `%` on a 0–100 grid, `×` multipliers
with 1.0 as the default, `N FPS`, `Off/2x/4x/8x`, `Unlimited`. A slider
step must map to a **whole display unit** (the deadzone 1.67%-step bug is
the canonical example to fix).
3. **Ini stability**: renaming a config key's storage unit is a migration.
Where the plan needs it, register a *new* key (e.g.
`Input.PadDeadzonePct`) and keep the old key as a read-once legacy alias —
the D333 `Input.AimMode`→`Input.AimStyle` alias pattern (`input.c:1962`).
Each migration gets a findings note.
4. **`kResetDefaults` stays in lockstep** with the config C initializers
(D356 Gate E): every default change in this plan updates the table in
`optionsoverlay.c` in the same commit; the `GE_WSPROBE_RESET` probe
re-verifies.
5. **Defaults = the config C initializers**, never a new menu-only default.
## 3. Row-by-row audit & proposal
Current state from `optionsoverlay.c` `rows[]` + the `configRegister*`
ranges (2026-09-27). "Display" = what the value column shows today.
### INPUT
| Row | Now (default · range · step · display) | PD port | Turok (notes) | Proposal |
|---|---|---|---|---|
| Mouse sensitivity | 100 · 1–500 · 5 · raw `100` | `MouseSpeedX/Y` float −30..30, def 2.5 (sign = invert) | single knob | **Display as a × multiplier**: keep storage (100 = 1.0×), value text `%.1f×` (v/100); range 10–300 (0.1–3.0×), step 10 (0.1×). Label stays "Mouse sensitivity". |
| Invert look (mouse) | Off/On | folded into the −/× sign of MouseSpeed | — | **Keep On/Off** (PD's negative-multiplier style is an alternative; a separate invert is simpler next to the × slider). Decision: confirm. |
| Aim style / Aim range | N64/Centred (PC) · PC/N64 | — (GE-only, D337/D338) | — | Keep as-is (GE-specific; hiddenIfOn already sane). |
| Invert look (controller) | Off/On | sign of stick scale | — | Keep. |
| Stick deadzone | 7000 raw · 0–30000 · 500 · ÷300 → `23%` (1.67%-steps) | raw 0–32767 per stick+axis, `DEFAULT_DEADZONE` | MISSING | **New key `Input.PadDeadzonePct` 0–100, step 1, default 23** (7000/30000×100); `input.c` scales back to raw at apply; old key becomes a legacy alias (principle 3). Display `N%` on a whole-% grid. |
| Trigger threshold | 23 · 1–99 · 1 · `%` | — (GE extra) | — | Keep. |
### GAMEPLAY
| Row | Now | Baseline | Proposal |
|---|---|---|---|
| Auto-aim / Sight / Look ahead / Ammo (profile) | Off/On (N64 watch 0/1) | N64 watch = ground truth | Keep; tag now reads **(profile)** (this doc's companion change). |
| Skip intro / No hit flash / All unlocked | Off/On | Turok: screen-shake/hit-flash family | Keep. |
| HUD scale | 100 · 75–150 · 5 · `%` | — | Keep (whole-% grid already; range cap D226). |
### GRAPHICS
| Row | Now | Baseline | Proposal |
|---|---|---|---|
| Anti-aliasing | 1/2/4/8, def 4, shows `None/2x/4x/8x`, restart | PD dropdown 2x/4x/8x/**16x**, def 4x | **Add 16x to the sequence** (`kMsaaSeq`, range clamp 1–16) — SDL/GL supports it; PD has it. Decision: perf cost on mid GPUs (restart already flagged). |
| Texture filter | Nearest / Bilinear / **3-Point** | PC convention: Nearest/Linear/Bilinear | **Rename the enum names to `Nearest / Linear / Bilinear`** (3-point *is* trilinear; the N64 term is the last one left on a PC row). Storage 0/1/2 unchanged. |
| Anisotropic filtering | 4 · 1–16 · 1 · `x` | Turok 1–16 | Keep; **dim/disable when filter = Nearest** (anisotropy only applies to filtered sampling) — `hiddenIfOff`-style guard, UI-only. |
| FOV scale | 100 · 50–150 · 5 · `%` | Turok 3: 60–120° slider | **Display in degrees**: the 100% baseline is the N64 vertical FOV scaled to the port's widescreen math — value text `N°` (v/100 × baseline, 1 decimal); storage unchanged. Decision: degrees vs % (Turok uses degrees; % is the existing mental model — pick one, recommend degrees). |
| Native widescreen / Widescreen auto FOV / Crop overscan | Off/On | — | Keep. |
| Draw distance | 150 · 100–400 · 25 · `%` (hidden under auto-FOV) | Turok: draw-distance row | Keep the % grid (25% steps read fine); **step 25 → display already whole-%**. No change. |
| LOD distance | 150 · 25–400 · 25 · `%` | — | Keep; note default 150 vs DrawDistance 150 is intentional parity. |
### AUDIO
| Row | Now | Baseline | Proposal |
|---|---|---|---|
| Music volume (profile) | 32767 · 0–32767 · 128 · ÷328 → 0.39%-steps | Turok: 0–100% | **step 128 → 328** (exactly 1 display-%; 32767/328 = 99.9 ≈ "100%"). Storage unchanged (N64 `VOLUME_MAX` semantics). |
| FX volume (profile) | same | same | same. |
| *(master volume)* | — | Turok trio | M3-backlog, out of scope (shape the section for it, D356 note). |
### VIDEO
| Row | Now | Baseline | Proposal |
|---|---|---|---|
| Fullscreen | Off/On | Turok: window mode (borderless missing) | Keep; borderless = separate backlog item. |
| Resolution | action row (presets) | both | Keep. |
| VSync | On/Off | both | Keep. |
| Frame rate cap | 60 · 0–1000 · 1 · `Uncapped/N FPS` | Turok: "up to 120 FPS" / uncapped | **Preset grid**: 0/30/60/120/144 (step = next preset, wraps to Uncapped); value text already `N FPS`/`Uncapped`. Decision: preset grid vs free 1-step (recommend presets; 117 FPS has no purpose). |
| Show FPS | Off/On | both | Keep. |
## 4. Terminology (end-user term = "Profile")
Decision (2026-09-27, UX review): the end-user term for a per-player
game file is **"Profile"** — the per-player bundle of progress +
per-file settings. Rejected: **"game file"** (the instruction manual's
current term) reads, in modern PC usage, as the *program's* own files
("Verify integrity of game files"), not the user's save; **"Save"** was
the fallback (universally understood) but reads as a verb in the row tag
and as progress-only, which these files are not. "Profile" covers both
and fits the dossier styling of the screen.
Final string set (both UIs: front options + F10 overlay):
| String | Where |
|---|---|
| **"Profile"** + value `N` | front options top row (was "Save file: File N") |
| **"(Profile N)"** / **"(none)"** | section-title annotation on save-scoped sections (was "(File N)"/"(no file)") |
| **"(per profile)"** | per-file row tag (was "(save)", then "(profile)", then "(game file)") |
| **"Select a profile"** | F10 unavailable value (was "Select file") |
**Manual sync TODO:** the instruction manual's "game file" should be
updated to "profile" (the manual is a port document we own).
"Bond file" remains in code identifiers/config keys (`Bond.*` rows,
`watchSettingsChooseFile`, ini keys) only -- never user-visible.
## 5. Gates
1. Port-only per principle 1; the deadzone key migration (principle 3) is
the only config-surface change and follows the D333 alias pattern.
2. `kResetDefaults` updated in the same commit as every default/range
change; `GE_WSPROBE_RESET` (front + `-level_33`) re-run, all sections
`failures=0`.
3. Build via `build-pc-cmd.bat` (the pi-shell MSYS env gap, D356 note).
4. Visual check owed (maintainer): front options screen + F10 in all five
sections; the overlap fix from the same session (the level-1 save-file
row no longer paints under the first content row).
## 6. Out of scope
- Key rebinding (Phase 4), master volume (M3), output-device selection,
borderless window, per-axis controller sensitivity (PD-style four-knob —
GE's one-knob design is D238/D304).
- Any `src/game` change; the N64 watch keeps its own values and wording.
+129
View File
@@ -0,0 +1,129 @@
# F10 visual refresh — GoldenEye dossier styling (proposal)
Status: **superseded by D363 after user playtest** — the dossier/paper
concept below was implemented as D362, then replaced with a dark
watch-green/Bank Gothic version. This is the historical proposal for a
port-layer F10 overlay proposal, not a replacement for GE's pause watch or the
front-end PC Options screen. Reference surfaces: `src/game/front.c`
`constructor_menu15_cheat`/`frontSetupMenuBackground`, the existing
`port/src/frontoptions.c` cheat-list-inspired options screen, and GE's in-stage
watch. Current overlay: `port/src/optionsoverlay.c` `optionsOverlayEmit`,
`overlayRowAtY`, `sliderBarSpan`, `maxVisibleRows`.
## 1. Direction and boundaries
Make F10 read as an **MI6 dossier laid over the game scene**, not a generic
SDL debug panel: dark translucent scrim, warm paper/ivory content card,
black Zurich-style menu text, red for active options/slider fills and an
occasional muted gold title/accent. The front-end PC Options already uses
GE's actual 3D folder and Zurich text; use it as the visual reference, not
as a display-list dependency. In-stage F10 must remain an inexpensive flat
2D overlay: no loading a 3D wallet model, new ROM assets, front-end init
functions or `src/game` modifications. Preserve the current two-level
category -> settings navigation and the shared settings/commit API.
The initial port-only implementation lives in `optionsOverlayEmit()` and
uses one shared `OvLayout` for both 320x240 stage and 440x330 front canvases.
Headless captures live under `scratch/d362-test/` (not distributed). Manual
visual review on a bright stage and controller/mouse feel-check remain owed.
## 2. Concrete visual hierarchy
- **Backdrop/card:** keep an adjustable translucent dim over the scene;
replace the opaque blue-black rectangle with a near-opaque ivory panel
(subtle border and one offset shadow). Do not mimic the actual folder
texture unless a later, measured prototype proves the simple card
insufficient. Preserve legible contrast against both dark and bright
levels, including safe-area crop and widescreen pillarboxes.
- **Masthead:** small gold/black `MI6 / OPTIONS` or `PC OPTIONS` heading,
fine black rule (a restrained `CLASSIFIED` stamp can be added later if
measured text fits) rather than a blue title/instruction block. Keep the
category name on the **same
page above its first setting**, especially INPUT over Mouse sensitivity.
- **Root:** numbered `1. Input` ... `5. Video`, GE's dossier/menu numbering
and title case as on `frontoptions.c`; a narrow translucent dark selection
box behind the focused item. Category order and labels stay unchanged in
the shared row table; casing is presentation-only.
- **Section:** a compact `PREVIOUS`/Back tab, title and optional
`(Profile N)` annotation, then two columns: label at left, value at right.
Unselected values in black, active/toggled values in GE's dark red
(`0xA00000FF` is the cheat-screen reference). Slider tracks are thin
charcoal with a red fill and an obvious knob; show the numerical value
outside the bar. A mixed-scope `(per profile)` marker must remain legible
without colliding with the value column; measure it and, if necessary,
use a small second line rather than silently removing the distinction.
- **Actions:** Reset retains its two-press/three-second confirmation and
displays `CONFIRM` in red only while armed. Quit remains visually distinct
from resets (and retains its existing activation semantics). Back/Close
affordances should match the actual click targets, not become decoration.
- **Footer:** one quiet, short line for mouse/arrows/controller, with an
explicit `ESC: Back / F10: Close` cue. No newly invented glyphs or fonts
that may be missing from the game's text bank. FPS readout is independent.
Suggested layout sketch (not pixel positions):
```
MI6 / PC OPTIONS CLOSE
-----------------------------------------------
PREVIOUS GRAPHICS
Anti-aliasing 2x
Texture filter Bilinear
Draw distance [------|----] 50/100
...
Reset to defaults [ENTER]
ESC Back / F10 Close
```
## 3. Implementation boundaries
1. Extract a port-only layout helper for fonts, palette,
panel rectangles and spacing; preserve `rows[]`, `rowAdjust`, persistence,
file scoping, reset logic and input bindings. Use GE's loaded Zurich Bold
font for body *only after* validating it exists on both stage and front
contexts; Bank Gothic can remain a fallback/header font. Text clipping
must use the whole viewport as `textRender` expects.
2. Replace only the fill/text passes in `optionsOverlayEmit()` first. Emit
fills before text as currently, reset GBI state explicitly and keep
`OV_BUF_CMDS`/DL size bounds. Avoid mutable texture uploads or new assets.
3. One shared geometry calculation must drive panel/card positions,
`maxVisibleRows`, row Y, slider-bar span, close/back hitboxes, and
`overlayRowAtY`. In particular, don't change visual row spacing without
updating hit-testing (D304/D316), and don't use naive OS-window pixel
mapping instead of `gfx_get_ui_screen_rect` (D316/D335). When a page
scrolls, keep its section title visible; a clipped row cannot be clicked.
4. Use measured text widths to reserve space for values, `Confirm`,
`(Profile N)` and restart markers. At 320x240, allow scrolling/short
contextual hints, not overlapping text or tiny glyphs. At 440x330 and
ultrawide, centre or constrain the card rather than stretching the
columns across the entire viewport.
5. Make any GE-like UI sound effects opt-in to a *separate* follow-up: F10
runs in controller polling, and D361 prohibits game save/audio calls
from that thread. A cosmetic refresh must not reintroduce that lockup.
## 4. Acceptance gates
- Compare screenshots with the existing front Options/cheat dossier and the
in-stage watch; user reviews the live palette/layout before closing the task.
- Native 320x240, front-end 440x330, windowed 4:3, 16:9 and 21:9,
fullscreen, safe-area crop on/off: INPUT title visible; no text clipping,
unreadable values, wrong hitboxes, phantom/duplicated bottom rows or
overflow in the display list. Check graphics's longest labels and AUDIO's
per-profile markers with and without a valid file.
- Mouse click/drag/wheel, arrows and D-pad/stick, ESC back, F10/Start/X
close, focused/armed reset, and quit action all behave **exactly** as
before. F10 Audio on file select still commits via D361's game-thread
queue; test both fresh and existing EEPROM without altering the user's
save (backup/restore for probes).
- With overlay closed, append no panel DL and retain existing golden frame
hashes (unless FPS display is explicitly enabled). Run `./build-pc.sh
ntsc-final`, link/duplicate-symbol checks, front-end + stage smoke,
`GE_WSPROBE_F10FRONT` and targeted live playtest. Log the result in
`docs/dev/findings.md` after implementation, not in advance.
## 5. Explicit non-goals
No fullscreen 3D folder in gameplay, changes to the N64 watch, new config
keys, row wording/value changes, save-format changes, controller remapping,
new persistent assets, or `src/game` behavioral edits. The front-end PC
Options screen is a **visual reference**, not part of the first styling
patch; harmonizing its tiny decorative details is a later optional pass.
+104 -6
View File
@@ -1,7 +1,9 @@
# GE_* environment probes — consolidated list
Every `getenv("GE_…")` in the tree, from a grep of `src/` + `port/` on
2026-08-31 (master `31d036da`). All are **env-gated**: unset = zero behaviour
Every `getenv("GE_…")` in the tree. The table prose is hand-curated;
**`tools_pc/gen_env_probes.py`** re-greps the live sites and reports drift
(NEW / GONE vars + a fresh file:line map) — run it before trusting the
File:line cells. Last reconciled 2026-09-28 (v0.4.0 release sweep: rows added for probes introduced since 09-21 — D383 bind-capture smoke, D388 option-tree walk, D406 front-options diagnostics, D354/D344 watch-settings & quit-frame harnesses, D318B boundary suite, z-fight grid; the drift checker now also sees the cached env-flag wrapper reads from `port/include/envflag.h`). All are **env-gated**: unset = zero behavior
change, so they are safe to leave in a release build (they only cost a
`getenv` on the guarded path). "Dead" below means *the finding it was built
for is closed* — the probe is strip-candidate scaffolding, not that it does
@@ -18,22 +20,97 @@ Two classes:
| Env var | File:line | What it does | Status |
|---|---|---|---|
| `GE_PCDUMP` | `port/fast3d/gfx_opengl.cpp:1079` (`is_pcdump`), `gfx_opengl.cpp:700`, `port/src/video.c:188`, `port/src/config.c:46` (`configGetFrameDump`) | Per-frame framebuffer PPM dump to `$PCDUMP` dir (verification-ritual golden captures, `tools_pc/framediff.py`/`pixcount.py`). Also settable via `[Debug] FrameDump` ini (env wins). **M-33/D168: the writer now emits rows top-to-bottom — captures before that fix are vertically flipped.** | **live** |
| `GE_INPUTSCRIPT` | `port/src/input.c:306` | Headless scripted controller-0 input: `"<frame>:<tok>,…;…"` (`SUP/SDOWN/SLEFT/SRIGHT/SNONE` sustain, buttons pulse). Sole input source when set. | **live** |
| `GE_INPUTSCRIPT` | `port/src/input.c:306` | Headless scripted controller-0 input: `"<frame>:<tok>,…;…"` (`SUP/SDOWN/SLEFT/SRIGHT/SNONE` sustain, buttons pulse; `ZHOLD`/`ZREL` sustain fire, D207 M-201). Sole input source when set. | **live** |
| `GE_INPUTLOG` | `port/src/input.c:329`, `port/src/config.c:53` (`configGetInputLog`) | Logs computed pad state (buttons + stick) each poll when non-zero. Also `[Debug] InputLog` ini. Also traces D165 `menuptr est/tgt/eff/stick`. | **live** |
| `GE_STARTMENU` (+ `GE_STARTMENU_PAGE`, `GE_STARTMENU_DIFF`) | `src/game/lv.c:398-402` (`#ifdef PORT`) | Boot straight into a front-end menu id (13=MISSION_COMPLETE, 10=BRIEFING, 7=MISSION_SELECT, 12=MISSION_FAILED, 6=MODE_SELECT); `_PAGE`=folder row (def 1=Dam), `_DIFF`=0..3. Crash-test a screen fast. | **live** |
| `GE_UNLOCK_ALL` | `src/game/debugmenu_handler.c:1098` (`get_debug_enable_all_levels_flag`, `#ifdef PORT`) | Mission-select shows every solo level (playtest jump-to-any-level aid). Cached in a `static int c`. | **live** |
| `GE_DEBUG_UNLOCKALL` | `src/game/file2.c:404` (`#ifdef PORT`) | Forces the "all levels unlocked" flag TRUE regardless of the shipped save — same effect as `GE_UNLOCK_ALL` but at the file2 unlock-query site. Cached in a `static int`. | **live** (playtest aid) |
| `GE_DEBUGMENU` | `src/boss.c:566` (`#ifdef PORT`) | Makes the L+R debug menu respond to live controller input (C-up+C-down open/close, stick + buttons read from pad 0) instead of the `#ifndef DEBUGMENU` path; unset reproduces that path exactly. Cached in a `static int`. | **live** (playtest aid) |
| `GE_OPTIONSOVERLAY` | `port/src/optionsoverlay.c:231` | Force-enable the F10 port-layer options overlay (D184 / PR #10) regardless of ini. `atoi != 0`. | **live** |
| `GE_D204` | `port/src/audio.c` (`audioSetNextBuffer`) | **Audio-health monitor (D204).** One line per 5 s of wall clock: `rt=` produced-audio/wall ratio (1.000 healthy; below 1 = the 30 Hz audio retrace is starving and the device is padding with silence), `q=` SDL queue depth / `queueLimit` (pinning at the limit with `drop=` climbing = overproduction, the "eventually no audio" state), `drop=` blocks discarded, `max=` largest block vs its `g_MaxFrameSize*4` allocation. **Deliberately cheap (~30 clock reads/s) — safe to leave on for a full playtest, unlike `GE_MIXERTRACE`.** The instrument of record for D202. | **live** |
| `GE_D204_OLD` | `port/src/audio.c` (`d204OldMode`) | Restores pre-D204 behaviour (`osAiGetLength()` reports the whole SDL queue depth; `queueLimit` back to 8192) so before/after can be A/B'd in **one binary**, without build-to-build variance. Temporary — remove when D204 closes. | **live** (D204 A/B) |
| `GE_AUDIOTRACE` | `src/snd.c` (`sndPlaySfx`, `sndDeactivate`), `src/game/propobj.c` (door sounds), `src/libultra/audio/load.c` (`alLoadParam`) | Per-`sndPlaySfx` resolution trace to `audiotrace.log`: `soundIndex` → `ALSound*`/keyMap/wavetable/base/len/book, the returned `ALSoundState*`, every `sndDeactivate`, and wavetable bind events. M-65 additions: `[VOICE+]`/`[VOICE-]` pairing every voice acquire/release with sound/state/flags (NB: written from two threads to one unbuffered FILE* — occasional torn lines, parse defensively), `[SLOTWRITE]` door slot ownership, `[RETRIGGER-POST]`, `[WAVELOOP]` (per-sound `ALADPCMloop` presence), `[DOORSND]` every door sound state change incl. an `ORPHAN-both-slots-busy` verdict. M-66b addition: `[EXPIRE]` — posted whenever the port-side ownerless-infinite-loop expiration is scheduled (sound/state/delay/fade); this line is **permanent** (part of the D202 option-C fix), unlike the rest of the probe set which is removable on close. M-67/M-69 additions: `[WIRE] t=µs filter=… <- table=…` (`src/libultra/audio/load.c`, every wavetable→decoder bind, µs-timestamped) and `[VOICEMAP]` (`src/libultra/audio/synthesizer.c`, full PVoice→decoder/resampler/env-state address map at synth init — this is how offline voicedump slot addresses tie to WIRE filter addresses). M-68 additions: `[EVT] t=µs type=N state=P` + `[STOP-EVT] … deltaUs=N` (event types per `src/snd.h`: 1=PLAY, 2=STOP, 8=VOL, 64=DECAY, 128=END). M-71 additions: `[VOL] t=µs state=… rawVol=… playing=…` (`src/snd.c`, per-VOL-event value before the slot-volume law) and `[DISTVOL] pos=(…) player=(…) dist=… vol=…` (`src/game/propobj.c`, the distance-attenuation funnel `sub_GAME_7F053894` — this is how an SFX's final volume is computed from hit position). NB: lines from the multiple file handles interleave — occasional torn/corrupt lines, parse defensively. `dumppos=` gives the exact byte offset into `audiodump.raw` (quantised to the 2880-byte audio block). Low cost. | **live** (D202/D205) |
| `GE_PULLTRACE` | `src/libultra/audio/load.c` (`alAdpcmPull`) | `[PASTEND]` assertion: fires if a voice's `f->sample` exceeds its wave's total samples while `nOver == 0` (i.e. an unstopped voice keeps decoding past the end instead of zero-filling). Rate-limited to 40 lines. M-65: 0 hits — exhausted voices go silent correctly. | **live** (D202) |
| `GE_DMEMWIPE` | `port/src/mixer.c` (`aClearBufferImpl`) | Zeroes the whole software DMEM at the frame boundary (detected as `alMainBusPull`'s clear of `AL_MAIN_L_OUT`, addr 1088). Tests whether audio state survives across frames in DMEM. M-65: no-op on output — ruled out. | **live** (D202) |
| `GE_NOWET` | `port/src/mixer.c` (`aSetVolumeImpl`) | Mutes the reverb send (`wetamt = 0`), isolating the reverb delay lines as a possible source of persistent signal. M-65: drone unchanged — ruled out (NB `aPoleFilterImpl` is still an unimplemented no-op, D199). | **live** (D202) |
| `GE_BANKDUMP` | `port/src/romdata.c` | `BANKSRC` lines: the source ROM-layout bank tree per `soundIndex` (instrument → sound → envelope pointers + `decayTime`). Used to prove sound 203's `decay=-1` is genuine ROM data, not a conversion bug. One-shot at load. | **live** (D202) |
| `GE_FORCEALARM` | `src/game/propobj.c` (`handle_alarm_gas_timer_calldamage`, `#ifdef PORT`) | **D207 diag (temporary).** Forces the in-game alarm klaxon (`alarmActivate`) on a frame counter — on ~2–10 s, off, on again — and emits a per-frame `[D207] f=… active=… ptr_alarm_sfx=… playstate=… locked=…` line to `audiotrace.log`. Repro harness for the "alarm SFX overrides other audio" report when a level's own alarm can't be triggered headlessly. Remove when D207 closes. | **live** (D207) — temporary |
| `GE_KEYMAPDUMP` | `port/src/romdata.c` | `KEYMAP` + `ENVSRC` lines: raw source bytes of every `ALKeyMap` / `ALEnvelope` in the ROM-layout blob, for byte-wise comparison against runtime values. One-shot at load. | **live** (D202) |
| `GE_AUDIODUMP` | `port/src/audio.c` (`audioSetNextBuffer`) | Raw-dumps every mixed s16/stereo/22050 Hz buffer to `audiodump.raw` in the CWD for offline waveform inspection. Length ÷ 88200 = seconds of audio produced. | **live** (D202) |
| `GE_VOICEDUMP` | `port/src/mixer.c` (`aEnvMixerImpl`) | Dumps each voice's resampled mono stream (post-resample/pre-envelope, i.e. the env-mixer input) to `voicedump.raw` as records of `[u32 stateAddr][u32 nSamples][u64 us][s16 × n]`. **The µs stamp is when the block was MIXED — records are rendered ahead, so a record's samples END at its stamp** (M-69: treating stamps as start offsets misplaces content by up to one ~33 ms block). `stateAddr` = the env-mixer state address from `[VOICEMAP]`. Used with `scratchpad/exact_match.py` for full-corpus per-voice sample verification. | **live** (D202) |
| `GE_MIXERTRACE` | `port/src/mixer.c`, `src/audi.c` (`amDmaCallback`), `src/libultra/audio/load.c` (`_decodeChunk`, `alLoadParam`) | Per-opcode mixer trace to `mixertrace.log` (DMEM addresses, ADPCM books, decoded frames, `[DMAREQ]`/`[DMAHIT]`/`[DMAMISS]`/`[BINDTABLE]`). **WARNING: unbuffered per-opcode `fprintf`, ~25 MB/min. It slows the process enough to starve the audio thread — M-63 measured "audio at 13 % of real time" that was entirely this probe's own artifact. NEVER use it for timing-sensitive measurement; use `GE_D204`.** Grep it, never read it whole. | **live** (D202) — timing-unsafe |
| `GE_TEXPITCH` | `port/fast3d/gfx_pc.cpp:924` (`gfx_tex_pitch_fix`, cached) | `=0` disables the D183 defensive de-stride in `import_texture` (default on). RC2/RC3 texture-shear A/B knob. | **live** (RC3 not finished) |
| `GE_TEXRAW` | `port/fast3d/gfx_pc.cpp:1079` | With `GE_TEXDUMP`, also dumps the raw pre-import source bytes (`texdump/rNNN_f…_s…_WxH.bin`) so a decode bug can be told from a source-data bug offline (D183). | **live** (RC3 not finished) |
| `GE_TEXDUMP_ADDR` | `port/fast3d/gfx_pc.cpp` (`import_texture`, cached env check) | D75/M-197: with value `<hex>`, dumps the raw source bytes of **every** import from that exact address (bypassing the 400-dump cap) to `texdump/aNNNNN_WxH.bin` + a `GE_TEXA[…]` log line with fmt/siz/size/min/max/nonzero byte stats. Distinguished "never loaded" from "loaded zeros" for the Nintendo-logo texture buffer (root-caused to the D50 sidecar converter dropping embedded blobs). | **live** (D75) |
| `GE_D219RAW` | `src/game/image.c` (`texReadUncompressed`/`texChannelsToPixels`, `#ifdef PORT`) | Diagnostic-only, print-only (no behavior change): logs the runtime texture decompressor's bitstream header (texnum/format/width/height/compmethod) for every non-zlib wide-pixel image, plus a dump of the first row of decoded texels exactly as stored in the pool (post-`PORT_PIXEL16`, i.e. byte-swapped for the 16-bit family) for RGBA16/RGB15/IA16. No texnum filter — lets any suspect texture be cross-referenced against the offline ROM census (`scratch/d219_census.txt`) without narrowing by hand. | **live** (D219, closed — kept for future runtime-decoder triage) |
| `GE_WRAPFIX` | `port/fast3d/gfx_pc.cpp:3166` (`gfx_set_wrap_fix`) | RC3 test override for the `Video.WrapFix` D74/D167 wrap-block path (`atoi != 0` wins over the ini). | **live** (RC3 not finished) |
| `GE_SAVELOG` | `src/game/file.c:46`, `src/game/file2.c:16` (`SAVELOG` macro), `src/boss.c:749`, `src/game/objective_status.c:297,343` | Traces the campaign save/unlock chain (objective completion → `end_of_mission_briefing` → `fileWriteSave` → EEPROM). Used to confirm D157. `g_savelogObjOnce` gates the per-criterion dump to the `bossReturnTitleStage` call. | **live** (strip once a few more level boundaries are playtested) |
| `GE_D160` | `src/boss.c:744`, `src/game/bondview2.c:791`, `src/aicommands.def:10055,10181,10236,10242,10247` (all `#ifdef PORT`) | Dam exit-cutscene (D148/D160) diagnostic: trace points in `bossReturnTitleStage`, `bondviewSetCameraMode`, and AI cmds `EndLevel`/`exit_level`/`CameraLookAtBondFromPad`/`CameraSwitch`. **Investigation in progress** — user owes a `GE_D160=1` Dam run. | **live** |
| `GE_D243` | `src/game/front.c` (`menu_init`, cached `s_d243`) | Per-menu-transition trace with a local per-tick frame counter (D243 stale-DL-burst screen identification). Counter is NOT numerically identical to the port's internal frame count — cross-reference by elapsed offset. | **live** (D243) |
| `GE_D243CAM` | `src/game/bondview2.c` (`bondviewFrozenCameraTick`, three `CAMERAMODE_POSEND` branches) | Dumps eye/look-at positions (+theta/verta on the `gBondViewCutscene` branch, +dt/angle/pad on the orbit branch) per call. **Found M-143**: the `gBondViewCutscene` branch fires and its look-at (`field_3C4/8/C`) sweeps through a filter catch-up after the cutscene warp — the shake mechanism. | **live** (D243) |
| `GE_D243M` | `src/game/bondview2.c` (`solo_char_load`, `bondviewRemovePlayerBody`, `playerTick`; cached `d243mEnabled`) + `src/game/objecthandler.c` (`drawjointlist`, both region variants) + `src/game/chr.c` (`chrTick`, `after_position_update:`) | M-151/M-154/M-159 probe for the Dam-abseil "frozen Bonds + shake": `D243M: CREATE/REMOVE` at both `bodyModel` sites; `D243M: tick` (idx/model/**propptr/chrptr**/cam modes/`prop->pos`/`field_488.pos`/`field_3B8`/`field_3C4-8-C`/**rp** = render_pos pointer, **never dereferenced** — M-154 fix 2: tick-time `render_pos` holds arbitrary stale slot contents until the first draw pass, so the pre-view root translation is logged by `chrdraw` only) while the model exists or POSEND/INTRO is active; `D243M: draw` per on-screen player render submission; `D243M: chrdraw` — M-154 per-frame model-draw census, one line per CHR render submission (Model* + raw root translation) while a scripted camera mode (POSEND/INTRO/SWIRL/FADESWIRL) is active; a second Bond instance = extra lines with *distinct* model pointers at the same frame (same model is drawn twice/frame — two passes — dedupe by (frame, model)); `D243M: onscreen` (M-159/M-162/M-163/M-166, `chr.c`) — one line per `chrTick` call while the probe is active: `vis=` `posIsOnScreen`'s result (M-159's frustum hypothesis, REFUTED by M-160) + the live `prop->pos` + `actiontype=`/`anim=` (M-160/M-164: which of `chrTick`'s branches fired, the attached-animation pointer) + `aidx=`/`animframe=` (M-166: the animation reverse-resolved to its `animation_table_ptrs1` index, GE_D193A's technique, + its current playback phase — tests whether the visible chr's animation *phase* at cutscene-entry, not just its identity, is what differs from N64) + `x2active=`/`x2raw=` (M-162/M-163, gated on `GE_D243X2`: whether the decisive save/restore experiment ran this tick, and the raw pre-restore position it overrode — the whole `prop->pos` vector as of M-163, not just Z). Silent during FPS play, silent on the `GE_D243X2` fields unless that env var is also set. Note: `field_3B8` x/z read at 10× world units in the smoke capture — not plain world position. | **live** (D243) |
| `GE_D243X2` | `src/game/chr.c` (`chrTick`, gated additionally on `d243mProbeActive()`) | **Not a probe — a decisive behavioral experiment, off by default, NOT a fix.** M-162/M-163: saves `prop->pos` right before `chrTick`'s position-update dispatch (`chr.c:2541-2617`) runs, restores it at `after_position_update:` — before the render-matrix computation later in the same tick can see the cycled value (unlike M-161, which restored too late to have any effect). Tests whether `chrTick`'s own animation-position write (M-160/M-164) is what produces the Dam-abseil shake. M-162 was Z-only (confirmed the mechanism on that axis, but the visible glitch persisted because that capture's chr cycled on Y instead); M-163 widened it to the whole vector. **M-163's live result: NEGATIVE for the visible shake** — `prop->pos` genuinely went smooth (log-verified) but the shake was unchanged, because the camera's look-at filter reads a *different* variable (`field_488.pos`, see `GE_D243X3`) that this experiment never touches. Kept in the tree as a real, useful (if ultimately not the visible-symptom fix) data point — always pair with `GE_D243M=1` so the `x2active=`/`x2raw=` fields on `D243M: onscreen` show whether/what it overrode. Do not ship. | **live, test-only** (D243) |
| `GE_D243X3` | `src/game/bondview2.c` (`playerTick`, the `field_488.pos` write site, gated on `g_CameraMode == CAMERAMODE_POSEND`) | **Not a probe — the experiment that actually confirmed the mechanism, off by default, NOT a fix.** M-167: skips the `field_488.pos.x/y/z` assignment (`bondview2.c:10495-10497`) entirely while POSEND is active, freezing it at whatever value it held the instant suppression engaged. `field_488.pos` — not `prop->pos` — is what the cutscene's leaky-integrator look-at filter (M-143, `field_3B8`/`field_3C4-8-C`) actually reads; it comes from `render_pos` (the model's animated-skeleton readback), an independent data path from `prop->pos`. **Live-confirmed positive: user — "yep camera shake is gone now."** Logs a companion `D243M: f488write frame=... idx=... x3active=... x3skip=... epoch=... f488pos=x,y,z` line (always-on when `GE_D243M=1`, independent of whether `GE_D243X3` itself is set) so the freeze is log-verifiable. **M-169: no longer freezes forever** — re-baselines (`x3skip=0`, write allowed through) exactly on the tick a legitimate shot-change teleport fires (`d243GetTeleportEpoch()`, bumped by `chrai.c`'s `AI_TRYTeleportingChrToPad` right after its real `setsuboffset()` call), addressing the M-168 postmortem's "camera looks the wrong way for later shots" incompleteness — untested live yet, next session's first move. **Superseded: D243 shipped its permanent fix (`4ccab22c`, M-170), which removed the X3 suppression block from `playerTick` — no live site remains.** Kept here as a tombstone; the generator flags it GONE. | **dead** (D243 closed — probe removed) |
| `GE_D243X4` | `src/game/chr.c` (`chrTick`, the `render_pos` snapshot/restore, gated on `d243mProbeActive()`) | **Not a probe — the M-168 experiment applying X3's technique to the actual draw-path consumer, off by default, NOT a fix.** Snapshots `model->render_pos[]` after a 2-tick warm-up once POSEND engages, then holds it steady by copying the cached contents back over each subsequent tick's freshly-computed buffer — `subcalcmatrices()` always still runs first (never skipped; the first two attempts skipped it and crashed on a reused-pointer stale `-1` tombstone, see findings.md D243 M-168). Addresses Bond's own rendered-body duplication (the consumer X3 never touched). Crash-free, stable, but same forever-freeze incompleteness as X3 pre-M-169. **M-169: same teleport-epoch re-baseline as X3** — the warm-up counter now also resets (re-snapshots) whenever `d243GetTeleportEpoch()` changes, not only on the active/inactive edge. Untested live yet. Revert once a real root-cause fix replaces it; do not ship as-is. | **live, test-only** (D243) |
| `GE_D243SWIRL` | `src/game/bondview2.c:10480` (`playerTick`, cached `d243SwirlTarget`) | D243 follow-up probe: `GE_D243SWIRL=<chrnum>` logs a per-tick line for that chr while the swirl/fade camera path is active (tick counter + state), to verify post-fix behavior of the swirl transition. Inert unless set to a non-negative chr number. | **live** (D243 verification) |
| `GE_D193` | `src/game/frametiming.c` (`waitForNextFrame`, `#ifdef PORT`) | Per-wall-second line: `render=` rendered frames, `sim=` sim ticks delivered (Σ of the `deltaFrames`/`g_ClockTimer` fed downstream), `clamped=` frames whose catch-up hit `FRAMETIMING_PORT_MAX_CATCHUP`, `maxraw=` largest pre-clamp delta. Healthy steady state = `render=30 sim=60 clamped=0 maxraw=2` (measured M-80, all levels idle). Use to check whether the wall-clock timer under-advances under real AI load (D193). Cheap; safe for a full playtest. | **live** (D193) |
| `GE_D193A` | `src/game/chr.c` (`chrTick` → `d193aSample`, plus `d193aMoved` in `chrUpdateAnim`; `#ifdef PORT`) | Per-wall-second per-ticked-chr locomotion telemetry (D193/D209): `chr=` chrnum, `act=` ACT_TYPE, `speed=` world-units travelled/s, `gtd=` g_GlobalTimerDelta, `tier=` `act_gopos.unk59` SPEED tier (0 walk / 1 run / 2 sprint; `-1` when not ACT_GOPOS), `gspd=` `act_gopos.speed` (**a turn rate, not a travel rate** — 0 means running straight), `typ=` PROP_TYPE (3=CHR, 6=VIEWER), `tick=`/`move=`/`frz=` chrTick entries vs `chrUpdateAnim` calls vs freeze-gated (move « tick = the chr is being visibility-culled out of its own locomotion; `move=0` at high `speed` = WAYMODE_MAGIC), `hid=` CHRFLAG_HIDDEN, then the root-motion scalars `scale` / `xlscale` / `animrate` / `playspeed` / `mspeed` (model->speed) / `endframe` / `anim` ptr / `aidx=` that pointer reverse-resolved to its `animation_table_ptrs1` index (40 walking, 41 sprinting, 42 running, 107 walking_unarmed — map via `src/game/initanitable.c`). `GE_D193A_CHR=<n>` restricts to one chrnum. **This probe found D209**: Cradle showed `tier=1` (RUN) with `aidx=40/107` (walk anims) on every sample. | **live** (D193/D209) |
| `GE_D193B` | `src/game/model.c` (`sub_GAME_7F06D3F4`, `#ifdef PORT`) | Rate-limited (1/97) raw anim root-motion decode trace (D193): `joint=` / `flip=` / `base=` mtx channel index / `frame=` / `tmp=` decoded s16 translation triple from the anim bitstream / `angle=` / `anim` ptr / `bd=`+`bs=` the D32 u32 bitDescriptors/bitStream addresses / `njoints`. M-80 Facility: `base=0`, `angle=0` **always** (root-motion facing delta never non-zero — top lead), `tmp` ≈ (±small, ~1086 hip-height, ±small). Compare `tmp`/`angle` magnitudes against a clean N64 decomp build of the same anim+frame. | **live** (D193) |
| `GE_D250` | `src/boss.c` (`bossMainloop`, wraps `lvlRender()`, `#ifdef PORT`) | Per-wall-second line: `[D250] lvlRender avg=Nus n=M` — average + count of `lvlRender()` (DL-building: culling/animation/scene traversal) calls that wall-second, isolated from `gfx_run()`'s software-RSP interpretation cost (already covered by the existing "frame rendered in Nus" line). Added M-120 to chase D250 (fps stuck ~45-50 on Dam, ruled resolution/MSAA/vsync/build-flags out already) — found `lvlRender` itself is cheap (~0.5-1.5ms avg), so it is NOT the missing ~10-15ms/frame either; the still-unexplained cost must be elsewhere in the per-retrace-message loop (scheduler/thread round-trip, audio, or something not yet isolated by a targeted timer). Cheap; safe for a full playtest. | **live** (D250) |
| `GE_D172` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_tri1` — SETCOMBINE dedup log + non-LOD multitex tri dump; `#ifdef PORT`) | Particle magenta/cyan colour (D172). Logs each distinct SETCOMBINE + active cycletype, and — for a 2-cycle tri that consumes TEXEL1 with `tex_lod=0` and a distinct 2nd tile — the tile fast3d *samples* for each texunit vs what the DL *configured*. **Found D172**: 40/40 particle tris configured tile 1 @ tmem 392 (fire) but sampled tile 0 (smoke) → `smoke*smoke`. FIXED (`gfx_lod_tile_offset` `return rdp.tex_lod ? 0 : i`). Probe kept inert. | **live** (D172 FIXED, probe kept) |
| `GE_D157` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_vertex` + `import_texture`; `#ifdef PORT`) | D219/D252's "rainbow particle" family — bullet-impact spark on Bunker1 door confirmed by screenshot to cycle magenta/green/cyan (M-157). Three sub-probes: original exact-color-match log (`g_BulletSparkColors[]` byte match → geometry_mode/LIGHTING + shaded RGB); **`D157T`** texture-identity-keyed vertex full-path log (tile 0 holds a loaded texture ≤16 KiB in frame window [90,240] → raw `cn[]`, post-shade RGBA, LIGHTING, geometry_mode, combine_mode, tile-0 tmem/fmt/siz/addr/size/line, uv; cap 400 + summary/200); **`D157I`** import census (small textures imported in the window, deduped per source address → addr/fmt/siz/size/tile/tmem). M-158: bug did NOT reproduce in 6 identical dev-box runs — probes left live to capture the full RGBA path on the next occurrence (user Steam Deck run with `GE_D157=1`). **M-201:** F12 opens a ~90-frame full-detail `D157TB` burst; continuous logging cut to a 2000-line baseline + 500 colour-match hits (the uncapped white match ran live play at 4–6 fps). | **TEMP** (D252 fixed M-201; remove after release) |
| `GE_DVM` | `src/game/gunfire.c` (`gunUpdateAndFire` gate dump + `gunRenderFirstPersonGunModels` render dump; `#ifdef PORT`) | 1P weapon viewmodel (D215). `[DVM]` line: the `field_87F` show/hide gate terms + `Gun_hand_without_item` sub-terms + `copy_of_body_obj_header` RootNode/Switches/numRecords. `[DVMr]` line: `87F` + `cmds_emitted` = gfx commands the weapon `subdraw()` produced (healthy ≈ 150; the D215 bug showed **1**). Rate-limited ~1/s per hand. **Found + verified D215** (`renderdata.flags` read 0 → all weapon DL nodes gated off). Probe kept inert. | **live** (D215 FIXED, probe kept) |
| `GE_DTEX` | `port/fast3d/gfx_pc.cpp:1001` | Per-`import_texture` param log (dims / line / size / lod / gen_mipmaps). Used to root-cause RC2 and D159. Kept as an inert diagnostic. | **live** (RC2/RC3 not finished) |
| `GE_TEXDUMP` | `port/fast3d/gfx_pc.cpp:1020`, `port/fast3d/gfx_opengl.cpp:700` | PPM dump of every uploaded texture + a `fmt/siz/palfmt/palidx/pal[0..3]` line. Root-caused D161 (Depot ceiling). Kept as an inert diagnostic. | **live** (RC3 not finished) |
| `GE_D116` | `port/fast3d/gfx_pc.cpp:1710,2362`, `src/game/textrelated.c:265,541` (cached `ge_d116`) | HUD/text "X-mirror" (D114/D116) vbo/uv trace. **D114/D116 CLOSED — NOT A BUG (M-33/D168): the captures were upside-down, not mirrored.** Probe is now dead scaffolding — strip on the next cleanup pass. | dead (D114/D116 closed) |
| `GE_D288` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_tri1`, right after the D233 trivial-reject guard; cached `ge_d288`) | M-152 diag for the Silo intro stray triangle: frame-gated to `num_dls` 550–755 (M-155), now **skips `any_behind_camera==1` triangles** (M-155 follow-up — the x/w divide is meaningless for w≤0; that subset was pure false-lead noise) and logs the render-mode word (`oml`) so a live capture can ID the culprit asset against level tables. Logs any triangle whose clip-space NDC bbox covers >50% of the screen or goes non-finite, with each vertex's `w`. Bare `-level_20` re-run: 1965 sane hits in the window, still zero for the target color RGB(83,62,44) — consistent with M-155's working theory that the bare boot's scripted camera never reaches the bug state; **headless probing is at its limit, live capture is next**. | **live** (D288) |
| `GE_D245V` | `src/game/sky.c:962` (`skyPortRenderPoly`-adjacent, `#ifdef PORT`) | D245 water-seam diag: per-call min/max of the recomputed S/T texture-coordinate ranges for the water fan vertices (rate-limited), to check whether the seam tracks a tc-shift boundary. Inert unset. | **live** (D245 OPEN) |
| `GE_D245_FIXEDSHIFT` | `src/game/sky.c:946` (`#ifdef PORT`) | D245 test override: pins the water tc shift `k` to the given integer (0..SKY_TC_MAX_SHIFT) instead of the computed per-vertex value, to A/B the allowShift=TRUE behavior. Off by default (unset = current behavior). | **live, test-only** (D245 OPEN) |
| `GE_D252` | `port/fast3d/gfx_pc.cpp:733` (`import_texture`, cached `s_d252`) | D252 rainbow-particle diag: one-shot per (address,size) import log of the tile state (tmem/fmt/siz/addr/size/line) for small textures so a suspect particle tile can be checked against its source bytes by hand. Remove once D252 is resolved. | **live** (D252 OPEN — non-reproducible) |
| `GE_D252POOL` | `src/game/image.c` (`texLoad`), `src/game/initmttex.c`, `port/src/gimgfixup.c` | **TEMP** (M-201): one line per new texture load (pool main/other, free bytes) + `POOL-FULL` events + stage pool range; `D252SYNC: N slots re-synced, M changed` per stage load (the D252 fix at work: M>0 from the 2nd stage on). Load-time only, no fps cost. | **TEMP** (D252 fixed; remove after release) |
| `GE_D245_OLDVTX=1` / `GE_D245_OLDTEX=1` | `src/game/sky.c` (`skyPortRenderPoly`) / `port/fast3d/gfx_pc.cpp` (CI8 import + tri-path dims) | **A/B** (M-201): restore the pre-fix s16 sky/water vertex path / the pre-fix 32x43, 16-wide water texture geometry. | **TEMP** (D245 fixed; remove post-release) |
| `GE_DYNTEXHASH_OFF=1` | `port/fast3d/gfx_pc.cpp` (`import_texture` cache key) | **A/B** (M-201, D341): disable the content-hashed cache key for dyn-pool textures (reverts to the stale address-only key). | **TEMP** (A/B switch) |
| `GE_D207S` | `src/game/gunfire.c` (player shot-sound block, `#ifdef PORT`) | **TEMP** (M-201): one line per player shot-sound decision: `D207S t=<g_GlobalTimer> hand item rate -> H1|H2|SKIP` (SKIP = both per-hand sound handles still held). Read-only, uncapped, cheap; use instead of `GE_AUDIOTRACE` for live captures (that probe does file I/O on the audio path). | **TEMP** (D207/D241) |
| `GE_D306C="f0 f1 [wmax]"` | `port/fast3d/gfx_pc.cpp` (after `zf_note_emit`) | **TEMP** (M-201): depth-state census of every emitted triangle in frames f0..f1 (GL depth mode, geometry_mode, other_mode_l, tile-0 texture, NDC centroid, z/w range, min w); optional `wmax` keeps near geometry only. Cap 60000 lines. | **TEMP** (D306 closed; remove post-release) |
| `GE_INTROCAM=<n>` | `src/game/bondview_r.c` (`#ifdef PORT`, intro camera pick) | **TEMP/test-only**: force the level intro's first camera index (Dam truck shot = 5). The PRNG draw is still consumed. Logs `D306: intro camera index` when set (or with `GE_D306C`). | **TEMP** (useful repro tool) |
| `GE_STARTWEAPON=<ITEM_IDS>` | `src/game/bondview_r.c` (`#ifdef PORT`, after the start-inventory setup) | **TEMP/test-only** (M-201, D303): adds the item + 400 rounds of its ammo and makes it the starting right-hand weapon (M16 = 13). Pair with `GE_INPUTSCRIPT` `ZHOLD`. | **TEMP** (useful repro tool) |
| `GE_D235` | `src/boss.c` (per-stage `while (!done)` block, after `memaReset`; `#ifdef PORT`) | D235 evidence probe: logs how many GL-texture-cache entries the per-stage `videoResetTextureCache()` call purges at each level load. A cold boot into a level should log 0; a death/restart (or any transition from a level that uploaded textures) logs >0 — direct evidence of the stale-entry mechanism. The clear itself is **not** gated on this var (it's the fix, see below); the var only controls the log line. | **live** (D235) |
| `GE_D235_OFF` | `src/boss.c` (same block) | **TEMP** A/B switch: skips the per-stage `videoResetTextureCache()`, i.e. reverts the D235 fix for one run. For comparing a restart with and without the fix. | **TEMP** (D235; remove when D235 closes) |
| `GE_D235_RELOAD=<frames>` | `src/boss.c` (same block + `bossMainloop` retrace branch) | **TEMP** reload harness: once the title stage loads after the first level (e.g. after a scripted watch Abort), wait `<frames>` retraces, then re-enter that level via `bossSetLoadedStage()`, the front end's own mission-start call. A headless death/restart without the pointer-menu walk. Pair with `GE_INPUTSCRIPT="800:START;1200:A;1300:RIGHT;1400:A"` (abort) on `-level_XX`. | **TEMP** (D235; remove when D235 closes) |
| `GE_D235_NEXT=<stage id>` | `src/boss.c` (with `GE_D235_RELOAD`) | **TEMP**: re-enter a *different* stage after the abort (level A → title → level B), used for D336 (Runway → Frigate). `GE_D85TEX=1` now also logs each stage's texture-pool size and address range (`src/game/initmttex.c`). | **TEMP** |
| `GE_D320R="tick:action;..."` | `port/src/d320repro.c` (hooked in `src/game/lv.c` per-tick PORT block) | **TEMP** forced-repro harness keyed on `g_GlobalTimer`. Actions: `roster`, `jump=<chr>,list=<hexid>,off=<n>`, `bit=<hex>`, `trace=<chr>`, `rename=<chr>,<num>`, `seed=<hi>,<lo>` (D320); `explode=<type>,<dist>` (explosionCreate ahead of Bond) and `cheat=gl|rl|inv|<id>` (game cheat activation) for D252 headless explosion/debris captures. | **TEMP** (D320/D252) |
| `GE_AIMLOG=1` (+ `GE_INPUTSCRIPT` tokens `MDX<n>` `MDY<n>` `RHOLD` `LHOLD` `RREL`) | `port/src/input.c` | D337 aim-model harness: scripted sustained mouse deltas (treated as a grabbed mouse) and aim holds, plus a per-poll line with aim state, active device, the game's drawn crosshair x, `crosshair_x/y_pos` and camera angles. Measures range, edge-scroll and jitter headlessly. | **live** |
| `GE_PERFSTAT=1` | `port/fast3d/gfx_pc.cpp` (`gfx_run`/`gfx_end_frame`), `port/fast3d/gfx_opengl.cpp` (start/end_frame), `src/boss.c` (main loop) | #92/D339 perf probe: per-300-frame averages of render phases (pre / display-list / post / swap+pace), tris and draw batches per frame, game-thread busy time per scheduler message, and a GL_TIME_ELAPSED GPU timer (meaningful only when GPU-bound). | **live** |
| `GE_D75V` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_vertex`, end of the copy/transform loop; cached env check) | D75 (Nintendo logo) per-batch vertex probe: for each `G_VTX` batch in frames `lo..hi`, logs frame#, source pointer, n_vertices, original dest_index, raw ob[0..2] of the first vertex, min/max of the transformed x/y/z/w across the batch, and the MP translation row. Used to prove the transform is correct (M-195) and that every batch encodes v0=0 (slot-overwrite hypothesis). Value form `"lo-hi"` (e.g. `300-900` = the logo screen); unset = off. | **live** (D75) |
| `GE_D75D` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_vertex` G_VTX case + `gfx_sp_tri1` rejection/emit gates; cached env check) | D75 (Nintendo logo) per-frame draw-path probe (M-196): for frames `lo..hi`, logs one line/frame with the V/T token stream (`seq`), max triangle vertex index, cumulative vertices loaded, and per-triangle accounting — trivial-reject / backface-cull counts, how many tris reach GL, how many of those are on-screen (NDC bbox ⊂ [−1,1]²), invisible-flag count, vertex-color min/max, first oml word, depth z/w range + NaN count, texture-sampler presence (`utex` = GL shader, `cutex` = combiner), combine mode, prim/env color. Used to REFUTE the slot-overwrite hypothesis (DL is streamed, max index 15) and narrow the bug to a texture-modulate material rendering black. M-197 added logo-mode `lg_*` fields (sampled only on tris with the logo combine word — the frame-level `utex`/`cutex` are first-tri-wins and were the source of the "combiner claims 0" false anomaly): `lg_utex`/`lg_tex` + tile-0 descriptor + texture buffer address/id/size for the logo material. Value form `"lo-hi"` (logo window ≈ `244-320` on a bare front-end boot); unset = off. Companion one-shot `D75DCC:` log in `gfx_generate_cc` fires when `combine_mode==0x009ffe4f19ffe4f1` to decode the logo's combine slots. | **live** (D75) |
| `GE_PDMOUSEAIM` | `port/src/input.c:487` (`pdMouseAimEnabled`) | Force on/off the PD-style mouse-aim model (D194 lineage); unset = default (on in N64 aim mode unless legacy GEPD aim is selected). | **live** |
| `GE_QUITFRAME` | `port/src/video.c:966` | D344 harness clean self-quit: exits after frame `<n>` completes and swaps (replaces `timeout` kills; the host never exits under an in-flight frame). Cached. | **live** (harness) |
| `GE_ROOMPOOL` | `port/src/video.c:371` (`portRoomPoolScale`) | Scales the port's room pool by a float factor — before/after captures, and a user opt-out if the larger pool ever causes trouble on a level. | **live** |
| `GE_BINDPROBE` | `port/src/input.c:885` | One-shot dump of the resolved PC bindings after setup (D383 bind-capture verification). | **live** (diagnostic) |
| `GE_BINDCAPTUREPROBE` (+ `GE_BINDCAPTUREACTION`) | `port/src/optionsoverlay.c:1000,1005` | D383 isolated host→menu smoke probe: when the overlay force-opens, injects a scripted bind-capture keypress (ESC/DEL/F2). Test data dir must be private — capture persists the key. | **live** (diagnostic) |
| `GE_OPTIONSOVERLAY_SECTION` | `port/src/optionsoverlay.c:989` | Isolated screenshot/probe of a nested F10 page by row key (opens that header; never a menu mode). | **live** (diagnostic) |
| `GE_OPTIONSOVERLAY_SELECTBACK` | `port/src/optionsoverlay.c:995` | Screenshot-only: pins the selection to the Back row when the overlay auto-opens, to check it against a normally selected setting on the same page. | **live** (diagnostic) |
| `GE_OPTIONTREEPROBE` | `port/src/optionsoverlay.c:888` | D388: opt-in in-memory walk of the real F10 page tree; writes no settings, binds or profile bytes; restores UI state afterward. | **live** (diagnostic) |
| `GE_OVPOINTERMAP` | `port/src/optionsoverlay.c:1726` | D391 pointer-map diagnostic: on LMB-down (and every 120 samples) logs SDL mouse coords vs GL drawable pixel coords + the hover row, to separate hit-band/scroll errors from coordinate-space mismatch. | **live** (diagnostic) |
| `GE_FOOTERPROBE` | `port/src/optionsoverlay.c:2232` | One-shot log of the overlay footer/help-line metrics (x extents). | **live** (diagnostic) |
| `GE_FRONTOPTIONS_SECTION` | `port/src/frontoptions.c:295` | Isolated headless screenshot of a nested front-end options page by key (changes navigation only; never edits profile or config values). | **live** (diagnostic) |
| `GE_FRONTNAVLOG` | `port/src/frontoptions.c:430,573` | Logs each front-options navigation select (section/row/item number). | **live** (diagnostic) |
| `GE_FRONTOPTS_WIDTHS` | `port/src/frontoptions.c:673` | D406b: one-shot label-metrics log for the options rows (layout diagnostics). | **live** (diagnostic) |
| `GE_FRONTOPTIONS_AUTO` | `port/src/frontoptions.c:818` | D406 test hook: enters the options screen without a mouse click so headless runs (`GE_INPUTSCRIPT` D-pad stepping + `GE_FRONTNAVLOG`) can exercise it. Never active without the env. | **live** (diagnostic) |
| `GE_WSPROBE` (+ `GE_WSPROBE_FRONT`, `GE_WSPROBE_RESET`, `GE_WSPROBE_F10FRONT`) | `port/src/watchsettings.c:616,568,512`, `port/src/optionsoverlay.c:1566` | D354 watch-settings probe family: `WSPROBE[=tick]` logs before/after the queued settings commit one tick apart; `_FRONT[=tick]` drives a headless front-end change scenario; `_RESET` prepares an options-reset after 120 front ticks and verifies persistence once the stage drains; `_F10FRONT` fires a one-shot F10→front poll after the file-select save becomes available (avoids the joyDisablePoll deadlock). | **live** (diagnostic) |
| `GE_ZF` (+ `GE_ZFTOL`, `GE_ZFBOX`) | `port/fast3d/gfx_pc.cpp:1653-1657` | Z-fight grid probe: per tri emit, buckets the centroid cell and flags cells that fight ≥5 times (cap 600 hits); `ZFTOL`=tolerance, `ZFBOX="x0 y0 x1 y1"`=world-box filter. | **live** (diagnostic) |
## Dxx diagnostic probes — finding closed unless noted
| Env var | File:line | Finding / what it logged | Status |
|---|---|---|---|
| `GE_D51` | `port/src/libultra.c:496`, `src/game/model.c:125,244,544` | msgQ 32-slot overflow watch / ModelSlot layout | dead (D51 closed) |
| `GE_D54` | — (all blocks stripped M-32, commit `49ce620a`) | music seq-table ABI / endianness (`ALMidiHdr`) | dead (D54 closed) — fully removed |
| `GE_D54` | — (all blocks stripped M-32, commit `49ce620a`) | music seq-table ABI / endianness (`ALMidiHdr`) | dead (D54 closed) — fully removed (kept here as a tombstone; the generator flags it GONE) |
| `GE_D154` | `src/game/bg.c:3418,3447,3632` (capped 64 calls) | bg room-GDL call trace (room / gdlidx / vtxoff / op / raw hdr words) | dead (D154 closed) |
| `GE_D227V` | `src/game/sky.c` (`skyPortRenderPoly`, `#ifdef PORT`) | Sky/water per-vertex dump: `S`/`T`/`w`/`1/w`, screen xy, the chosen tc shift `k`, the baked `tc`, and whether the tile capture succeeded. **Found D227**: one sky quad spans ~30,000 texels in S and T but `Vtx.tc` is S10.5 in an `s16` (+/-1024 texels), so 7 of its 8 texture coordinates overflowed and wrapped -- the starburst/"two sky processes" seam. Root cause was a unit error (`unk20`/`unk24` are already S10.5, not texels; M-82's `*32` was spurious). FIXED M-100b. Probe kept inert. | **live** (D227 FIXED, probe kept; useful for D229 water) |
| `GE_D176` | `src/game/bgfog.c:459,471`, `src/game/sky.c:325,381` (all `#ifdef PORT`) | D176 sky/fog env-match + sky-vert trace (Path B, PR #18) | **live** (D176(a)/(b) OPEN) |
| `GE_D178` | `src/game/front.c:6586,6594` (`#ifdef PORT`) | briefing-data u16 decode trace (blank-objectives, D143/D178) | dead (D178 closed) |
| `GE_D56` | `src/game/model.c:227,830` | watch `Model` raw-offset reads | dead (D56 closed) |
| `GE_D60` | `port/src/libultra.c:521,735` | gfx frame msgQ delivery trace | dead (D60 closed) |
| `GE_D61` | `port/src/libultra.c:795` (`s_d61opened`) | opens a one-shot log file | dead (D61 closed) |
@@ -44,6 +121,8 @@ Two classes:
| `GE_D69STAN` | `src/game/stan.c:266,276` | D69 stan-tile trace | dead |
| `GE_D85DUMP` | `src/game/bg.c:2510` | D85 `bgWidenRoomGdl` 8→16B dump | dead (D85 closed) |
| `GE_D85TEX` | `src/game/image.c:2479`, `src/game/tex.c:859,1050` | D85 texpool-full events (Depot analysis) | dead (D85 closed) — was useful for §3 |
| `GE_D217TEX` | `src/game/image.c:234` (`texInflateZlib`) | D217: logs `texnum`/`format`/`numcolours`/`palette[0..1]` straight off the decode bitstream (before any arena/pointer math) — the decode-correctness check that proved the affected texture (grip, `texnum=1608`) decodes byte-identically across levels, ruling out a decode/ROM-data bug. | **live** (D217 open — investigation instrument) |
| `GE_D229` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_tri` dual-texunit probe + `import_texture`) | D229: per-draw dump of both texunits' tile/tmem/fmt/siz/UV state for 2-cycle draws, plus a one-shot confirm line when the RGBA16-over-CI8 import remap fires (the fix path). Inert by default. | **live** (D229 fixed — probe kept for the Surface 2 / sweep verification) |
| `GE_D86` | `src/game/model.c:6295`, `src/game/objecthandler_2.c:143` | D86 model rwdata trace | dead (D86 closed) |
| `GE_D87` | `src/game/ramromreplay.c:296,363,381` | D87 `ramromfilestructure` endianness | dead (D87 closed) |
| `GE_D88` | `src/game/prop.c:1367`, `src/game/stan.c:3090` | D88 `Usetup*Z` propDef stream | dead (D88 family closed; D88.4 resolved) |
@@ -52,6 +131,21 @@ Two classes:
| `GE_D104` | `src/game/bg.c:654` (rate-limited, `d104c`) | D104 depth-clear trace | dead (D104 closed) |
| `GE_D71LOG` | `port/fast3d/gfx_pc.cpp:645` | D71 fast3d trace | dead (D71 closed) |
| `GE_D75` | `src/game/title.c` `sub_GAME_7F007F30` (capped 8) | D75 Bug 2 gun-barrel model probe: logs `chrModelInstance`/`gunModelInstance` ptr+obj+numMatrices, `render_pos`, `renderData.mtxlist`, `g_GfxMemPos`, `osVirtualToPhysical(render_pos)` | **LIVE** (D75 Bug 2 OPEN — see §F "D75 Bug 2 — RUNTIME PROBE") |
| `GE_D305` | `src/snd.c:420` (`sndHandleEvent` desync branch, `#ifdef PORT`) | D305: logs the sound-preemption desync state when it is detected (after the NULL-guard fix made the crash itself impossible) — confirms whether `Game.AllUnlocked` correlates or was coincidental, and whether it recurs. Inert unset. | **live** (D305 monitoring) |
| `GE_D309` | `src/game/chrai.c:810,3424`, `src/game/chraction.c:10139` (all `#ifdef PORT`) | D309 freeze probe: logs AI label-not-found restarts (`chrai.c`) and the run-to-bond persistent-list step-on path (`chraction.c`, capped 400 lines) — the diagnostics used to root-cause the elusive complete freeze (D309/D310). Inert unset. | **live** (D309/D310 fixed — kept for recurrence) |
| `GE_D314` | `port/src/optionsoverlay.c:681` (cached `s_d314Enabled`) | D314 F10-flicker probe: logs the overlay's viGetY / visible-row count / scroll / selection each poll. Inert unset; strip candidate once D314 is triaged. | **live** (D314 pending triage) |
| `GE_D318R` | `port/src/random.c:69,147` (`#ifdef PORT`) | PRNG golden probe (D284/D318): logs the first 4000 gameplay-PRNG draws with tick + caller return address, and every `randomSeed` set — verifies the bit-exact port of `random.s` and which call sites consume the stream. Inert unset. | **live** (D284 verification) |
| `GE_RSEED` | `port/src/random.c:134` (`#ifdef PORT`) | Test-only boot-seed override: `GE_RSEED=<hex64>` pins `g_randomSeed` at first seed instead of the wall-clock-derived value — for determinism and N64-parity tests. Inert unset. | **live, test-only** |
| `GE_RSEED_LV` | `port/src/d318watchdog.c:232` (`#ifdef PORT`) | One-shot level-start seed log: logs `g_randomSeed` at the first tick after level start (optionally armed for a specific tick via `<hex>@<tick>`) — ties a saved/loaded game's PRNG state to a level for D318-class repro. Inert unset. | **live** (D318 verification) |
| `GE_D318T` | `port/src/d318watchdog.c:270` (`#ifdef PORT`) | D318 watchdog timeline probe: per-tick log of the Facility ai_22 softlock signature window (c78 list/offset/attack state, c67 health) while the watchdog watches. Inert unset. | **live** (D318 verification) |
| `GE_D322` | `port/src/audio.c` (`audioSetNextBuffer`), `src/snd.c` (`sndD322PoolSummary`) | D322 pool-telemetry probe: one line per 5 s of wall clock with the three pool layers a long session can exhaust — `sfx=count/max` (8-voice SFX soft cap), `evtq=n/64` (event queue occupancy; full = silent post drops, M-71), `pv free/lame/alloc` (24 physical synth voices shared by SFX + music; `-1`s before sndInit), plus the D204 pipeline fields. Cheap (~30 reads/s), campaign-safe; read together with `GE_D204=1`. | **live** (D322 capture) |
| `GE_D318W` | `port/src/d318watchdog.c:136` (`#ifdef PORT`) | **Not a probe — the D318 port-side watchdog itself, ON by default.** Detects the Facility ai_22 hold-pose softlock signature (a latent N64 race, see findings.md D318) for 600 consecutive ticks and re-seeds c78's attack via the game's own fresh-attack entry point. `GE_D318W=0` opts out; logs `D318W:` lines when it acts. **Permanent fix, not strip-candidate.** | **live** (D318 fix — permanent) |
| `GE_D294` | `src/boss.c:498` (`#ifdef PORT`) | One line per stage reset: stage number + current MA malloc value (D294 pool investigation). | **live** (D294) |
| `GE_D294BANK` | `src/game/bg.c:605` | D294: restarts the bank-window counter on stage change (pairs with the `GE_D104` windowing). | **live** (D294) |
| `GE_D303` | `port/fast3d/gfx_pc.cpp:1717` | D303: logs up to 200 tri emits with per-vertex bbox + w-sum (degenerate-triangle hunt). | **live** (D303) |
| `GE_D307` | `src/game/prop.c:709` | D307: load-time dump of every CCTV record's inputs + derived orientation, for word-for-word PC-vs-ROM comparison. | **live** (D307) |
| `GE_D318B` (+ `GE_D318B_ALL`, `GE_D318B_CLK`) | `port/src/d318bprobe.c:91,97,210` | D318/D320 boundary probe suite (M-199): per-tick ground truth of the aim-hold/derail/anim-pin state machine for env-selected chrs (`"5,9"`, max 4); `_ALL` logs every ACT_ATTACK tick; `_CLK=N` locksteps N sim ticks per rendered frame. Read-only. **TEMP — remove when D318/D320 conclude.** | **TEMP** (D318/D320) |
| `GE_D329` (+ `GE_D329LOG`) | `src/game/model.c:3516,3519` (`#ifdef PORT`) | **The D329 anim-pin fix gate — ON by default** (unlike the other probes here): re-derives a stale pinned frameb when the endframe no longer blocks it. `GE_D329=0` A/B-disables; `GE_D329LOG` logs each trigger (cap 2000). Cached. | **live** (D329 fix) |
## Notes
@@ -62,5 +156,9 @@ Two classes:
- `[Debug]` ini keys (`Debug.FrameDump`, `Debug.InputLog`) mirror
`GE_PCDUMP` / `GE_INPUTLOG` as a fallback (env var wins) — M-26,
`port/src/config.c`.
- Not GE_-prefixed but same category: `GE_DETERM` is **designed, not
implemented** (fixed-tick determinism mode, §F D117).
- Not GE_-prefixed but same category: `GE_DETERM` is **implemented but
experimental — not yet achieving its determinism goal** (fixed-tick
mode, §F D117, M-52 four-pass writeup); `GE_DETERM_TRACE` (M-52 4th
pass) adds instrumented clock-event logging when used alongside it, for
diffing two runs to localize remaining divergence. Both env-gated,
default off, zero cost when unset.
+217
View File
@@ -0,0 +1,217 @@
# GEPD-inspired input overhaul — FINAL PLAN (issues #89, #90)
Status: **plan only** — no code changes yet. Companion to `port/src/input.c`
(current state: D118/D165/D166/D194/D214/D223/D238 lineage).
## 1. The reference (no longer vendored in the public repo)
The GEPD Edition Mouse Injector source was vendored at `reference/mouse-injector/`
and **removed from the public repo on 2026-09-19** (GPLv2 code + a prebuilt
binary inside an MIT project; never compiled here). See
`reference/mouse-injector/README.md` for provenance and how to re-vendor it
locally if you need the original. The file that mattered was
**`games/goldeneye.c`** (~380 lines, the whole GE aim model), plus `device.c`
(raw mouse polling) and `maindll.h` (tickrate — the units every per-tick
constant assumes). The model is quoted inline below and in the D194 findings;
the ported result lives in `port/src/input.c`.
### What the Mouse Injector does (GE)
### What the Mouse Injector actually does (GE)
| Aspect | GEPD implementation (`games/goldeneye.c`) |
|---|---|
| Mouse source | Raw driver deltas via ManyMouse (`device.c`), cursor locked by periodic `SetCursorPos`. No OS pointer accel, no deadzone. |
| **Hipfire look** | **Direct camera write**: `camx += XPOS/10 * (SENS/40) * (fov/basefov)` degrees per tick; `camy` likewise, clamped ±90 (−20 in tank). Linear in px — this is why GEPD "just feels right". |
| **Aim mode (R)** | Crosshair *position* accumulator `crosshairpos += XPOS/10 * SENS/292`, clamped to ±`CROSSHAIRLIMIT` (5.159° = screen edge). Gun/arm pose derived per-weapon from a 33-entry offset table. **View scrolls only past 72% of the way to the edge**: `aimx = (ratio−0.72) * 475 * timestep`, scaled by `fov/basefov`. |
| FOV coupling | Every term carries `(fov/basefov)` — scopes automatically get finer control. `basefov` = min(60, override). |
| Tank | Separate direct write to the tank x-rotation global (`0x80036484`), different scale (`360/TANKXROT * 2.5`); crosshair X locked to 0 when driving with tank equipped. |
| Menus | Mouse moves the menu cursor directly (`menux/y += px/10*SENS*6`, clamped to screen rect); aim button acts as B (back) in menus; wheel = weapon cycle macro. |
| Safety gates | Injection only when `camera==4||0 && menupage==11 && !dead && !watch && !pause && !mproundend`. |
| Extras | Crouch *toggle* mode, dedicated reload key (ROM hack — not portable to us), FOV override + viewmodel repositioning (ROM hacks — we have the clean equivalents in `port/`), intro skip on FIRE+AIM. |
### Already inherited (do not redo)
- **D194 GEPD-mirror aim** (`aimGepdCompute()`): crosshair-position model,
±5.159° clamp, 72% edge scroll at 475/s, fov scaling, per-tick overwrite of
`crosshair_x/y_pos` + `gun_azimuth_angle/turning`. This *is* GEPD's aim mode.
- D223 mouse-wheel weapon cycling (A / A+Z synthesis) — mirrors
`GE_Controller()`'s weapon macro; D214 ini key binds; click-to-lock capture;
F10 options overlay with aim/turn sensitivity sliders; D194(b) dt-decoupled
look; menu pointer via direct `cursor_h/v_pos` writes (mirrors GEPD's
`menux/y` writes).
### Remaining inheritances (this plan)
GEPD's *hipfire* is a direct linear camera write — we still route hipfire
through the N64 stick curve (#89). Also un-inherited: raw-input-by-default,
crouch toggle, GEPD-style safety gates on our always-live look paths, and its
sensitivity/FOV coupling discipline. That's WIs 1–3 + 6 below.
### Reference split (GEPD vs PD port)
Two references, two jobs (decided during #89/#90 triage):
- **GEPD Mouse Injector** (was `reference/mouse-injector/`, removed from the
public repo 2026-09-19 — see its README stub) = *what the mouse does to the
camera*. The only reference with GE-specific knowledge (camera fields,
crosshair limits, tank case, safety gates). Primary for WI-1.
- **PD PC port checkout** (standing reference, see AGENTS.md) = *how a PC game
presents and captures input*: cursor-lock behavior, raw input handling,
sensitivity units/scaling, and the shipped in-game options/keybind UI.
Primary for WI-2 conventions and WI-5's menu design. PD knows nothing about
GE — never use it for camera math.
## 2. Issue #89 — "mouse movement is not uniform; slow motion barely registers"
### Diagnosis (high confidence)
The complaint is the **hipfire** path. Today hipfire converts mouse px into an
*analog stick value* (`sx += (int)(hipEdx * hipSens * MOUSE_TURN_GAIN)`), and
the game then runs that stick through its own N64-era natural-turn response —
a **quadratic-ish curve with a soft floor** (D238: "saturates the game's
quadratic natural-turn curve at ~13 px/poll"). Consequences, exactly as the
reporter describes:
1. Slow mouse motion → small stick values → squared-off tiny turn rates →
"almost unrecognised".
2. Fast motion saturates the curve at full 315°/s — so the usable band is
narrow and the feel is non-uniform (bang-bang-ish).
3. `(int)` truncation of the stick adds a quantized dead zone on top.
GEPD/PD-PC never hit this because **they write the camera angles directly** —
the stick curve is bypassed entirely, and sensitivity is linear in px with
FOV scaling. We already do exactly that in aim mode (D194); hipfire is the one
leftover N64-stick-shaped path.
Secondary contributors worth folding in:
- `Input.MouseRawInput` defaults to **0** — on X11/Wayland (the #90 reporter
is on Linux) OS pointer acceleration still shapes relative deltas; GEPD
reads the raw driver. Windows is largely unaffected.
- Sensitivity is spread over 4 knobs (`MouseSensitivity`, `MouseTurnSpeed`,
`AimModeSens`, legacy `MouseAimSpeed`) — the reporter "tried adjusting
ge007.ini" and couldn't find the one that mattered for hipfire.
### Work items
**WI-1 (core fix): direct camera writes for hipfire look.**
Mirror GEPD's hipfire branch in `input.c`.
*Pre-flight:* skim the PD port's `port/src/input.c` mouse path first and match
its conventions (sensitivity units, dt handling, focus-loss behavior) so the
result feels like PD PC rather than a reinvention. GEPD supplies the GE camera
math; PD supplies the UX shape.
Implementation:
- New path `hipDirectCompute()` next to `aimGepdCompute()`: convert this
poll's dt-scaled px to degrees using the *current* viewport
(deg/px = fovy/viewH, or hfov/viewW — reuse whatever D194 absolute aim used),
scale by `MouseTurnSpeed * MouseSensitivity`, and add straight to
`g_CurrentPlayer->vv_theta` / `vv_verta` (clamp ±90). Same confinement as
today: only when `idx==0`, cursor captured, in a running stage.
- **GEPD's safety gates** must be ported over (aim mode got away without them
because it requires RMB held; hipfire is always live): check the decomp
equivalents of GEPD's `camera==4||0 && menupage==11 && !dead && !watch &&
!pause` before writing. `menupage` → `current_menu` (already have); the rest
are `player`/game globals — read-only, no logic change.
- **Tank**: GEPD writes a separate tank-rotation global with its own scale and
locks crosshair X to 0 when driving with the tank equipped. Find the decomp
symbol for `0x80036484` (tank.c) and mirror it; if too fiddly, phase 2 —
tanks are rare in single-player.
- Keyboard turn (arrows → stick) keeps the existing stick path; only mouse px
goes direct. Escape hatch: `Input.MouseDirectLook=1` default, `0` reverts to
the current stick path (kept as-is).
- This makes hipfire and aim mode share one linear mental model: px → screen
angle, FOV-scaled, dt-normalized. Expect to then simplify or delete the
D166/D238 curve-tuning knobs that become inert (`MouseAimCurve`, `AimBand`,
legacy `MouseAimSpeed`) — *after* playtest, and keep the config keys as
no-op aliases so existing inis don't break.
**WI-2: raw input by default.** Flip `Input.MouseRawInput` default to 1 (SDL
hints already handled in `inputInit`). Verify Windows relative-mode deltas are
unaffected; this mainly fixes Linux feel. Keep the escape hatch.
**WI-3: one honest sensitivity number.** With WI-1, hipfire and aim both scale
off px→degrees, so `MouseSensitivity` becomes genuinely master. Add it to the
F10 overlay (see WI-5), document the knob hierarchy in the generated ini
header comment, and re-calibrate defaults against GEPD's SENS≈40 ≈ 1:1
screen-angle mapping (our current 38/50/100 trio should land close; measure,
don't guess).
**Verification:** `GE_INPUTLOG` linearity check — log px-in vs deg-out at slow
and fast speeds (ratio must be constant); headless script harness for the
stick-fallback path; manual playtest checklist (hipfire slow/fast, aim mode,
scope zoom, tank, menu, death/cutscene safety gates). New finding `Dxxx` in
`docs/dev/findings.md`, index row added.
## 3. Issue #90 — in-game key bindings + "ini binds don't work" + sensitivity menu
### Diagnosis candidates for "binds don't work properly" (repro before fixing)
1. **Not a bug, GE design:** in-game, a fresh A edge *without* Z held is the
weapon-cycle-forward signal (D223 documents `weaponForwardOffset`). The
default Action bind includes E — so "pressing E changes weapon" is the
game's own behavior, and "Escape reloads/activates switches" is the default
Cancel=Escape → B button. The user's mental model (PC: E = interact)
collides with GE's A/B semantics. Address via documentation + possibly a
PC-idiomatic default set (product decision; GEPD itself uses Q=A, E=B).
2. **Ini syntax footgun:** keys must be written as `Input.Bind.Action=E` (or
`Bind.Action=E` under `[Input]`). A bare `Action=E` under `[Input]` resolves
to `Input.Action`, which is unregistered and silently ignored. `configLoad`
also ignores unknown keys with no warning — add a log line for unrecognized
keys so users get feedback.
3. **Key-name format:** binds use SDL scancode names (`"Left Ctrl"`, `"Up"`,
`"Space"`); nothing documents this in the ini. Add per-key comments in
`configSave` output and/or a `# valid names: ...` header.
4. There may still be a real parse bug lurking (`strtok` on the seeded buffer,
constructor ordering vs `configLoad`) — **repro first**: write an ini with
`Input.Bind.Action=E`, launch, confirm via `GE_INPUTLOG` that E alone
produces A. Fix whatever actually breaks; don't fix hypotheticals.
### Work items
**WI-4: keybind audit + ini ergonomics.** Repro (above), fix real bugs, add
unknown-key warnings, document scancode names in the saved ini. Cheap, do first
— it unblocks #90's half regardless of the UI work.
**WI-5: F10 overlay — bindings section + master sensitivity.**
Model the UX on the PD port's shipped in-game options menu (standing reference
checkout) where anything is player-facing. `optionsoverlay.c` already has
row/hit-test/scroll infrastructure and owns all input while open; what's missing:
- A new row kind `ROW_BIND`: label = action, value = first bound key name;
click → "press a key…" state that captures the *next* scancode (keyboard or
mouse button), writes it into `g_bindStr[a]` (replacing or appending —
decide: GEPD/PD use one primary + optional secondary; simplest v1 = replace
single key, keep multi-key only for ini power users), then `inputRebuildBinds()`
+ persist via the existing config save path.
- Expose `Input.MouseSensitivity` (master), and after WI-1 trim the panel to
master + per-mode fine-trim + invert Y (drop inert legacy rows).
- Bind capture must swallow all input while armed (overlay already does this)
and handle ESC = cancel, Backspace/Delete = clear.
**WI-6 (nice-to-have, from GEPD): crouch toggle.** GEPD's `CROUCHTOGGLE` state
machine (`GE_Crouch`) is ~20 lines and pure port-side (stance flag write).
Low priority; include only if WI-1..5 land cleanly.
## 4. Suggested order
1. **WI-4** (keybind repro/fix + ini docs) — small, unblocks #90 partially.
2. **WI-1** (direct hipfire) + **WI-2** (raw input default) — the #89 fix;
playtest-heavy, needs the safety gates done right.
3. **WI-5** (overlay binds + master sens) — completes #90.
4. **WI-3** (sensitivity consolidation/cleanup) — after WI-1 defaults settle.
5. **WI-6** (crouch toggle) — optional garnish.
## 5. Constraints reminders
- All work is `port/`-only: direct struct writes are the established D194
pattern (read/write live game state from the input poll, no logic change).
Any temptation to "fix" the natural-turn curve in `src/game` = rule-2 sign-off
territory — don't.
- Keep every removed/renamed ini key registered as an alias so shipped inis
keep loading; log deprecations at startup.
- Findings: new `Dxxx` entries for WI-1 (hipfire direct look), the #90 bind
repro outcome, and the overlay bind UI; update `findings-index.csv`.
+27 -13
View File
@@ -10,22 +10,36 @@ once the game is playable start-to-finish without crashes.
|----|---------|----------------|
| ~~**D114/D116**~~ | ~~HUD text + ammo digits render each glyph individually X-mirrored~~ | **CLOSED — NOT A BUG (M-33, §F D168).** The `GE_PCDUMP` PPM writer never reversed `glReadPixels` rows, so every capture was upside-down; "mirrored" was a misread of inverted asymmetric content. The game renders correctly on hardware (developer-confirmed). Writer fixed (`gfx_opengl.cpp`). "Every stage verified non-mirrored" was correct — there was no mirror. |
| ~~**D74 wrap-block**~~ | ~~`gfx_pc.cpp:1546/1551` PORT block is dead code (guard `cms & G_TX_WRAP` always false; indexes per-texunit arrays with the vertex index → OOB if reached).~~ | **RESOLVED (M-30, RC3/D167).** The in-vertex-loop wrap block was reworked and hoisted to a per-texunit pre-wrap window (`gfx_pc.cpp` ~1574, now `cms`-correct and indexed by texunit not vertex), behind the `Video.WrapFix` knob (default OFF). No longer dead code or OOB. See §F "RC3 · D167". |
| **D75** | ~~Nintendo logo mispositioned; gun-barrel Bond model absent~~; cast-roll character models — status unknown. (Rareware logo OK.) | **Shrunk (M-33, upright captures after D168).** The "overlaps D114 shared mirror" note is withdrawn. **Gun-barrel Bond RENDERS correctly** (walk + fire, upright) — the "absent" reports were the flipped capture. **Nintendo logo IS still broken** — renders as two overlapping white ellipsoids shifted left, no wordmark/geometry (a real `logoinst` model transform/geometry bug, not a flip). Cast roll not re-captured — recheck with `GE_PCDUMP="1400-2200:25"`. `findings.md` §F "D75 Bug 2 — M-33 UPDATE". |
| **D392** | In the opening attract-mode Train demo, a soldier consistently looks more hunched on PC than in 1964/GEPD; his hand intersects his body at the wrong position. User comparison: `docs/img/animation-glitch-pc-port.png` and `docs/img/animation-glitch-1964gepd.png`. | **FIXED — user visually verified (D392).** Train's normal opening (`-level_25`) has a central firing guard (`chr=5`) whose torso/shoulder aim offsets reached `aimupback=-0.855`, `shoulder=-0.606,+0.868` radians by tick 100. `chrlvUpdateAimendbackShoulders` used N64 `((f32*)arg1)[12/13/16/17]` offsets into a firing table whose first member widens from a 4-byte to an 8-byte pointer union on PC; it read *other* floats and bent the torso/arms to wrong angles. `#ifdef PORT` now reads `max_up/max_down/free_arm_frac_up/free_arm_frac_down` by name; with the same short Train intro probe the torso offset stays 0, shoulders ~0.028/+0.014 at tick 100. User's original comparison: `docs/img/animation-glitch-pc-port.png` vs `docs/img/animation-glitch-1964gepd.png`. The user subsequently confirmed the soldier's pose looked fixed in-game; this session's image tool itself cannot display PNGs. No ramrom demo override needed; ordinary `-level_25` reproduces. |
| **D75** | ~~Nintendo logo mispositioned; gun-barrel Bond model absent~~; cast-roll character models — status unknown. (Rareware logo OK.) | **Nintendo logo + GoldenEye logo + legal page FIXED (M-197, user-confirmed live 2026-09-21).** Root cause: the D50 sidecar converter (`tools_pc/d43_emit.py`) reserved + remapped the model's embedded texture-blob span but never copied the pixel bytes into the emitted RZ image → TEXEL0 sampled a zeroed buffer → the texture-modulate logo rendered black (M-195/M-196 had proven emission/transform/combiner all correct). Same latent bug hit `PgoldeneyelogoZ` + `PlegalpageZ`. Fix = tooling-only (blob raw-copy + round-trip check); ntsc sidecars regenerated; logo window now renders 23k px (7.59%) vs 0–33 pre-fix. Gun-barrel Bond renders correctly (M-33). Cast roll still unverified — recheck with `GE_PCDUMP="1400-2200:25"`. `findings.md` §F D75 (M-195/M-196/M-197 updates) + M-197 at file tail. |
| **D76** | 2D disclaimer screen only partially drawn. | §F D76. |
| **D77** | No audible music on PC. | Phase 3 (audio). §F D77. |
| ~~**D77**~~ | ~~No audible music on PC.~~ | **RESOLVED (M-104, PR #42) — in-level music now plays.** Root cause was a D6-class missing `return` in `sub_GAME_7F0C0BF0` (`mp_music.c`) that zeroed the seqp volume on the only in-level trigger path; menu/intro music bypassed it and was always fine. The remaining audio-*quality* gap (wrong-sounding instruments on some tracks — elevator named specifically — plus occasional garbling) is tracked as **D230**. §F D77/D230. |
| ~~**D116 (HUD)**~~ | ~~proportional-font strings + ammo counter mirrored~~ | **CLOSED — NOT A BUG (M-33, §F D168).** Same capture-orientation artifact as D114. |
| **D148** | Level-end cinematics don't play. Dam: hitting the exit trigger should show Bond rappelling down the dam (the bungee jump); instead it cuts straight to the mission-complete report. | Scripted cutscene / cinematic-camera + Bond rappel anim. Found M-27 during the first full Dam→Facility playthrough. Progression is unaffected — the report and next briefing load fine. **M-31 (§F D160):** static-traced the Dam end ailists (`ai_24`→`ai_17`, `camera_switch`→`CutsceneRecord` type-46 lookup). Diagnostic `GE_D160=1` shipped (`c95713f5`) — user runs Dam to the exit with it set; the trace pins whether the cutscene AI runs, and whether the propDef-index walk (D122/D132 family) hands `camera_switch` a bogus cutscene record. Leading hypothesis: propDef `sizepropdef` stride desync (medium confidence), not D75 anim-model. |
| **D149** | Front-end MISSION COMPLETE / mode-select 3D models (dossier, wallets) render as garbled full-screen geometry or not at all — one of the compiled sub-DLs (seg5+0x9ee4) is malformed, D146 now skips it instead of aborting. | D75 family. §F D146. |
| **D170** | Named-NPC "flee" scripted behaviour looks wrong on PC: on Silo, Ourumov breaks into a full run the instant he alerts ("Ourumov: Kill him!") rather than the N64 walk/retreat; developer reports Trevelyan behaves the same way on Cradle. | **Observed, not investigated** (M-34, from a Silo playtest capture; user bug report — no fix wanted yet). Could be a genuine ailist / `chrTick` divergence, OR the D155/D156 wall-clock `deltaFrames` locomotion-anim speed-up (a catch-up spike playing the walk cycle at 2–3×). **Verify first:** capture the same beat against N64 reference footage before assuming it diverges — Ourumov may be scripted to flee. If it's real, suspect the frametiming clamp (§F D155/D156) or the Cradle-family AI path (§F D123). |
| **D171** | Silo missile-shaft: a large translucent grey triangle sweeps across the lower frame while panning down the shaft (~3 s of the capture). Looks like a stray poly / near-plane clip fan, same family as D106 (portal near-plane min>max). | **Observed, not investigated** (M-34, Silo capture). Cosmetic, intermittent, tied to camera angle in the open vertical shaft. |
| **D172** | Bullet-impact / blood particle spray renders magenta or cyan instead of dark red (visible on guards hit at close range on Silo). | **Observed, not investigated** (M-34, Silo capture). A channel-order / CC-mode error in the particle-sprite path (magenta = R+B, cyan = G+B) — not the D64/D120 blood-decal family (those are crash/hang + missing decals, not colour). Likely a texture-format or env-colour decode in `blood_animation.c` / the fast3d sprite path. |
| **D178** | Objectives not listed correctly on the pre-mission briefing screen (the mission overview shown before starting a level). | **M-36 triage: the objective lines are BLANK, not wrong.** `GE_STARTMENU=10` Dam briefing renders the header fine ("OHMSS / Agent: James Bond / Mission 1: Arkangelsk / Part i: Dam / PRIMARY OBJECTIVES:") then just the bullet "a." with no text and no b/c/d. → **D143/D151 blank-text family** (`langGet()` → NULL → silent blank), *not* a propDef walk / difficulty decode. **Family B** in `docs/dev/M-36-TRIAGE.md` — find which lang bank the front-end briefing flow fails to load. Re-confirm the exact shape in a *real* briefing (the `GE_STARTMENU` boot skips real save state). Close relatives: D151 (watch objective text blank — type-35/23 `text` slot u16→u32 fix, M-28; interactive re-verify was still pending), D143 (briefing/objective text blank via `langGet` NULL), D157 (objective difficulty byte offset — could make difficulty-gated objectives appear/disappear wrongly). The pre-start briefing is a *different surface* from the in-level watch, so D151's fix may not cover it. **Next:** ask user which level + what exactly shows vs expected; compare against `docs/dev/LEVEL-OBJECTIVES.md` (regenerated post-D157); if lines are blank → D143/D151 family (`langGet` NULL guard = silent blank), if wrong objectives listed → propDef type-23 record walk / difficulty decode. |
| **D148** | Level-end cinematics glitch. Dam: the Bond rappelling/bungee cutscene plays but the camera violently shakes and Bond's own model appears to duplicate/glitch. **Cutscenes are buggy in many places across the game** (D173 level-start floating, D292 death-sinking, Cradle spin) — treat D148/D243 as the tracking entry for the whole cutscene-reliability problem, not just Dam. | **2026-09-17 (D243 M-107 through M-168, ~18 session-passes): the mechanism is now understood in real depth, and the camera-shake half is fixed (diagnostically, live-confirmed).** Full history + code citations: `docs/dev/findings.md` `## D243` (long, multi-pass — look up via `findings-index.csv`, read the M-159 through M-168 paragraphs for the current understanding, don't linear-read). Short version for planning purposes: **(1) Root mechanism** — the cutscene teleports the *visible* Bond chr between shot positions correctly (`AI_TRYTeleportingChrToPad`, `chrai.c:4064`), but the actual rappel *animation* commands run on a separate, invisible driver chr (M-145) — the visible chr never gets told to play it, so it just keeps ticking whatever animation it already had equipped before the cutscene grabbed it. Every tick, that stale animation's own root motion gets read back into the chr's render position (`chrUpdateAnim`→`getsuboffset`, `model.c:684`), producing a small repeating cycle that reads as "shaking"/"glitching" on screen. Whether this exact mechanism is itself decomp-faithful (with the real PC/N64 divergence being *which* animation phase Bond happens to inherit at the cutscene boundary, not a code-path bug) is still an open question (M-165/M-166) — not yet answered. **(2) Two independent render paths, not one bug** — the camera's look-at filter reads `field_488.pos` (derived from `render_pos`, the model's animated-skeleton transform); the model's own on-screen draw reads `render_pos` directly (`objecthandler.c`'s `drawjointlist`). Fixing one does not fix the other; two live-test cycles were spent learning this the hard way (M-162/M-163 → M-167). **(3) Camera shake: FIXED at the mechanism level, live-confirmed by the user** ("camera shake is gone now") by freezing `field_488.pos` during the cutscene (`GE_D243X3`, `bondview2.c:10495`) — diagnostic-only, not shippable as-is (see next). **(4) Bond's own model duplication: mechanism-level fix attempted (`GE_D243X4`, `chr.c`, freezing `render_pos`'s contents) — stable after fixing two crashes (model-pointer-reuse hazard, see `porting-notes.md`'s two D11 updates), but freezing unconditionally also breaks the cutscene's *legitimate* per-shot camera/model repositioning** (the camera now looks the wrong direction for later shots; Bond still looks stuck) — not a fix yet. **Next steps for whoever picks this up** (none attempted yet): (a) re-baseline the `GE_D243X3`/`GE_D243X4` freeze on a position-delta threshold (a jump much larger than the observed intra-shot cycling amplitude, already characterized in the M-153/M-160 captures, is very likely a real shot-change teleport, not cycling) instead of freezing forever; or (b) hook the freeze/re-baseline directly to `AI_TRYTeleportingChrToPad` firing, the precise, unambiguous signal for "a legitimate reposition just happened"; (c) separately, find the actual N64-vs-PC divergence point per M-165/M-166 (log `aidx=`/`animframe=`, already wired into `D243M: onscreen`) to determine whether a real, permanent fix is even a `port/`-layer timing correction or needs a rule-2 sign-off for a genuine behavioral difference — this determines which of (a)/(b) is worth turning into a real fix vs. a stopgap. |
| ~~**D149**~~ | ~~Front-end MISSION COMPLETE / mode-select 3D models (dossier, wallets) render as garbled full-screen geometry or not at all.~~ | **CLOSED — user-verified fixed (M-87 playtest).** No longer reproduces; presumably fixed as a side effect of other front-end/D75-family work since M-49. D75 family. §F D146. |
| **D170** | Named-NPC "flee" scripted behavior looks wrong on PC: on Silo, Ourumov breaks into a full run the instant he alerts ("Ourumov: Kill him!") rather than the N64 walk/retreat; developer reports Trevelyan behaves the same way on Cradle. **M-49 full playthrough: escalated — Cradle (`-level_51`) is "bugged out completely" from Trevelyan's behavior; the final level's scripted progression breaks. Now folded into / superseded by §F D193 (NPC AI locomotion too slow).** | **Observed, not investigated** (M-34, from a Silo playtest capture; user bug report — no fix wanted yet). Could be a genuine ailist / `chrTick` divergence, OR the D155/D156 wall-clock `deltaFrames` locomotion-anim speed-up (a catch-up spike playing the walk cycle at 2–3×). **Verify first:** capture the same beat against N64 reference footage before assuming it diverges — Ourumov may be scripted to flee. If it's real, suspect the frametiming clamp (§F D155/D156) or the Cradle-family AI path (§F D123). |
| **D171** | Silo missile-shaft: a large triangle sweeps across the frame while panning down the shaft. **Promoted to `docs/dev/findings.md` §F D288 (M-141)** with a user-reproduced, precisely-bounded, visually-documented repro (frames ~562-741 of a bare `-level_20` boot) — read that entry for the current state, not this row. | See D288. |
| **D172** | Bullet-impact / blood particle spray renders magenta or cyan instead of dark red (visible on guards hit at close range on Silo). | **M-82: ROOT CAUSE FOUND + FIXED (PR #34).** `GE_D172` probe on a user Silo firefight: 40/40 particle tris configure tile 1 = RGBA16 fire @ tmem 392 but fast3d **samples tile 0 (smoke) for both texunits** — `gfx_lod_tile_offset()` returns 0 unconditionally (D107 blurry-ceiling fix; `video.c:225` disables `gfx_detail_textures_enabled`). `G_CC_INTERFERENCE` = `smoke*smoke` → magenta/cyan. Fix: `return rdp.tex_lod ? 0 : i` — only fold to base tile for real LOD/mip draws. (0xB9 + cycle-type were both false trails, ruled out earlier in M-82.) **Owed: in-game eyeball — blood should be dark red.** Detail: `D172-PARTICLE-COLOR-RESEARCH.md`. |
| ~~**D178**~~ | ~~Objectives not listed correctly on the pre-mission briefing screen (the mission overview shown before starting a level).~~ | **CLOSED — user-verified fixed (M-87 playtest): "main menu text is good now."** No longer reproduces. M-36 triage below kept for reference. **M-36 triage: the objective lines are BLANK, not wrong.** `GE_STARTMENU=10` Dam briefing renders the header fine ("OHMSS / Agent: James Bond / Mission 1: Arkangelsk / Part i: Dam / PRIMARY OBJECTIVES:") then just the bullet "a." with no text and no b/c/d. → **D143/D151 blank-text family** (`langGet()` → NULL → silent blank), *not* a propDef walk / difficulty decode. **Family B** in `docs/dev/M-36-TRIAGE.md` — find which lang bank the front-end briefing flow fails to load. Re-confirm the exact shape in a *real* briefing (the `GE_STARTMENU` boot skips real save state). Close relatives: D151 (watch objective text blank — type-35/23 `text` slot u16→u32 fix, M-28; interactive re-verify was still pending), D143 (briefing/objective text blank via `langGet` NULL), D157 (objective difficulty byte offset — could make difficulty-gated objectives appear/disappear wrongly). The pre-start briefing is a *different surface* from the in-level watch, so D151's fix may not cover it. **Next:** ask user which level + what exactly shows vs expected; compare against `docs/dev/LEVEL-OBJECTIVES.md` (regenerated post-D157); if lines are blank → D143/D151 family (`langGet` NULL guard = silent blank), if wrong objectives listed → propDef type-23 record walk / difficulty decode. |
| **D175** | In-game stutter / brief hang during normal play — observed opening a door on Runway (mission 3, `-level_35`) and also on Surface. No backtrace captured (gdb attach missed the window; it self-recovered). | **Observed, not investigated** (user QA report). Matches known transient-hang classes rather than looking new: D155 (post-slow-load catch-up spiral — clamped), D156 (anim NaN loop — guarded, prints one-shot stderr diag if it fires), D134 (SP/DP task-done event swallowed — fixed M-22, Runway was an original victim), D147/D152 (audio `s_imLock` steal = ~2 s hiccup). Door-open specifically = new room first visible → one-off texture-import/frame spike (likely benign). **Next:** reproduce under `tools_pc/debug.ps1` with the box idle; check console for the D156 stderr diag; if it recurs, attach gdb mid-hang and pattern-match `bt` (import_texture = benign slow frame; model.c:3131 spin = D156; pthread_mutex_lock on s_imLock = D147/D152). |
| **D176** | Surface exterior renders wrong (two co-located defects). **(a)** Sky is solid **black** — the skybox / sky gradient does not draw at all (reference: warm sunset cloud gradient). **(b)** The perimeter **tree-line backdrop renders as grey TV-static / comb interlacing** instead of snow-laden pines — the geometry is present and roughly the right silhouette but every tree texture is scrambled noise. Ground, tunnel and the dish/bunker building draw fine. | **Observed, screenshots committed** (M-35, user QA; `docs/img/bugs/d176-surface-exterior-our-build.png` vs `-reference.png`). Level is the shared Surface exterior — user reached it Dam→Surface on Agent so this is **Surface (i) `-level_36`** (confirm it also affects `-level_43`). **(a)** black sky: **M-36 partial (findings.md "D176(a)").** Env data is correct (`Sky.Clouds=1`, `CloudRGB=240,120,30`, `SkyImageId=0`); `skyRender` runs the cloud path and classifies corners correctly (switch case 12), but `skyRenderTri`/`skyRenderFull` emit nothing — not even the `env->RGB` fill shows. Not root-caused: next is probing `sub_GAME_7F097388` projected coords, verifying `skywaterimages[0]` resolves on PC (`image_bank.c` GIMG offset `0xFB4`), and a fast3d sky-tri trace. `GE_D176=1` probe shipped. **M-37: ROOT-CAUSED** — `skyRenderTri`/`skyRenderFull` build geometry *exclusively* as `G_RDPHALF_*` immediates, which `gfx_pc.cpp` deliberately no-ops (identical no-op in the PD port); fix = decode the RDPHALF tri-raster stream in fast3d (~1 session, genuine new code). **M-42: `-level_22` (Statue) confirmed a clean headless repro** — bare boot, frame ~360, upper half pure black; use it to verify the decoder. `-level_36`/`-43` intro cams face down (not a sky test without `GE_INPUTSCRIPT`). **(b)** tree/rock static: **M-36 triage — NOT the D159 family.** `texSwapAltRowBytes` is a full `#ifdef PORT` no-op (`image.c:2199`), it cannot cause this. Real cause: `port/fast3d/gfx_pc.cpp` `import_texture_*` assume `line_size_bytes == full_image_line_size_bytes` (`SUPPORT_CHECK`; rgba16's is commented out *"trips ... with a garbage size"* `:658`) → non-PoT / sub-rect tiles get a flat linear read → diagonal row shear = grey static. **Family A** in `docs/dev/M-36-TRIAGE.md`. Fix: honour `line_size_bytes` as the real source pitch. Same defect as D182(2). |
| **D174** | "No blood effect" — user reports no visible blood when shooting guards / into surfaces. | **User QA report; likely D120, unverified.** Decals: the D120 `PointUsage[]` converter fix (M-30) was never interactively verified and only takes effect after a full sidecar regen (`d43 && d69 && d88 --regen`) — check whether the playtest build had regenerated sidecars before concluding the fix failed. Spray: D172 observed the particle spray *drawing* (wrong colour, magenta/cyan) on Silo — if the user sees no spray either, that's new info (different weapon/range, or the D172 decode is worse than "wrong colour"). First step: BUNKER1 firefight with regenerated sidecars; if decals still absent, instrument the `chr.c:3328` decal walk. |
| **D173** | Third-person Bond model (the player-representing figure shown at level start) spawns too high — floats well above the ground "a lot of the time" while the actual player spawn is correct. Same symptom on the Cuba end credits: JB stands ~6 ft in the air above Natalya. | **Observed, not investigated** (user QA report). D75 animated-model family but a *position* defect, not absence — M-33 confirmed the gun-barrel Bond renders fine upright. Suspects: (a) intro/credits puppet chr spawn position vs the model's base transform; (b) anim root-motion accumulation (`modelSetAnimFrame2WithChrStuff`, D156 area) offsetting the rendered joints above the body origin; (c) a `coord3d`/packed-float decode in the intro camera/model setup. "Player spawning is OK" localises it to the render-side model, not `g_CurrentPlayer` state. |
| **D182** | SELECT FILE screen degrades after navigating away and back (main menu → file select, or backing out of file select and re-entering). On the fresh screen the 007 gun-barrel spiral / Bond silhouette renders behind the file boxes; after a round-trip **the Bond image stops drawing entirely** and the whole background gains the **comb / interlacing** artifact (horizontal line scramble). File boxes, text and the Copy/Erase icons still draw. | **Observed, screenshot committed** (M-35, user QA; `docs/img/bugs/d182-file-select-backout-20260902.png`). Two symptoms, likely linked: **(1)** background 3D image stops rendering after re-entry → a front-end model/DL whose `render_pos` / segment binding is only valid on first construction (D149 / D75 `constructor_menuXX` family — the dossier/wallet models have the same "renders once" behaviour). **(2)** interlacing = **M-36 triage: Family A**, the `import_texture_*` `line` vs `full_image_line` shear (see D176(b) / `docs/dev/M-36-TRIAGE.md`) — **not** the D159 `texSwapAltRowBytes` path (that is a full PORT no-op). Open question: why only *after* a round-trip — first import may land `line == width`, re-import may not, or the tile descriptor is stale on re-entry. Repro: `GE_STARTMENU` to file-select, back out with ESC/B, re-enter; `GE_DTEX` on the spiral texture across both visits. Shares defect (2) with D176(b). |
| **1P weapon viewmodel** | In-level first-person weapon models don't draw at all (no gun visible while playing). | Already documented — **AUDIT-M6 #5** (`docs/dev/AUDIT-M6-player-offsets.md`, "prime suspect") + §F D115 "still open (MED)"; M-8 handoff listed it NOT STARTED with a plan: instrument `gunfire.c:600-661` setup + `field_87F` gating first, then check `weaponModel.render_pos` arena lifetime and `weaponRwPool[192]` sizing. Note M-32b's "render_pos/dynAllocate RULED OUT" probe was the **front-end gun-barrel** path (`title.c`), not this in-level path — candidate #5 still stands. Distinct from the watch-page "dark/faint weapon model" (D75, D141 note). |
| **D176** | Surface exterior renders wrong (two co-located defects). **(a)** Sky is solid **black** — the skybox / sky gradient does not draw at all (reference: warm sunset cloud gradient). **(b)** The perimeter **tree-line backdrop renders as grey TV-static / comb interlacing** instead of snow-laden pines — the geometry is present and roughly the right silhouette but every tree texture is scrambled noise. Ground, tunnel and the dish/bunker building draw fine. | **Observed, screenshots committed** (M-35, user QA; `docs/img/bugs/d176-surface-exterior-our-build.png` vs `-reference.png`). Level is the shared Surface exterior — user reached it Dam→Surface on Agent so this is **Surface (i) `-level_36`** (confirm it also affects `-level_43`). **(a)** black sky: **M-36 partial (findings.md "D176(a)").** Env data is correct (`Sky.Clouds=1`, `CloudRGB=240,120,30`, `SkyImageId=0`); `skyRender` runs the cloud path and classifies corners correctly (switch case 12), but `skyRenderTri`/`skyRenderFull` emit nothing — not even the `env->RGB` fill shows. Not root-caused: next is probing `sub_GAME_7F097388` projected coords, verifying `skywaterimages[0]` resolves on PC (`image_bank.c` GIMG offset `0xFB4`), and a fast3d sky-tri trace. `GE_D176=1` probe shipped. **M-37: ROOT-CAUSED** — `skyRenderTri`/`skyRenderFull` build geometry *exclusively* as `G_RDPHALF_*` immediates, which `gfx_pc.cpp` deliberately no-ops (identical no-op in the PD port); fix = decode the RDPHALF tri-raster stream in fast3d (~1 session, genuine new code). **M-42: `-level_22` (Statue) confirmed a clean headless repro** — bare boot, frame ~360, upper half pure black; use it to verify the decoder. **M-47 + read-only consolidation:** Path B draft exists (`scratchpad/sky.patch`, `skyPortRenderPoly` PORT hook — geometry+texture reach GL, two unit defects open) and the full RDPHALF wire format is now decoded (S15.1 half-packing, 8-component perspective-interpolated vertex vectors); next session is mechanical per `docs/dev/D176a-SKY-NOTES.md`. `-level_36`/`-43` intro cams face down (not a sky test without `GE_INPUTSCRIPT`). **M-82: PR #18 rebased onto main + Defect-1 fix applied** — cloud tile is `s_skywaterimages[0]` = 64×64 IA8; the patch's `tc = S·32` scale is correct (matches GE's `width<<5` idiom, `glass2.c:678`), the bug was `(s16)` overflow on the ±20k-texel horizon S/T; fix = per-primitive phase-fold to the 64-texel period (`sky.c` `skyPortRenderPoly`, `#ifdef PORT`). Builds clean, no crash (bunker1/statue/surface1 verify PASS). **Unverified visually** — owed: one `-level_22` screenshot (see `D176a-SKY-NOTES.md` "verification ask"). Defect 2 (horizon-band coverage / straddling tris) still open → adaptive tessellation. **(b)** tree/rock static: **M-36 triage — NOT the D159 family.** `texSwapAltRowBytes` is a full `#ifdef PORT` no-op (`image.c:2199`), it cannot cause this. Real cause: `port/fast3d/gfx_pc.cpp` `import_texture_*` assume `line_size_bytes == full_image_line_size_bytes` (`SUPPORT_CHECK`; rgba16's is commented out *"trips ... with a garbage size"* `:658`) → non-PoT / sub-rect tiles get a flat linear read → diagonal row shear = grey static. **Family A** in `docs/dev/M-36-TRIAGE.md`. Fix: honour `line_size_bytes` as the real source pitch. Same defect as D182(2). |
| ~~**D233**~~ | ~~All levels (intermittent): near doors — especially doors you can't see through to the inside — the indoor level geometry vanishes and you can see straight through it into the room, while world objects (computer terminals, furniture) in that space still draw.~~ | **FIXED + VISUALLY VERIFIED (M-107 fix, M-108 verify).** `port/fast3d/gfx_pc.cpp` `gfx_sp_tri1` was trivial-rejecting visible triangles that had a behind-camera (`w<0`) vertex. Status cell here was stale at "not investigated" — corrected M-119. §F D233. |
| ~~**D234**~~ | ~~Dam: the guard towers along the actual dam structure are invisible except for the first one.~~ | **FIXED + VISUALLY VERIFIED (M-108).** Same underlying mechanism as D233. Status cell here was stale at "not investigated" — corrected M-119. §F D234. |
| **D235** | Wrong textures load after dying/restarting a level — clarified: not Facility-specific, seen on multiple levels; trigger is death/restart (level reload), not a general build regression. | **Observed, not investigated** (M-106, user QA report, clarified same session). This is a state-leak on level re-entry, not a one-level texture-decode bug — prime suspect is the texture pool / CI-cache (D217/D228 neighbourhood) not fully resetting between a level teardown and reload (stale arena base, a palette-cache entry keyed by a reused address, or a texpool index not rewound). Needs: confirm it does NOT repro on a fresh level boot (only after death/restart); which surfaces; a screenshot. Next: instrument `texInitPool`/the CI-cache clear path for what does/doesn't reset on reload vs cold boot. |
| **D236** | Surface 1: the 2D billboard trees that appear at the start of the level and around the map don't draw as discrete tree sprites — they render as a wall/sheet of tree texture instead of the proper 2D tree objects. | **FIXED (M-198, pass 26) — awaiting user play-test sign-off.** Root cause was a texture-BINDING bug, not decode/alpha/mip/geometry: GE binds the four Surface 1 treeline cards (`tex1198`-`tex1201`, RGBA5551 64x17 cut-outs) as `TEXTURETYPE_DETAIL`, where `texHandleType1` (`tex.c`) loads the *detail* texture at TMEM 0 and the real base image at TMEM `texGetSizeInBytes(detail,0)` on tiles 1+. fast3d's `gfx_lod_tile_offset` folded every `G_TL_LOD` draw to tile 0 (D107, correct for TEXTURETYPE_LOD/MIPMAP), so the port drew the shared detail texture -- `tex2465`, an opaque 32x32 IA8 noise tile, which is exactly the 'noise painter' D267/D276 measured at 1023/1024 texels opaque -- and never imported the cards at all. Fix: when a tile pair differs in both TMEM address and `fmt`/`siz` under `G_TL_LOD`, sample the base image (tile 1). `Video.DetailBaseTile`, default on. Live-verified on Surface 1 (discrete snow-covered pines) and Surface 2; Depot clean; bunker1 golden bit-identical with the fix on/off; `type=1` bindings exist only in sevx/depo/sevb/sev, so every other level is provably untouched. Full trail: `findings.md` D236 pass 26; generalised as `porting-notes.md` §D12.
| **D249** | Streets and Egyptian (the two biggest/most-open levels) show culling problems on far-away geometry at *default* FOV — distant geometry drops/pops incorrectly. Also a QoL ask: improve LOD in general, but gate it behind a toggle so N64-accurate ("old school") LOD stays selectable. | **Observed, not investigated** (M-119, user QA report). Distinct from D222 (high-FOV near-edge culling) and D218 (fog/far-clip tint). Two candidate mechanisms: (a) portal/room-visibility cutting off too aggressively at range on large multi-room layouts (D233/D234 neighbourhood — check for a far-plane/depth-range sibling to D233's w<0 trivial-reject fix), or (b) GE's own LOD/detail-swap system not ported faithfully (mip-tile / simple-model substitution at distance, `gfx_lod_tile_offset` family). First checks: `GE_PCDUMP` a long sightline on Streets/Egyptian where geometry visibly pops; determine which of (a)/(b) is firing before designing the fix or the requested toggle. §F D249. |
| **D217** | 1P weapon viewmodel + character-model CI textures render with a wrong palette on many levels / multiple weapons — PP7 grip + hand render flat green on Facility & Depot, "weird or missing" gun textures elsewhere; Bond's watch face too. Surfaced by D215 (viewmodel now draws). Clean on Bunker1. | **Defect ① FIXED (M-90), pending human playtest eyeball — full detail in `findings.md` §H "D217 — M-90" and §F D217.** Root cause: weapon texpool arena bases land %8==4 on PC (memp bump drift from pointer bloat); `texAlignIndices` absolute 8-byte row padding then shifts every palette 4 bytes early while model-DL `G_LOADTLUT` reads the N64 offset (grip count=1 load lands on next tenant's header slot → `0x1A00` green; larger loads read a 2-entry zero pad — the M-86 tell). Fix: `texInitPool` rounds the base up to 8 under `#ifdef PORT` (no-op on N64); 21/21 sweep PASS. Two defects: ① **visible green = misaligned texpool arena** (fixed as above). ② **CI texture-cache collision** (no palette-content key) — proven, **fix landed on branch `fix/d217-model-texture-palette` `f6fc0399`** (palette FNV-1a hash in the CI cache key), owes a 21-level `verify.sh sweep`; does not fix ①. |
| **D174** | "No blood effect" — user reports no visible blood when shooting guards / into surfaces. | **User QA report; likely D120, unverified.** Decals: the D120 `PointUsage[]` converter fix (M-30) was never interactively verified and only takes effect after a full sidecar regen (`d43 && d69 && d88 --regen`) — check whether the playtest build had regenerated sidecars before concluding the fix failed. Spray: D172 observed the particle spray *drawing* (wrong color, magenta/cyan) on Silo — if the user sees no spray either, that's new info (different weapon/range, or the D172 decode is worse than "wrong color"). First step: BUNKER1 firefight with regenerated sidecars; if decals still absent, instrument the `chr.c:3328` decal walk. |
| **D173** | Third-person Bond model (the player-representing figure shown at level start) spawns too high — floats well above the ground "a lot of the time" while the actual player spawn is correct. Same symptom on the Cuba end credits: JB stands ~6 ft in the air above Natalya. | **Confirmed still present across the full 21-level playthrough (M-49).** **Observed, not investigated** (user QA report). D75 animated-model family but a *position* defect, not absence — M-33 confirmed the gun-barrel Bond renders fine upright. Suspects: (a) intro/credits puppet chr spawn position vs the model's base transform; (b) anim root-motion accumulation (`modelSetAnimFrame2WithChrStuff`, D156 area) offsetting the rendered joints above the body origin; (c) a `coord3d`/packed-float decode in the intro camera/model setup. "Player spawning is OK" localises it to the render-side model, not `g_CurrentPlayer` state. |
| **D182** | SELECT FILE screen degrades after navigating away and back (main menu → file select, or backing out of file select and re-entering). On the fresh screen the 007 gun-barrel spiral / Bond silhouette renders behind the file boxes; after a round-trip **the Bond image stops drawing entirely** and the whole background gains the **comb / interlacing** artifact (horizontal line scramble). File boxes, text and the Copy/Erase icons still draw. | **Observed, screenshot committed** (M-35, user QA; `docs/img/bugs/d182-file-select-backout-20260902.png`). Two symptoms, likely linked: **(1)** background 3D image stops rendering after re-entry → a front-end model/DL whose `render_pos` / segment binding is only valid on first construction (D149 / D75 `constructor_menuXX` family — the dossier/wallet models have the same "renders once" behavior). **(2)** interlacing = **M-36 triage: Family A**, the `import_texture_*` `line` vs `full_image_line` shear (see D176(b) / `docs/dev/M-36-TRIAGE.md`) — **not** the D159 `texSwapAltRowBytes` path (that is a full PORT no-op). Open question: why only *after* a round-trip — first import may land `line == width`, re-import may not, or the tile descriptor is stale on re-entry. Repro: `GE_STARTMENU` to file-select, back out with ESC/B, re-enter; `GE_DTEX` on the spiral texture across both visits. Shares defect (2) with D176(b). **2026-09-27 (D397):** user re-reported the comb/interlace on the file-select background AND on the **intro gun-barrel** — the latter is a *first-time* render, so the defect is persistent (not only after a round-trip), refuting the "stale descriptor on re-entry" hypothesis. D159 (odd-row swap) is no-op'd and D183 pitch shear is disproven for level_36 + a no-op for the 299×440×1 row-strip. Next step is a live `GE_PCDUMP` capture + ROM ground-truth decode of the gun-barrel RLE image to separate ground-truth dithered content from a GL sampling/texture-cache artifact. See findings.md D397. |
| **D197** | Character face/head textures wrap wrong — the face texture repeats around the side of the head "and continues weird" instead of clamping to the front. Seen on **Silo** (M-49 playtest), intermittent ("sometimes"). Reads as **`G_TX_CLAMP` not honoured** for the character-skin tile in fast3d (→ `GL_REPEAT`, so the face UV region tiles around the skull), or wrong `shifts`/`shiftt` / LOD-tile selection on the head texture, or the D167 non-PoT wrap-period issue (head textures are often non-PoT) hitting skinned models. | **Observed, not investigated** (M-49, user bug report — needs a capture + which chr/level). Distinct from D176(b)/D182(2) (those are *bg wall* pitch shear). Related: RC3/D167 (`Video.WrapFix`, non-PoT wrap period) + the `texWriteTileLods` `shifts=shiftt=lod` note in findings §F "D176(b) → 4. candidate 2". First checks: does `gfx_pc.cpp` map N64 `cms/cmt` `G_TX_CLAMP` → `GL_CLAMP_TO_EDGE` for character DLs; is the head tile getting the base tile vs an LOD tile (D107); try `Video.WrapFix=1` on a repro. |
| **D195** | Transparency / alpha-blended surfaces render wrong — reported on **Control** (Severnaya bunker basement — Natalya's console room; heavy on glass partitions and the translucent projected/holographic wall displays). Exact failure mode not yet pinned by the reporter (M-49 playtest): candidates are alpha-blended geometry drawing fully opaque, translucent panels drawing invisible, or draw-order/sort so translucent surfaces occlude what's behind. | **Observed, not investigated** (M-49, user bug report — needs a `GE_PCDUMP` capture + which surfaces). fast3d render-mode / combiner path. Same neighborhood as D161 (CI8 tile + `G_TT_NONE` → garbage), D172 (particle-sprite channel order), D176(b) (texture pitch shear) — but this is a *blend/alpha* defect, not a texture-decode one. Note D128 already touched Control's `specialportalarray` (portal struct stride) — check whether the affected surfaces are portal-adjacent. First checks: does `gfx_pc.cpp` honour `G_RM_*` XLU render modes + `G_AC_THRESHOLD`/`G_CC` alpha for these DLs; is `gDPSetRenderMode` `w1` being parsed (cf. findings §F "zbuf-clear w1 scribble"); is Z-write-vs-blend ordering right for XLU surfaces. Get the reporter to say: opaque-where-should-be-clear, invisible, or wrong edges? |
| **D252** | Particle effects cycle through a rainbow palette — bullet-impact sparks and lingering smoke/explosion residue drift through the full hue wheel over time (~10 shots to sweep it) instead of holding their intended grey/orange palette (M-124, user playtest). Full mechanism + suspect in findings §F **D252**: colour comes from 15 distinct `IMAGE_SMOKE_N`×`IMAGE_FIRE_N` (RGBA16) pairs the particles age through; prime suspect is `import_texture_rgba16` deriving width/height from the tile line size (32×7 instead of 16×14 for the fire tiles), scrambling each stage's sprite into a different wrong hue. | **Documented only — not fixed for v0.2.0-pre** (user instruction). Confirm with one dumped fire tile vs raw `IMAGE_FIRE_N` bytes, then derive width/height from the LOADBLOCK dtx/dlh. D172/M-82 lineage. |
| **D323** | Distant geometry on 15 levels samples driver-generated mips from the finest texture level; N64 samples Rare's hand-authored mip chain for explicit-LOD textures bound under `G_TL_LOD` (heaviest: Statue 378 binds, Aztec 806). Overwhelmingly CI8/IA8 — subtle fidelity gap, no reported artifact; the only RGBA5551 cut-outs are the D236 set (already handled). | **OPEN / parked** — mechanism confirmed, census in `tools_pc/bg_lod_audit.py`, fix path documented in findings §F **D323** (upload GL mip chains for TEXTLOD-on bindings). Not worth doing speculatively; revisit only if a playtest flags mushy distant detail. |
| **D335a** | **RESOLVED 2026-09-23.** Gun-barrel backdrop (Bond's spotlight) leaked into the pillarbox bars while sliding in from off-canvas. Fix: `menu_jump_constructor_handler` emits a full-canvas `gDPSetScissor` right after the CENTER wrap (only when wider than 4:3), which fast3d squeezes to the pillarbox, so off-canvas draws clip at the 4:3 edge as the N64 RDP clips at the screen edge. Verified with a 16:9 barrel capture. |
| **D335b** | Native widescreen: the F10 options overlay still stretches with the window (legible, pre-existing look). Pillarboxing it needs `optionsoverlay.c`'s click mapping (`gfx_get_ui_screen_rect`) remapped into the centred region in step. Polish, post-playtest. |
| **#105** | Dam: uncovered water-reflection pixels show the procedural cyan fill instead of the stage sky (residual after the water/wall-reflection work; visible boundary between reflected geometry and sky in the settled Enhanced/Remaster capture). | Backlogged 2026-09-28 from [issue #105](https://github.com/jkdansereau/goldeneye-pc-port/issues/105); post-v0.4.0 graphics queue. Note: the issue's "D329" cross-ref is the issue author's own numbering — our D329 is the anim-pinning fix. |
| **1P weapon viewmodel** | **RESOLVED (M-83, D215).** In-level first-person weapon models drew nothing. Root cause: `gunRenderFirstPersonGunModels` built its `ModelRenderData` via `*(ModelRenderData*)&D_80035CC0` — a reinterpret across two separately-declared adjacent globals that only lands right on the N64 linker + 4-byte pointers; on x86-64 `renderdata.flags` read 0, gating every weapon DL node off in `modelRenderNodeGundl`. All 3 ranked suspects in VIEWMODEL-RESEARCH.md were ruled out first by the `GE_DVM=1` probe. Fixed `#ifdef PORT` (explicit field init). `subdraw` 1→153 gfx cmds; owed one BUNKER1 eyeball. |
## Resolved (for reference)
File diff suppressed because it is too large. Load diff
+11 -4
View File
@@ -47,7 +47,7 @@ Objective status: `src/game/objective_status.c`
- Notes / defects:
```
## 21 solo levels (mission order — numbers = `-level_XX`)
## The 20 solo missions (+ ending sequence; mission order — numbers = `-level_XX`)
| # | Level | -level | Briefing asset |
|---|---|---|---|
@@ -71,7 +71,14 @@ Objective status: `src/game/objective_status.c`
| 18 | Cradle | 41 | UbriefcradZ |
| 19 | Aztec | 28 | UbriefaztZ |
| 20 | Egypt | 32 | UbriefcrypZ |
| 21 | Cuba | 54 | (post-Egypt unlock) |
| — | *Ending sequence* ("Cuba") | 54 | not a playable mission — cast/credits stage (D129) |
(Mission table: `front.c:433`. Cuba = `LEVELID_CUBA`, not in the standard
folder list — unlocked after Egypt.)
(Mission table: `front.c`, `mission_folder_setup_entries` — exactly 20
playable missions; the game's own enum says it, `SP_LEVEL_MAX // 20` in
`bondconstants.h`). "Cuba" (`-level_54`, `LEVELID_CUBA`) is **not** a
selectable mission: after the final mission the front end jumps straight to
it for the extended cast display + credits scroll
(`front.c` `selected_stage = LEVELID_CUBA` → `do_extended_cast_display(TRUE)`;
`bondviewRenderCredits`, D129). Older docs and sweep logs count it as a 21st
"level" — the campaign is 20 missions; "21/21" sweep results mean 21 stage
boots, of which 20 are missions.)
+29 -1
View File
@@ -1,4 +1,10 @@
# LEVEL-STATUS — 21 solo-level load+render sweep (WS4)
# LEVEL-STATUS — level load+render sweep (WS4) — 20 solo missions + ending sequence
**Counting note:** the campaign is **20 playable missions** (`SP_LEVEL_MAX //
20` in `bondconstants.h`; `mission_folder_setup_entries` in `front.c` has
exactly 20 entries). The sweep's 21st boot target, "Cuba" (`-level_54`,
`LEVELID_CUBA`), is the end-of-campaign cast/credits stage (D129), not a solo
mission — so "21/21 PASS" below means *21 stage boots*, 20 of them missions.
Method: bare `./build-pc/ge007.x86_64.exe -level_XX` (per-level `-m*` pools
auto-injected, D121), `GE_PCDUMP="80-260:40"`, ~24 s watchdog then
@@ -7,6 +13,28 @@ auto-injected, D121), `GE_PCDUMP="80-260:40"`, ~24 s watchdog then
0x140000000). "render %" = `tools_pc/pixcount.py` non-clear on the last
captured frame. Binary at `f2beae4b` + this session's build.
**M-79/M-81 catch-up note (added M-91, since these fixes were never given a
LEVEL-STATUS entry of their own).** Both crashers below (D191) and the
AI-pacing bug (D193) that blocked Cradle were subsequently root-caused,
fixed, and playtest-verified: D191 in M-79 (user replayed both trigger
scenes — Bunker ii first-guard kill, Statue post-Trevelyan cutscene — neither
crashes; **21/21 solo levels crash-free**), D193 in M-81 (user-verified on
Cradle itself — Trevelyan and the guards run at correct pace; root cause was
D209). **No known crashing levels as of M-91.** What's still open is a full
21-mission end-to-end re-playthrough confirming completability on the
current build (the "v0.2.0 gate" in `docs/HANDOFF.md`) — the M-49 playtest
below predates both fixes and should not be read as current.
**M-49 (2026-09-04) — full-campaign playtest, v0.1.0 win64 bundle.**
User played every solo mission on the packaged build (fresh save). **19 / 21
completable start to finish.** Only two crashes in the whole game — **Bunker ii
(`-level_27`) and Statue (`-level_22`)**, same fault PC `0x1400801b1`
(`modelGetNodeRwData` node-tree walk), one root cause = **D191** (pointer-width
/ byte-swap family, findings §F). This retires the old "expect crashes past the
level intro" framing for 19/21 levels. Biggest non-crash complaint: **D193** —
AI locomotion too slow, breaks Cradle's scripted progression. v0.1.0 tagged off
this state.
**M-30 re-sweep (2026-08-31, build after D157/D120/RC2): 21 / 21 PASS — no
crash logs.** First all-green sweep incl. Cuba (54) — the D129 credits-path
crash no longer reproduces on a bare `-level_54` boot (Cuba rendered 92.5%
+45
View File
@@ -0,0 +1,45 @@
# M7 — Reversible AllUnlocked: investigation and fix plan
**v0.4.0 release decision (2026-09-28, user):** ship AllUnlocked as a
**loudly-documented, EXPERIMENTAL, irreversible** option for this release rather
than hiding/disabling it or landing a safe architecture. The toggle stays (default
OFF); the in-game row is now labelled **"All unlocked (EXPERIMENTAL)"** with the ON
value displayed as **"ON - UNSAFE"** in both the F10 overlay and the front PC
Options screen, and the README Known-issues bullet already tells users to back up
`data/ge007.eep` before enabling it. A proper fix (Candidate A port-only shadow or
Candidate B non-mutating virtual query) remains the v0.5.0 work item.
Status: **investigation, NO FIX APPROVED OR LANDED**. Root cause/repro: `docs/dev/findings.md` D387; original feature: D257, fresh-save repair: D281, old-CRC migration: D297. This plan supersedes the M7 "no-code proof" in `docs/dev/notes/MODERN-OPTIONS-PLAN.md`. Follow `AGENTS.md` rule 2 and `docs/dev-process.md` §7 before any `src/game` edit.
## User-visible contract to settle
Recommended: OFF (after restart, as presently documented) must display only genuinely earned mission progress/cheats; ON may *temporarily* expose every level, difficulty, 007 mode and cheat, without modifying durable progress. Achievements **earned while ON without an active cheat** should survive OFF, including difficulty inheritance, timed cheats and profile operations. Enabling/disabling must never silently delete existing genuine save records. Do not auto-repair an EEPROM already containing baked fake unlocks: the bytes lack provenance. The user has not explicitly signed off on a game-code alternative.
## Confirmed facts (source + isolated data)
- `port/src/libultra.c:geEepromRW` first migrates legacy CRCs on the five-slot block-4 read, then `geEepromPatchAllCheats` fills unlocked-cheat bits and every zero completion time with 0x3FF and re-CRCs a **temporary copy**. `fileValidateSaves` (`src/game/file2.c:527`) accepts that copy into `saves[]`. `fileWriteSave` (`file2.c:86`) writes the entire 96-byte live slot, not only the setting/progress that changed.
- Private data under `AppData/Local/Temp/ge-m7-20260927/`: the older `data/ge007 copy.eep` has two genuine times and only cheat `04 00 00` in active folder 0 (slot 4). ON or OFF boot without a save yields identical EEPROM bytes after the same legacy CRC migration. ON plus existing `GE_WSPROBE_FRONT=600` performs an ordinary Music-volume save to slot 4: cheats become `ff ff 0f`, 58 empty times become 0x3FF, two real times remain. OFF + restart validates the written CRC and retains all fakes. Full repro/logs: D387. Current `data/ge007.ini` and `data/ge007.eep` were never opened for write; copies and SHA-256s in that private test directory.
- `main.c` seeds debug unlock flags only at boot; an in-session OFF toggle leaves those RAM flags set until restart. `Game.AllUnlocked` defaults OFF; the relevant UI currently gives no explicit restart warning.
- More than a write-mask problem: a successful mission runs `end_of_mission_briefing` (`src/game/file.c:41–68`) → `fileUnlockStageInFolderAtDifficulty` (`file2.c:828`) → `fileOverwriteSaveSlotWithNewSave` (`file2.c:794`), **rotating the profile to a free EEPROM slot** and freeing the old slot. `fileCheckSaveStageDifficultyTime` only updates if the real time beats the synthetic 0x3FF. Genuine lower-difficulty completions and long (>1023-second) runs can also have legitimate 0x3FF times; value alone is not provenance. Fake cheat bits cause `fileGetIsCheatUnlocked` to return true, so `end_of_mission_briefing` skips a genuinely earned timed-cheat award. A port-only sanitizer keyed by physical slot or by `time==0x3FF` is incorrect.
- The current user EEPROM already has two nonfree slots with 60/60 max times and all cheat bits, consistent with an earlier write-back. Two separate older EEPROM backups exist in `data/`, but replacing or auto-merging them would risk later real progress; do not touch or auto-repair originals.
## Architecture decision before code
**Candidate A — port-only EEPROM shadow/merge:** keep authoritative *raw* progress per logical profile outside patched `saves[]` and translate every write to durable real-only state. Must cover direct settings writes, profile rotation, copy/delete/reset, earned times (including 0x3FF), timed cheat grants currently suppressed by fake reads, five-slot CRCs, and old-CRC migration. Physical-slot-only interception demonstrably fails. A read/write-only adapter may have insufficient information to recognize some genuine completions or cheat awards; prove a full event/provenance model in an isolated prototype before promising this path. No new sidecar that can be lost/desynced without a recovery design.
**Candidate B — non-mutating virtual query:** retire `geEepromPatchAllCheats`, keep raw `saves[]` and EEPROM intact, and make the *opt-in* UI/gameplay availability queries report AllUnlocked without changing saved bits/times. Likely small PORT-gated touch points: `fileGetIsCheatUnlocked` (`src/game/file2.c:391`, existing D225 developer-only query override), `fileIsStageUnlockedAtDifficulty` (`file2.c:681`; cheat-enabled mission selection requires `STAGESTATUS_COMPLETED`, not merely the debug-flag `UNLOCKED`), and the raw earned-cheat check in `end_of_mission_briefing` (`src/game/file.c:41–68`), which must still record real cheat awards while virtual availability is ON. Audit every caller and the Rare/PD analogue first. A live-only toggle may require restoring debug flags safely; simplest contract is **takes effect on restart** and an explicit UI label. This alters opt-in `src/game` behavior and is **not** the ABI/layout exception: no implementation or implicit sign-off. If port-only is ruled out, prepare the exact file/line, reference, reason, scope and precedent required by `docs/dev-process.md` §7; obtain explicit user approval and document the grant in a new RULE-2-SIGNOFF finding before edits.
**Release fallback:** if neither safe architecture is ready, do not present the current toggle as a reversible enhancement. Get an explicit release decision on hiding/disabling it vs shipping a loudly irreversible option; default-off alone does not protect users who toggle it on. Keep existing saves intact.
## Test matrix (private files ONLY)
1. Capture baseline hashes of copied current ini/EEPROM; separate valid older-progress, fully fresh zero EEPROM, already-contaminated, legacy-CRC, and mixed/multiple-folder fixtures. Do not copy old saves back into `data/`.
2. No-write ON boot then OFF restart: raw five slots unchanged except *explicit* D297 migration; correct UI availability in both states.
3. ON settings writes via `GE_WSPROBE_FRONT` (normal slot), in-stage F10 watch writes, and save-file chooser/reset: only real settings/intentional reset persist; CRC valid; no synthetic completions/cheats in raw file.
4. Real mission completion at agent and higher difficulty (including inherited lower difficulty), fast timed-cheat award, slow/capped 0x3FF completion, and cheat-enabled mission selection. Verify actual new earned records after OFF restart, no artificial ones. Build an isolated gameplay harness if manual replay is impractical; do not use a settings-write test as a substitute for mission completion.
5. Save-slot rotation (`fileOverwriteSaveSlotWithNewSave`), profile copy/delete, all four folders plus free slot, and reopen across process restart. Confirm all five CRCs via `GE_SAVELOG=1` and raw bit-level diff; compare copied baseline profile identity rather than just physical slot index.
6. Default-OFF NTSC/PAL/JP builds and post-landing 21-level default-render comparison; config/EEPROM originals hash-verified **after** all tests. Human test of ON/OFF UI text and behavior. Existing contaminated saves need an explicit *separate*, opt-in recovery plan; never silently rewrite them.
## Other wave item
M5 remains under user review: FPS cap and MSAA are independently exposed already; dropping the two-setting Low-end preset is a separate decision, not a dependency of this M7 investigation.
+11
View File
@@ -1,6 +1,11 @@
# In-game PC options menu — design + resume point
Status: **config-system foundation landed (M-35); menu surface not built.**
(Updated 2026-09-27: the menu surface *has* since been built and iterated
as D346 -> D356; the current layout and per-section reset live in
`docs/dev/D356-SETTINGS-REGROUP-PLAN.md` + `docs/dev/WATCH-SETTINGS-PLAN.md`
and `docs/dev/findings.md` D346–D356. This doc remains the historical
M3-scope record.)
Pattern follows `docs/dev/AUDIO-PLAN.md` — a plan doc that a later session
executes.
@@ -85,6 +90,12 @@ or are flagged "restart".
## 4. Alternative if an in-fiction surface is required later
> **2026-09-27: route B chosen (v0.4.0 intake).** The entry point is on the
> file select screen, not a main options menu (GE's front end has none). M1
> landed as D343: an "Options" label that opens the F10 overlay, via one
> Rule-2 `#ifdef PORT` draw hook. M2 (a GE-styled screen that reuses the
> overlay's rows) follows.
Route (B): a new front-end screen `constructor_menuXX_pcoptions` +
`menuXX_pcoptions_navigation` in `front.c` under `#ifdef PORT`, reached from
a new "PC OPTIONS" row on the main options menu. Each row calls a
+68
View File
@@ -0,0 +1,68 @@
# QoL Inventory — PD-vs-GE options diff
Status: **partially implemented.** M-83 landed three port-only quick wins —
DisplayFPS (D213), anisotropic filtering (D212), and the FOV slider (D211, via
`WIDESCREEN-FOV-PLAN.md` Phase 4). Rebinding / SkipIntro / per-pad tuning /
window HiDpi still open. Companion to `WIDESCREEN-FOV-PLAN.md` and
`UNLOCKED-FPS-PLAN.md`.
Method: diffed the config surface of both ports — every `configRegister*`
key in `pd_port/port/src/*.c` vs `port/src/config.c` + the F10 overlay
(`port/src/optionsoverlay.c`) + PD's `optionsmenu.c`. Principle applied:
**where PD has a QoL feature that is achievable here, it is the standard;
GE keeps its own extras.**
Classification key — **P** = port-only (no rules issues), **G** = needs a
game-code exception (rule #2 sign-off, per `WIDESCREEN-FOV-PLAN.md` framing).
## GE already has (baseline — keep)
Input: mouse enable/capture/aim+turn speed/invert Y/Y scale/smoothing/raw
input, aim band, hipfire pitch speed, menu pointer speed + mode (GE's menu
cursor is injected entirely in `port/src/input.c` — no game code), pad
deadzone/trigger %. Video: VSync, FpsCap, MSAA, TextureFilter,
FixMipTextures, WrapFix, Fullscreen, Window geometry. Game:
ScreenShakeIntensity. Debug: FrameDump, InputLog.
## PD has, GE lacks (candidates)
| Feature | PD location | Class | Effort | Notes / priority |
|---|---|---|---|---|
| **Key rebinding** _(DONE M-83, D214)_ | `port/src/input.c` `[Input.Bind]` section | P | M | Keyboard only, 12 actions, `Input.Bind.* = Key,Key` (SDL scancode names), file-driven like PD. Defaults = prior FPS layout (verified). Per-controller pad rebinding + a UI still open. |
| **SkipIntro** _(DONE M-83, D216)_ | `Game.SkipIntro` -> `src/game/lv.c` | P | S | Boots to the SELECT FILE menu, skipping the legal screen + Nintendo/Rare/GoldenEye logo attract loop. Reuses the game's own post-intro route (sets `is_first_time_on_main_menu=FALSE` + `menu_update=MENU_FILE_SELECT`); one `#ifdef PORT` line in the existing GE_STARTMENU block. Verified headless (screenshots). |
| **DisplayFPS** _(DONE M-83, D213)_ | config `Video.DisplayFPS` | P | S | Top-right readout, config-only; ~0.5 s sample window. |
| **Anisotropic filtering** _(DONE M-83, D212)_ | `Video.Anisotropy` (1–16, dflt 4) | P | S | fast3d already had the GL hook + a hardcoded 4×; M-83 exposed it as config. MipmapFilter / TextureFilter2D still not surfaced. |
| Per-pad tuning: per-stick deadzones, stick scale, rumble scale, device index, swap sticks, C-button mapping | `input.c` padsCfg block | P | M | Medium. Matters for real-controller users; PD pattern is a config section per pad. **M-87 survey addendum:** PD does *incremental* hot-plug (`SDL_CONTROLLERDEVICEADDED/REMOVED` opens/closes individual pads into the first free slot, remembers `deviceIndex=-1` on removal so a replugged pad returns to its configured seat, `input.c:473-491`); GE's port instead does a full rescan-close-and-reopen-everything (`inputRescanPads()`, `input.c:983-1000`) and the game itself latches the controller mask at boot, so a pad added later merges into controller 0 for play. PD's incremental-with-remembered-seat approach is the natural implementation shape for this row and sidesteps the boot-latch merge — fold into the design when this row gets built, not a separate item. |
| **Rumble** _(DONE D401 — D224 / M-87, re-promoted 2026-09-27)_ | `port/src/input.c` + `port/src/libultra.c` + `port/src/optionsoverlay.c` | P | M | D401 implements the PD precedent per `docs/dev/RUMBLE-PLAN.md`: `inputRumbleSupported()`/`inputRumble()` → `SDL_GameControllerRumble()` when the pad reports haptics (SDL ≥ 2.0.18 `HasRumble`, else `SDL_JoystickIsHaptic` + known-type Windows fallback), per-pad haptics detection (`SDL_GameControllerHasRumble` / `SDL_JoystickIsHaptic` + known-type Windows fallback), a single global `Input.RumbleScale` (0..1, dflt 0.5) config + one `Rumble strength` slider row; `libultra.c` sets `CONT_CARD_ON` in `g_contStatus` for rumble-capable pads and routes `osPfsInit` → `PFS_ERR_DEVICE`, `osMotorInit/Start/Stop` → the haptics API — the two M-87 addendum blockers that kept `joyRumblePakInit` from arming. Zero `src/` edits. **Real-pad haptics confirmed working by the user (2026-09-28).** Plan item 3 landed the same day: the original four per-player rows were replaced (user decision) with one global `Rumble strength` slider in the F10 overlay + front options screen (`optionsoverlay.c`, shared `rows[]`), 0.05 detents, `kResetDefaults` 0.5; the four `Input.PlayerN.RumbleScale` ini keys were dropped (stale keys self-clean on the next save). A disabled-when-unsupported row state is deferred with the per-pad tuning row (needs a generic row-disabled mechanism). Unverified: `RumbleScale=0` silence check (trivial early-out). |
| FakeGamepads / FirstGamepadNum / UseHIDAPI | `input.c` | P | S–M | Niche (local MP on PC); defer until MP is actually played. |
| Window polish: DefaultFullscreen/Maximize, CenterWindow, AllowHiDpi, ExclusiveFullscreen | `video.c` vid* block | P | S | HiDpi + center are the useful ones on modern Windows. |
| VSync adaptive (−1..10) | `Video.VSync` range | P | S | Note for FPS plan — GE's 0/1 toggle is fine until then. |
| **CenterHUD** (0/1/2 = left/center/right) | `Game.CenterHUD` → `g_HudAlignModeL/R` | G* | — | Already scoped in `WIDESCREEN-FOV-PLAN.md` Option B (Phase 1 decision). Not a separate item. |
| Per-player FovY + FovAffectsZoom | `Game.Player%d.FovY`, `g_PlayerExtCfg` | G | M | PD does this in game code. GE's port-only RSP FOV slider (widescreen plan Phase 4) covers the global case; per-player is a stretch goal needing the exception class. |
| MemorySize (4–2048 MB emulated RAM) | `Game.MemorySize` → `g_OsMemSizeMb` | P? | S | Check how GE sizes its OS memory heap; if fixed, make it a config knob (stability lever for heavy levels). |
| MaxExplosions / GEMuzzleFlashes / DisableMpDeathMusic | PD decomp edits | G | S | Low priority; rule-#2 cost > benefit. Skip unless asked. |
| GlareBrightness / OverexposureScale / FramebufferEffects | PD post-FX | — | — | Not applicable: GE's fast3d has no framebuffer-effect path. Excluded. |
| **HUD scale → D226 (M-87, new).** Ammo counter + bottom-left pickup/status text + top-of-screen dialogue all funnel through one function, `textRenderOutlined()` (`textrelated.c:688`), from two `bondview2.c` call sites — plain RDP texture-rectangles at native glyph size, no scale factor anywhere. Menus/options/MP UI share the same function but must NOT be scaled by this (different constraint, own layout). | P | M | User-requested. No PD precedent. Scale-about-anchor (bottom stays bottom, top stays top), matching D211's HUD-static principle. F10 slider candidate alongside FovScale. |
| **F10 overlay: mouse-wheel scroll + more settings + category grouping → D237 (M-106, new).** The F10 options overlay (`port/src/optionsoverlay.c`) works but has no wheel-scroll navigation and only a minimal row set. User ask: (1) scroll with the mouse wheel; (2) expose more settings that can easily be surfaced — explicitly `Game.NoHitFlash` (D232, screen-flash setting), plus other cheap candidates from this inventory's baseline list (e.g. `Video.Anisotropy`, `Video.TextureFilter`, `Input.MouseYScale`, `Input.MouseSmoothing`, `Input.AimBand`); (3) organise rows by category — mouse settings together, video together, etc. (group headers over the existing flat registry walk). | P | M | User-requested. No PD precedent needed — pure port-layer overlay work on top of the existing `config.c` registry; wheel event already reaches the port (`inputPostWheel`, D223). Cross-ref `OPTIONS-MENU-PLAN.md` §3. |
| **Mouse sensitivity rework → D238 (M-106, new).** User wants normal PC-shooter mouse settings: (1) a *separate* RMB aim-mode sensitivity; (2) ideally ONE single master sensitivity driving both turn + aim, **linked by default**, with a future option to set them independently (today `Input.MouseAimSpeed` dflt 16 and `Input.MouseTurnSpeed` dflt 100 are two unrelated keys, `input.c:188-190`); (3) up/down should feel mostly the same as left/right — GEPD Mouse Injector cited as the good-feel reference. Implementation shape: one master `Input.MouseSensitivity` feeding both paths with a per-mode scale factor defaulting to equal, plus independent override keys that decouple only when set; audit the Y-axis gain asymmetry (`AIM_GAIN` vs `MOUSE_TURN_GAIN`, `MouseYScale`, smoothing) so vertical tracks horizontal. Lump with the overall mouse/input fixes (D194/D165/D166 lineage; the SOLITAIRE natural-pitch scheme switch noted on D166 may subsume part of this). | P | M | User-requested. No PD precedent — GE-specific feel work in `port/src/input.c` + `config.c`. |
| **Click-to-lock / always-grab parity → D239 (M-106, new).** User: click-to-lock should be the default (it already is — `mouseCaptureMode = 1`, `input.c:185`); "always grab" feels bad on the file-select menu and is not 1:1 with click-to-lock; if parity is too complicated, **drop always-grab and remove the setting**. Re-confirms the M-82 decision to rip out `MouseCaptureMode=0` (`docs/BACKLOG.md` "M-82 playtest feedback"; D192 documents mode 0's unreachable-outer-cells defect). Recommendation: take the fallback — delete mode 0 + the config key, keep click-to-lock only. | P | S | User-requested; closes D192 by deletion rather than fix. |
| **Debug unlock-all → D225 (M-87, new, dev-tooling not player QoL).** One env-gated `#ifdef PORT` short-circuit at `fileGetIsCheatUnlocked()` (`src/game/file2.c:391`) unlocks **both** all-mission-select **and** the full cheat menu in one change — the two gates already share that single function. Bonus: exposes leftover Rare dev cheats (`CHEAT_LINEMODE` wireframe, `CHEAT_BONDPHASE`, `CHEAT_DEBUG_POS/UNK5`) that are otherwise unreachable. | P | S | User-requested testing convenience ("always enable all level access + all cheats... goes a long way"). Default off, pure query-time override, zero save-data/gameplay-logic touch. |
## GE has, PD lacks (keep as-is)
`Video.FixMipTextures`, `Video.WrapFix`, `Input.AimBand`,
`Input.HipfirePitchSpeed`, `Input.MouseSmoothing`, `Debug.FrameDump`,
`Debug.InputLog`. No action.
## Suggested Phase 4 order (after audio; ordering vs widescreen/FPS TBD)
1. **Quick wins (all P, small):** SkipIntro, DisplayFPS, aniso + mipmap
filters, window HiDpi/center.
2. **Rebinding** (P, medium) — the headline Phase 4 item; PD pattern is
config-string driven, no UI required for parity.
3. **Per-pad tuning** (P, medium).
4. Niche: fake gamepads / HIDAPI (when MP gets played); MemorySize knob
(after the heap check).
Everything here is parked behind audio like the other plans; nothing in this
inventory implies an implementation decision yet.
+264
View File
@@ -0,0 +1,264 @@
# Rumble Pak → real gamepad haptics (port plan)
Status: **IMPLEMENTED — finding D401 (2026-09-27/28).** Items 1+2+3 all
landed (port layer only; zero `src/` edits), re-promoting the
DEPRIORITIZED finding **D224** (M-87) to DONE. **Real-pad haptics confirmed
working by the user (2026-09-28).** Menu shape: **one global `Rumble
strength` slider** (`Input.RumbleScale`, user decision 2026-09-28 — the
four per-player rows were dropped). Only the `RumbleScale=0` silence check
remains unverified (trivial early-out path).
Scope: wire the N64 Rumble Pak path (already present in `src/`) through to
`SDL_GameControllerRumble()` on the PC. **Port-layer only — zero `src/game` or
`src/` logic edits**, so rule 2 / ABI-exception do not apply.
Reference: the PD PC port (local checkout `../pd_port`, `port/src/input.c`
+ `port/src/libultra.c`) implements the identical feature for the same Rare
engine family. Copy its shape; the only friction is that **GE's `src/joy.c`
calls the classic libultra names** (`osMotorInit/Start/Stop`, `osPfsInit/
osPfsIsPlug`) while PD's engine renamed them (`osMotorProbe`,
`__osMotorAccess`, `osPfsInitPak`). We translate names, not logic.
---
## Ground truth (current state)
Game side — **already correct, untouched:**
- `src/joy.c` full Rumble-Pak state machine: `joyRumblePakInit` (:178),
`joyRumblePakTick` (:298), `joyRumblePakStart`/`Stop`. Call sites (verified
2026-09-27 for D401) live in `gunfire.c:3284` (start) / `:3288` (stop),
`bondview2.c:9722` (start) / `:9726` (stop, incl. 2P second pad),
`lv.c:1688` (stop at level start), plus `sched.c:272` (stop on level
teardown).
- `src/motor.c` is **EXCLUDED** from the PC build (N64 SI-DMA driver), replaced
by no-op stubs. CMakeLists.txt:525 documents this.
Port side — **all no-oped today, in `port/src/libultra.c`:**
- `osPfsInit` (:1413) → `return PFS_ERR_NOPACK;`
- `osPfsIsPlug` (:1415) → `*pattern = 0; return 0;`
- `osMotorInit` (:1417) → `return -1;`
- `osMotorStart` (:1419) → `return -1;`
- `osMotorStop` (:1420) → `return -1;`
- `g_contStatus[i].status = 0;` (:1102) — `CONT_CARD_ON` **never set**.
Gamepad side — **exists, no change needed:**
- `port/src/input.c` owns `static SDL_GameController *pads[MAX_PADS]`
(MAX_PADS = 4, aligned with `MAXCONTROLLERS`), open/close, hotplug,
`inputComputePad()`. This is the exact object `SDL_GameControllerRumble()`
needs.
## The two upstream blockers (D224 M-87 addendum — MUST land together)
`joyRumblePakInit` (src/joy.c:178) only drives the motor if BOTH pass:
1. `g_ContStatus[index].status & CONT_CARD_ON` → today always 0, so init
never even runs.
2. `osPfsInit(...)` returns `PFS_ERR_ID_FATAL` **or** `PFS_ERR_DEVICE` →
today returns `PFS_ERR_NOPACK`, so `osMotorInit` is never attempted.
If the SDL wiring is done without (1)+(2), the state machine parks at
`RUMBLEPAKINITSTATE_NOT_READY` forever and nothing rumbles despite "looking
wired." Both fixes are in the same file we already touch, so no extra cost.
---
## Work items
### 1. `port/src/input.c` — detection + API
Add per-pad fields (GE uses flat statics, not PD's `padsCfg[]` array):
```c
static int padRumbleOn[MAX_PADS]; // set where pads[i] is opened (line ~582)
static float padRumbleScale[MAX_PADS] = { 0.5f, 0.5f, 0.5f, 0.5f };
```
On pad open (in the `SDL_GameControllerOpen` path, line ~575-589), mirror PD:
```c
#if SDL_VERSION_ATLEAST(2, 0, 18)
padRumbleOn[i] = SDL_GameControllerHasRumble(pads[i]);
#else
padRumbleOn[i] = SDL_JoystickIsHaptic(SDL_GameControllerGetJoystick(pads[i]));
if (!padRumbleOn[i]) {
const SDL_GameControllerType t = SDL_GameControllerGetType(pads[i]);
padRumbleOn[i] = t && (t != SDL_CONTROLLER_TYPE_VIRTUAL); // Win no-haptic fallback
}
#endif
```
On pad close, `padRumbleOn[i] = 0;`. GE has no `inputCloseController`
(PD-only) — the close sites are `inputDestroy()` and the close loop in
`inputRescanPads()`, both of which zero it (`inputOpenPads` re-detects on
open anyway).
New public fns (declare in `port/include/input.h`, define in input.c):
```c
int inputRumbleSupported(int idx); // 0<=idx<MAX_PADS ? padRumbleOn[idx] : 0
void inputRumble(int idx, float strength, float time); // scaled SDL_GameControllerRumble
float inputRumbleGetScale(int idx);
void inputRumbleSetScale(int idx, float v);
```
`inputRumble` body (from PD, verbatim logic):
```c
if (idx < 0 || idx >= MAX_PADS || !pads[idx]) return;
if (padRumbleScale[idx] <= 0.f) return;
if (padRumbleOn[idx]) {
strength *= padRumbleScale[idx];
if (strength <= 0.f) { strength = 0; time = 0; }
else { strength *= 65535.f; time *= 1000.f; }
SDL_GameControllerRumble(pads[idx], (Uint16)strength, (Uint16)strength, (Uint32)time);
}
```
In init, add the two PS4/5 hints (PD input.c:696-697) so BT pads report
haptics:
```c
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, "1");
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, "1");
```
Register per-pad config (in the existing `configRegister*` block, ~line 2457+):
```c
char key[48];
for (int c = 0; c < MAX_PADS; ++c) {
sprintf(key, "Input.Player%d.RumbleScale", c + 1);
configRegisterFloat(key, &padRumbleScale[c], 0.f, 1.f);
}
```
Note: **`strFmt` is NOT available in `input.c`** (0 uses, no decl in
`port/include`) — use a local `sprintf` buffer as above. `configRegisterFloat`
is declared in `port/include/config.h` (already included by input.c:80); it's
used elsewhere (video.c) so the symbol is already in the link set.
### 2. `port/src/libultra.c` — rewire the shims
`contSnapshotFromKeyboard` (:1096): set the status bit for connected,
rumble-capable pads (need a per-channel capability check → call
`inputRumbleSupported(i)`; it's declared in input.h so include it):
```c
g_contStatus[i].type = connected ? CONT_TYPE_NORMAL : 0;
g_contStatus[i].status = (connected && inputRumbleSupported(i)) ? CONT_CARD_ON : 0;
```
Replace the 5 accessory stubs (:1413-1420) with real routing (PD libultra.c:235-351):
```c
s32 osPfsInit(OSMesgQueue *q, OSPfs *pfs, int channel) {
(void)q;
// "pretend the Rumble Pak is present" when the real pad can rumble:
// returning PFS_ERR_DEVICE is what joyRumblePakInit (src/joy.c:186-190)
// needs to proceed to osMotorInit.
return inputRumbleSupported(channel) ? PFS_ERR_DEVICE : PFS_ERR_NOPACK;
}
s32 osPfsIsPlug(OSMesgQueue *q, u8 *pattern) {
(void)q;
if (pattern) {
*pattern = 0;
for (int i = 0; i < MAXCONTROLLERS; ++i)
if (inputRumbleSupported(i)) *pattern |= (u8)(1 << i);
}
return 0;
}
s32 osMotorInit(OSMesgQueue *mq, OSPfs *pfs, int channel) {
if (pfs && inputRumbleSupported(channel)) {
pfs->queue = mq;
pfs->channel = channel;
pfs->activebank = 0xff; // PD's osMotorProbe stores this too
// NOTE: PFS_MOTOR_INITIALIZED is NOT defined in GE's headers, and
// joy.c never reads pfs->status — it only checks (osMotorInit()==0)
// to set RUMBLEPAKINITSTATE_READY (src/joy.c:190). Skip the status store.
return 0;
}
(void)mq; (void)channel;
return PFS_ERR_NOPACK;
}
s32 osMotorStart(OSPfs *pfs) {
if (!pfs) return PFS_ERR_NOPACK;
// N64 motor has no duration; the game's joyRumblePakTimer60 (joy.c) turns it
// off. Issue one generous window, PD-style ("hope the timer stops it"):
inputRumble(pfs->channel, 1.0f, 5.0f);
return 0;
}
s32 osMotorStop(OSPfs *pfs) {
if (!pfs) return PFS_ERR_NOPACK;
inputRumble(pfs->channel, 0.0f, 0.0f); // zero-duration / zero-strength = stop
return 0;
}
```
**JPN note (2026-09-27):** `_HW_VERSION_1` defines `MAXCONTROLLERS` as 6
while `MAX_PADS` is 4 — the `MAXCONTROLLERS`-bounded loops above are safe
because `inputRumbleSupported()` bounds-checks against `MAX_PADS` and reports
unsupported (silent) for `i >= 4`. `osPfsIsPlug` has **no GE game-side call
sites** — its rewiring is completeness/hygiene only; the load-bearing stubs
are `osPfsInit`/`osMotor*`.
Headers confirmed: `OSPfs` (`include/PR/os.h:282`) has `queue` + `channel`; the
error macros `PFS_ERR_DEVICE` (:642) and `PFS_ERR_ID_FATAL` (:641) and
`PFS_ERR_NOPACK` (:631) are all defined there. **`PFS_MOTOR_INITIALIZED` is NOT
defined** anywhere in GE's headers and `joy.c` never reads `pfs->status`, so it
is not needed. `input.h` is already included by libultra.c (line 50), so
`inputRumbleSupported` is directly callable there.
### 3. (DONE 2026-09-28) options-menu slider — single global
Landed in `port/src/optionsoverlay.c` (not PD's `optionsmenu.c` — GE's F10
overlay + front options screen share `rows[]`, walked by `frontoptions.c`
via the `optionsRow*` accessors): ONE `ROW_SLIDER` row `Rumble strength`
on `Input.RumbleScale` (0.05 detents, generic `CONFIG_OPT_FLOAT` display
`%.2f`), in the INPUT section after the controller rows, plus a
`kResetDefaults` entry (0.5, the `gRumbleScale` C initializer) so INPUT's
"Reset to defaults" covers it.
**Decision (user, 2026-09-28):** the original four-per-player-row design
(`Rumble (Player 1..4)` on `Input.Player%d.RumbleScale`) was coded and
reviewed, then replaced with the single global row **before its first
compile**: the scale applies to every rumble-capable pad, and the four
`Input.PlayerN.RumbleScale` ini keys are dropped — stale keys in an
existing ini log four harmless `unknown key` `LOG_NOTE` lines at config
load and self-clean on the next `configSave` (which rewrites the whole
file with the registered keys). Per-player rumble tuning stays a G-class
stretch goal (QOL-INVENTORY) — and a per-pad override-on-top-of-global
design would be a no-op for everyone anyway, because `configSave`
rewrites all registered keys and the "user explicitly set this?" signal
is burned as soon as any key exists at the 0.5 default. The
row-disabled-when-unsupported state is deferred with the per-pad tuning
row (the overlay has no generic row-disabled mechanism); the row is inert
(no-op) on non-rumble setups.
**Original plan (for reference):** PD wires the per-pad scale into the
options menu (`optionsmenu.c:533-540`): `inputRumbleGetScale(player)`,
`inputRumbleSetScale(player, v/10)`, disabled when
`!inputRumbleSupported(player)`. In GE this maps onto
`port/src/optionsoverlay.c` / the per-pad tuning row (QOL-INVENTORY.md).
Config file value alone is enough for the feature; the UI is the polish.
---
## Verification ritual (AGENTS.md)
- [x] `./build-pc.sh ntsc-final` compiles clean (2026-09-27). New
`inputRumble*` defined once in input.c, declared in input.h; no new
undefined/duplicate symbols (full `/linkcheck` sweep pending at D401
close).
- [x] `CONT_CARD_ON` + `PFS_ERR_DEVICE` land (the two-blocker addendum);
the state machine's READY path is provable by code read — the runtime
proof is folded into the real-pad test below.
- [x] **By-hand, real pad:** user confirmed the rumble works on a real
controller (2026-09-28) — fire a weapon (`gunfire.c:3284` recoil),
explosion rumble, 2P second pad (`bondview2.c:9722/9726`). Non-rumble
pad and mouse/keyboard-only stay silent by construction (`pads[idx]`
NULL / `padRumbleOn` 0 ⇒ `inputRumble` no-op).
- [ ] `Input.RumbleScale=0` fully silences (unverified — the trivial
`gRumbleScale <= 0` early-out in `inputRumble`); max value loud
(confirmed implicitly by the working playtest at scale 0.5).
## Finding log
Done (2026-09-27): **D401** added to `docs/dev/findings.md` (§F index +
full entry) documenting the D224 implementation + the two-blocker
resolution; the `QOL-INVENTORY.md` M-87/D224 row re-promoted
DEPRIORITIZED → DONE (D401); `docs/dev/findings-index.csv` regenerated.
---
## Effort
- Items 1+2: **~1–2 h** coding (mostly a rename + copy from PD) + build/linkcheck.
- Hands-on pad testing: **~30 min–1 h** (the only genuinely open-ended part —
feels-right tuning).
- Item 3 (UI slider): **~1 h**, landed 2026-09-28 as the single global
`Rumble strength` row.
**Total for working core: half a day max, <2 h of pure code.**
+192
View File
@@ -0,0 +1,192 @@
# Unlocked FPS Plan
Status: **proposal, parked** — documented while audio work (project Phase 3) is
active. Do not start Phase 0 until audio is done; first action when resumed is
the Phase 0 measurement pass.
**M-119 note:** this plan's premise below ("default preset = ~30fps") is now
stale — **D248** (M-118) found and fixed a real port bug (a pointer-width
scheduler ABI bug) that had been silently halving the game to 30fps since the
port's inception; the corrected default is a genuine 60fps. Re-measure Phase 0
against the corrected baseline before resuming this plan. Also see **D250**
(M-119, open): the exposed real 60fps cost now drops to 20-40fps intermittently
on some levels — a performance pass may be a prerequisite for this plan, not
just a baseline re-measurement.
Related: `docs/dev/findings.md` §D117 (GE_DETERM), §D155 (catch-up clamp),
`port/src/libultra.c` (pacemaker), `src/game/frametiming.c`, `src/sched.c`.
## Decisions (agreed)
1. **Default preset = the original pace** — whatever the game currently does
(measured ~30 fps; Phase 0 pins the exact rate). All testing, level
sweeps, determinism runs (`GE_DETERM`), and playtest baselines stay on the
default preset. Its code path must remain byte-for-byte what it is today.
2. **Unlocked FPS (60 / 120 / uncapped) is EXPERIMENTAL.** It ships behind a
clearly-labelled option and may be less polished than the default.
3. **Hard requirement:** raising the presentation rate must never change game
speed. Sim rate is invariant across presets. This is "the main speed-up
issue" and it gates everything else.
4. **Follow the PD PC port where feasible** (`pd_port` checkout): wall-clock
accumulator pacing with remainder carry, vsync + framerate-limit options,
safety cap when vsync is off.
## Non-goals (for now)
- Motion interpolation between sim ticks (the only thing that would make 120
fps *look* like 120 fps). Requires per-entity prev/cur state in the
DL-building game code — rule #2 territory, parked as a separate proposal.
- Changing the sim rate itself (running logic at 120 Hz with halved constants
is explicitly out — game logic).
## Background: why raising FPS today speeds up the game
One clock drives everything:
- `portTickThread` (`port/src/libultra.c`) posts a VI-retrace message on a
free-running grid (60 Hz NTSC / 50 Hz PAL, `g_tickIntervalUs`).
- `src/sched.c` `__scMain` blocks on that retrace; `__scHandleRetrace`
notifies clients → **exactly one sim tick + one render per retrace**.
- `osGetCount()` is wall-clock scaled to 46.5 MHz. `waitForNextFrame()`
(`src/game/frametiming.c`) converts elapsed time to `deltaFrames`, which
becomes `g_ClockTimer` (`src/game/lv.c:1008`) driving the per-tick sim loops.
The quantization is the trap: `nextFrameTime = (elapsed + Q/2) / Q` (Q = one
frame quantum). The `+Q/2` bias **rounds every interval up to at least 1
tick**, and `waitForNextFrame` spins until ≥1 tick is due. Consequence:
**sim speed tracks the retrace rate almost 1:1** — 30 Hz retrace → 30 ticks/s,
120 Hz retrace → ~120 ticks/s (each 8.3 ms interval rounds up to a full
tick). That is exactly the observed "anything faster speeds up the game".
Two useful existing properties:
- GE's game code already handles `g_ClockTimer == 0` and multi-tick deltas
everywhere (`for (i = 0; i < g_ClockTimer; i++)`, explicit `== 0` guards in
bondhead/bondview2/propobj/explosion). The N64 used this for dropped frames.
- D155 already clamps catch-up to 6 ticks after a real-time hitch.
## What the PD port does (reference)
- `src/game/timing.c` `frametimeCalculate()`: wall-clock accumulator with
remainder carry (`lostframetime60t`) → `diffframe60` / `diffframe240` ticks
due **per rendered frame**; render loop runs at display rate.
- Vsync + framerate limit options (`VIDEO_MAX_FPS` 200/240); vsync-off with
no cap forces the safety cap.
- Feasibility mapping to GE:
- *Pacing math (accumulator + carry)* → adoptable **port-only**. ✓
- *Render frames carrying zero sim ticks* → blocked in GE by
`waitForNextFrame`'s ≥1-tick enforcement (`frameDelay`, only settable
under `LEFTOVERDEBUG`). A crafted `osGetCount()` cannot defeat it (the
spin + `+Q/2` bias always yield ≥1). Needs a game-code change → **parked**
(see "Parked" below), and note it buys little without interpolation.
## Phases
### Phase 0 — Baseline & ground truth (port-only)
Goal: know exactly what the default preset does before touching anything.
- [ ] Measure actual sim rate and present rate of the current default build
(speedgraph + a `video.c` counter; add a periodic log line
`sim=..Hz render=..Hz`). Confirm NTSC vs PAL build.
- [ ] Find why the default is ~30 rather than the console's 60 Hz. Candidates:
- tick-thread 4 ms chunked-sleep drift under host load;
- `__scTaskReady` double-buffer check (`sched.c`) stalling a task every
other retrace;
- `__scHandleRetrace`'s client flag read `*((s32*)client + 2)` is
**out-of-bounds of `OSScClient`** (struct is only `next`+`msgQ`; the
decomp even says "im wrong size"). For `gfxClient[0]` it happens to
read `gfxClient[1].next` (zero) — but this is layout luck, worth a
documented finding either way.
- [ ] Record a Dxxx finding with the measured rates and root cause.
- [ ] **Decision gate:** if 30 fps turns out to be a port artifact rather than
original behavior, document it and decide with data whether the default
preset keeps the current pace (test continuity) or is fixed. Default
assumption: keep current behavior as the "original" preset either way;
a fix becomes its own item.
### Phase 1 — PD-style pacing hardening (port-only)
Makes the default preset stable and drift-free; no game-code changes.
- [ ] Replace the free-running tick grid in `portTickThread` with a
wall-clock accumulator + remainder carry (PD `frametimeCalculate` math):
post ticks when real time says they're due, carry the remainder, bounded
catch-up after hitches (D155 philosophy). No drift under load, no burst
of back-to-back ticks.
- [ ] `video.c`: proper frame limiter for fractional caps (spin+sleep hybrid),
triple-buffered swap chain, and the PD safety rule (vsync off + no cap →
force a max cap so "unlocked" can't spin).
- [ ] Config: keep `Video.VSync` / `Video.FpsCap`; add whatever Phase 2 needs.
### Phase 2 — Experimental presentation rates (port-only)
The user-visible "60 / 120 / unlocked" feature. Game speed is invariant **by
construction**: sim + render stay on the original tick clock; only *present*
runs faster by showing each rendered frame more than once.
- [ ] Frame duplication at the present stage (`video.c` / fast3d SDL swap
path, near `gfx_pre_swap_hook`): after a fresh frame is presented,
re-present the last finished frame on extra vsyncs until the next sim
tick's frame is ready. Target rates: 60, 120, uncapped (safety-capped à
la PD `VIDEO_MAX_FPS`).
- [ ] Input at present rate: sample SDL input continuously in
`port/src/input.c`; each sim tick consumes the latest state (compare
against PD `joy.c`). This is the real tangible win of the feature: up
to ~half a frame of latency removed even without interpolation.
- [ ] Options menu entries, clearly labelled **experimental**
(`port/src/optionsoverlay.c`), default preset untouched.
- [ ] Audio check: `src/audi.c` uses real-time `osGetTime()` and its own
thread; libaudio buffer sends ride the sim/retrace clock. Verify nothing
divides by an assumed frame rate; expect no changes.
### Phase 3 — Verification & sign-off
- [ ] `GE_DETERM` regression: default preset simulation path byte-identical to
pre-change (existing determinism diffs keep passing).
- [ ] Preset-invariance playtest: a timed in-game event (e.g. a fixed-length
animation or timer) takes the same real time on default / 60 / 120 /
unlocked. This is the acceptance test for decision #3.
- [ ] Hitch recovery: force a multi-second stall (debugger pause / asset load),
confirm D155 clamp holds and no speed spiral on any preset.
- [ ] `./build-pc.sh` + `/linkcheck` ritual; new findings in §F/§H with index
labels.
## Timing-consumer audit (each gets a line in the finding)
| Consumer | Clock class | Action |
|---|---|---|
| `src/game/frametiming.c` (`waitForNextFrame`, `updateFrameCounters`) | sim-time | unchanged; stays tick-locked |
| `src/boss.c:403,517` (main-loop gate, `MAIN_LOOP_TICK_INTERVAL`) | sim-time | unchanged; re-verify vs new pacemaker |
| `src/usb.c:387,405` (LibDragon flash timeouts) | real-time | fine as-is; document |
| `src/speed_graph.c` | measurement | fine; source of the rate log line |
| `libultrare/audio/env.c`, `reverb.c` `lastCnt` | perf counters only | fine; document |
| `src/audi.c` `g_DeltaTime` etc. (`osGetTime`) | real-time | fine; verify in Phase 2 |
## Parked (needs explicit sign-off, separate proposals)
1. **True decoupled sim/render (PD's zero-tick render frames).** Minimal
change class: relax `waitForNextFrame`'s ≥1-tick enforcement so rendered
frames can carry `deltaFrames = 0` (GE already guards `g_ClockTimer == 0`
everywhere). This is a *behavior-affecting* game-code edit gated off by
default — outside the ABI-only D3x exception, needs its own rules decision.
Perceptual benefit over Phase 2 duplication is small **without**
interpolation, so do this only as a prerequisite for #2.
2. **Motion interpolation** (prev/cur entity state, lerp at render time).
The actual "120 fps looks like 120 fps" feature. Large, invasive, in
DL-building game code. Separate proposal; not implied by this plan.
## Open questions
- Phase 0 outcome: is the ~30 fps default a port artifact? (fix vs keep — see
decision gate)
- PAL builds: default stays 50 Hz pace; experimental present rates identical
option set. Confirm no PAL-specific pacing assumptions in Phase 1 math.
- Naming/caps for "unlocked": adopt PD's safety-cap value (200/240) or expose
the cap as a slider?
**M-87 PD-legacy-survey note (candidate 8):** if this plan stalls and GE stays
fixed-tick, PD's `Game.TickRateDivisor` (0–10, slow-motion/perf lever) +
`Game.ExtraSleep` (`port/src/main.c:39,167-168`, applied `src/game/timing.c:49`)
is a cheap borrowable fallback lever — not otherwise relevant since this plan's
whole direction is decoupling sim from frame rate, the opposite of PD's
fixed-tick-with-a-divisor approach.
+515
View File
@@ -0,0 +1,515 @@
# v0.4.0 Plan — local working document (NOT pushed; update as items move)
**2026-09-27 user release decision:** defer M7 / AllUnlocked save-safety work
for v0.4.0 (D387); do not ask for a live-save test or treat the feature as
safe. Before shipping, hide/disable the unsafe toggle or explicitly agree
on a release limitation — default OFF alone does not prevent a user from
turning it ON. The user will do a full campaign playtest *instead of* the
21-level automated sweep; record the campaign result as the release gate,
not as a sweep PASS. **Playtest interruption D393/D394:** physical-pad
A use / X reload / Y weapon cycle was user-tested. After the user supplied
the StrategyWiki Xbox 1.1 Jinx mapping, D394 moved crouch to either stick
click, LB to aim, B to a port-side owned-gadget cycle, and retired LB/RB
weapon cycling. Build linked; user physically confirmed the new button roles work, including
stick-click crouch, LB aim, RB no weapon cycling and B gadget cycling (the
N64 port has no separate gadget HUD icon). **D395:** both controller-accessible
binding menus now refuse pad-initiated keyboard/mouse capture and allow B to
cancel an active modal, pending pad-only retest; this is not gamepad rebinding.
**D396:** F10 B now backs/closes, A selects/toggles without changing sliders;
left/right adjusts sliders. NTSC linked; physical pad retest owed. Inspect B's item category/order during campaign play if multiple mission
items are owned: the current ID-range filter includes some mission objects.
RB's remastered-graphics toggle cannot be replicated with N64 assets, and
Back remains the PC options shortcut.
Created 2026-09-21 from a fresh triage of `findings-index.csv`,
`GRAPHICS-BACKLOG.md`, `docs/BACKLOG.md`, `ROADMAP-1.0.md` (M-139 phase order),
`QOL-INVENTORY.md`, and the 2026-09-20 HANDOFF.
**Release goal (user-set):** fix everything that still makes the port not 1:1
with the N64 original; *then* improvements outside the OG game, plus stability.
Sequencing follows M-139's R → Q → F order, split into four phases below.
## Baseline
- **v0.3.0 is shipped** (tag on origin at `4827bc86`); `main` is 7 docs-only
commits ahead. Branch `release/v0.4.0` off `main` when Phase 1 starts.
- All 20 solo missions (plus the ending-credits sequence) load/render/crash-free; campaign completable end-to-end
(Agent, playtested). Steady 60 fps. Audio done. Saves done.
- Known-issue list = README "Known issues" section (D303, D245, D308/
D306, D307, D311, D318 disclosure, widescreen-stretch, D249,
D240/D241, D257/D259/D281 AllUnlocked caveat). ~~D252~~ retired (M-188),
~~D75~~ fixed (M-197, user-confirmed), ~~D236~~ fixed (M-198, pass 26 —
awaiting play-test sign-off).
## Triage of every open item (as of this triage)
### Bucket A — Fidelity (port diverges from N64 behavior/appearance/sound)
| ID | Item | State / notes |
|----|------|---------------|
| D320 | 17 aim-hold AI lists in 8 levels carry the as-authored D318 softlock race; Facility `ai_19`, Control `ai_9`, Depot `ai_12` high/med-high | Static sweep done; **live probe-verification owed** (GE_D318T-style timeline on the 3 lists). If one freezes: generalize `d318watchdog.c` (parameterize list/offset/chr/entityid), not per-list copies. N64 softlocks permanently — watchdog stays a port-side recovery, disclosed like D318. |
| D252 | Rainbow particles still intermittent on both platforms after the vertex-buffer fix | Second cause unrooted; prime suspect `import_texture_rgba16` fire-tile width/height from tile line size (32×7 vs 16×14). README known issue. |
| D245 | Water seam between two patterns, moves with view (REOPENED) | M-142 probe: tc-shift k flips during ordinary rotation; opt-in `GE_D245_FIXEDSHIFT` candidate build-verified, needs A/B vs N64 reference before making it default. |
| D303 | Wrong extra long muzzle flash (M16 + others) | **Investigated — pipeline + data ruled out.** Flash path (`gunUpdateAndFire` → `rwmtx[1]/[2]`, `dorottex` DLPRIMARY arms) is byte-identical game code; M16's `MuzzleFlashExtension`=6.0 is the legitimate max stat; Gm16Z flash-node data byte-faithful through d43. Headless runs can't fire weapons (guards don't shoot an idle player), so a TEMP `GE_D303=1` probe in `gfx_pc.cpp` logs every very-tall-thin emitted triangle with full RDP state — **blocked on a live firing capture** (playtest brief item 17). |
| D306/D308 | Z-fighting: Dam intro/truck wheels + general/broader instances | **Investigated — real fighting confirmed, port-vs-faithful open.** Depth precision is equivalent to N64 (24-bit both sides; GE's RSP writes full 16.16 post-transform coords, not s16), so level-authored coincident geometry is the prime suspect and likely fights on N64 too. TEMP `GE_ZF=1` flicker probe in `gfx_pc.cpp` (recurrence-filtered; `GE_ZFBOX` region filter) shows sustained oscillation (counts ~1000) in the Dam-intro truck region. Closing it as faithful needs an N64 reference of the same spot; a PC-only mitigation (float depth) would diverge from N64 and needs a rule-2-style decision. |
| D307 | In-level security camera props face backwards on some levels (Bunker) | **Investigated 2026-09-21 — no port-side bug found.** Full orientation path traced (`setupCctv` → per-frame `RotY(unkC8)×unk84`); every input verified N64-faithful against the ROM (PcctvZ switch data, boundpads incl. the legitimate load-time room-scale ×1.8542, record config words); all math helpers are byte-identical `src/game` code. Remaining suspect if it genuinely differs from N64: fast3d handling of the non-orthonormal lens matrix. **Blocked on user repro detail** (which camera/level + screenshot + ideally N64 compare). `GE_D307=1` diagnostic kept in-tree. |
| D236 | Surface 1 billboard trees render as a solid wall of texture | **FIXED (M-198, pass 26, `425237f9`):** root cause was a texture-BINDING bug — `TEXTURETYPE_DETAIL` cards drawn from the detail tile (opaque IA8 noise) instead of the base image; `gfx_lod_tile_offset` now samples tile 1 when the pair differs (`Video.DetailBaseTile`, default on). Live-verified Surface 1/2 + Depot; other levels provably untouched. Awaiting user play-test sign-off; probes stripped (`0503d335`). |
| D235 | Wrong textures after death/restart (level reload), multiple levels | **Headless verification 2026-09-23 (`2a6bc2bb`)**: fix clean on reload; bug NOT reproduced even with the fix off. Needs the real die → mission select → restart A/B (`GE_D235_OFF=1`). |
| D288 | Silo intro: huge flat untextured triangle sweeps the frame (frames ~562–741 of bare `-level_20`) | **Does NOT reproduce on the current build (2026-09-23, `30cf76b8`)**; D271 A/B ruled out as the fixer. Close if the Silo playtest does not see it. |
| D294 | Statue: CCCP monument ground renders solid black, self-recovers in 10–30 s | **FIXED pending user playtest** (2026-09-22). Note: its "3× pool corrupts anim" symptom was D336, so the 2× cap can be revisited. |
| D291 | Dam: texture glitches when the truck drives past | Not investigated. |
| D249 | Distant geometry drops out on Streets/Egyptian at default FOV | M-142 ruled out far-clip + LOD-multiplier; narrowed to portal/room-visibility BFS. **First check overlap with D271's portal-culling fix** (branch state, see Phase 0). Needs live screenshot+location. |
| D195 | Control: transparency/alpha-blended surfaces render wrong (glass partitions, holographic displays) | **FIXED** (index: gfx_sp_vertex fix) — plan row was stale (2026-09-23 reconcile). |
| D197 | Character face textures wrap around the head instead of clamping (Silo, intermittent) | **CLOSED** (user playtest v0.2.0-pre) — plan row was stale (2026-09-23 reconcile). |
| D182(2)/D176(b) | File-select background degrades after round-trip (comb/interlacing); Surface tree-line static — shared "texture pitch/shear" defect | M-36 Family A; `import_texture_*` line-size assumption. Distinct from the disproven D183 hypothesis. |
| D176(a) | Sky: RDPHALF tri-raster decode; horizon-band coverage (Defect 2) still open | Path B draft in `scratchpad/sky.patch`, phase-fold fix applied, **never visually verified** — one `-level_22` screenshot owed. Branch `land/d176a-sky-perspective-scroll` exists (forgejo remote) — audit + port to main. |
| D75 | Front-end logos: Nintendo logo degenerate (tiny orbiting fragment); Rareware filtering wrong | **FIXED (M-197, user-confirmed live 2026-09-21)**: D50 sidecar converter dropped embedded texture blobs. Remaining: pal/jpn sidecar regen on ROM-holding machines. |
| D290 | Watch in-game "controller" section shows no control-pad graphic | **FIXED 2026-09-23 (`a5804d10`)**: D264-pattern template read in `watchRenderController` + 3 pointer-width/raw-Gfx sites. Sibling sweep also found + fixed **D331** (shell casings never rendered, `94dff811`). |
| D174/D172 | Blood: decals never interactively verified (D120 needs sidecar regen); spray color fixed but owed an eyeball | BUNKER1 firefight with regenerated sidecars (`d43 && d69 && d88 --regen`). |
| D240/D241 | Gunshot SFX cadence drifts; PP7/AK47 goes silent under heavy fire near looping sounds | D241 high-confidence = D207 reproduced (check D207 entry status first — index row is stale/ambiguous). Fold into the D207 voice-cap family. |
| D322 | Long-session audio degradation (issue #87, v0.3.0 full campaign): from Silo onward audio worsens; by Caverns/Cradle OST inaudible + SFX "come and go"; restart cures → session-accumulating state | OPEN — static triage done (teardown audit clean, mixer stateless, D202 leak class covered only for ownerless `count==-1` loops and only validated single-level; D305 already records a pool/list desync). Ranked: voice-pool exhaustion/counter drift > queue starvation > evtq saturation. `GE_D322` pool-telemetry probe shipped 2026-09-21; needs a campaign capture with `GE_D322=1 GE_D204=1`. |
| D311 (residual) | Upstream anim-poison source still open (freeze class guarded) | **Likely root-caused by D336** (head-bob alias read); headless Caverns clean at 2× pool. Close after one live Caverns combat session. |
| D258 | PAL/JP asset conversion broken at the source-data level | Repair path verified, not applied. Region fidelity — NTSC-only users unaffected, but it blocks "faithful in every region" and any JP/PAL ROM use. |
| D160 (residual) | Cutscene reliability PARTIAL (D243 family mostly fixed via M-189/M-190) | Residual = whatever the live GE_D160/GE_D243 campaign pass shows. |
### Bucket B — Stability (crashes/freezes/hangs; not N64-parity per se)
| ID | Item | State / notes |
|----|------|---------------|
| D289 | Dam computer-room crash (Deck, one occurrence, never repro'd) | Needs a human re-test on a current build (D255+). If it never recurs → close as untracked. |
| D287 | Windows: real, non-self-recovering freeze after ~15–20 min idle + click-back | One confirmed occurrence; `gfxFrameMsgQ` full is a lead. Consider crash-dump wiring on Windows for the next hit. |
| D286 | Deck: ~8 s self-recovering frame freeze on Caverns | One confirmed occurrence, no repro trigger. |
| D175 | Brief stutter/hang (door-open on Runway/Surface) | Matches known transient-hang classes; reproduce under `tools_pc/debug.ps1` idle-box before believing it's new. |
| D285 (residual) | Audio-thread race fix was PARTIAL | Re-check the thread-lock state vs D305's NULL guard coverage. |
| D281 | AllUnlocked breaks audio + RMB aim when enabled with zero saves written | User-facing mitigation shipped; proper fix = make the feature robust or gate it harder (see Phase 3 decision). |
| — | getenv caching in hot paths (**DONE 2026-09-23, `d9d7cd35`**) (post-release cleanup, HANDOFF M-192) | D302 fixed one instance; sweep for stragglers. Cheap perf/stability hygiene. |
| D117 | Frame nondeterminism (GE_DETERM attempted, not achieved) | Parked as a research item; do not block the release on it. |
### Bucket C — Improvements / QoL (outside OG game; after fidelity)
| ID | Item | Notes |
|----|------|-------|
| Widescreen | Currently **stretched 4:3**, not native — README discloses this | **DONE 2026-09-23 (D334 world `d5d9a36a`, D335 HUD/front end `f62bceaf`)**: native at any aspect, default on, F10 toggle; Rule-2 approved. Owed: user HUD playtest at 16:9/21:9; D335a cosmetic. |
| D237 | F10 overlay: mouse-wheel scroll, more settings, category grouping | **DONE 2026-09-23 (`91bd0f84`)**: F10 category headers + 5 more rows (wheel scroll already existed). |
| D314/D315 | F10 flicker (user-deprioritized) + main-vs-in-game menu scaling | D315 deferred from v0.3.0; bigger design question — lands naturally with the overlay work. |
| D238 | Mouse sensitivity rework (master sens, RMB aim sens, Y-gain parity) | Partly superseded: aim modes folded (D337, N64 default via PD integration; Centred (PC) opt-in), Aim range PC/N64 (D338). Remaining: sensitivity feel tuning. |
| D239 | Click-to-lock parity: delete always-grab mode 0 + config key | Small; closes D192 by deletion. |
| GEPD controls (M3) | **IMPLEMENTED, targeted playtest owed (D378/D380/D383–D386).** One effective keyboard/mouse layout, no `Input.Layout` selector: Ctrl crouches without aim; E interacts without reloading; R reloads without interacting. Input → Bindings → Movement/Actions edits two Primary/Secondary keyboard-or-Mouse 1–5 slots in both UIs (D388). Mouse 3 (right) is Aim default; wheel still cycles weapons. Menu arrows/Enter/Escape and mouse clicks remain fixed. See `MODERN-OPTIONS-PLAN.md` M3; earlier preset/B-button text there is historical. |
| Modern controller presets (D394) | **1.1 Jinx button roles physically confirmed; precise gadget category still open**: A action, X reload, B cycle owned gadgets, Y cycle weapons, L3/R3 crouch, RT fire, LT/LB aim, D-pad movement, Start pause. RB updated-graphics and Back scores are XBLA remaster-specific: no HD assets/score view in the N64 port; reserve RB, keep Back→PC options. **Planned** named selector: 1.1 Jinx (L move+strafe, R look+turn), 1.2 Christmas (L move+turn, R look+strafe), 1.3 Frost (L look+strafe, R move+turn), 1.4 Elektra (L look+turn, R move+strafe). Face/trigger roles remain fixed across schemes. Add an `Input.ControllerPreset` enum with default 1.1, F10/front-options selector, preserve per-pad inversion/deadzone, separate keyboard `NaturalPitch` from the pad selector, adapt both stick axes and D-pad per scheme, and verify in watch/menu/tank and physical controller before labeling each selectable. Do not treat these four presets as already implemented. |
| Modern options wave (parity set, 2026-09-27) | **In progress, not a single opt-in Modern page (D388).** M1: Input → Bindings and Gameplay → HUD, shared by F10/front; M2: Original red crosshair plus White/custom RGB, style, size and visibility implemented (D379/D381/D382); M3: single effective layout and rebinding (above). **M5: redundant Low-end toggle removed (D388); Frame rate cap and Anti-aliasing remain independently adjustable, stored values preserved on migration.** M4 GE+ knockback/hitstun/damage-sound/death-cam toggles parked for v0.5.0, not v0.4.0 blockers. **M7 BLOCKED (D387):** AllUnlocked ON can persist synthetic cheats/times after any profile-settings save; OFF does not remove them. Do not run on live saves; a safe design and multi-profile fixture tests are owed. PNG screenshot upgrade dropped from this wave; dev PPM dump remains. The M1–M7 detailed notes retain their historical design record in `MODERN-OPTIONS-PLAN.md`. |
| D226 | Player-adjustable HUD scale (ammo counter, pickup text, dialogue) | **DONE 2026-09-24 (`30720354`)**: Game.HudScale 75–150% (user-verified), fast3d rect scaling about anchors, dialogue/bottom width-capped. |
| Controller rebinding UI (D395) | **Not implemented.** The two Input → Bindings → Movement/Actions pages edit keyboard/mouse slots only. Pad-only users could previously enter a capture modal and get stuck; pad-triggered capture is now disabled in both UIs and B cancels a capture started by keyboard/mouse (NTSC build linked; physical pad-only retest owed). Controller presets (D394) are separate from arbitrary pad rebinding; design an explicit controller-actions page, persistence/migration and capture/cancel semantics before enabling pad capture. | Post-v0.4 scope decision; no promise that keyboard binding slots can hold pad buttons. |
| Rumble (D224) | Wire `osMotor*` → `SDL_GameControllerRumble` (PD precedent exists) | Deprioritized M-87; pick up if gamepad support is in scope for v0.4.0 — user call. |
| Per-pad tuning / hot-plug | PD incremental hot-plug + remembered seats; deadzone/scale/rumble per pad | Medium; real-controller users. |
| Window polish | HiDpi, center window, maximize/default-fullscreen | Small P-class wins. |
| Unlocked FPS | Plan parked; premise changed (D248/D250 fixed — default is now a true 60) | Phase 0 re-measurement before any work; motion interpolation stays out. |
### Bucket D — Infra / housekeeping
- **D244** self-hosted CI runner link failure — fix before the release tag so
CI can build win+linux bundles (or confirm GitHub runners suffice).
- Post-release v0.3.0 cleanup (HANDOFF): remove GE_OBJT probes
(`chraction.c` chrSet/UnsetStageFlags, `propobj.c` GAS line); M-183/M-185
clamp removal pending full-cutscene re-verification; D309 probe decision
(keep for v0.4.0 debugging or remove).
- Working-tree archaeology: ~~uncommitted D236 second-pass state~~ **RESOLVED
before this plan was written — `532092c7`/`6807555c` (Sep 15) already landed
the D266 IA4/texedge decode + D271 portal-culling fixes on main and dropped
the D267–D280 probes.** Consequence: Phase 1 item 9 (D249) can proceed with
the D271 fix in-tree; the D236 tree-wall work continues from D276's oml/fmt=3
lead on top of the landed decode fixes.
## Session additions (2026-09-23/24, local)
- **D336** Frigate "everything breaks but the gun" (uninitialised head-bob alias fields, D100 follow-up): FIXED, user-confirmed.
- **D337/D338** aim modes folded + Aim range option: user-approved ("Seems good").
- **D339** #92 low-end perf: `GE_PERFSTAT` probe; CPU budget measured (~2 ms/frame), GPU the likely bottleneck; needs target hardware.
- **#105** (Enhanced/Remaster water reflection) refers to a `feature/graphics-final-stack` branch that isn't on origin or local; not v0.4.0 scope. Its "D329" label collides with ours, so watch for that if that work merges.
## Phases
### Phase 0 — Hygiene (½–1 session; do before any fidelity work)
1. Branch `release/v0.4.0` off `main`.
2. ~~Resolve the uncommitted D236/D271 tree state~~ already landed (`532092c7`).
3. Remove GE_OBJT probes + playtest-folder `.bat`; decide D309 probe fate;
getenv-cache sweep.
4. Fix/confirm CI runner (D244) so release tooling works end-to-end.
5. Carry-over verifications from the v0.3.0 handoff that were owed:
Runway AllUnlocked toggle re-test, menu-click spot check (D316/D317 both
closed — just confirm no regression on the new tree).
### Release gate (independent of the phases below)
**D322 (long-session audio degradation, issue #87)** is a campaign-breaking
defect in shipped v0.3.0 and does not wait for the phase ladder: the
`GE_D322` probe is already built and smoke-tested — request the campaign
capture (`GE_D322=1 GE_D204=1`) from the reporter now, and run its decision
rules as soon as the log lands. If the capture implicates a port-side leak,
the fix slots into Phase 2; if it's a rule-2 behavioral difference, it gets
its own sign-off item.
### Phase 1 — Fidelity wave (the release's core)
Ordered by (crash risk → visibility → effort); each item ends with its own
verification (headless where possible, human eyeball/ear as merge gate):
1. **D320 live verification + watchdog generalization** — the only open
item that can hard-lock progression on 8 levels; probes already exist.
2. **D289 re-test** (crash class; one human pass, then close or chase).
3. **Batch "one-off geometry/texture anomalies": D288 + D294 + D291** —
all are single-location, bounded, visually documented; likely share a
fast3d import/culling root cause. One investigation thread.
4. ~~**D252 rainbow particles second cause**~~ — **retired (M-188):** the
user's symptom closed as a duplicate of the D219 fix (live retest clean);
decode + dimensions proven correct (M-141/M-156). Re-open only on a
fireball-rainbow re-report.
5. **D245 water seam** — candidate fix exists; A/B against N64 reference,
then flip default (A1-class fidelity call).
6. **D303 + D306/D308 z-fighting family** — small visual parity items; batch
per commit cadence. (~~D307 camera flip~~ investigated 2026-09-21: no
port-side bug found, all inputs verified N64-faithful — blocked on user
repro detail; see Bucket A row.) D303 + D306/D308 both investigated
2026-07-19: no port-side bug found in either (pipeline/data/precision all
verified N64-faithful); capture probes `GE_D303` and `GE_ZF` are in-tree —
both now blocked on user-side captures (firing repro / N64 reference).
Nothing further to do agent-side until those land.
7. ~~**D236 tree wall**~~ — **done (M-198, pass 26):** `TEXTURETYPE_DETAIL`
tile-binding fix in fast3d (`Video.DetailBaseTile`); live-verified,
probes stripped. Only the user play-test sign-off remains.
8. ~~**D235 texture reload leak**~~ — **done (M-193):** port-side
`TextureCacheMap` now cleared on every stage transition via
`videoResetTextureCache()` in boss.c; 21/21 sweep pixel-identical; live
death/restart confirm is a playtest item.
9. **D249 culling** — *after* the D271 overlap check; live screenshot+location
first, BFS instrumentation second.
10. **Audio parity: D322 long-session degradation first** (issue #87 — the
only audio report that breaks a full campaign; `GE_D322` probe already
shipped, get the campaign capture, then apply the decision rules in the
finding), **then D240/D241** (fold into D207 family) + verify D230's bass
fix by ear on the elevator track.
11. **Front-end fidelity batch: D290 watch pad graphic, D182(2)/
D176(b) pitch-shear defect, D176(a) sky Defect 2** (verify the patch
visually first — it's been unverified for weeks). (~~D75 logos~~ fixed
M-197; ~~D195 Control alpha~~ FIXED per findings; ~~D197 face wrap~~
CLOSED at v0.2.0-pre playtest — both were stale in this list.)
12. **D174/D172 blood verification** (regen sidecars, BUNKER1 firefight).
13. **D258 PAL/JP asset conversion repair** — apply the verified repair path;
re-run the region sweep. Decide scope: ship JP/PAL-accurate or document
NTSC-only for v0.4.0 (user call).
14. **D311 upstream anim-poison source** — close out the last open thread of
the freeze class so the known-issue bullet can be dropped.
Exit criteria: every README "Known issues" bullet either fixed or explicitly
re-disclosed with a v0.5.0 pointer; no new fidelity regressions (21-level
sweep after each landing).
### Phase 2 — Stability wave
1. **D287/D286 freeze investigation** — wire Windows crash dumps (or an
in-process fault log) so the next occurrence is diagnosable; pattern-match
against `gfxFrameMsgQ` lead and the D309 teardown-race class.
2. **D175 stutter** — idle-box repro under `tools_pc/debug.ps1`; likely a
known transient-hang class, confirm and close or chase.
3. **D285 residual** — re-audit audio thread-locking vs D305's guard.
4. **D281 AllUnlocked no-save path** — make it robust (root-cause the
sndHandleEvent/aim interaction) or gate the toggle until a save exists.
User call on which.
5. Long-idle soak test (both platforms) as the stability acceptance bar.
### Phase 3 — Improvements / QoL (only after Phases 1–2 are green)
Order by user-visible value (2026-09-27 refresh — several rows since DONE, marked):
1. ~~**Native widescreen**~~ — **DONE 2026-09-23** (D334/D335; playtest sign-off owed).
2. ~~**F10 overlay wave: D237 + D314/D315**~~ — D237 **DONE 2026-09-23**; D314/D315 still open (user-deprioritized; lands with further overlay work).
3. **Input feel: D238 sensitivity rework, D239 delete always-grab, window
HiDpi/center; rumble + per-pad tuning if gamepad is in scope (user call).**
4. ~~**D226 HUD scale**~~ — **DONE 2026-09-24**.
5. Unlocked FPS: re-measure Phase 0 against the post-D248/D250 baseline and
decide whether v0.4.0 or v0.5.0.
6. **Modern options wave — substantially implemented, NOT release-complete**
(D377–D389). Five functional roots; Input → Bindings → Movement/Actions
and Gameplay → HUD work in both menus. One effective PC layout with
dedicated E/R/Ctrl; two bind slots including Mouse 1–5; crosshair
visibility, Original/White/colour, style and size. Redundant Low-end
preset retired, but FPS cap/MSAA remain. M4 GE+ toggles deferred to
v0.5.0, M6 PNG screenshot removed from this release. **M7 blocked by
persisted fake save progress (D387); do not test with a live save**.
Physical acceptance, M7 release decision, 21-level visual gate and
documentation/commit reconciliation are still open. Earlier "one
opt-in section/default-off/preservation proof" estimates were superseded;
the original research remains in `MODERN-OPTIONS-PLAN.md`.
### Phase 4 — Release ritual
1. `./build-pc.sh ntsc-final` clean; `/linkcheck`; full 21-level
`tools_pc/verify.sh sweep --json`.
2. Full campaign playtest sign-off (user), incl. the D320-probe levels and a
water level + Surface + Silo (the Phase 1 visual fixes).
3. Refresh README Known Issues, `.github/release-notes.md`, findings index;
regenerate `agentic-development.md` stats from git (still owed since
v0.3.0 prep).
4. Push `release/v0.4.0` → tag `v0.4.0` → CI draft bundles → validate both
artifacts → fast-forward main.
## Explicitly out of scope for v0.4.0
- macOS/ARM, LAN multiplayer.
- Motion interpolation / sim-rate changes (UNLOCKED-FPS non-goals).
- D117 frame determinism (research-parked).
- **Mod support and HD texture packs — both post-1.0, both researched; see below.**
### Post-1.0, researched and tracked (do not start in v0.4.0)
Both were investigated 2026-09-22 against the Perfect Dark PC port fork
`perfect-dark-dabs-mod-dabs-mod` (**MIT**, Ryan Dwyer — same license as this
tree; `AGENTS.md` already names the PD port a standing reference, so code may be
adapted with attribution). Fuller sequencing notes live in the local
`docs/dev/notes/MODERN-PORT-FEATURES-BACKLOG.md` §5/§6, which is
gitignored — the
durable record is `docs/dev/findings.md` and the summary here. The
drop-in mod support design itself is expanded in the local
`docs/dev/notes/MOD-LOADER-PLAN.md` (2026-09-27: C1–C4 components, v1
scope, acceptance bar).
**Mod support (ROM patches) — viable, medium–large.** Findings **D325–D328**.
- Community mods *repack* the ROM, so vanilla offsets are useless: Goldfinger 64
(12→24 MB, 797/803 slices differ) and even the content-neutral Random Eye
randomizer (identical ROM size, yet 803/803 slices differ — content shifted
+7392). Both die in `tools_pc/d43_emit.py:273` on a vanilla-offset zlib slice.
- **The fix is not a loader rewrite.** PD's `port/src/modimport.c` states our own
constraint verbatim and instead diffs the patched ROM against stock, extracting
what differs into a **mod directory** its loader already reads. The game keeps
running its stock ROM at fixed compiled offsets.
- GE's file table is in the ROM after all (D327 said otherwise; corrected by
D328): 1172-compressed data segment at ROM `0x21990` — one raw inflate, which
`port/src/rzdecomp.c` already wraps — table at `0x252c4`, 727 records of 12
bytes, pointers `0x80020d90`-based, size by differencing adjacent addresses,
exactly `obInit` (`src/game/ob.c:126`). Verified: **681 exact offset+size
matches vs `filelist.u.csv`, 0 mismatches**; discovery needs **no hardcoded
offsets** and reads Random Eye's own table to **704/726 files byte-identical**.
- Remaining work: extraction in C (proven), patch decoding (PD's `rompatch.c` is
an MIT VCDIFF/BPS/IPS decoder — **removes the GPLv2 xdelta3 constraint**),
converters taking the extracted table, and the real effort: a
**mod-directory override layer** — `port/src/romdata.c` reads the ROM only and
has no analogue of PD's `romfile.moddir` file slots.
- **Hard limit:** mods that *add levels* need new stage-table entries in
`src/game` — rule 2, not the ABI/layout exception. Either a sign-off or scope
down to the existing 20 slots. ASM/code mods stay impossible (ROM code is never
executed here).
**HD texture packs — medium, well-scaffolded. Answers issue #100** ("High
Resolution Textures", open, `enhancement` — a user asking whether HD textures can
be used with the port). Architecture studied from PD's
`port/include/texpack.h` + `port/fast3d/gfx_pc.cpp`; no finding written yet, so
this entry and backlog §6 are the record.
- **Identity is the whole problem, not decoding.** The renderer only ever sees
the pointer `gDPSetTextureImage` was handed, so PD keeps an address →
texture-number registry populated where `texLoad()` runs and invalidates it
when the pool drops (a stale entry names the *wrong* texture). Then three-tier
matching: texture number → texel checksum (for textures embedded in model
files) → per-record. Packs are `textures/<texnum>.png`; decode happens on a
worker so loading does not hitch; the hook is inside `import_texture`, gated so
it costs one lookup when off.
- Most scaffolding exists here: our `gfx_pc.cpp` is the same lineage
(`gfx_texture_cache`, `:92,579`), `g_Textures[].texturenum` exists
(`src/game/chr.c:3894`), and **`GE_TEXDUMP` already dumps textures**
(`port/fast3d/gfx_pc.cpp:1205`) — it just names them by sequence
(`r000_f3_s1_64x64.bin`) rather than texture number, so dumps cannot round-trip.
Renaming that is the single change turning the probe into a pack dumper. A PNG
decoder is the one new dependency, and zlib is already required.
- Step 1 (the identity registry) is the only novel design decision, and it
records identity where textures load — a `src/game` touch needing `#ifdef PORT`
plus a §F note. Settle that at design time, not mid-implementation.
## Session log (agent-driven, 2026-09-20 evening)
**Phase 0 — done except user-side items:**
- `release/v0.4.0` cut off `main` (which already carried the D266/D271 fixes,
`532092c7` — the "uncommitted tree state" item was stale; plan updated).
- D322 probe work committed (`7bb43260`); GE_OBJT probes removed (D321 closed,
M-192 item 7, `56ac2ab8`); D309 probe **kept** (inert, cheap, recurrence
value for the D309/D310 freeze class).
- getenv hot-path sweep (D302 follow-up): function-level scan of all GE_*
sites found no frame/triangle-rate stragglers in fast3d; cached four
per-frame/per-hit guards in `src/game/bg.c` (GE_D104 room pass, GE_D63
primary+secondary render logs, GE_D154 inner GDL walk) — all `#ifdef PORT`,
mechanical.
- D244 (self-hosted CI runner): needs `pacman -Syu` on the box itself —
**user-side action**, not locally fixable; fallback (`#ifdef PORT` thin
wrapper for the `#pragma weak` alias) only if the toolchain update fails.
- 21-level sweep gate: running on the new tree (see `scratchpad/sweeps/`).
- Carry-over D316/D317 + Runway AllUnlocked re-tests are interactive → folded
into the big playtest brief below.
**Phase 1 — in flight:**
- Item 1 (D320): generic pin-signature detector added to `d318TimelineTick`
(`GE_D318T=1` now covers any chr: ACT_ATTACK + live-fire attacktype +
byte-stable animframe for 10 s → one `D320T: PIN?` line per chr, repeating
every 10 s while pinned). Detection only — the recovery watchdog stays
hardcoded to ai_22 per D320's own instruction until a second list freezes.
Live verification of Facility ai_19 / Control ai_9 / Depot ai_12 beats =
playtest work (see brief).
- Item 3 (D288/D294/D291 batch): D288 probe refined per M-155 follow-up
(any_behind_camera==0 only — the w<0 bbox math was a false lead; oml word
now logged for asset ID). D294: `GE_D104` room-visibility trace confirmed
sufficient (curRoom/roomsDrawn/budget + per-room load state every 10 s);
needs live Statue repro at FovScale≈130 + max draw distance. D291: still
needs live Dam truck details — lowest value of the three.
- Item 13 (D258 PAL/JP): **deferred pending user scope call** (ship
multi-region or document NTSC-only for v0.4.0) — no PAL/JP ROMs available
here to verify a blind CSV rename against.
- Item 4 (D235 wrong textures after death/restart): **mechanism found + fix
applied (M-193).** M-108's refutation of the game-side CI-cache theory
stands, but the *port* has its own `TextureCacheMap` (source-address keyed,
no content check) that was never invalidated on stage transitions —
`videoResetTextureCache()` existed with zero callers. Now wired into the
per-stage reset block in boss.c (`#ifdef PORT`, after `memaReset`).
Pixel-identical re-uploads; golden sweep running to confirm. Live
death/restart confirmation + a `GE_D235` >0 capture are playtest items.
- D311 upstream poison source (M-194): static pass done — all
`sub_GAME_7F06FE90` callers pass arg2=16.0f (denominator safe); the
old→new lerp only circulates values; `bheadAdjustAnimation` is the one
path where a live player-velocity corruption reaches `model->speed`. Both
speed-write choke points (`modelSetAnimSpeed`, `modelSetAnimation2`) now
log `D311: … ANOMALY` on |v|>1e4/NaN — next Caverns occurrence is a
one-line diagnosis. No env var (D156-guard pattern).
- Item 4 (D252 rainbow particles): **effectively closed — no agent work
left.** Re-read the finding this session: M-141 refuted the width/height
suspect (line=4 → correct 16×14, cross-confirmed by the GE_TEXDUMP
is_fire_shape filter), M-156 proved the ROM bytes decode to a clean
white-hot→ember progression, and **M-188 closed the user's actual symptom
(lingering residue sprites) as a duplicate of the D219 fix** — user
retested live: "seems fine now." Only a *fireball* rainbow re-report would
re-open it. Plan item 4 retired.
- Item 10 (D207 alarm klaxon): **blocked on user repro info, not agent-
workable.** M-77b implemented the designed last-resort LOOPED-preemption
pass + `[D207-DROP]`/`[D207-YIELD]` counters, BUNKER1 forced-alarm soak
showed zero drops / zero yields (pass 1 always finds a victim), and the
change was reverted. The finding now asks for: which level, which
weapon/situation, and a `GE_AUDIOTRACE=1` capture (or clip) of the failing
alarm — then decide between heavy-level 8/8 starvation (re-apply the
reverted pass), an alarm-stop-path leak, or pure perception (klaxon mix
volume vs N64). Added to the playtest brief as a low-effort ask.
- Full 21-level golden sweep: **21/21 PASS** on the final tree (`bb508e5c`),
confirming D235 fix + D311 probe + all Phase 0 changes are pixel-identical.
- **D75 (Nintendo logo) investigation — M-195, no root cause yet but two
suspects eliminated.** (a) Fresh `GE_D63` run: all 23 logo Primary GDLs
emitted every frame, zero unknown-opcode warnings → emission + DL
consumption fine; PC GDLs byte-faithful to N64 source. (b) New `GE_D75V`
per-batch vertex probe: transformed coords match the wordmark geometry
exactly (MP translation matches the lookat) → basematrix / f32→s32 /
matrix-list suspects eliminated. **Leading hypothesis:** every `G_VTX`
batch encodes v0=0, so multi-batch nodes overwrite shared vertex slots
0..n−1 (only the last batch survives; higher-indexed triangles read stale
slots) — consistent with the single tiny orbiting fragment observed. Next:
ROM-dump one node's GDL stream and cross-reference `G_TRI4` index ranges
against per-batch counts; fix would be port-layer fast3d slot management.
Probe code (`gfx_pc.cpp`, `videoGetFrameCount()` in video.c/h) uncommitted
at end of session — commit with the doc updates.
- **D307 (CCTV facing backwards) investigated — no port-side bug found.**
Env-gated `GE_D307=1` probe in `setupCctv` dumps every input + derived
orientation per record. Bunker1 (`UsetupsevbunkerZ`) decode vs live: model
switch offset = `PcctvZ` (NOT `GcameraZ`, that's the gun) sw[0]
bit-for-bit; boundpads match the pccg sidecar once the legitimate load-time
room-scale (`pad->pos *= get_room_data_float2()`, ≈1.8542 here) is applied;
record config words (incl. `unkCC`) match; all matrix/atan2 helpers are
byte-identical `src/game` code. Conclusion: PC orientation values == what
the N64 computes; if the symptom is real vs N64, the remaining suspect is
fast3d's handling of the non-orthonormal lens matrix (`unk84` row 0 scaled
0.1×). Blocked on user repro detail (which camera/level + screenshot).
Findings entry updated; probe kept in-tree (D60/D88 pattern).
- **D303 (extra long muzzle flash) investigated — no port-side bug found yet,**
blocked on live capture. Mapped the full flash pipeline (`gunUpdateAndFire`
builds `rwmtx[1]` = yaw×scale×flashext and `rwmtx[2]` from `Switches[2]`; the
flare arms are `dorottex` DLPRIMARY nodes) — all byte-identical game code,
matrix helpers verified index-for-index. Ruled out: weapon stats (M16's
flashext=6.0 is the legitimate max, sniperrifle matches; the ext≥3 ⊆
CAN_DUAL_WIELD overlap is design coincidence) and model conversion (Gm16Z
DLPRIMARY records + vertex blocks byte-faithful per d43 round-trip). Added a
TEMP `GE_D303=1` probe in `gfx_pc.cpp` (cached-getenv; logs very-tall-thin
emitted triangles with blend/oml/comb/prim/env/w) — 75 s unattended runs on
Bunker1/Dam/Depot show only static scene geometry, so the artifact needs a
live firing repro. Next: user fires the M16 under `GE_D303=1` and shares the
log lines; the spurious quad's RDP state will identify its source.
- **D306/D308 (z-fighting family) investigated — real fighting confirmed, cause
framed.** Verified GE's RSP writes post-transform x/y/z/w as full 32-bit 16.16
(not s16 8.8), so the port's float pipeline + `GL_DEPTH24_STENCIL8` matches N64
depth precision — no port-side precision deficit exists. Built a TEMP
`GE_ZF=1` flicker probe in `gfx_pc.cpp` (96×54 screen-cell grid, per-cell depth
history, requires non-monotonic a-b-a oscillation within GE_ZFTOL recurring ≥5×
at the same cell; `GE_ZFBOX` region filter) — earlier center-pair and simple
flicker variants saturated on tessellation noise. Dam (`-level_33`) unattended:
sustained hits in the lower-left truck region with per-cell oscillation counts to
~970 → D306's "transparent areas" are real depth-test alternation. Prime suspect
is level-authored coincident geometry (fights on N64 too); confirming port-only
needs an N64 reference of the same spot, and a mitigation (e.g. float depth)
would diverge from N64 — parked pending that.
- **Plan/doc refresh:** D75 (M-197) and D236 (M-198) marked fixed throughout
(Bucket A rows, Phase 1 items 6/7/11, playtest brief); findings index
regenerated; README Known Issues to match.
### Big playtest brief (for the user, when the tree is final)
Build: `./build-pc.sh ntsc-final`. Set **`GE_D322=1 GE_D204=1`** for the whole
campaign (D322 pool telemetry — the release gate). For the three D320 beats,
add **`GE_D318T=1`** on: Facility (the captive-scientists monologue/execution
beat), Control (the ai_9 guard beat), Depot (the execution beat) — watch for
`D320T: PIN?` lines; a frozen run keeps printing every 10 s.
Checklist beyond the normal play:
1. **D322**: full campaign, share `ge007.log` after (decision rules in the
finding).
2. **D316/D317 regression confirm**: click menu rows at a few window sizes,
esp. low rows (crop overscan bars) with SafeAreaCrop on/off; toggle
Game.AllUnlocked back and forth in-game.
3. **D289**: Dam computer room — interact with the terminals (was a one-off
crash, likely D255-class, already fixed — confirm it stays gone).
4. **D294**: Statue, set FovScale ≈130 + max DrawDistance/LodDistance, walk
past the CCCP monument at ground level; if the black-ground window hits,
note the time (a `GE_D104=1` run there is the follow-up capture).
5. **D291**: Dam — watch for texture glitches as the truck drives past;
screenshot + which surface.
6. **D175**: door-open on Runway/Surface — note any stutter/hang.
7. **D287**: leave a level idle+unfocused 15–20 min, click back (the
non-self-recovering freeze; one prior occurrence).
8. **D320 beats** as above with `GE_D318T=1`.
9. Audio by-ear: elevator track bass (D230 verify), PP7/AK47 under heavy fire
near looping sounds (D240/D241).
10. **D235**: on any level, die and restart (or reload a save) in the same
session, then look around — textures should be correct (no swapped/wrong
stage textures). Run once with `GE_D235=1` and share the log if anything
looks off (a `>0` count means a stale-cache hit slipped through).
11. **D288 (Silo)**: play Silo normally; if you see the wrong-color target /
geometry anomaly, set `GE_D288=1 GE_PCDUMP=1` for a 30–60 s run around it
and share log + captures (the probe logs the offending oml word for asset
ID).
12. **D311 (Caverns)**: play Caverns ~2 min of normal combat; if the anim-
speed freeze/poison recurs, the log will now carry a one-line
`D311: … ANOMALY` naming the corrupting call site — share it.
13. **D207 (alarm klaxon)**: if gunfire goes quiet during an alarm, note the
level + weapon + situation; ideally re-run that spot with
`GE_AUDIOTRACE=1` and share the log (or a short clip).
14. **D75 (front-end logos)**: confirm the Nintendo/Rareware logos + legal
page still render correctly (fixed M-197; user-confirmed once already).
15. **D236 (Surface trees)**: Surface 1/2 — confirm the treeline renders as
discrete tree sprites, not a wall (fixed M-198; probes stripped).
16. **D307 (security cameras)**: if any in-level CCTV camera looks like it's
facing backwards, note which level + which camera (screenshot) — the
orientation math is verified N64-faithful, so a concrete repro is what's
needed to keep chasing it.
17. **D303 (extra long muzzle flash)**: run with `GE_D303=1` in your terminal,
fire the M16 (and any other gun that shows the extra upward flash) until
you see it, then share the `D303:` log lines from around that moment —
each line carries the RDP state of a candidate quad and will identify the
spurious draw call's source.
18. **AllUnlocked preservation (M7) BLOCKED — D387.** Do not run this
against a real save: even changing a profile setting while ON persists
synthetic cheats and 0x3FF completion times; OFF requires a restart and
does not undo already-written fakes. First define safe earned-progress
semantics, fix in `port/`, and verify on private copies with settings
writes, mission completion, OFF restart and multi-slot CRC checks.
M4 modern toggles are parked to v0.5.0.
## Conventions reminder
- One findings entry per investigation; the index tail moves constantly —
it ended at **D370** as of 2026-09-27 (the F10 drag entry), so new
labels start **D371+** (the old "after D322" note is long stale; always
confirm the tail of `findings-index.csv` before filing).
- Fidelity fixes: port-layer or documented A1/ABI exception only; genuine
behavioral differences need the rule-2 sign-off (`docs/dev-process.md`).
- Every subagent reads `docs/porting-notes.md` first and appends to it.
+96
View File
@@ -0,0 +1,96 @@
# 1P weapon viewmodel — static research (read-only)
> **RESOLVED 2026-09-09 (M-83, D215).** None of the three ranked suspects below
> was the cause — all were ruled out by the `GE_DVM=1` probe (added this
> session). The bug was `renderdata.flags == 0` in `gunRenderFirstPersonGunModels`:
> the N64 `*(ModelRenderData*)&D_80035CC0` reinterpret-across-adjacent-globals
> breaks under x86-64 pointer widening + no guaranteed global layout. Fixed
> `#ifdef PORT` in `src/game/gunfire.c`. See findings.md D215. This doc is kept
> for the ruled-out analysis only.
Companion to `GRAPHICS-BACKLOG.md` ("1P weapon viewmodel") and findings D115
item #5 / AUDIT-M6 #5. Goal: narrow the five audit suspects to a ranked,
probe-ready list. No code changed by this doc.
## Path map (verified in source)
- **Setup (per frame):** `bondview2.c:9040` → `gunUpdateAndFireBothHands()`
(`gunfire.c:1018`) → `gunUpdateAndFire(handnum)` (`gunfire.c:327`). Inside,
the weapon-model block at `gunfire.c:599-662`: computes `field_87F` (the
show/hide gate), then if set: `dynAllocate(numMatrices × sizeof Mtxf)` →
identity-fill → `modelCalculateRwDataLen` → `modelInit(HAND_WEAPON_MODEL,
&copy_of_body_obj_header[handnum], HAND_WEAPON_RWPOOL)` →
`hand->mtxlist = rwmtx` and (PORT/D102)
`weaponModel.render_pos = (RenderPosView*)rwmtx`.
- **Render (same frame, later):** `gunRenderFirstPersonGunModels` loop at
`gunfire.c:1532-1540`: **`if (handptr->field_87F == 0) continue;`** then
`subdraw(&renderdata, HAND_WEAPON_MODEL(handptr))` (`model.c:5326`) which
does `gSPSegment(gdl, 3, osVirtualToPhysical(mdl->render_pos))` and walks
`mdl->obj->RootNode` — **a NULL RootNode returns silently, drawing
nothing**.
## Statically ruled out (with evidence)
1. **Transient-arena lifetime** (the audit's prime suspect). `dynAllocate`
is a bump allocator on the vtx pool (`src/game/dyn.c:123`), reset only by
`dynSwapBuffers()` at frame end. Setup and render both happen inside one
frame, so `render_pos`/`mtxlist` are valid when consumed — in solo play.
(Split-screen mid-frame swaps would be a separate question; out of scope.)
2. **Word-index / record-size mismatch** (`flashvisptr = RWPOOL +
RwDataIndex`, `gunfire.c:650`). D52 kept `Model.datas` as `u32*` on PC
precisely so N64 word math holds, and `RwDataIndex` values are computed
at runtime by `modelCalculateRwDataIndexes` from the *PC* record sizes —
self-consistent. (Self-consistent ≠ in-bounds; see suspect 2.)
3. **Segment/render_pos mechanism itself.** Every working model in the game
(bodies, props) uses the same `gSPSegment(3, render_pos)` +
`bondviewTransformManyPosToViewMatrix` machinery — and it renders. The
**watch menu draws the same weapon models** via a different path
(`set_enviro_fog_for_items_in_solo_watch_menu`, `gunfire.c:1630+`, local
matrices, no 87F gate, no hand rw-pool) and they appear (albeit faint —
D75/D141 note). So the model assets and headers are very likely fine; the
defect is in-level-path-specific.
## Ranked suspects + probe spec
**#1 — `field_87F` gating (`gunfire.c:599-610`).** The clear-condition
disjunction contains `(Gun_hand_without_item(handnum) == 0)`. That function
(`gun.c:812-816`) returns `hand_invisible[h] > 0 || (hand_item[h] == 0 &&
field_2A44[h] < 0)` — so the term is TRUE (→ gate cleared → gun hidden)
whenever the hand *has* an item and isn't invisible. Read naively, that
hides the gun exactly when you're holding one; since the N64 shows the gun,
the runtime values of `hand_item`/`field_2A44`/`hand_invisible` must differ
from their names suggest — but this is the single most likely place for a
PC-only divergence (e.g. a field the port initializes differently). If 87F
is 0, the render loop `continue`s and the symptom is exactly "no gun, no
crash".
**#2 — rw-pool capacity.** PC records are wider than N64's (pointer fields);
`weaponRwPool[192]` (768 B) was D102's scaled-up budget, but nothing bounds-
checks it. The `#ifdef DEBUG` print at `gunfire.c:620` still tests the *N64*
32-word threshold — stale on PC. If `numRecords` words exceed 192,
`modelInitRwData` overruns the pool into whatever follows it in `struct
hand` → corruption that could kill the draw (or worse, silently).
**#3 — stale `copy_of_body_obj_header`.** Populated only at item load
(`gun.c:924` shallow copy / `gun.c:931-951` `load_object_fill_header`). If a
PC asset-load quirk zeroed it for the in-level path, `RootNode == NULL` and
`subdraw` no-ops silently. Lower priority given the watch menu renders the
same models — but cheap to confirm in the same probe.
**Probe (one session, PORT-only, env-gated `GE_DVM=1`, zero behavior
change):** in the `field_87F != 0` block at `gunfire.c:611+`, print once per
frame while a pistol is held in BUNKER1: `handnum`, `item`, each clear-term
value + final `field_87F`, `&copy_of_body_obj_header[h]`, `numMatrices`,
`numRecords` (words) vs 192, `RootNode`, `rwmtx`. One run triages all three
suspects: 87F==0 → suspect #1 (log which term); numRecords>192 → #2;
RootNode NULL/garbage → #3.
## Fix-shape notes (for when the probe lands)
- If #1: the fix is a `#ifdef PORT` initialization/correction of whichever
hand field diverges — **not** an edit to the gate logic itself (rule #2);
document as Dxxx with the N64-vs-PC value table.
- If #2: enlarge `weaponRwPool` to the measured max + a PORT bounds guard
that prints (mirrors the existing DEBUG print, correct threshold).
- If #3: trace the `load_object_fill_header` call for the in-level weapon
load; likely an asset-offset issue in the port's loader path.
+63
View File
@@ -0,0 +1,63 @@
# Watch-backed settings in PC Options and F10 — proposal for review
Status: **watch-backed subset implemented (D350), robustness fixes landed (D352), Turok-style regrouping done (D353), front-end F10 routing + transparent default Bond-file selection landed (D354: non-stage contexts use the chooser path, default = explicit pick > `selected_folder_num` > first valid file, with a one-shot `fileBuildWriteNewSave(FOLDER1)` fallback; GE_WSPROBE/GE_WSPROBE_FRONT headless probes), the in-stage BGM resync landed (D355: the Music commit also drives the track-1 BGM player, matching the game's own MP-path resync; by-ear confirmed), and the settings regroup landed (D356: functional sections INPUT/GAMEPLAY/GRAPHICS/AUDIO/VIDEO, PD-style "(File N)" scoping annotations, N64-watch rows off the menu surface, per-section reset-to-defaults = Gate E; GE_WSPROBE_RESET headless probes front + in-stage).** Wider M3 remains outstanding: master volume, key rebinding. PC-only per-file persistence and explicit Bond-file chooser approved; details below. See `docs/dev/findings.md` D350–D356 for verification. This is the watch-integration part of M3, not a replacement for the native watch menu. The older tracked `docs/dev/OPTIONS-MENU-PLAN.md` and local `docs/dev/notes/OPTIONS-MENU-PLAN.md` describe the wider M3 scope (master volume, window mode/display, resets).
## Goal and ownership
Show GE's existing watch settings in both PC settings surfaces (`MENU_PC_OPTIONS` and F10) without maintaining a second copy of each value. Both surfaces already consume one row table in `port/src/optionsoverlay.c`; the watch remains an independent UI editing the **same underlying GE value**. Changes made in either direction must be visible on the next read. Preserve the N64 watch, its behavior, and the N64 build.
**Approved ownership:** per-file GE settings belong in the Bond save; machine-wide PC settings belong in `ge007.ini`. GE's original watch edits are *not* persisted on normal play (D349 below): **watch-made changes remain session-only**, while **PC-UI-made changes persist to the selected Bond file**. This is a deliberate port-side QoL divergence, not existing GE behavior. Do not register watch-backed settings as ini variables, copy watch values into config globals, or overwrite a newly loaded Bond file with stale ini values. A proposed PC master volume is separate: it multiplies the final output and does not replace the watch's Music/FX volumes.
## Inventory and proposed rows
Source of truth: `src/game/options.c` (`game_options_entries`, `cur_player_get/set_*`, `get_mTrack2Vol`/`set_mTrack2Vol`, `sub_GAME_7F0A91A0`), `src/game/file2.c:1246-1350` (per-folder serialization), and `port/src/input.c:~939` (forced control style). Audit exact getter/setter availability and initialization before implementing each row.
| Watch control | PC presentation | Notes |
|---|---|---|
| Music / FX volume | Audio section, separate Music and FX sliders | GE range 0..32767, stored in the Bond save at 8-bit precision; use actual GE APIs, not an SDL channel gain in place of either. Master volume stays a distinct global row. |
| Look up/down (reverse/upright), auto-aim, aim control (hold/toggle), sight on screen, look ahead, ammo on screen | Bond file / gameplay subsection, with GE-specific labels | These are the watch's `game_options_entries` and save bits. Watch look-up/down is distinct from PC MouseInvertY and PadLookInvertY: the independent inversions compose (XOR). Label/help text must explain the stacking; do not combine their stored values. |
| Screen (Full/Wide/Cinema), Ratio (Normal/16:9) | Separate clearly marked GE presentation controls, if useful | Not PC window resolution/mode or `Video.NativeWidescreen`. Verify interactions with native widescreen/crop before exposing. |
| Controller style / controller diagram | Defer pending input design | The diagram is not a setting. `inputComputePad(0)` currently reasserts Honey or Solitaire from `Input.NaturalPitch` every poll, including after save load or watch changes; `bondview.c` and multiplayer load also force Honey in some contexts. A naive style row would be immediately undone. Decide whether the watch selector should be informational, limited to supported PC mappings, or integrated with the port mapping; no row until this is reconciled. Dual-controller styles need their own decision. |
This inventory is not permission to expose every field automatically. Especially do not silently merge identically named PC and GE toggles that have different input paths or persistence.
### Persistence ground truth (D349)
`fileSaveSettingsForFolder` (`src/game/file2.c:1246`) has one caller, `fileClearSavefileForFolder` (`:1379`), reached from the erase path. Normal watch edits change live values but do **not** write the file's `options`/`music_vol`/`sfx_vol` fields or EEPROM. `init_watch_at_start_of_stage` (`src/game/options.c:415`) calls `fileLoadSaveSettingsForSelectedFolder`, so the next stage reloads those fields and discards unsaved watch edits. This is a GE/N64 quirk, not a PC regression; Gate A should re-audit indirect writes before implementation and record any correction to D349.
Thus a front-end row that edits only live watch state will be silently undone before gameplay. The maintainer approved **port-side per-file persistence of PC-UI changes**, without changing the N64 save format or the N64 watch's session-only behavior. Do not auto-persist edits made directly in the watch.
## UI / data design
1. Add a **watch-backed row type** to the existing shared row model, not another settings menu or fake config registration. Its read/write/availability callbacks operate on GE's real option API; existing config-backed rows continue to work unchanged. Keep enum labels, range/step and value formatting in the shared row table so F10 and the GE-styled screen cannot diverge.
2. Use GE's existing getters/setters for **all eight proposed entries** (`src/game/options.c:526-598`) and both volume controls (`get_mTrack2Vol`/`set_mTrack2Vol`, `call_sndGetSfxSlotFirstNaturalVolume`/`sub_GAME_7F0A91A0`). For FX use the same game volume API as the watch, not an SDL channel gain. No `src/game` bridge or Rule-2 game-code edit is expected for these rows. Preserve the watch's side effects; persistence, if approved, is a separate port-side operation.
3. **Threading is a design gate:** F10 input runs on the scheduler thread (`inputComputePad(0)`); the GE-styled screen and watch run on the game thread. Do not directly mutate game/watch state from the F10 thread merely because a getter or pointer is accessible. Route F10 writes as bounded requests to a safe game-thread point, and publish a safe, current snapshot for F10 rendering/reads, or establish an equivalently reviewed synchronization scheme. Cancel or retarget queued writes when the active folder/stage changes; never apply a write to the next folder by accident. Check audio/music initialization before **writing** Music/FX in the front end: the music getter reads a global, but the setters may touch not-yet-ready playback state.
4. Make scope visible to the player: use a dedicated **Bond file** section/page in `MENU_PC_OPTIONS` for GE watch options, with an **explicit Bond-file chooser**; never trust `selected_folder_num` on that screen (frontoptions init even assigns it a fallback). F10 in-stage targets the active file with no chooser. **D354 supersedes the "do not silently choose folder 1" stance:** all non-stage contexts (options screen *and* F10 on file select) lazily default to *explicit pick > `selected_folder_num` > first valid folder* so the section works transparently; an explicit "Edit file" cycle (still including "none") always overrides, and a game-thread `fileBuildWriteNewSave(FOLDER1)` one-shot builds a blank file 1 when no valid save exists. Switching chooser folders must not change the game's active file. D350 reads saved fields directly without calling `fileLoadSettingsForFolder` (which would audibly change front-end music). Unvalidated/absent slots are skipped. Test new/blank files in a normal boot as well as the D299 `GE_STARTMENU` shortcut.
5. **Approved port-side persistence:** resolve the actual save-slot pointer with `fileGetSaveForFoldernum(folder)`; `saves[folder]` is **not** generally the folder's physical slot (`fileWriteSave` requires a pointer into `saves[SAVESLOT1..SAVESLOTRAMROM)`). For F10, use a validated in-place path: serialize the live settings into a **temporary copy** with `fileSaveSettingsForFolder(&copy)`, then copy only the PC-UI-edited field/bit(s) into the real save-slot pointer and call `fileWriteSave(save)` with existing CRC/EEPROM handling. In the front end, where no GE player is active, merge only the selected field directly into the chosen saved record instead of loading it into live watch state. Retain `OPTION_CONTROLTYPE` from the real save: the port reasserts Honey/Solitaire every poll and must not overwrite stored controller style. This field-scoped merge also prevents a watch-only edit to a *different* field from piggybacking onto the next PC-UI save, preserving the approved session-only asymmetry. Validate the path against absent/free slots and ensure the live state belongs to **only** the chosen folder (do not load or copy values from a different folder). Never call `fileClearSavefileForFolder` as a generic settings save. Toggle/enum changes persist on value change; slider drags persist on release, not each detent. F10 requests must reach the correct game-thread/file target before write. The GE watch's own edits remain session-only. Test stage load, file switch, copy/erase, and restart for cross-file contamination; no N64 game-code changes. **Known erase edge:** GE's `fileClearSavefileForFolder` serializes *current* live settings into the reset file; after editing B, erasing A could copy B's live options into wiped A (also possible on N64 after watch edits). Document/test, do not change game code.
6. For future per-section reset-to-defaults: ini rows reset from registered PC defaults; watch rows reset to **`BLANKSAVEDATA`/`DEFAULT_OPTIONS`** (`bondconstants.h:2151`, `file2.h:23`) for the selected file, not from a global ini snapshot. New saves start with Music/FX 0xFF (full volume) and auto-aim/sight/look-ahead/ammo enabled. Separate confirmation and persistence policy before wiring a button.
## Sequence / review gates
- **Gate A — behavior audit + D349:** re-verify the erase-only write and stage-start reload (including indirect save paths); record the ground-truth quirk, defaults, stored control-type preservation, erase cross-file edge and exact API for each row. Trace folder-to-physical-slot mapping, FX/music setter readiness, watch-to-gameplay consumers, composed inversion and the F10/game-thread boundary. Approved policy: explicit chooser + PC-UI edits persist; watch edits remain session-only.
- **Gate B — foundation: IMPLEMENTED in D350**, including explicit file chooser, CRC guard, stage/file identity and thread-safe F10 command/snapshot path. D352 (review of `1303c0eb`) fixed the remaining robustness gaps: pending commands now survive stage transitions (hold-not-drop), the UI-thread chooser state is spinlocked with a front-screen guard, the queue coalesces to one pending command per field, and every silent no-op path logs a warning. Manual gamepad/mouse playtest still owed.
- **Gate C — audio: ROWS IMPLEMENTED in D350**, Music/FX save path headless-verified. Watch↔F10 live and audible checks still owed. Global master gain is a separate, not-yet-implemented M3 item.
- **Gate D — watch gameplay: ROWS IMPLEMENTED in D350** for six GE watch booleans; auto-aim and watch look-bit per-file saves headless-verified. Each watch/in-game effect and two-file isolation still need hands-on checks. Screen/Ratio remain a separate product decision.
- **Gate E — control style and resets:** design decisions first, then separate work. Avoid exposing a control-style row that the port immediately overwrites. Do not make a global reset wipe per-file watch settings.
## Acceptance checks
- Watch changes show in both PC surfaces without reopening/restarting, and PC changes show in the watch; no extra watch-key values in `ge007.ini`.
- With two Bond files, change Music/FX and a gameplay toggle in A via PC UI; switch to B, verify B retains its own values; return to A, start a stage, quit/restart and reload, verify A's values. Repeat an edit via F10 while in-stage. Drag sliders: only one save on release, not per detent. PC-UI persistence is a *port QoL test*, not a claim about N64; watch-only edits should still revert on stage reload. Check that stored controller-style bits survive PC saves and that file copy and erase retain GE behavior (including the erase cross-file edge above).
- F10 changes are not lost or applied to another file across a stage/menu transition. Check full-range sliders, watch's save quantization, no unexpected audio spikes, and no race/crash on quit.
- Menu layouts: use a dedicated Bond file section/page plus Audio; `MAX_PROWS=15` is a **per-page** cap in `frontoptions.c`, with `MAX_PAGES=8`; F10 scrolls. Check the 440x330 front-end page, F10 visible rows, mouse, keyboard, and real pad navigation; ASCII-only new text for region safety. Check NTSC headless captures, widescreen, golden gate, and PAL/JP build where possible (only NTSC ROM locally).
- Build/link checks and the normal verification ritual after implementation. Document any game-side change as its own Rule-2 request before editing it.
## Decisions and remaining scope
- **Approved:** PC-UI edits persist per selected Bond file; watch-only edits stay session-only. The front screen gets an explicit file chooser; F10 uses the active file. Preserve stored controller type; toggles/enums save on change, slider drags on release. No `src/game` behavior change or N64 save-format change.
- **Still separate product decisions:** whether/how to expose GE Screen/Ratio alongside PC video settings; whether to expose control style at all given the port's forced mapping (deferred); whether Bond-file reset needs its own confirmation rather than being bundled with global PC defaults.
- **Landed 2026-09-27 (D352/D353, port-only):** bridge robustness fixes (D352) and the Turok-style row-table regrouping (D353: six sections — DISPLAY, GRAPHICS, BOND FILE, MOUSE / AIM, CONTROLLER, GAME; the music/FX sliders sit in BOND FILE; the page title annotates the selected file; `buildPages` overflow logs).
- **Landed 2026-09-27 (D354, port-only):** front-end F10 routing + transparent default Bond-file selection (see item 4 above); in-stage F10 verified working by the `GE_WSPROBE` probe.
- **Landed 2026-09-27 (D355, port-only):** the Music commit now also resyncs the in-stage BGM (track 1) via `musicTrack1ApplySeqpVol`, plus a once-per-activation resync on level entry; by-ear confirmed. N64 watch path unchanged.
- **Landed 2026-09-27 (D356, port-only):** the settings regroup (plan `docs/dev/D356-SETTINGS-REGROUP-PLAN.md`): Turok-style pages renamed to functional sections (INPUT/GAMEPLAY/GRAPHICS/AUDIO/VIDEO, all below MAX_PROWS), the redundant N64-watch rows (`Bond.Look`, `Bond.AimControl`) commented out of the menu surface (keys + watch paths stay live, D181/D216 style), the `__HdrBond`/`__BondFile` rows retired for the front-only top save-file row, per-file sections annotated `"(File N)"` via the `saveScoped` flag, and the Turok reset rows replaced by a real per-section Reset-to-defaults (edge-triggered two-step arm->confirm within 3 s; watch rows -> BLANKSAVEDATA through the D352 commit path, ini rows -> the port's C initializers; `__Resolution` a documented exclusion). GE_WSPROBE_RESET front + in-stage probes headless-verified.
- A standalone AUDIO section (master volume + the config-side volume keys) and per-section reset-to-defaults (Gate E) are the next settings-scope items; key rebinding stays Phase 4.
+85 -59
View File
@@ -1,19 +1,26 @@
# Agentic development: two AI coding agents porting GoldenEye 007
---
title: Agentic Development
description: A case study in two AI coding agents porting a Nintendo 64 game to PC; the setup, timeline, handoff workflow, and an honest assessment of what worked.
---
*A case study in AI-agent collaboration on a large, low-level codebase. A
GoldenEye 007 Nintendo 64 → desktop port, taken from "builds a ROM" to
"playable front end" in about two weeks — driven mostly by two coding agents (a
local open-weight model, Qwen 3.8, on a single RTX 5090, and a hosted frontier
model, Claude / Claude Code), handing work back and forth through shared
written notes under one person's part-time direction. This is the goal, the
method, the timeline, and an honest read on what did and didn't work.*
## Agentic development: two AI coding agents porting GoldenEye 007
*How this port was actually built: a local open-weight model (Qwen 3.8 on a
single RTX 5090) and a hosted frontier model (Claude / Claude Code) handing
work back and forth through shared written notes, under one person's
part-time direction. The goal, the setup, the timeline, and an honest read on
what did and didn't work.*
> *Snapshot as of the v0.3.0 milestone (20 Sep 2026; findings through D321).
> v0.4.0 (28 Sep) extended the timeline with the options/input work -- the
> README's Background section carries the final project numbers.*
## Contents
- [Why this project exists](#why-this-project-exists)
- [The setup](#the-setup)
- [Timeline](#timeline)
- [By the numbers](#by-the-numbers) — [Commit velocity](#commit-velocity) · [Who did what](#who-did-what)
- [By the numbers](#by-the-numbers); [Commit velocity](#commit-velocity) · [Who did what](#who-did-what)
- [The handoff workflow](#the-handoff-workflow)
- [Assessment](#assessment)
@@ -21,7 +28,7 @@ method, the timeline, and an honest read on what did and didn't work.*
The playable port is real, but it is not the primary deliverable. The goal was
to **test how well coding agents hold up on a large, unfamiliar, low-level
codebase** — one with none of the properties that make web-app work easy for
codebase**; one with none of the properties that make web-app work easy for
an LLM:
- ~230 translation units of decompiled Nintendo 64 game C, compiled
@@ -36,7 +43,7 @@ an LLM:
Concretely it set out to validate a **two-agent arrangement**: a
locally-hosted open-weight model doing the bulk of the work on a single
consumer GPU, a hosted frontier model brought in for a collaborative phase,
and — the part that turned out most interesting — the two **handing work back
and, the part that turned out most interesting, the two **handing work back
and forth** through shared written artifacts, directed by one human.
## The setup
@@ -44,28 +51,28 @@ and forth** through shared written artifacts, directed by one human.
| | |
|---|---|
| Local agent | `unsloth/Qwen3.8-27B-GGUF:UD-Q4_K_XL` on a single **NVIDIA RTX 5090**, driven mainly through the **[pi](https://pi.dev/)** coding agent. Unsloth Desktop (Unsloth's local model runtime, which can drive agents such as Claude Code) was also trialed but not used significantly. |
| Hosted agent | **Claude**, via **Claude Code**, on a Claude Pro subscription — mostly **Sonnet 5**, with **Opus 5** used as an escalation tier for the hardest problems and whenever there was subscription budget to spend on it |
| Hosted agent | **Claude**, via **Claude Code**, on a Claude Pro subscription; mostly **Sonnet 5**, with **Opus 5** used as an escalation tier for the hardest problems and whenever there was subscription budget to spend on it |
| Human | one person: direction, work partitioning, integration, and every build / playtest / frame-capture the agents could not run |
| Base | fork of the [GoldenEye 007 decompilation](https://github.com/n64decomp/007) (years of prior work by Larry Ficken ("kholdfuzion") and contributors) |
| Reused | the [Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark)'s `fast3d` software RSP — same Rare engine family |
| Reused | the [Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark)'s `fast3d` software RSP; same Rare engine family |
The port is the GoldenEye-specific porting work **on top of** those two
existing bodies of work; it is not a from-scratch reimplementation of either.
In practice the models formed a **three-tier escalation**: the local model
handled well-scoped work, Sonnet 5 took what it stalled on, and Opus 5 was
reserved for the bugs that needed the most reasoning held at once — the same
reserved for the bugs that needed the most reasoning held at once; the same
"escalate when stuck" move applied at every level.
## Timeline
All dates from this repository's own commit history (August 2026).
All dates from this repository's own commit history (August–September 2026).
```mermaid
gantt
dateFormat YYYY-MM-DD
axisFormat %b %d
title GoldenEye 007 PC port — from fork to playable front end
title GoldenEye 007 PC port; from fork to v0.3.0
section Local model - Qwen 3.8 via pi
Fork + PC-port scaffolding :m1, 2026-08-16, 1d
Full compile + link (~230 TUs) :m2, 2026-08-20, 1d
@@ -81,63 +88,78 @@ gantt
SDL input layer (kbd/mouse/gamepad) :2026-08-29, 1d
Front-end flow (menu to briefing to start) :m7, 2026-08-30, 1d
File-backed EEPROM saves :2026-08-31, 1d
section Month one - front end to v0.3.0
Audio mixer (music + SFX) :2026-09-02, 7d
Steam Deck hardening + F10 overlay QoL :2026-09-14, 3d
Final fix sweep (D309-D321) + release review :2026-09-18, 3d
v0.3.0 released (full campaign playtested) :milestone, 2026-09-20, 0d
```
- **Day 0** (16 Aug): repository forked, PC-port scaffolding added.
- **Day 4** (20 Aug): the entire ~230-TU game + libultra set compiles and
links as a host binary.
- **Day 6** (22 Aug): first real frames render.
- **Day 8** (24 Aug): the whole intro sequence renders — logos, gun-barrel,
- **Day 8** (24 Aug): the whole intro sequence renders; logos, gun-barrel,
cast roll.
- **Day 11** (27 Aug): second agent joins.
- **Day 13** (29 Aug): **all 21 solo missions load, render, and survive an
unattended play window without crashing.**
- **Day 14** (30–31 Aug): front end playable end to end (menu → mission
select → difficulty → briefing → start); file-backed saves.
- **Day 35** (20 Sep): **v0.3.0 released** — the full campaign playtested
end to end at Agent difficulty, audio complete, bundles for Windows,
Linux and Steam Deck.
So roughly **two weeks**, one person part-time, to take a decompilation from
"builds an N64 ROM" to "boots on desktop, renders every solo level, playable
through the front end into the early game" — with audio and a set of cosmetic
issues still outstanding.
through the front end into the early game". The rest of month one (29 Aug –
20 Sep) took it from there to a public release: the 21-level sweep to
full-campaign completion, the audio mixer, input and Steam Deck polish,
widescreen FOV scaling, and the v0.3.0 bundles — with a set of cosmetic
issues documented rather than outstanding.
## By the numbers
| | |
|---|---|
| Calendar time | 16 days (16 Aug – 1 Sep 2026), one person part-time |
| Commits on the port | ~223 |
| Root-caused bugs logged | 162 (`D1`–`D169` in [`findings.md`](findings.md); some later merged or withdrawn) |
| Handoff sessions | ~33 (`M-2` … `M-33`) |
| Game-source files given `#ifdef PORT` ABI edits | 63 files, 241 blocks |
| New port-layer / tooling files | 128 |
| Port layer | ~17,000 lines C/C++ (`port/`) |
| PC asset-conversion tooling | ~6,300 lines Python (`tools_pc/`) |
| Outcome | intro + all 21 solo missions render, front end playable; audio not yet done |
| Calendar time | 36 days (16 Aug – 20 Sep 2026), one person part-time |
| Commits on the port | ~835 |
| Root-caused bugs logged | `D1`–`D321` in [`findings.md`](https://github.com/jkdansereau/goldeneye-pc-port/blob/main/docs/dev/findings.md), 199 tracked entries in the index (some labels later merged or withdrawn) |
| Handoff sessions | 190+ (`M-2` … `M-192`, and counting) |
| Game-source files given `#ifdef PORT` ABI edits | 71 files, ~385 blocks |
| New port-layer / tooling files | ~119 |
| Port layer | ~24,450 lines C/C++ (`port/`) |
| PC asset-conversion tooling | ~7,180 lines Python (`tools_pc/`) |
| Outcome | **v0.3.0 released**: full campaign playable end to end (all 21 missions playtested at Agent difficulty) at a steady 60 fps, full audio (music + SFX), file-backed saves; Windows, Linux and Steam Deck; known cosmetic defects documented in the finding log |
### Commit velocity
```mermaid
xychart-beta
title "Commits per day"
x-axis ["8/16", "8/17", "8/20", "8/21", "8/22", "8/23", "8/24", "8/27", "8/28", "8/29", "8/30", "8/31", "9/1"]
y-axis "commits" 0 --> 60
bar [1, 2, 2, 6, 9, 2, 4, 7, 57, 39, 39, 46, 9]
title "Commits per week (week start)"
x-axis ["8/16", "8/23", "8/30", "9/6", "9/13", "9/20"]
y-axis "commits" 0 --> 250
bar [20, 109, 203, 141, 220, 6]
```
Days with no commits are omitted. The 8/25–8/26 gap is between phases; the
step up from 8/28 onward is the collaborative phase in full swing, including
the parallel multi-agent "bursts" used near the end.
Weekly totals (the final week is release day only). The step up from the
8/23 week onward is the collaborative phase in full swing, including the
parallel multi-agent "bursts"; the 9/13–9/19 week peaks on a single-day
84-commit burst (9/15) when the Steam Deck playtest-feedback batch landed
at once.
### Who did what
```mermaid
pie showData title "Commits by agent (raw count)"
pie showData title "Commits by agent (raw count, snapshot through early September)"
"Claude" : 161
"Local model (Qwen 3.8 / pi)" : 62
```
(Phase A — the first 26 commits, to 24 Aug — was entirely the local model,
solo; from 27 Aug on the two agents worked in parallel.)
(Phase A, the first 26 commits, to 24 Aug, was entirely the local model,
solo; from 27 Aug on the two agents worked in parallel. The raw tally was
not re-run for the final third of the project; treat the milestone-weighted
estimate below as the better number.)
| Milestone / workstream | Primary agent | Weight |
|---|---|---|
@@ -147,20 +169,20 @@ solo; from 27 Aug on the two agents worked in parallel.)
| Offline asset-conversion architecture + first converters (`tools_pc/`) | local model | large |
| Intro rendering (logos → gun-barrel → cast) | local model | medium |
| Stage load unblocked (`D69`–`D87`) | Claude | medium |
| 21-level crash sweep — ~12 crash classes root-caused (`D88`–`D169`) | Claude | large |
| 21-level crash sweep; ~12 crash classes root-caused (`D88`–`D169`) | Claude | large |
| SDL input layer (keyboard / mouse / gamepad, mouse-look) | Claude | medium |
| Front-end flow (menu → briefing → start), EEPROM saves | Claude | medium |
| Parallel struct-layout / converter static audits | local model | medium |
| Continuing tasks after Claude hit a usage limit | local model | small |
| The hardest structural bugs (matrix handedness, format specs) | Claude | — |
| Every build, playtest, frame capture, integration, and direction | human | — |
| The hardest structural bugs (matrix handedness, format specs) | Claude | n/a |
| Every build, playtest, frame capture, integration, and direction | human | n/a |
**Estimated effort split.** The raw commit count above (~28% local / ~72%
Claude) undercounts the local model: Phase A landed the build system and boot
chain in relatively few, large commits, and the local model kept contributing
~15–20% of Phase B in parallel. Weighting by milestone difficulty rather than
raw commits — the foundation is a heavier third of the project than its commit
share suggests — the developer's estimate is roughly:
raw commits; the foundation is a heavier third of the project than its commit
share suggests; the developer's estimate is roughly:
```mermaid
pie showData title "Agent effort, milestone-weighted"
@@ -180,7 +202,7 @@ result worth taking away.
This is the part worth paying attention to.
```mermaid
flowchart LR
flowchart TD
H["Human: direction, integration,<br/>build + playtest verification"]
C["Claude / Claude Code<br/>frontier, hosted"]
Q["Qwen 3.8 via pi<br/>open-weight, local RTX 5090"]
@@ -199,28 +221,30 @@ flowchart LR
Both agents worked against the **same three written artifacts**, which is what
let them substitute for each other:
1. **`HANDOFF.md`** — the current state, the immediate next task, and the
1. **`HANDOFF.md`**; the current state, the immediate next task, and the
environment gotchas. Originally a session-to-session note for one agent, it
became the **interface between the two agents**: when Claude reached a
usage limit mid-problem, the local model picked the task up from the
HANDOFF state and continued; when the local model hit a bug that needed
deeper structural reasoning, it wrote up where it was and Claude took over.
2. **`findings.md`** — the chronological finding log. 162 numbered entries,
each a root cause with `file:line` evidence and the fix. New agents (either
model) are pointed at the relevant entries before they start.
3. **`porting-notes.md`** — the append-only "recurring bug classes" file. The
2. **`findings.md`**; the chronological finding log. 321 numbered `D`
entries by v0.3.0 (199 tracked in the index; some labels merged or
withdrawn), each a root cause with `file:line` evidence and the fix. New
agents (either model) are pointed at the relevant entries before they
start.
3. **`porting-notes.md`**; the append-only "recurring bug classes" file. The
single highest-leverage artifact: it stopped both models from
re-deriving the same class of N64→PC bug over and over.
Around these, the working rules (full detail in
[`../dev-process.md`](../dev-process.md)):
- **file-partitioned tasks** — each agent's task scoped to a disjoint set of
- **file-partitioned tasks**; each agent's task scoped to a disjoint set of
files so patches never collided;
- **visible budgets** — every investigation task carried an explicit
- **visible budgets**; every investigation task carried an explicit
"N build→run cycles" limit and a defined fallback (revert probes, write up
with a confidence rating);
- **the human owns verification** — building, running the game, capturing a
- **the human owns verification**; building, running the game, capturing a
frame, and judging it against N64 reference footage was never delegated.
## Assessment
@@ -229,7 +253,7 @@ Honest notes, for anyone weighing whether this transfers.
**Worked well**
- The **local model carried the groundwork phase** — build system, boot
- The **local model carried the groundwork phase**; build system, boot
chain, OS shims, and the offline-converter architecture. None of it was
rewritten later. A 27B open model on one consumer GPU was genuinely productive
on this.
@@ -238,14 +262,14 @@ Honest notes, for anyone weighing whether this transfers.
because the accumulated notes were good. This is also what made the two
models interchangeable on a given task.
- **Handoff on limit** turned Claude's usage cap from a hard stop into a
slowdown — the local model kept the problem moving.
slowdown; the local model kept the problem moving.
- Bounded behavioural bugs (a truncated pointer, a byte-swap off by one) are a
good fit for an agent given a tight loop and a reference to diff against.
**Worked poorly / needed the human**
- **Anything requiring the running game.** Build, playtest, capture a frame,
decide whether it looks right — that loop was the human's job throughout,
decide whether it looks right; that loop was the human's job throughout,
and it was the bottleneck.
- **Non-deterministic bugs.** Frame-timing stalls and concurrent-build
flakiness repeatedly fooled agents into "fixing" regressions that were not
@@ -255,10 +279,12 @@ Honest notes, for anyone weighing whether this transfers.
rather than a fix, even with the budget raised.
- **The capability gap is a gradient, not a wall.** The local model was strong
on well-scoped work and weaker when a bug needed several interacting facts
held at once — those went to Sonnet 5, and the few that stalled Sonnet went
held at once; those went to Sonnet 5, and the few that stalled Sonnet went
to Opus 5. Each tier earned its place on the problems the tier below it
couldn't close.
**Still outstanding** (parked below crash/level work): audio is not
implemented; several front-end 3D transforms and some text rendering are
wrong. See [`GRAPHICS-BACKLOG.md`](GRAPHICS-BACKLOG.md).
**Still outstanding** (the v0.3.0 known-issues list): cosmetic rendering
defects (particle colours, Surface 1 billboard trees, front-end logos, water
seam), stretched-not-native widescreen, PAL/JP support, a controller
rebinding UI, and macOS/ARM builds. See the README's Status section and
[`GRAPHICS-BACKLOG.md`](https://github.com/jkdansereau/goldeneye-pc-port/blob/main/docs/dev/GRAPHICS-BACKLOG.md).
+287
View File
@@ -0,0 +1,287 @@
# GENERATED by tools_pc/gen_findings_index.py -- do not hand-edit.
# Grep this to pick which findings.md entries to read; it is not authoritative.
label,bucket,one_liner,status_raw
D24-D30,RESOLVED,green threads / kernel / crash handler,resolved
D31-D42,RESOLVED,"boot ABI: ANIM_DATA, music seq-table, libaudio banks, Globalimagetable rebase, GL context, rsp toggle",resolved
D43-D50,RESOLVED,"model-file loading → offline sidecar ""Plan B"" (d43_emit.py); D50.6 texCopyGdls full-slot copy",resolved
D51-D58,RESOLVED,"font pixeldata, osGetCount tick rate, model RW pools, cseq BE header, synth slots, RLE menu bg, watch raw offsets, gun-barrel DL",resolved
D59-D68,RESOLVED,"intro render: blood-RLE clobber, DMA validate, OSMesgQueue, HEADS/BODIES sentinels, romCopy width, image_entry, Globalimagetable BE→LE",resolved
D70-D74,RESOLVED,"intro-logo pixels: C-array bswap, UV path, sinf/cosf DVAL(), texture-import truncation",resolved
D75,PARTIAL,front-end 3D model transforms (D77 audio split out below —,PARTIAL (M-197 fixed the logo texture blobs; v0.4.0 campaign 2026-09-28:
D76 · D164,FIXED,Legal screen only draws line 1 —,"FIXED — this row and the ## D164 entry below were stale; the proposed one-line #ifdef PORT fix was actually committed 8dfc578f4 (2026-08-31), well before this correction (2026-09-18)."
D159,FIXED,"front-end wallet-Bond photo ""interlaced""/combed (RC1 / D149) —",FIXED (#ifdef PORT no-op the swap in image.c)
D161,FIXED,Depot (-level_30) ceiling = bright-blue speckle + radial rays (B2).,FIXED (port/fast3d/gfx_pc.cpp)
D165 · D166,FIXED,"Input-layer polish (M-31, port-only):",FIXED
D180 · D181,FIXED,Native-PC input pass (QoL run):,FIXED (2026-09-26 bookkeeping:
D186,OPEN,"Video.FpsCap throttles the sim, not just presentation (M-39 diag, M-42 clamp).","OPEN (2026-09-26, user direction:"
D187,FIXED,"HUD ammo-type/weapon icon vanished at -O2 (M-46 root-cause, M-47 fix + verify).","FIXED (src/game/gunfire.c, #ifdef AVOID_UB); PR #19"
D188,FIXED,Linux -O2 boot crash:,"FIXED (src/libultrare/libc/xprintf.c, src/sprintf.c, #ifdef PORT); PR #22"
D189,FIXED,Linux -fstack-protector aborts on the decomp's latent stack-buffer overruns (M-48) —,"FIXED (CMakeLists.txt, src/game/stan.c #ifdef PORT); PR #23"
D190,CLOSED,"Linux -level_45 SIGSEGV at load in add_ptr_to_objective (objective.c:64) (M-48, open).","CLOSED — not a port bug; level 45 is MP-only and has no solo setup file in the ROM (root-caused M-105, reproduced on Windows)."
D191,FIXED,Windows -O2 release-bundle crash:,FIXED + PLAYTEST-VERIFIED (M-79).…
D193,FIXED,NPC AI locomotion is too slow —,FIXED + user-playtest-verified (M-81) —…
D194,FIXED,Mouse input needs another tuning pass —,FIXED (2026-09-26: user reports the v0.3.0→v0.4.0 mouse changes feel solid and glitch-free; code-side validation owed).
D195,FIXED,"Transparency / alpha surfaces broken on Control (M-49, user playtest) —","FIXED (port/fast3d/gfx_pc.cpp, gfx_sp_vertex) —…"
D197,CLOSED,"Character face/head textures wrap around the head, seen on Silo (M-49, user playtest).",CLOSED — user playtest v0.2.0-pre
D198,FIXED,alAdpcmPull/_decodeChunk used K0_TO_PHYS (an unconditional & 0x1FFFFFFF mask) on 64-bit heap pointers (src/libultra/audio/load.c).,FIXED (src/libultra/audio/load.c) —
D199,FIXED,Phase-3 software audio mixer:,FIXED (2026-09-26 bookkeeping:
D200,FIXED,-level_09 segfault: reverb's ALDelay.input/output are u32 —,FIXED — 5-line cast in reverb.c (this session)
D196,FIXED,"OS mouse cursor stays visible after closing the F10 options overlay in click-to-lock mode (M-49, user playtest).","FIXED (6e758f1d, merged to main) —…"
D192,CLOSED,Front-end mission/level-select grid pointer still can't reach the outer cells —,"CLOSED — the buggy mode 0 was removed entirely in the v0.2.0 prep pass (see D239); click-to-lock is now the only capture mode, so the unreachable-cells path no longer exists"
D221,FIXED,SELECT FILE mouse hit-box offset —,"FIXED (M-91) — explicit {min,max} coord2d pairs under #ifdef PORT; human mouse-playtest confirmed."
D222,FIXED,High Video.FovScale breaks culling —,"FIXED, awaiting visual verification (M-121)."
D223,FIXED,QoL ask: mouse-wheel weapon cycling should be directional like a normal PC shooter —,"FIXED (M-105, port/src/input.c) —…"
D224,PARTIAL,"QoL gap (M-87, from a PD-PC-port survey):","LANDED via D401 (2026-09-27, re-promoted from DEPRIORITIZED)."
D225,FIXED,"Dev-tooling QoL (M-87, user ask):","FIXED (2026-09-18) — GE_DEBUG_UNLOCKALL=1 short-circuits fileGetIsCheatUnlocked() (src/game/file2.c, #ifdef PORT, cached getenv) to always return TRUE; default off, no save mutation."
D226,FIXED,"QoL ask: player-adjustable HUD scale for the ammo counter, bottom-left pickup/item status text, and top-of-screen dialogue/subtitle text.","FIXED (2026-09-24): Game.HudScale 75–150% (user-verified, capped at 150) via fast3d G_HUDSCALE_EXT rect scaling about per-element anchors; dialogue/bottom messages width-capped."
D227,FIXED,Sky renders as two independently-moving halves —,"FIXED (M-100b, PR #43) —"
D229,FIXED,"IsWater-level water renders green, pulses blue<->green (M-101; Frigate -level_26).","FIXED (port-layer) — ZLIB+CI8 water image now imports via the CI8 palette path (gfx_pc.cpp); Frigate verified live (""looks better""); sweep + Surface 2 by-eye still owed."
D230,FIXED,In-level music quality:,"FIXED (M-121, v0.2.0) —"
D231,CLOSED,"QoL batch: mute-on-focus-loss + screenshot-hotkey productization (M-105, roadmap Tier 3, port-only).","CLOSED — PARKED/DEPRIORITIZED (user, 2026-09-25)."
D233,FIXED,All levels (intermittent):,"FIXED (port/fast3d/gfx_pc.cpp, gfx_sp_tri1) —…"
D234,FIXED,"Dam: guard towers along the dam structure invisible except the first one (M-106, user QA report).",VISUALLY VERIFIED FIXED (M-108) —…
D244,OPEN,Self-hosted CI runner (corpyvt6agent01) link failure:,"OPEN, not yet fixed. Next…"
D228,FIXED,AK47 white missing-texture patches after D217's texpool fix —,"FIXED (port/fast3d/gfx_pc.cpp, palette_to_rgba32) —"
D204,FIXED,PC audio ran ~2 % below real time permanently (AI feedback loop's 3 ms setpoint vs PC jitter).,"FIXED (port/src/audio.c, port/src/libultra.c, port/include/audio.h) —"
D203,CLOSED,"Steam Deck (SteamOS, x86_64 Linux) v0.1.0 release bundle:",CLOSED — root cause + fix in D253.
D201,FIXED,"src/libultra/audio/bnkf.c bank/instrument/sound/wavetable relocation offsets were s32, truncating the real 64-bit base pointer.",FIXED (src/libultra/audio/bnkf.c) —
D202,FIXED,"Silenced PPK ""slap"" + broader real-time mixer corruption (M-52→M-67).","FIXED (2026-09-26, user by-ear:"
D206,FIXED,PC played every SFX one bank slot too high —,"FIXED (M-76, src/snd.c:1001, #ifdef PORT) —"
D207,FIXED,"Alarm klaxon starves combat SFX and doesn't recover after it stops (surfaced by D206, M-77).",MONITORING — user 2026-09-26:
D208,FIXED,Player weapon-fire sound goes permanently silent mid-firefight (all other SFX unaffected) —,FIXED + user playtest-verified (M-78d) —
D209,FIXED,Every AI character in the game is locked to a *walk* animation regardless of the speed the ailist commands —,FIXED + user-playtest-verified (M-81).
D210,FIXED,chrToPatrol() leaves act_patrol.lastvisible60 uninitialised on PC —,"FIXED (chraction.c:3872, #ifdef PORT → self->act_patrol.lastvisible60 = -1;, N64 line under #else).…"
D205,CLOSED,"PC plays wrong/extra SFX for gameplay events (gunfire slap layer, explosion→scream, armour pickup, melee→Klobb idx 106) —",CLOSED (M-78) — root cause was D206; all four symptoms resolved + user-confirmed by ear.
D152+,FIXED,D152 addendum (M-31):,"FIXED (src/snd.c, port/src/libultra.c, #ifdef PORT) —"
D168,FIXED,GE_PCDUMP / F12 PPM captures were vertically flipped —,FIXED (port/fast3d/gfx_opengl.cpp); D114 / D116 reclassified as capture-orientation artifacts (below).
D169,FIXED,Front-end mouse pointer can't reach the outer cells of the mission/level-select grid —,"FIXED (M-33, port/src/input.c, port-only, no #ifdef PORT)."
D178,FIXED,Pre-mission briefing screen:,FIXED (M-36) — high confidence
D175,CLOSED,In-game stutter / brief hang during normal play —,"CLOSED (v0.4.0 campaign sign-off 2026-09-28, user:"
D176,FIXED,"Surface exterior renders wrong (-level_36), two independent defects.","(a) scroll-speed FIXED (fidelity pass; HIGH confidence on root cause, MODERATE on N64 parity); M-93 playtest + M-98 Defect-2 starburst screenshot in the sections."
D177,FIXED,Ladders non-functional —,FIXED (M-36) — high confidence; interactive climb test owed
D172,FIXED,"Bullet-impact / blood / spark particles render magenta or cyan instead of dark red (M-82, ROOT CAUSE FOUND + FIXED).",FIXED (port/fast3d/gfx_pc.cpp gfx_lod_tile_offset)…
D174,CLOSED,"""No blood effect"" (user QA report).","CLOSED — faithful (2026-09-26, user side-by-side on 1964:"
D183,CLOSED,"M-36 Family A (""texture line/pitch shear"") is DISPROVEN for the Surface repro (-level_36).","CLOSED (2026-09-26, user-approved:"
D173,FIXED,Third-person Bond model spawns too high —,"MOSTLY FIXED as a side effect of D243 (2026-09-18, user-confirmed)."
D179,FIXED,Packaged build crashes after the logos —,"Diagnosed, not fixed —"
D180,FIXED,"Native-PC input pass (M-XX QoL run, qol/native-pc-input-menu, port-only).",FIXED (2026-09-26 bookkeeping:
D181,FIXED,Game.ScreenShakeIntensity —,FIXED (2026-09-26 bookkeeping:
D184,FIXED,F10 port-layer options overlay —,FIXED (2026-09-26 bookkeeping:
D211,FIXED,"PC FOV slider — Video.FovScale (M-83 QoL wave, port-only, WIDESCREEN-FOV-PLAN.md Phase 4).",FIXED (2026-09-26 bookkeeping:
D212,FIXED,Anisotropic-filtering config wiring —,FIXED (2026-09-26 bookkeeping:
D213,FIXED,On-screen FPS readout —,FIXED (2026-09-26 bookkeeping:
D214,FIXED,"Keyboard rebinding — [Input.Bind] config section (M-83, port-only, PD parity, Phase 4 debt).",FIXED (2026-09-26 bookkeeping:
D215,FIXED,1P weapon viewmodel never drew in-level —,"FIXED (src/game/gunfire.c, #ifdef PORT)."
D216,FIXED,"Game.SkipIntro — boot straight to the file-select menu (M-83, port + one #ifdef PORT game-code line, PD parity, Phase 4 quick win).",FIXED (2026-09-26 bookkeeping:
D217,FIXED,1P weapon viewmodel + 3P Bond model textures garbled —,FIXED + merged (PR #47 + PR #49); white residual split out as D228.
D218,FIXED,Wide FOV exposes the per-level draw-distance / fog boundary —,FIXED (2026-09-26 reclassified:
D219,RESOLVED,"PPK explosions render purple on Dam (M-87, user QA, ""not 3D but noting"").","RESOLVED (M-115, 2026-09-12) for the original PPK-purple symptom."
D220,CLOSED,"1P muzzle flash position shifts upward at higher Video.FovScale (M-87, user QA, Jungle/M16).","CLOSED — merged into D303 (2026-09-26, user:"
D85,CLOSED,Room primary/secondary DL → texLoadFromGdl garbage.,"CLOSED (2026-09-26, user-approved:"
D86 · D87,RESOLVED,modelInitRwData truncated ptr · attract-demo BE ramromfilestructure.,resolved
D88.4,RESOLVED,propDefs polymorphic record stream not byteswapped → setupDoor crash.,resolved (d88_propdefs.py); layout audit M-20 → D132
D132,PROPOSED,D88 propDefs layout audit (M-20):,proposed fix (not applied) —
D157,FIXED,"Campaign never unlocks the next level (M-30, user bug —","FIXED (src/bondtypes.h, #ifdef PORT —…"
D160,FIXED,Dam level-end cutscene (Bond off the dam / bungee) never plays —,"FIXED (user, 2026-09-26:"
D89-D92,RESOLVED,"stage-load→frame: stan zero-fill overrun, portal address trunc, chr/AI spawn ptrs",resolved
D93-D102,RESOLVED,struct-pun / hardcoded-size pass:,resolved (D95 pool bump partial)
D103-D107,RESOLVED,"BUNKER1 viewport height, depth-buffer clear (G_CLEAR_DEPTH_EXT), portal near-plane, LOD mip tile",resolved
D108-D112,RESOLVED,skeletal models: d43_emit.py put_f32 byte-reversal bug,resolved
D113-D116,FIXED,portal BFS ok · matrix chain ok · player raw-offset audit + gunfire.c THROW* · HUD text X-mirror,"D114/D116 CLOSED — NOT A BUG (M-33, see D168):"
D117,OPEN,"Frame nondeterminism root-caused; GE_DETERM attempted, not achieved.","OPEN (mode attempted, not achieved)"
D118,RESOLVED,"SDL input layer; M-24 mouse-look rework (mode-aware map, real INI); D118b/c FIXED.",resolved (D118a residual)
D150,FIXED,watch OBJECTIVES / BRIEF page crash:,FIXED (src/str.c #ifdef PORT); interactive re-verify pending
D260-D265,CLOSED,"v0.2.1 hotfix batch: D260 wheel direction swap (fixed) · D261 watch-menu nav auto-repeat (fixed, feel-check owed) · D262 watch item-preview model missing → roo…",CLOSED (2026-09-26 bookkeeping:
D266-D269,CLOSED,"D236/D265 wall, second pass:",CLOSED (2026-09-26 bookkeeping:
D270,CLOSED,Backfilled (M-132) — this label was never given its own row; only code comments and the HANDOFF archive documented it.,CLOSED (2026-09-26 bookkeeping:
D271,FIXED,"D236 wall, third pass —",FIXED-but-insufficient (src/game/bg.c #ifdef PORT) —
D276,CLOSED,"D236 wall, eighth pass —",CLOSED (2026-09-26 bookkeeping:
D279,CLOSED,Backfilled — this label was consumed by an undocumented partial session (2026-09-14 ~23:00) that left artifacts in scratch/ (d279_run.log… —,CLOSED (2026-09-26 bookkeeping:
D281,FIXED,"Game.AllUnlocked (D257) still breaks audio, and can also break right-mouse aim, when enabled before any save has ever been written (v0.2.1 user testing… —","FIXED (2026-09-23, port/src/libultra.c):"
D282,FIXED,"QoL ask (v0.2.1 user testing, 2026-09-15):","FIXED (port/src/input.c, #ifdef not needed —"
D283,OPEN,"Steam Deck first-run preset (D-unnumbered in port/src/video.c/main.c, videoApplySteamOSDefaults()) did not apply for a user who added the game via Steam… —","OPEN — mechanism understood, root cause (Desktop-vs-Game-Mode STEAMOS env var difference) unverified on real hardware; user-facing workaround documented in README/release notes."
D280,CLOSED,"D236 wall, ninth pass —",CLOSED (2026-09-26 bookkeeping:
D275,CLOSED,"D236 wall, eighth pass —",CLOSED (2026-09-26 bookkeeping:
D274,CLOSED,"D236 wall, seventh pass —",CLOSED (2026-09-26 bookkeeping:
D273,CLOSED,"D236 wall, fifth pass —",CLOSED (2026-09-26 bookkeeping:
D272,CLOSED,"D236 wall, fourth pass —",CLOSED (2026-09-26 bookkeeping:
D137,FIXED,right-mouse (aim-sight) crash:,FIXED (src/game/gunfire.c #ifdef PORT —
D119 · D120,FIXED,guard-attack weapons_held[]->chr pun crash · blood-stain PointUsage[] chain hang,D119 fixed · D120 FIXED (M-30)
D120,FIXED,Blood-decal PointUsage[] chain —,FIXED (tools_pc/d43_emit.py).
D121,RESOLVED,WS1 frictionless per-level boot (bare -level_XX injects its -m* row).,resolved
D122,FIXED,per-level prop/item model-load crash (Dam/Facility/Runway modelLoad/modelInitRwData):,converter fixed; residual chr/fast3d crashes on those levels are separate
D123,FIXED,crash class C1: chrIsNotDeadOrShot NULL deref on 6 levels (Dam/Runway/Frigate/Statue/Streets/Cradle).,"converter fixed (OBJ_ID_WORDS); C1 cleared on all 6, residual crashes are fast3d (C2)"
D125,RESOLVED,crash classes C3+C6 (Aztec/Bunker2/Surface2):,resolved (M-16b) — line 374 → 8 NULs; all 21 sidecars regen'd; Aztec -level_28 now PASSES (was C3 CRASH).
D126,RESOLVED,"crash classes C3r/C4/C6 (Bunker2 -level_27 door7F054FB4 propobj.c:13523, Depot -level_30 prop.c:902, Surface2 -level_43 loadobjectmodel.c:393):",resolved — d88_propdefs.py PROPDEF_PC_BYTES[30/32/33/35]=24 + typed handler; loadobjectmodel.c sizepropdef PORT returns 6.
D124,FIXED,crash class C2: fast3d bad texture pointer.,Jungle fixed (port/src/gimgfixup.c); Facility → D130
D131,RESOLVED,crash class C2m (Jungle -level_37):,"resolved — 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 all handled earlier).…"
D133,RESOLVED,intro render triage (M-20):,"RESOLVED — not a regression, parked (D75/D76, GRAPHICS-BACKLOG.md)"
D134,FIXED,"Frame-2 boot hang / sweep ""NO-FRAMES"" flakiness —",FIXED (port/src/libultra.c)
D135,FIXED,"Firefight crash — bgTestHitOnObj (propobj.c:8446) is an unported N64 GBI parser (M-22, WS6 playtest).","FIXED (src/game/propobj.c, #ifdef PORT —…"
D136,RESOLVED,"Stage-unload crash — objFree obj->prop reads float garbage (M-22, WS6 Facility playthrough).",ROOT-CAUSED → D139 (it was NOT a layout bug —
D139,FIXED,Stage-unload crash root cause + fix (M-23).,"FIXED (src/game/cleanup_objects.c, #ifdef PORT —…"
D140,FIXED,Pause-menu crash (M-23 open → M-27 fixed).,"FIXED (src/game/bondview.h, src/game/bondview2.c; #ifdef PORT)."
D141,FIXED,"Watch-menu item-model crash (M-27), exposed by D140.","FIXED (src/game/gunfire.c, #ifdef PORT)."
D152,FIXED,"Mission-failed / objective-failed → permanent black screen (M-28, user bug report —","FIXED (user, 2026-09-26:"
D154,?,"bg.c room-geometry ray/hit-test GBI parser port (M-28, WRITTEN / UNVERIFIED / UNCOMMITTED).",M-30 re-audit + fixes (still playtest-gated for final verify).
D156,FIXED,"Facility outro-cutscene ""hang"" —","FIXED (user, 2026-09-26:"
D155,FIXED,"Facility outro-cutscene end → ""hang"" = unclamped deltaFrames spiral (M-29, user bug report).","FIXED (src/game/frametiming.c, #ifdef PORT —…"
D153,RESOLVED,"-level_09 frame ~900–1400 0xc0000005 = D117/D134 load flakiness, NOT a bug (M-28).",NOT A BUG — closed. Run anchors on a quiet machine.
D151,FIXED,Watch data all blank —,"FIXED (src/bondtypes.h, #ifdef PORT —…"
D130,RESOLVED,"crash class C2 (Facility -level_34, Runway -level_35):",resolved — stage all 6 N64 fields in a local f[6] before writing any.
D164,FIXED,"Disclaimer / legal screen only draws the first line (D76 root cause, M-31).",FIXED — corrected 2026-09-18:
D165,FIXED,"Front-end menu cursor doesn't feel like a mouse pointer (M-31, user bug B5 follow-up).","FIXED (port/src/input.c; Input.MenuPointerMode, Input.HipfirePitchSpeed added)."
D166,FIXED,Hipfire mouse pitch is digital while yaw is analog —,FIXED (port/src/input.c).…
D235,CLOSED,Wrong textures load after dying/restarting a level —,"CLOSED (user, 2026-09-26:"
D236,CLOSED,"Surface 1: 2D billboard trees render as a wall/sheet of tree texture instead of discrete camera-facing tree sprites (M-106, user QA report).","CLOSED (M-198, pass 26, 425237f9; probes stripped 0503d335):"
D237,FIXED,"QoL ask (M-106, user QA):","FIXED (2026-09-23): category headers + 5 more settings (SkipIntro, Show FPS, pad invert/deadzone/trigger); wheel scroll already existed."
D238,FIXED,"QoL ask (M-106, user QA):",FIXED (2026-09-26: covered by D337/D338 aim work; user reports mouse feel good; code-side validation owed).
D239,CLOSED,"QoL ask (M-106, user QA):",CLOSED — executed in the v0.2.0 prep pass (port-only)…
D240,CLOSED,"Player gunshot SFX timing/cadence reportedly does not match the N64 original's rate (M-106, user QA report).","CLOSED (v0.4.0 campaign sign-off 2026-09-28, user:"
D241,CLOSED,Player gunshot SFX goes silent under some conditions —,"CLOSED as the D207 family (v0.4.0 campaign sign-off 2026-09-28, user:"
D242,FIXED,Player-death sequence UX regression vs N64 original:,"FIXED/CLOSED (2026-09-26, user:"
D243,FIXED,"Dam end-of-level cutscene (the abseil/bungee sequence, D148/D160's subject):","FIXED, live-confirmed by the user (M-189/M-190, 2026-09-18)."
D245,FIXED,IsWater water: two competing patterns with a visible seam that moves as the player looks around (mouse/aim) —,"FIXED (M-201, 2026-09-26, user-verified vs 1964):"
D246,FIXED,"Tiny pixel strips at the left and right edges of the screen, ""like overscan"" —","FIXED (2026-09-18 session, port-only, Video.SafeAreaCrop)."
D247,FIXED,Black bars top and bottom of screen —,"FIXED (2026-09-18 session, port-only, Video.SafeAreaCrop)."
D248,FIXED,"Port runs a stable 30 FPS instead of 60 FPS game-wide (M-117, user asked to verify FPS after the D209 timing-keystone fixes).","FIXED, root-caused to a genuine pointer-width ABI bug in src/sched.c (compiled for the PC build, not excluded)."
D249,OPEN,"LOD/culling breaks down on large open levels (Streets, Egyptian named specifically) at *default* FOV —","OPEN — M-142 static review ruled out the unscaled-far-clip and LOD-multiplier suspects (both already correct); CLOSED (v0.4.0 campaign sign-off 2026-09-28, user:"
D250,FIXED,"FPS is inconsistent/low on many levels after 60 Hz shipped (M-119, user QA; D248 follow-on).","FIXED — per-call getenv() cost was the bottleneck; rock-steady 60-61 fps after, user re-tested 1440p/4K clean."
D251,FIXED,F10 options overlay QoL pass:,"FIXED (port-only, port/src/optionsoverlay.c)."
D252,FIXED,"Particle effects cycle through a rainbow palette (M-124, user playtest of the v0.2.0-pre candidate).","FIXED (M-201, 2026-09-26, user-verified):"
D253,FIXED,Facility (and any level with a FORCECROUCH/LADDER linked special tile) SIGSEGV on Linux/Steam Deck —,FIXED — Deck-verified:
D254,FIXED,Linux crash handler destroyed its own logs:,FIXED — field-verified.
D255,FIXED,Facility firefight SIGSEGV on Steam Deck (v0.2.0-pre CI bundle; D253 fix confirmed working).,"FIXED (M-140) — D3x-class 32-bit pointer truncation (vtxstore_fix_refs read Model* through the mistyped ChrRecord.chrflags 4-byte-enum alias instead of ObjectRecord.model), not the destroy-path race…"
D256,FIXED,Saves (and config) silently failed to persist on Linux whenever the game was not launched from the directory containing data/ —,FIXED (path half) — save-flow detail still owed from the user.
D257,FIXED,New Game.AllUnlocked option (F10; default flipped OFF for v0.2.0 —,FIXED — verified by user playtest (PR #79 merged); default flipped OFF for v0.2.0 (D259).
D259,FIXED,Game.AllUnlocked (D257) breaks audio on a fresh save:,FIXED in v0.2.1 —…
D260,FIXED,v0.2.1 (user request):,"FIXED — build green; one-line branch swap, trivially reviewable."
D261,FIXED,v0.2.1 (user request:,FIXED — feel-check received; W/S sensitivity follow-up logged above.
D262,FIXED,v0.2.1 (user report):,"FIXED (primary cause, D264) —"
D264,FIXED,"D262's root cause (ABI/layout class, cf.","FIXED — build green, probes removed; visual confirmation pending."
D263,RESOLVED,v0.2.1 (user report):,"RESOLVED (not a bug), refined (v0.2.1 further user testing, 2026-09-15)."
D265,CLOSED,"D236 investigation (port-layer, port/fast3d/gfx_pc.cpp):",CLOSED (2026-09-26 bookkeeping:
D258,PROPOSED,PAL/JP asset conversion is broken at the source-data level —,DEFERRED (user call 2026-09-28:
D284,FIXED,port/src/random.c's MIPS64 dsll32/dsrl32 shift helpers were backwards —,"FIXED, 21-level sweep clean, no crashes —"
D285,FIXED,Steam Deck SIGSEGV during D255 live-testing = SEPARATE bug:,"FIXED (user, 2026-09-26:"
D286,CLOSED,"User-confirmed real ~8s frame freeze on Caverns, Steam Deck —","CLOSED (2026-09-26, user-approved:"
D287,FIXED,"User-confirmed real, non-self-recovering freeze on Windows (M-141, 2026-09-15/16) after ~15-20 minutes with no input, triggered by clicking back into the… —",FIXED (user playtest 2026-09-26:
D288,CLOSED,"Silo intro: a huge, flat-shaded, untextured brown/tan triangle sweeps across the frame during the camera pan down the missile shaft (M-141, user report —… —","CLOSED — not reproduced on the current build (2026-09-23 capture) and the 2026-09-25 user Silo playtest confirms it gone (""silo fixed"")."
D289,FIXED,"Dam: a crash in the computer room, not reproducible on a second attempt (M-141, Steam Deck user playtest).","FIXED (user, 2026-09-26:"
D290,FIXED,"Watch's in-game ""controller"" section shows no control-pad graphic (M-141, Steam Deck user playtest).","FIXED (2026-09-23): D264-pattern template read in watchRenderController (flags read 0), plus 3 pointer-width/raw-Gfx sites on the same path; controller model verified rendering on Windows."
D291,CLOSED,"Dam: texture glitches when the truck drives past (M-141, Steam Deck user playtest).","CLOSED — duplicate of D306, faithful (M-201, 2026-09-26:"
D292,FIXED,Death animations are glitched —,"MOSTLY FIXED as a side effect of D243 (2026-09-18, user-confirmed alongside D173) —"
D293,FIXED,"QoL ask: no discoverable way to quit the game from the UI (M-141, Steam Deck user playtest —","FIXED (M-141) — ""Quit to desktop"" row added to the F10 overlay; build-verified Win+Linux, not yet live-confirmed.…"
D294,FIXED,"Statue: ground geometry near the large CCCP/hammer-and-sickle monument briefly renders solid black over a wide area, then self-recovers 10-30 seconds later… —","FIXED, user playtest confirmed (2026-09-25:"
D295,FIXED,"Campaign-halting crash at the end of Control Center on 00 Agent difficulty, then a crash on every subsequent relaunch/resume (GitHub #87, user campaign playtes…",FIXED (M-149) — M-148 root-caused + reproduced on HEAD; fix landed:
D296,CLOSED,"Control Center, 00 Agent:","CLOSED (2026-09-26, user-approved:"
D297,FIXED,v0.3.0 silently wipes ALL pre-fix player saves on first launch (D284 side effect; found during D295/M-148).,FIXED (M-150) — migration shim landed:
D298,CLOSED,Full HUD (not just the aim-mode crosshair) missing in a v0.3.0 dev build —,"CLOSED (2026-09-26, user:"
D299,FIXED,Game.SkipIntro crashed selecting any save slot except one —,"FIXED — fileValidateSaves() (only called from the skipped legal screen) never ran under SkipIntro, leaving saves[] all-zero; added the missing call to the SkipIntro shortcut (src/game/lv.c, #ifdef PO…"
D300,FIXED,Hipfire look feels non-uniform (issue #89):,"FIXED, user-confirmed live —"
D301,FIXED,"fileGetIsCheatUnlocked() NULL-deref, unguarded on every call site, not just the D299/SkipIntro one (GitHub #87, two reports with an identical crash PC/fault ad…",FIXED locally on release/v0.3.0 (not pushed) —
D302,FIXED,"Render FPS regression to ~40-45fps on release/v0.3.0 (user QA on the local v0.3.0 pre-release build, 2026-09-18) —","FIXED — 4 uncached getenv() calls added by D236 passes 10/12/14/16 directly inside gfx_sp_tri1 (port/fast3d/gfx_pc.cpp, per-triangle) cached with the same static int x = -1 pattern D250 established e…"
D303,FIXED,"Some muzzle flashes (M16 named specifically, ""and some others"") draw wrong —","FIXED (M-201, 2026-09-26):"
D304,FIXED,"Mouse sensitivity menu overhaul (user request, 2026-09-18):","FIXED — see full entry for both the range/parity pass and the same-day follow-up (collapsed to a single Input.MouseSensitivity menu row + fixed overlayUpdateScroll()'s unconditional bottom-anchor, th…"
D305,FIXED,"Steam Deck SIGSEGV on the v0.3.0 pre-release build (Cavern, early in the level, Game.AllUnlocked previously enabled on the session/save) —",FIXED (defensive guard) —
D306,CLOSED,"Dam intro/truck wheels render with odd transparent-looking areas, possibly z-fighting (user QA, 2026-09-18).","CLOSED — faithful, confirmed on 1964 (M-201):"
D307,FIXED,"In-level security camera entities can be flipped facing the wrong direction (backwards) on some levels, e.g.","FIXED (M-201, 2026-09-26, user-verified):"
D308,CLOSED,"Occasional z-fighting confirmed on some levels' geometry, general/broader than the D306 truck-wheel instance (user QA, 2026-09-18).","CLOSED — faithful (2026-09-26, user:"
D309,FIXED,Complete non-self-recovering freeze (Windows) returning from Caverns to the intro screen —,FIXED via D310 — user-playtest-confirmed 2026-09-18 (Caverns→intro clean; the spin record itself observed resuming correctly in the post-fix probe log).
D310,FIXED,"RULE-2-SIGNOFF: chraiitemsize() mis-sizes 1-byte AI_PRINT records (NUL-scans forward), corrupting AI execution for any list containing an active PRINT —",FIXED — user-playtest-confirmed 2026-09-18 (v2:
D311,FIXED,Complete freeze in modelSetAnimFrame2WithChrStuff root-motion loop —,FIXED — root cause D336 (bheadAdjustAnimation read never-written placeholder fields); the #ifdef PORT guard is retained as a safety net.
D312,FIXED,"Throwable items (mines, grenades, etc.) can clip through walls in certain spots (user report, v0.3.0 pre-release playtest, 2026-09-18).",FIXED — bgBuildRoomVtxBounds mis-reads the G_VTX vertex count on PC (Gfx_le.dma.par bit-layout shift):
D313,FIXED,Bullet impacts do not appear on some walls/surfaces —,"FIXED — same root cause as D312, merged diagnosis; same one-line fix."
D314,FIXED,"F10 menu items flicker randomly/intermittently (reported on the main-menu context, user report, v0.3.0 playtest build, 2026-09-18).",FIXED (user playtest 2026-09-26) —
D315,CLOSED,Menu scaling differs between main menu and in-game —,"CLOSED (2026-09-26, user:"
D316,FIXED,"Mouse-clicking some menu items glitches and lands on the item below instead (user report, v0.3.0 playtest build, 2026-09-18).",FIXED — root-caused to two compounding bugs (naive mouse->logical mapping ignoring the safe-area crop; asymmetric row hit-test band) in port/src/optionsoverlay.c + port/fast3d/gfx_pc.cpp.
D317,CLOSED,"Crash when toggling Game.AllUnlocked back and forth in-game (user report, v0.3.0 playtest build, 2026-09-18) —","CLOSED — symptom of D316 (confirmed instant window close, no WER report; the D316 hit-band bias let a click on ""All unlocked"" fall through to the adjacent ""Quit to desktop"" row)."
D318,FIXED,Facility (regular play path):,FIXED via D329 (2026-09-26 bookkeeping:
D319,FIXED,"Frame cap slider was mostly a dead zone; replaced with a plain 30/60 toggle (user ask, 2026-09-18).",FIXED — new ROW_FPSCAP cycle (30/60 only) in port/src/optionsoverlay.c; default changed to 60 in port/src/video.c.
D320,FIXED,"D318-class sweep: aim-hold → _update AI lists in 8 levels carry the same as-authored softlock race (static analysis, 2026-09-20).",FIXED via D329 (2026-09-26 bookkeeping:
D321,FIXED,D318 trigger chain fully mapped:,FIXED via D329 (2026-09-26 bookkeeping:
D322,FIXED,Long-session audio degradation:,"FIXED (Windows) for the periodic ""garbled"" bursts —"
D323,CLOSED,G_TL_LOD bindings of explicit-LOD textures:,"CLOSED — parked as a fidelity enhancement, not a bug (2026-09-26, user-approved:"
D324,FIXED,"GE's <stdlib.h> stub declares nothing, so TUs that include it get NO CRT prototypes —",FIXED (2026-09-22) — proven impact:
D325,CLOSED,ROM-mod (xdelta) compatibility:,CLOSED as a question —
D326,CLOSED,Random Eye randomizer repacks the ROM too —,CLOSED as a question —
D327,RESOLVED,"Mod support, resolved plan:",RESOLVED — superseded by D328; approach (a) is NOT ruled out (these scans missed the 1172-compressed data segment).
D328,CLOSED,"Mod support is viable and D327's ""ruled out"" was wrong:",CLOSED as a question —
D329,FIXED,The D318/D320 guard anim pin is a tick-granularity divergence:,"FIXED — Rule-2 high-FPS fix in model.c; clk=1 no longer pins (WSL + native Windows), 0 triggers at clk=2; bunker1 golden failure predates it (on/off A/B); user playtest confirms N64-matching Ourumov…"
D330,FIXED,"#103 (v0.3.0, Win 11):",FIXED (port/src/input.c) —
D331,FIXED,Ejected shell casings never rendered on PC:,"FIXED (gunfire.c #ifdef PORT, explicit template); PP7 casing verified on native Windows (before:"
D332,FIXED,Reticule jitter in aim mode (RMB) —,FIXED (fix/mouse-pd-model) —
D333,FIXED,"#104 (v0.3.0, Linux):",FIXED (2026-09-26 bookkeeping:
D334,FIXED,"Native widescreen, world side (WIDESCREEN-FOV-PLAN Option B Phase 2):",FIXED (2026-09-26 bookkeeping:
D335,FIXED,"Native widescreen, HUD + front end (WIDESCREEN-FOV-PLAN Phase 3):",FIXED (2026-09-26 bookkeeping:
D336,FIXED,"Frigate (and any level) ""all polygons break except the gun"" after turning:","FIXED (src/game/bondhead.c #ifdef PORT, ABI/layout); headless repro (Runway → exit → Frigate → turn, D294 2× room pool) clean, D156 0 fires; golden 3/3; Win + Linux builds."
D337,FIXED,Aim modes folded (user decision):,FIXED (2026-09-26 bookkeeping:
D338,FIXED,"Input.AimRange (F10 ""Aim range""):",FIXED (2026-09-26 bookkeeping:
D339,OPEN,"#92 (low-end ~30 fps, Celeron N3060 / HD 400) —","OPEN (2026-09-26, user:"
D341,FIXED,"Intro gun-barrel blood drip draws as static blocks/lines instead of dripping (user, v0.4.0, 2026-09-26; death blood fine).","FIXED (M-201, user-verified 2026-09-27):"
D342,FIXED,"File select: folder bodies and Bond photos vanish after selecting a file and backing out (user, v0.4.0, 2026-09-26).","FIXED (M-201, user-verified):"
D343,FIXED,"File select: PC ""Options"" label opens the settings overlay (v0.4.0 feature, M1 of the route-B options screen, 2026-09-27); also fixes F10 on file select jumpin…","FIXED (M1, closed): one Rule-2 #ifdef PORT draw hook (user sign-off 2026-09-27); headless + user-verified (mouse, F10, >30 s, widescreen, physical controller on the bottom-bar label and options scree…"
D344,FIXED,Host PC bugchecks (BSOD 0x119 VIDEO_SCHEDULER_INTERNAL_ERROR) during game test runs; probable cause:,"FIXED (probable cause; orderly quit + GE_QUITFRAME), headless + user-verified; no dump, watch for recurrence."
D345,FIXED,"Front-end crosshair snaps away from the pointer on screen/page changes (file select → mode select setCursorPOSforMode(0), front.c:2516; every MENU_PC_OPTIONS p…","Port-only, user-verified (parts a–g):"
D346,CLOSED,PC-settings wording pass:,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
D347,FIXED,F10 overlay controller:,"FIXED (port-only, optionsoverlay.c):"
D348,?,"File-select ""PC Options"" label:",SUPERSEDED by D398 (glyph replaced with the IMG_DOT texture; no new art).
D349,CLOSED,GE watch settings are not normally saved to the Bond file:,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
D350,CLOSED,Watch-backed PC options:,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
D351,FIXED,Existing D346 value/unit rows had positional initializers shifted into hidePtr:,"FIXED (port-only): designated row initializers restore unit, deadzone divisor and hide-if-auto fields; build clean, headless smoke clean."
D352,FIXED,D350 review of 1303c0eb:,"FIXED (port-only, port/src/watchsettings.c):"
D353,FIXED,Turok-style settings regrouping:,"FIXED (port-only, optionsoverlay.c/frontoptions.c):"
D354,FIXED,Front-end F10 Bond rows silently disabled:,"FIXED (port-only, port/src/watchsettings.c):"
D355,FIXED,F10/options Music slider inaudible in-stage:,"FIXED (port-only, port/src/watchsettings.c):"
D356,FIXED,"Settings regroup: Turok-style pages renamed to functional sections (INPUT/GAMEPLAY/GRAPHICS/AUDIO/VIDEO) with PD-style ""(File N)"" scoping annotations, the redu…","FIXED (port-only, port/src/optionsoverlay.{h,c} + port/src/watchsettings.{h,c} + port/src/frontoptions.c; plan docs/dev/D356-SETTINGS-REGROUP-PLAN.md):"
D358,FIXED,D356 level-1 overlap:,"FIXED (port-only, port/src/frontoptions.c):"
D359,CLOSED,"Settings UI file terminology settled on the end-user term ""Profile"":",CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
D360,CLOSED,F10 INPUT heading scrolled away; F10 navigation and distance defaults (2026-09-27).,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
D361,FIXED,Main-menu F10 Audio freezes the game:,"FIXED (port-only, port/src/watchsettings.c + F10 probe in optionsoverlay.c):"
D362,CLOSED,F10 GoldenEye dossier-style visual refresh (2026-09-27).,CLOSED (superseded by D363 after user playtest):
D363,FIXED,F10 return to a dark overlay with the N64 watch's green/Bank Gothic visual language (2026-09-27).,"FIXED (palette/font user-approved, 2026-09-27); D364 follows up on profile tags, click alignment and slider calibration."
D364,FIXED,Settings UI feedback:,FIXED (user mostly accepted; D365 follows up on annotation size and click targets).
D365,CLOSED,F10 annotation hierarchy and visible mouse change targets (2026-09-27).,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
D366,CLOSED,Input calibration label:,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
D367,?,F10 full-width control boxes misaligned with their contents (2026-09-27).,SUPERSEDED by D368 after user playtest:
D368,?,Simplify F10 click targets:,"INPUT RULE KEPT; tinted strips user-rejected, removed in D369."
D369,FIXED,Remove F10 row boxes/strips entirely (2026-09-27).,FIXED (awaiting live acceptance):
D370,CLOSED,F10 distance/volume drags hurt frame rate (2026-09-27).,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
D371,FIXED,"GEPD key-layout preset + reload/crouch bindings, v0.4.0 modern options wave M3 (2026-09-27).",FIXED (headless-verified; awaiting user live accept):
D372,FIXED,"Low-end perf preset (v0.4.0 modern options wave M5, 2026-09-27).",FIXED (headless-verified; awaiting user live accept):
D373,FIXED,"In-game crosshair on/off + colour (v0.4.0 modern options wave M2, 2026-09-27).",FIXED (user live-tested; rule-2 sign-off recorded 2026-09-27):
D374,FIXED,"GEPD becomes the default key layout; preset layering fix (v0.4.0 M3 playtest feedback, 2026-09-27).",FIXED (headless + user live-tested):
D375,CLOSED,Crouch wired to the wrong pad bit:,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
D376,CLOSED,Crouch follow-up + modern-options spot check (2026-09-27).,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
D377,FIXED,GEPD independent crouch (2026-09-27).,FIXED (scripted stance verified; user says live crouch was good; pad/hold-vs-toggle specifics owed):
D378,FIXED,GEPD use/reload separation (2026-09-27).,FIXED (headless edges tested; user says E/R seemed good; targeted door/partial-mag cases owed):
D379,FIXED,Crosshair colour: authored texture is red (2026-09-27).,FIXED (cyan frame capture; live yellow/blue recheck owed):
D380,FIXED,One PC keyboard layout; PD-style rebind design (2026-09-27).,FIXED (follow-up D383):
D381,CLOSED,"RULE-2-SIGNOFF: opt-in RGB, size and Beta crosshair (2026-09-27).",CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
D382,CLOSED,Crosshair menu placement and slider hitches (2026-09-27).,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
D383,PARTIAL,PD-style keyboard rebinding in both options UIs (2026-09-27).,PARTIAL (2026-09-28).
D384,CLOSED,Front PC Options key-bind navigation feedback + two-slot UI (2026-09-27).,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
D385,CLOSED,Real mouse 1–5 action rebinding in the two-slot editor (2026-09-27).,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
D386,CLOSED,Correct Aim default from SDL middle (Mouse 2) to right (Mouse 3) (2026-09-27).,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
D387,OPEN,M7 AllUnlocked's read-time fake unlocks leak into saved EEPROM (2026-09-27).,OPEN (isolated user-data-copy save/untoggle repro; no fix):
D388,CLOSED,Nest functional binding/HUD menus and remove redundant M5 toggle (2026-09-27).,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
D389,CLOSED,F10 footer clipping and ambiguous submenu links (2026-09-27).,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
D390,CLOSED,F10 pinned Back selection did not visibly change (2026-09-27).,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
D391,CLOSED,F10 click at glyph bottom could select next row (2026-09-27).,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
D392,FIXED,Train intro soldier hunched; hand intersects torso vs 1964 —,FIXED (#ifdef PORT ABI/layout in chraction.c); Train aim-angle probe confirms correction; user visually verified the soldier looks fixed against 1964.
D393,FIXED,Controller B/Y both crouch; no in-game use button —,FIXED + user live-tested A use / X reload / Y weapon cycle.
D394,PARTIAL,Xbox 1.1 Jinx controller parity —,PARTIAL (2026-09-28).
D395,RESOLVED,Controller trapped in keyboard/mouse-only binding capture —,"RESOLVED (v0.4.0 campaign sign-off 2026-09-28, user:"
D396,RESOLVED,"F10 pad B/A used as slider decrement/increment, not back/select —","RESOLVED (v0.4.0 campaign sign-off 2026-09-28, user:"
D397,RESOLVED,SELECT FILE background + intro gun-barrel show an interlaced/comb look —,RESOLVED (2026-09-28):
D398,FIXED,PC Options label: D348's procedural slider glyph replaced with an existing mainfolderimages texture (2026-09-27).,"SUPERSEDED by D400 (icon removed, text-only label; purple-hover bug moot; the D400 textMeasure arg-swap bug was found and fixed there)."
D399,PARTIAL,File-select bottom row (SELECT FILE text + Copy/Erase + PC Options dot/label) shifted 15px left as a block (user sign-off 2026-09-27).,"LANDED (port-only, D343 class):"
D400,PARTIAL,"File-select ""PC Options"" label:","LANDED (port-only, frontoptions.c):"
D401,CLOSED,Rumble Pak → real gamepad haptics (re-promotes D224/M-87):,CLOSED (2026-09-28). Rumble shipped in v0.4.0 (D401); Steam Deck real-pad validation:
D402,RESOLVED,First clean Linux (glibc / SteamOS) build of release/v0.4.0 broke in two port-layer include-resolution spots MinGW tolerated (bondconstants.h first-include cyc…,RESOLVED (Linux/Ubuntu-24.04 build → glibc-2.41-compatible binary for the Steam Deck):
D403,RESOLVED,Deck v0.4.0-pre test:,"RESOLVED (2026-09-27, user-confirmed on the Deck:"
D404,RESOLVED,"""Aim style: Centred (PC)"" (Input.AimMode=1, D333/D337) has no effect when aiming with a gamepad; the row's own doc comment over-promises (""applies to every aim…","RESOLVED (2026-09-28, port-layer only):"
D405,CLOSED,"fast3d/OpenGL: MSAA above the driver max black-screens; two glBindFramebuffer calls pass an index where a GL name is required (PR #106, italoarruda, 2026-09-25…","CLOSED (2026-09-28). Merged from PR #106 (code file only; security-reviewed clean - port-layer GL state fixes, no external input, no allocations)."
D406,RESOLVED,front-end PC Options screen:,"RESOLVED (2026-09-28, port-layer only, Steam Deck-validated):"
D407,PARTIAL,Tanks cannot be boarded/exited on PC —,"PARTIAL (fix landed in v0.4.0, port-only):"
D408,OPEN,Game.SkipIntro skips the post-mission failure dossier:,"OPEN (cosmetic, not root-caused past the handoff site)."
1 # GENERATED by tools_pc/gen_findings_index.py -- do not hand-edit.
2 # Grep this to pick which findings.md entries to read; it is not authoritative.
3 label bucket one_liner status_raw
4 D24-D30 RESOLVED green threads / kernel / crash handler resolved
5 D31-D42 RESOLVED boot ABI: ANIM_DATA, music seq-table, libaudio banks, Globalimagetable rebase, GL context, rsp toggle resolved
6 D43-D50 RESOLVED model-file loading → offline sidecar "Plan B" (d43_emit.py); D50.6 texCopyGdls full-slot copy resolved
7 D51-D58 RESOLVED font pixeldata, osGetCount tick rate, model RW pools, cseq BE header, synth slots, RLE menu bg, watch raw offsets, gun-barrel DL resolved
8 D59-D68 RESOLVED intro render: blood-RLE clobber, DMA validate, OSMesgQueue, HEADS/BODIES sentinels, romCopy width, image_entry, Globalimagetable BE→LE resolved
9 D70-D74 RESOLVED intro-logo pixels: C-array bswap, UV path, sinf/cosf DVAL(), texture-import truncation resolved
10 D75 PARTIAL front-end 3D model transforms (D77 audio split out below — PARTIAL (M-197 fixed the logo texture blobs; v0.4.0 campaign 2026-09-28:
11 D76 · D164 FIXED Legal screen only draws line 1 — FIXED — this row and the ## D164 entry below were stale; the proposed one-line #ifdef PORT fix was actually committed 8dfc578f4 (2026-08-31), well before this correction (2026-09-18).
12 D159 FIXED front-end wallet-Bond photo "interlaced"/combed (RC1 / D149) — FIXED (#ifdef PORT no-op the swap in image.c)
13 D161 FIXED Depot (-level_30) ceiling = bright-blue speckle + radial rays (B2). FIXED (port/fast3d/gfx_pc.cpp)
14 D165 · D166 FIXED Input-layer polish (M-31, port-only): FIXED
15 D180 · D181 FIXED Native-PC input pass (QoL run): FIXED (2026-09-26 bookkeeping:
16 D186 OPEN Video.FpsCap throttles the sim, not just presentation (M-39 diag, M-42 clamp). OPEN (2026-09-26, user direction:
17 D187 FIXED HUD ammo-type/weapon icon vanished at -O2 (M-46 root-cause, M-47 fix + verify). FIXED (src/game/gunfire.c, #ifdef AVOID_UB); PR #19
18 D188 FIXED Linux -O2 boot crash: FIXED (src/libultrare/libc/xprintf.c, src/sprintf.c, #ifdef PORT); PR #22
19 D189 FIXED Linux -fstack-protector aborts on the decomp's latent stack-buffer overruns (M-48) — FIXED (CMakeLists.txt, src/game/stan.c #ifdef PORT); PR #23
20 D190 CLOSED Linux -level_45 SIGSEGV at load in add_ptr_to_objective (objective.c:64) (M-48, open). CLOSED — not a port bug; level 45 is MP-only and has no solo setup file in the ROM (root-caused M-105, reproduced on Windows).
21 D191 FIXED Windows -O2 release-bundle crash: FIXED + PLAYTEST-VERIFIED (M-79).…
22 D193 FIXED NPC AI locomotion is too slow — FIXED + user-playtest-verified (M-81) —…
23 D194 FIXED Mouse input needs another tuning pass — FIXED (2026-09-26: user reports the v0.3.0→v0.4.0 mouse changes feel solid and glitch-free; code-side validation owed).
24 D195 FIXED Transparency / alpha surfaces broken on Control (M-49, user playtest) — FIXED (port/fast3d/gfx_pc.cpp, gfx_sp_vertex) —…
25 D197 CLOSED Character face/head textures wrap around the head, seen on Silo (M-49, user playtest). CLOSED — user playtest v0.2.0-pre
26 D198 FIXED alAdpcmPull/_decodeChunk used K0_TO_PHYS (an unconditional & 0x1FFFFFFF mask) on 64-bit heap pointers (src/libultra/audio/load.c). FIXED (src/libultra/audio/load.c) —
27 D199 FIXED Phase-3 software audio mixer: FIXED (2026-09-26 bookkeeping:
28 D200 FIXED -level_09 segfault: reverb's ALDelay.input/output are u32 — FIXED — 5-line cast in reverb.c (this session)
29 D196 FIXED OS mouse cursor stays visible after closing the F10 options overlay in click-to-lock mode (M-49, user playtest). FIXED (6e758f1d, merged to main) —…
30 D192 CLOSED Front-end mission/level-select grid pointer still can't reach the outer cells — CLOSED — the buggy mode 0 was removed entirely in the v0.2.0 prep pass (see D239); click-to-lock is now the only capture mode, so the unreachable-cells path no longer exists
31 D221 FIXED SELECT FILE mouse hit-box offset — FIXED (M-91) — explicit {min,max} coord2d pairs under #ifdef PORT; human mouse-playtest confirmed.
32 D222 FIXED High Video.FovScale breaks culling — FIXED, awaiting visual verification (M-121).
33 D223 FIXED QoL ask: mouse-wheel weapon cycling should be directional like a normal PC shooter — FIXED (M-105, port/src/input.c) —…
34 D224 PARTIAL QoL gap (M-87, from a PD-PC-port survey): LANDED via D401 (2026-09-27, re-promoted from DEPRIORITIZED).
35 D225 FIXED Dev-tooling QoL (M-87, user ask): FIXED (2026-09-18) — GE_DEBUG_UNLOCKALL=1 short-circuits fileGetIsCheatUnlocked() (src/game/file2.c, #ifdef PORT, cached getenv) to always return TRUE; default off, no save mutation.
36 D226 FIXED QoL ask: player-adjustable HUD scale for the ammo counter, bottom-left pickup/item status text, and top-of-screen dialogue/subtitle text. FIXED (2026-09-24): Game.HudScale 75–150% (user-verified, capped at 150) via fast3d G_HUDSCALE_EXT rect scaling about per-element anchors; dialogue/bottom messages width-capped.
37 D227 FIXED Sky renders as two independently-moving halves — FIXED (M-100b, PR #43) —
38 D229 FIXED IsWater-level water renders green, pulses blue<->green (M-101; Frigate -level_26). FIXED (port-layer) — ZLIB+CI8 water image now imports via the CI8 palette path (gfx_pc.cpp); Frigate verified live ("looks better"); sweep + Surface 2 by-eye still owed.
39 D230 FIXED In-level music quality: FIXED (M-121, v0.2.0) —
40 D231 CLOSED QoL batch: mute-on-focus-loss + screenshot-hotkey productization (M-105, roadmap Tier 3, port-only). CLOSED — PARKED/DEPRIORITIZED (user, 2026-09-25).
41 D233 FIXED All levels (intermittent): FIXED (port/fast3d/gfx_pc.cpp, gfx_sp_tri1) —…
42 D234 FIXED Dam: guard towers along the dam structure invisible except the first one (M-106, user QA report). VISUALLY VERIFIED FIXED (M-108) —…
43 D244 OPEN Self-hosted CI runner (corpyvt6agent01) link failure: OPEN, not yet fixed. Next…
44 D228 FIXED AK47 white missing-texture patches after D217's texpool fix — FIXED (port/fast3d/gfx_pc.cpp, palette_to_rgba32) —
45 D204 FIXED PC audio ran ~2 % below real time permanently (AI feedback loop's 3 ms setpoint vs PC jitter). FIXED (port/src/audio.c, port/src/libultra.c, port/include/audio.h) —
46 D203 CLOSED Steam Deck (SteamOS, x86_64 Linux) v0.1.0 release bundle: CLOSED — root cause + fix in D253.
47 D201 FIXED src/libultra/audio/bnkf.c bank/instrument/sound/wavetable relocation offsets were s32, truncating the real 64-bit base pointer. FIXED (src/libultra/audio/bnkf.c) —
48 D202 FIXED Silenced PPK "slap" + broader real-time mixer corruption (M-52→M-67). FIXED (2026-09-26, user by-ear:
49 D206 FIXED PC played every SFX one bank slot too high — FIXED (M-76, src/snd.c:1001, #ifdef PORT) —
50 D207 FIXED Alarm klaxon starves combat SFX and doesn't recover after it stops (surfaced by D206, M-77). MONITORING — user 2026-09-26:
51 D208 FIXED Player weapon-fire sound goes permanently silent mid-firefight (all other SFX unaffected) — FIXED + user playtest-verified (M-78d) —
52 D209 FIXED Every AI character in the game is locked to a *walk* animation regardless of the speed the ailist commands — FIXED + user-playtest-verified (M-81).
53 D210 FIXED chrToPatrol() leaves act_patrol.lastvisible60 uninitialised on PC — FIXED (chraction.c:3872, #ifdef PORT → self->act_patrol.lastvisible60 = -1;, N64 line under #else).…
54 D205 CLOSED PC plays wrong/extra SFX for gameplay events (gunfire slap layer, explosion→scream, armour pickup, melee→Klobb idx 106) — CLOSED (M-78) — root cause was D206; all four symptoms resolved + user-confirmed by ear.
55 D152+ FIXED D152 addendum (M-31): FIXED (src/snd.c, port/src/libultra.c, #ifdef PORT) —
56 D168 FIXED GE_PCDUMP / F12 PPM captures were vertically flipped — FIXED (port/fast3d/gfx_opengl.cpp); D114 / D116 reclassified as capture-orientation artifacts (below).
57 D169 FIXED Front-end mouse pointer can't reach the outer cells of the mission/level-select grid — FIXED (M-33, port/src/input.c, port-only, no #ifdef PORT).
58 D178 FIXED Pre-mission briefing screen: FIXED (M-36) — high confidence
59 D175 CLOSED In-game stutter / brief hang during normal play — CLOSED (v0.4.0 campaign sign-off 2026-09-28, user:
60 D176 FIXED Surface exterior renders wrong (-level_36), two independent defects. (a) scroll-speed FIXED (fidelity pass; HIGH confidence on root cause, MODERATE on N64 parity); M-93 playtest + M-98 Defect-2 starburst screenshot in the sections.
61 D177 FIXED Ladders non-functional — FIXED (M-36) — high confidence; interactive climb test owed
62 D172 FIXED Bullet-impact / blood / spark particles render magenta or cyan instead of dark red (M-82, ROOT CAUSE FOUND + FIXED). FIXED (port/fast3d/gfx_pc.cpp gfx_lod_tile_offset)…
63 D174 CLOSED "No blood effect" (user QA report). CLOSED — faithful (2026-09-26, user side-by-side on 1964:
64 D183 CLOSED M-36 Family A ("texture line/pitch shear") is DISPROVEN for the Surface repro (-level_36). CLOSED (2026-09-26, user-approved:
65 D173 FIXED Third-person Bond model spawns too high — MOSTLY FIXED as a side effect of D243 (2026-09-18, user-confirmed).
66 D179 FIXED Packaged build crashes after the logos — Diagnosed, not fixed —
67 D180 FIXED Native-PC input pass (M-XX QoL run, qol/native-pc-input-menu, port-only). FIXED (2026-09-26 bookkeeping:
68 D181 FIXED Game.ScreenShakeIntensity — FIXED (2026-09-26 bookkeeping:
69 D184 FIXED F10 port-layer options overlay — FIXED (2026-09-26 bookkeeping:
70 D211 FIXED PC FOV slider — Video.FovScale (M-83 QoL wave, port-only, WIDESCREEN-FOV-PLAN.md Phase 4). FIXED (2026-09-26 bookkeeping:
71 D212 FIXED Anisotropic-filtering config wiring — FIXED (2026-09-26 bookkeeping:
72 D213 FIXED On-screen FPS readout — FIXED (2026-09-26 bookkeeping:
73 D214 FIXED Keyboard rebinding — [Input.Bind] config section (M-83, port-only, PD parity, Phase 4 debt). FIXED (2026-09-26 bookkeeping:
74 D215 FIXED 1P weapon viewmodel never drew in-level — FIXED (src/game/gunfire.c, #ifdef PORT).
75 D216 FIXED Game.SkipIntro — boot straight to the file-select menu (M-83, port + one #ifdef PORT game-code line, PD parity, Phase 4 quick win). FIXED (2026-09-26 bookkeeping:
76 D217 FIXED 1P weapon viewmodel + 3P Bond model textures garbled — FIXED + merged (PR #47 + PR #49); white residual split out as D228.
77 D218 FIXED Wide FOV exposes the per-level draw-distance / fog boundary — FIXED (2026-09-26 reclassified:
78 D219 RESOLVED PPK explosions render purple on Dam (M-87, user QA, "not 3D but noting"). RESOLVED (M-115, 2026-09-12) for the original PPK-purple symptom.
79 D220 CLOSED 1P muzzle flash position shifts upward at higher Video.FovScale (M-87, user QA, Jungle/M16). CLOSED — merged into D303 (2026-09-26, user:
80 D85 CLOSED Room primary/secondary DL → texLoadFromGdl garbage. CLOSED (2026-09-26, user-approved:
81 D86 · D87 RESOLVED modelInitRwData truncated ptr · attract-demo BE ramromfilestructure. resolved
82 D88.4 RESOLVED propDefs polymorphic record stream not byteswapped → setupDoor crash. resolved (d88_propdefs.py); layout audit M-20 → D132
83 D132 PROPOSED D88 propDefs layout audit (M-20): proposed fix (not applied) —
84 D157 FIXED Campaign never unlocks the next level (M-30, user bug — FIXED (src/bondtypes.h, #ifdef PORT —…
85 D160 FIXED Dam level-end cutscene (Bond off the dam / bungee) never plays — FIXED (user, 2026-09-26:
86 D89-D92 RESOLVED stage-load→frame: stan zero-fill overrun, portal address trunc, chr/AI spawn ptrs resolved
87 D93-D102 RESOLVED struct-pun / hardcoded-size pass: resolved (D95 pool bump partial)
88 D103-D107 RESOLVED BUNKER1 viewport height, depth-buffer clear (G_CLEAR_DEPTH_EXT), portal near-plane, LOD mip tile resolved
89 D108-D112 RESOLVED skeletal models: d43_emit.py put_f32 byte-reversal bug resolved
90 D113-D116 FIXED portal BFS ok · matrix chain ok · player raw-offset audit + gunfire.c THROW* · HUD text X-mirror D114/D116 CLOSED — NOT A BUG (M-33, see D168):
91 D117 OPEN Frame nondeterminism root-caused; GE_DETERM attempted, not achieved. OPEN (mode attempted, not achieved)
92 D118 RESOLVED SDL input layer; M-24 mouse-look rework (mode-aware map, real INI); D118b/c FIXED. resolved (D118a residual)
93 D150 FIXED watch OBJECTIVES / BRIEF page crash: FIXED (src/str.c #ifdef PORT); interactive re-verify pending
94 D260-D265 CLOSED v0.2.1 hotfix batch: D260 wheel direction swap (fixed) · D261 watch-menu nav auto-repeat (fixed, feel-check owed) · D262 watch item-preview model missing → roo… CLOSED (2026-09-26 bookkeeping:
95 D266-D269 CLOSED D236/D265 wall, second pass: CLOSED (2026-09-26 bookkeeping:
96 D270 CLOSED Backfilled (M-132) — this label was never given its own row; only code comments and the HANDOFF archive documented it. CLOSED (2026-09-26 bookkeeping:
97 D271 FIXED D236 wall, third pass — FIXED-but-insufficient (src/game/bg.c #ifdef PORT) —
98 D276 CLOSED D236 wall, eighth pass — CLOSED (2026-09-26 bookkeeping:
99 D279 CLOSED Backfilled — this label was consumed by an undocumented partial session (2026-09-14 ~23:00) that left artifacts in scratch/ (d279_run.log… — CLOSED (2026-09-26 bookkeeping:
100 D281 FIXED Game.AllUnlocked (D257) still breaks audio, and can also break right-mouse aim, when enabled before any save has ever been written (v0.2.1 user testing… — FIXED (2026-09-23, port/src/libultra.c):
101 D282 FIXED QoL ask (v0.2.1 user testing, 2026-09-15): FIXED (port/src/input.c, #ifdef not needed —
102 D283 OPEN Steam Deck first-run preset (D-unnumbered in port/src/video.c/main.c, videoApplySteamOSDefaults()) did not apply for a user who added the game via Steam… — OPEN — mechanism understood, root cause (Desktop-vs-Game-Mode STEAMOS env var difference) unverified on real hardware; user-facing workaround documented in README/release notes.
103 D280 CLOSED D236 wall, ninth pass — CLOSED (2026-09-26 bookkeeping:
104 D275 CLOSED D236 wall, eighth pass — CLOSED (2026-09-26 bookkeeping:
105 D274 CLOSED D236 wall, seventh pass — CLOSED (2026-09-26 bookkeeping:
106 D273 CLOSED D236 wall, fifth pass — CLOSED (2026-09-26 bookkeeping:
107 D272 CLOSED D236 wall, fourth pass — CLOSED (2026-09-26 bookkeeping:
108 D137 FIXED right-mouse (aim-sight) crash: FIXED (src/game/gunfire.c #ifdef PORT —
109 D119 · D120 FIXED guard-attack weapons_held[]->chr pun crash · blood-stain PointUsage[] chain hang D119 fixed · D120 FIXED (M-30)
110 D120 FIXED Blood-decal PointUsage[] chain — FIXED (tools_pc/d43_emit.py).
111 D121 RESOLVED WS1 frictionless per-level boot (bare -level_XX injects its -m* row). resolved
112 D122 FIXED per-level prop/item model-load crash (Dam/Facility/Runway modelLoad/modelInitRwData): converter fixed; residual chr/fast3d crashes on those levels are separate
113 D123 FIXED crash class C1: chrIsNotDeadOrShot NULL deref on 6 levels (Dam/Runway/Frigate/Statue/Streets/Cradle). converter fixed (OBJ_ID_WORDS); C1 cleared on all 6, residual crashes are fast3d (C2)
114 D125 RESOLVED crash classes C3+C6 (Aztec/Bunker2/Surface2): resolved (M-16b) — line 374 → 8 NULs; all 21 sidecars regen'd; Aztec -level_28 now PASSES (was C3 CRASH).
115 D126 RESOLVED crash classes C3r/C4/C6 (Bunker2 -level_27 door7F054FB4 propobj.c:13523, Depot -level_30 prop.c:902, Surface2 -level_43 loadobjectmodel.c:393): resolved — d88_propdefs.py PROPDEF_PC_BYTES[30/32/33/35]=24 + typed handler; loadobjectmodel.c sizepropdef PORT returns 6.
116 D124 FIXED crash class C2: fast3d bad texture pointer. Jungle fixed (port/src/gimgfixup.c); Facility → D130
117 D131 RESOLVED crash class C2m (Jungle -level_37): resolved — 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 all handled earlier).…
118 D133 RESOLVED intro render triage (M-20): RESOLVED — not a regression, parked (D75/D76, GRAPHICS-BACKLOG.md)
119 D134 FIXED Frame-2 boot hang / sweep "NO-FRAMES" flakiness — FIXED (port/src/libultra.c)
120 D135 FIXED Firefight crash — bgTestHitOnObj (propobj.c:8446) is an unported N64 GBI parser (M-22, WS6 playtest). FIXED (src/game/propobj.c, #ifdef PORT —…
121 D136 RESOLVED Stage-unload crash — objFree obj->prop reads float garbage (M-22, WS6 Facility playthrough). ROOT-CAUSED → D139 (it was NOT a layout bug —
122 D139 FIXED Stage-unload crash root cause + fix (M-23). FIXED (src/game/cleanup_objects.c, #ifdef PORT —…
123 D140 FIXED Pause-menu crash (M-23 open → M-27 fixed). FIXED (src/game/bondview.h, src/game/bondview2.c; #ifdef PORT).
124 D141 FIXED Watch-menu item-model crash (M-27), exposed by D140. FIXED (src/game/gunfire.c, #ifdef PORT).
125 D152 FIXED Mission-failed / objective-failed → permanent black screen (M-28, user bug report — FIXED (user, 2026-09-26:
126 D154 ? bg.c room-geometry ray/hit-test GBI parser port (M-28, WRITTEN / UNVERIFIED / UNCOMMITTED). M-30 re-audit + fixes (still playtest-gated for final verify).
127 D156 FIXED Facility outro-cutscene "hang" — FIXED (user, 2026-09-26:
128 D155 FIXED Facility outro-cutscene end → "hang" = unclamped deltaFrames spiral (M-29, user bug report). FIXED (src/game/frametiming.c, #ifdef PORT —…
129 D153 RESOLVED -level_09 frame ~900–1400 0xc0000005 = D117/D134 load flakiness, NOT a bug (M-28). NOT A BUG — closed. Run anchors on a quiet machine.
130 D151 FIXED Watch data all blank — FIXED (src/bondtypes.h, #ifdef PORT —…
131 D130 RESOLVED crash class C2 (Facility -level_34, Runway -level_35): resolved — stage all 6 N64 fields in a local f[6] before writing any.
132 D164 FIXED Disclaimer / legal screen only draws the first line (D76 root cause, M-31). FIXED — corrected 2026-09-18:
133 D165 FIXED Front-end menu cursor doesn't feel like a mouse pointer (M-31, user bug B5 follow-up). FIXED (port/src/input.c; Input.MenuPointerMode, Input.HipfirePitchSpeed added).
134 D166 FIXED Hipfire mouse pitch is digital while yaw is analog — FIXED (port/src/input.c).…
135 D235 CLOSED Wrong textures load after dying/restarting a level — CLOSED (user, 2026-09-26:
136 D236 CLOSED Surface 1: 2D billboard trees render as a wall/sheet of tree texture instead of discrete camera-facing tree sprites (M-106, user QA report). CLOSED (M-198, pass 26, 425237f9; probes stripped 0503d335):
137 D237 FIXED QoL ask (M-106, user QA): FIXED (2026-09-23): category headers + 5 more settings (SkipIntro, Show FPS, pad invert/deadzone/trigger); wheel scroll already existed.
138 D238 FIXED QoL ask (M-106, user QA): FIXED (2026-09-26: covered by D337/D338 aim work; user reports mouse feel good; code-side validation owed).
139 D239 CLOSED QoL ask (M-106, user QA): CLOSED — executed in the v0.2.0 prep pass (port-only)…
140 D240 CLOSED Player gunshot SFX timing/cadence reportedly does not match the N64 original's rate (M-106, user QA report). CLOSED (v0.4.0 campaign sign-off 2026-09-28, user:
141 D241 CLOSED Player gunshot SFX goes silent under some conditions — CLOSED as the D207 family (v0.4.0 campaign sign-off 2026-09-28, user:
142 D242 FIXED Player-death sequence UX regression vs N64 original: FIXED/CLOSED (2026-09-26, user:
143 D243 FIXED Dam end-of-level cutscene (the abseil/bungee sequence, D148/D160's subject): FIXED, live-confirmed by the user (M-189/M-190, 2026-09-18).
144 D245 FIXED IsWater water: two competing patterns with a visible seam that moves as the player looks around (mouse/aim) — FIXED (M-201, 2026-09-26, user-verified vs 1964):
145 D246 FIXED Tiny pixel strips at the left and right edges of the screen, "like overscan" — FIXED (2026-09-18 session, port-only, Video.SafeAreaCrop).
146 D247 FIXED Black bars top and bottom of screen — FIXED (2026-09-18 session, port-only, Video.SafeAreaCrop).
147 D248 FIXED Port runs a stable 30 FPS instead of 60 FPS game-wide (M-117, user asked to verify FPS after the D209 timing-keystone fixes). FIXED, root-caused to a genuine pointer-width ABI bug in src/sched.c (compiled for the PC build, not excluded).
148 D249 OPEN LOD/culling breaks down on large open levels (Streets, Egyptian named specifically) at *default* FOV — OPEN — M-142 static review ruled out the unscaled-far-clip and LOD-multiplier suspects (both already correct); CLOSED (v0.4.0 campaign sign-off 2026-09-28, user:
149 D250 FIXED FPS is inconsistent/low on many levels after 60 Hz shipped (M-119, user QA; D248 follow-on). FIXED — per-call getenv() cost was the bottleneck; rock-steady 60-61 fps after, user re-tested 1440p/4K clean.
150 D251 FIXED F10 options overlay QoL pass: FIXED (port-only, port/src/optionsoverlay.c).
151 D252 FIXED Particle effects cycle through a rainbow palette (M-124, user playtest of the v0.2.0-pre candidate). FIXED (M-201, 2026-09-26, user-verified):
152 D253 FIXED Facility (and any level with a FORCECROUCH/LADDER linked special tile) SIGSEGV on Linux/Steam Deck — FIXED — Deck-verified:
153 D254 FIXED Linux crash handler destroyed its own logs: FIXED — field-verified.
154 D255 FIXED Facility firefight SIGSEGV on Steam Deck (v0.2.0-pre CI bundle; D253 fix confirmed working). FIXED (M-140) — D3x-class 32-bit pointer truncation (vtxstore_fix_refs read Model* through the mistyped ChrRecord.chrflags 4-byte-enum alias instead of ObjectRecord.model), not the destroy-path race…
155 D256 FIXED Saves (and config) silently failed to persist on Linux whenever the game was not launched from the directory containing data/ — FIXED (path half) — save-flow detail still owed from the user.
156 D257 FIXED New Game.AllUnlocked option (F10; default flipped OFF for v0.2.0 — FIXED — verified by user playtest (PR #79 merged); default flipped OFF for v0.2.0 (D259).
157 D259 FIXED Game.AllUnlocked (D257) breaks audio on a fresh save: FIXED in v0.2.1 —…
158 D260 FIXED v0.2.1 (user request): FIXED — build green; one-line branch swap, trivially reviewable.
159 D261 FIXED v0.2.1 (user request: FIXED — feel-check received; W/S sensitivity follow-up logged above.
160 D262 FIXED v0.2.1 (user report): FIXED (primary cause, D264) —
161 D264 FIXED D262's root cause (ABI/layout class, cf. FIXED — build green, probes removed; visual confirmation pending.
162 D263 RESOLVED v0.2.1 (user report): RESOLVED (not a bug), refined (v0.2.1 further user testing, 2026-09-15).
163 D265 CLOSED D236 investigation (port-layer, port/fast3d/gfx_pc.cpp): CLOSED (2026-09-26 bookkeeping:
164 D258 PROPOSED PAL/JP asset conversion is broken at the source-data level — DEFERRED (user call 2026-09-28:
165 D284 FIXED port/src/random.c's MIPS64 dsll32/dsrl32 shift helpers were backwards — FIXED, 21-level sweep clean, no crashes —
166 D285 FIXED Steam Deck SIGSEGV during D255 live-testing = SEPARATE bug: FIXED (user, 2026-09-26:
167 D286 CLOSED User-confirmed real ~8s frame freeze on Caverns, Steam Deck — CLOSED (2026-09-26, user-approved:
168 D287 FIXED User-confirmed real, non-self-recovering freeze on Windows (M-141, 2026-09-15/16) after ~15-20 minutes with no input, triggered by clicking back into the… — FIXED (user playtest 2026-09-26:
169 D288 CLOSED Silo intro: a huge, flat-shaded, untextured brown/tan triangle sweeps across the frame during the camera pan down the missile shaft (M-141, user report —… — CLOSED — not reproduced on the current build (2026-09-23 capture) and the 2026-09-25 user Silo playtest confirms it gone ("silo fixed").
170 D289 FIXED Dam: a crash in the computer room, not reproducible on a second attempt (M-141, Steam Deck user playtest). FIXED (user, 2026-09-26:
171 D290 FIXED Watch's in-game "controller" section shows no control-pad graphic (M-141, Steam Deck user playtest). FIXED (2026-09-23): D264-pattern template read in watchRenderController (flags read 0), plus 3 pointer-width/raw-Gfx sites on the same path; controller model verified rendering on Windows.
172 D291 CLOSED Dam: texture glitches when the truck drives past (M-141, Steam Deck user playtest). CLOSED — duplicate of D306, faithful (M-201, 2026-09-26:
173 D292 FIXED Death animations are glitched — MOSTLY FIXED as a side effect of D243 (2026-09-18, user-confirmed alongside D173) —
174 D293 FIXED QoL ask: no discoverable way to quit the game from the UI (M-141, Steam Deck user playtest — FIXED (M-141) — "Quit to desktop" row added to the F10 overlay; build-verified Win+Linux, not yet live-confirmed.…
175 D294 FIXED Statue: ground geometry near the large CCCP/hammer-and-sickle monument briefly renders solid black over a wide area, then self-recovers 10-30 seconds later… — FIXED, user playtest confirmed (2026-09-25:
176 D295 FIXED Campaign-halting crash at the end of Control Center on 00 Agent difficulty, then a crash on every subsequent relaunch/resume (GitHub #87, user campaign playtes… FIXED (M-149) — M-148 root-caused + reproduced on HEAD; fix landed:
177 D296 CLOSED Control Center, 00 Agent: CLOSED (2026-09-26, user-approved:
178 D297 FIXED v0.3.0 silently wipes ALL pre-fix player saves on first launch (D284 side effect; found during D295/M-148). FIXED (M-150) — migration shim landed:
179 D298 CLOSED Full HUD (not just the aim-mode crosshair) missing in a v0.3.0 dev build — CLOSED (2026-09-26, user:
180 D299 FIXED Game.SkipIntro crashed selecting any save slot except one — FIXED — fileValidateSaves() (only called from the skipped legal screen) never ran under SkipIntro, leaving saves[] all-zero; added the missing call to the SkipIntro shortcut (src/game/lv.c, #ifdef PO…
181 D300 FIXED Hipfire look feels non-uniform (issue #89): FIXED, user-confirmed live —
182 D301 FIXED fileGetIsCheatUnlocked() NULL-deref, unguarded on every call site, not just the D299/SkipIntro one (GitHub #87, two reports with an identical crash PC/fault ad… FIXED locally on release/v0.3.0 (not pushed) —
183 D302 FIXED Render FPS regression to ~40-45fps on release/v0.3.0 (user QA on the local v0.3.0 pre-release build, 2026-09-18) — FIXED — 4 uncached getenv() calls added by D236 passes 10/12/14/16 directly inside gfx_sp_tri1 (port/fast3d/gfx_pc.cpp, per-triangle) cached with the same static int x = -1 pattern D250 established e…
184 D303 FIXED Some muzzle flashes (M16 named specifically, "and some others") draw wrong — FIXED (M-201, 2026-09-26):
185 D304 FIXED Mouse sensitivity menu overhaul (user request, 2026-09-18): FIXED — see full entry for both the range/parity pass and the same-day follow-up (collapsed to a single Input.MouseSensitivity menu row + fixed overlayUpdateScroll()'s unconditional bottom-anchor, th…
186 D305 FIXED Steam Deck SIGSEGV on the v0.3.0 pre-release build (Cavern, early in the level, Game.AllUnlocked previously enabled on the session/save) — FIXED (defensive guard) —
187 D306 CLOSED Dam intro/truck wheels render with odd transparent-looking areas, possibly z-fighting (user QA, 2026-09-18). CLOSED — faithful, confirmed on 1964 (M-201):
188 D307 FIXED In-level security camera entities can be flipped facing the wrong direction (backwards) on some levels, e.g. FIXED (M-201, 2026-09-26, user-verified):
189 D308 CLOSED Occasional z-fighting confirmed on some levels' geometry, general/broader than the D306 truck-wheel instance (user QA, 2026-09-18). CLOSED — faithful (2026-09-26, user:
190 D309 FIXED Complete non-self-recovering freeze (Windows) returning from Caverns to the intro screen — FIXED via D310 — user-playtest-confirmed 2026-09-18 (Caverns→intro clean; the spin record itself observed resuming correctly in the post-fix probe log).
191 D310 FIXED RULE-2-SIGNOFF: chraiitemsize() mis-sizes 1-byte AI_PRINT records (NUL-scans forward), corrupting AI execution for any list containing an active PRINT — FIXED — user-playtest-confirmed 2026-09-18 (v2:
192 D311 FIXED Complete freeze in modelSetAnimFrame2WithChrStuff root-motion loop — FIXED — root cause D336 (bheadAdjustAnimation read never-written placeholder fields); the #ifdef PORT guard is retained as a safety net.
193 D312 FIXED Throwable items (mines, grenades, etc.) can clip through walls in certain spots (user report, v0.3.0 pre-release playtest, 2026-09-18). FIXED — bgBuildRoomVtxBounds mis-reads the G_VTX vertex count on PC (Gfx_le.dma.par bit-layout shift):
194 D313 FIXED Bullet impacts do not appear on some walls/surfaces — FIXED — same root cause as D312, merged diagnosis; same one-line fix.
195 D314 FIXED F10 menu items flicker randomly/intermittently (reported on the main-menu context, user report, v0.3.0 playtest build, 2026-09-18). FIXED (user playtest 2026-09-26) —
196 D315 CLOSED Menu scaling differs between main menu and in-game — CLOSED (2026-09-26, user:
197 D316 FIXED Mouse-clicking some menu items glitches and lands on the item below instead (user report, v0.3.0 playtest build, 2026-09-18). FIXED — root-caused to two compounding bugs (naive mouse->logical mapping ignoring the safe-area crop; asymmetric row hit-test band) in port/src/optionsoverlay.c + port/fast3d/gfx_pc.cpp.
198 D317 CLOSED Crash when toggling Game.AllUnlocked back and forth in-game (user report, v0.3.0 playtest build, 2026-09-18) — CLOSED — symptom of D316 (confirmed instant window close, no WER report; the D316 hit-band bias let a click on "All unlocked" fall through to the adjacent "Quit to desktop" row).
199 D318 FIXED Facility (regular play path): FIXED via D329 (2026-09-26 bookkeeping:
200 D319 FIXED Frame cap slider was mostly a dead zone; replaced with a plain 30/60 toggle (user ask, 2026-09-18). FIXED — new ROW_FPSCAP cycle (30/60 only) in port/src/optionsoverlay.c; default changed to 60 in port/src/video.c.
201 D320 FIXED D318-class sweep: aim-hold → _update AI lists in 8 levels carry the same as-authored softlock race (static analysis, 2026-09-20). FIXED via D329 (2026-09-26 bookkeeping:
202 D321 FIXED D318 trigger chain fully mapped: FIXED via D329 (2026-09-26 bookkeeping:
203 D322 FIXED Long-session audio degradation: FIXED (Windows) for the periodic "garbled" bursts —
204 D323 CLOSED G_TL_LOD bindings of explicit-LOD textures: CLOSED — parked as a fidelity enhancement, not a bug (2026-09-26, user-approved:
205 D324 FIXED GE's <stdlib.h> stub declares nothing, so TUs that include it get NO CRT prototypes — FIXED (2026-09-22) — proven impact:
206 D325 CLOSED ROM-mod (xdelta) compatibility: CLOSED as a question —
207 D326 CLOSED Random Eye randomizer repacks the ROM too — CLOSED as a question —
208 D327 RESOLVED Mod support, resolved plan: RESOLVED — superseded by D328; approach (a) is NOT ruled out (these scans missed the 1172-compressed data segment).
209 D328 CLOSED Mod support is viable and D327's "ruled out" was wrong: CLOSED as a question —
210 D329 FIXED The D318/D320 guard anim pin is a tick-granularity divergence: FIXED — Rule-2 high-FPS fix in model.c; clk=1 no longer pins (WSL + native Windows), 0 triggers at clk=2; bunker1 golden failure predates it (on/off A/B); user playtest confirms N64-matching Ourumov…
211 D330 FIXED #103 (v0.3.0, Win 11): FIXED (port/src/input.c) —
212 D331 FIXED Ejected shell casings never rendered on PC: FIXED (gunfire.c #ifdef PORT, explicit template); PP7 casing verified on native Windows (before:
213 D332 FIXED Reticule jitter in aim mode (RMB) — FIXED (fix/mouse-pd-model) —
214 D333 FIXED #104 (v0.3.0, Linux): FIXED (2026-09-26 bookkeeping:
215 D334 FIXED Native widescreen, world side (WIDESCREEN-FOV-PLAN Option B Phase 2): FIXED (2026-09-26 bookkeeping:
216 D335 FIXED Native widescreen, HUD + front end (WIDESCREEN-FOV-PLAN Phase 3): FIXED (2026-09-26 bookkeeping:
217 D336 FIXED Frigate (and any level) "all polygons break except the gun" after turning: FIXED (src/game/bondhead.c #ifdef PORT, ABI/layout); headless repro (Runway → exit → Frigate → turn, D294 2× room pool) clean, D156 0 fires; golden 3/3; Win + Linux builds.
218 D337 FIXED Aim modes folded (user decision): FIXED (2026-09-26 bookkeeping:
219 D338 FIXED Input.AimRange (F10 "Aim range"): FIXED (2026-09-26 bookkeeping:
220 D339 OPEN #92 (low-end ~30 fps, Celeron N3060 / HD 400) — OPEN (2026-09-26, user:
221 D341 FIXED Intro gun-barrel blood drip draws as static blocks/lines instead of dripping (user, v0.4.0, 2026-09-26; death blood fine). FIXED (M-201, user-verified 2026-09-27):
222 D342 FIXED File select: folder bodies and Bond photos vanish after selecting a file and backing out (user, v0.4.0, 2026-09-26). FIXED (M-201, user-verified):
223 D343 FIXED File select: PC "Options" label opens the settings overlay (v0.4.0 feature, M1 of the route-B options screen, 2026-09-27); also fixes F10 on file select jumpin… FIXED (M1, closed): one Rule-2 #ifdef PORT draw hook (user sign-off 2026-09-27); headless + user-verified (mouse, F10, >30 s, widescreen, physical controller on the bottom-bar label and options scree…
224 D344 FIXED Host PC bugchecks (BSOD 0x119 VIDEO_SCHEDULER_INTERNAL_ERROR) during game test runs; probable cause: FIXED (probable cause; orderly quit + GE_QUITFRAME), headless + user-verified; no dump, watch for recurrence.
225 D345 FIXED Front-end crosshair snaps away from the pointer on screen/page changes (file select → mode select setCursorPOSforMode(0), front.c:2516; every MENU_PC_OPTIONS p… Port-only, user-verified (parts a–g):
226 D346 CLOSED PC-settings wording pass: CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
227 D347 FIXED F10 overlay controller: FIXED (port-only, optionsoverlay.c):
228 D348 ? File-select "PC Options" label: SUPERSEDED by D398 (glyph replaced with the IMG_DOT texture; no new art).
229 D349 CLOSED GE watch settings are not normally saved to the Bond file: CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
230 D350 CLOSED Watch-backed PC options: CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
231 D351 FIXED Existing D346 value/unit rows had positional initializers shifted into hidePtr: FIXED (port-only): designated row initializers restore unit, deadzone divisor and hide-if-auto fields; build clean, headless smoke clean.
232 D352 FIXED D350 review of 1303c0eb: FIXED (port-only, port/src/watchsettings.c):
233 D353 FIXED Turok-style settings regrouping: FIXED (port-only, optionsoverlay.c/frontoptions.c):
234 D354 FIXED Front-end F10 Bond rows silently disabled: FIXED (port-only, port/src/watchsettings.c):
235 D355 FIXED F10/options Music slider inaudible in-stage: FIXED (port-only, port/src/watchsettings.c):
236 D356 FIXED Settings regroup: Turok-style pages renamed to functional sections (INPUT/GAMEPLAY/GRAPHICS/AUDIO/VIDEO) with PD-style "(File N)" scoping annotations, the redu… FIXED (port-only, port/src/optionsoverlay.{h,c} + port/src/watchsettings.{h,c} + port/src/frontoptions.c; plan docs/dev/D356-SETTINGS-REGROUP-PLAN.md):
237 D358 FIXED D356 level-1 overlap: FIXED (port-only, port/src/frontoptions.c):
238 D359 CLOSED Settings UI file terminology settled on the end-user term "Profile": CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
239 D360 CLOSED F10 INPUT heading scrolled away; F10 navigation and distance defaults (2026-09-27). CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
240 D361 FIXED Main-menu F10 Audio freezes the game: FIXED (port-only, port/src/watchsettings.c + F10 probe in optionsoverlay.c):
241 D362 CLOSED F10 GoldenEye dossier-style visual refresh (2026-09-27). CLOSED (superseded by D363 after user playtest):
242 D363 FIXED F10 return to a dark overlay with the N64 watch's green/Bank Gothic visual language (2026-09-27). FIXED (palette/font user-approved, 2026-09-27); D364 follows up on profile tags, click alignment and slider calibration.
243 D364 FIXED Settings UI feedback: FIXED (user mostly accepted; D365 follows up on annotation size and click targets).
244 D365 CLOSED F10 annotation hierarchy and visible mouse change targets (2026-09-27). CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
245 D366 CLOSED Input calibration label: CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
246 D367 ? F10 full-width control boxes misaligned with their contents (2026-09-27). SUPERSEDED by D368 after user playtest:
247 D368 ? Simplify F10 click targets: INPUT RULE KEPT; tinted strips user-rejected, removed in D369.
248 D369 FIXED Remove F10 row boxes/strips entirely (2026-09-27). FIXED (awaiting live acceptance):
249 D370 CLOSED F10 distance/volume drags hurt frame rate (2026-09-27). CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
250 D371 FIXED GEPD key-layout preset + reload/crouch bindings, v0.4.0 modern options wave M3 (2026-09-27). FIXED (headless-verified; awaiting user live accept):
251 D372 FIXED Low-end perf preset (v0.4.0 modern options wave M5, 2026-09-27). FIXED (headless-verified; awaiting user live accept):
252 D373 FIXED In-game crosshair on/off + colour (v0.4.0 modern options wave M2, 2026-09-27). FIXED (user live-tested; rule-2 sign-off recorded 2026-09-27):
253 D374 FIXED GEPD becomes the default key layout; preset layering fix (v0.4.0 M3 playtest feedback, 2026-09-27). FIXED (headless + user live-tested):
254 D375 CLOSED Crouch wired to the wrong pad bit: CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
255 D376 CLOSED Crouch follow-up + modern-options spot check (2026-09-27). CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
256 D377 FIXED GEPD independent crouch (2026-09-27). FIXED (scripted stance verified; user says live crouch was good; pad/hold-vs-toggle specifics owed):
257 D378 FIXED GEPD use/reload separation (2026-09-27). FIXED (headless edges tested; user says E/R seemed good; targeted door/partial-mag cases owed):
258 D379 FIXED Crosshair colour: authored texture is red (2026-09-27). FIXED (cyan frame capture; live yellow/blue recheck owed):
259 D380 FIXED One PC keyboard layout; PD-style rebind design (2026-09-27). FIXED (follow-up D383):
260 D381 CLOSED RULE-2-SIGNOFF: opt-in RGB, size and Beta crosshair (2026-09-27). CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
261 D382 CLOSED Crosshair menu placement and slider hitches (2026-09-27). CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
262 D383 PARTIAL PD-style keyboard rebinding in both options UIs (2026-09-27). PARTIAL (2026-09-28).
263 D384 CLOSED Front PC Options key-bind navigation feedback + two-slot UI (2026-09-27). CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
264 D385 CLOSED Real mouse 1–5 action rebinding in the two-slot editor (2026-09-27). CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
265 D386 CLOSED Correct Aim default from SDL middle (Mouse 2) to right (Mouse 3) (2026-09-27). CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
266 D387 OPEN M7 AllUnlocked's read-time fake unlocks leak into saved EEPROM (2026-09-27). OPEN (isolated user-data-copy save/untoggle repro; no fix):
267 D388 CLOSED Nest functional binding/HUD menus and remove redundant M5 toggle (2026-09-27). CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
268 D389 CLOSED F10 footer clipping and ambiguous submenu links (2026-09-27). CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
269 D390 CLOSED F10 pinned Back selection did not visibly change (2026-09-27). CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
270 D391 CLOSED F10 click at glyph bottom could select next row (2026-09-27). CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
271 D392 FIXED Train intro soldier hunched; hand intersects torso vs 1964 — FIXED (#ifdef PORT ABI/layout in chraction.c); Train aim-angle probe confirms correction; user visually verified the soldier looks fixed against 1964.
272 D393 FIXED Controller B/Y both crouch; no in-game use button — FIXED + user live-tested A use / X reload / Y weapon cycle.
273 D394 PARTIAL Xbox 1.1 Jinx controller parity — PARTIAL (2026-09-28).
274 D395 RESOLVED Controller trapped in keyboard/mouse-only binding capture — RESOLVED (v0.4.0 campaign sign-off 2026-09-28, user:
275 D396 RESOLVED F10 pad B/A used as slider decrement/increment, not back/select — RESOLVED (v0.4.0 campaign sign-off 2026-09-28, user:
276 D397 RESOLVED SELECT FILE background + intro gun-barrel show an interlaced/comb look — RESOLVED (2026-09-28):
277 D398 FIXED PC Options label: D348's procedural slider glyph replaced with an existing mainfolderimages texture (2026-09-27). SUPERSEDED by D400 (icon removed, text-only label; purple-hover bug moot; the D400 textMeasure arg-swap bug was found and fixed there).
278 D399 PARTIAL File-select bottom row (SELECT FILE text + Copy/Erase + PC Options dot/label) shifted 15px left as a block (user sign-off 2026-09-27). LANDED (port-only, D343 class):
279 D400 PARTIAL File-select "PC Options" label: LANDED (port-only, frontoptions.c):
280 D401 CLOSED Rumble Pak → real gamepad haptics (re-promotes D224/M-87): CLOSED (2026-09-28). Rumble shipped in v0.4.0 (D401); Steam Deck real-pad validation:
281 D402 RESOLVED First clean Linux (glibc / SteamOS) build of release/v0.4.0 broke in two port-layer include-resolution spots MinGW tolerated (bondconstants.h first-include cyc… RESOLVED (Linux/Ubuntu-24.04 build → glibc-2.41-compatible binary for the Steam Deck):
282 D403 RESOLVED Deck v0.4.0-pre test: RESOLVED (2026-09-27, user-confirmed on the Deck:
283 D404 RESOLVED "Aim style: Centred (PC)" (Input.AimMode=1, D333/D337) has no effect when aiming with a gamepad; the row's own doc comment over-promises ("applies to every aim… RESOLVED (2026-09-28, port-layer only):
284 D405 CLOSED fast3d/OpenGL: MSAA above the driver max black-screens; two glBindFramebuffer calls pass an index where a GL name is required (PR #106, italoarruda, 2026-09-25… CLOSED (2026-09-28). Merged from PR #106 (code file only; security-reviewed clean - port-layer GL state fixes, no external input, no allocations).
285 D406 RESOLVED front-end PC Options screen: RESOLVED (2026-09-28, port-layer only, Steam Deck-validated):
286 D407 PARTIAL Tanks cannot be boarded/exited on PC — PARTIAL (fix landed in v0.4.0, port-only):
287 D408 OPEN Game.SkipIntro skips the post-mission failure dossier: OPEN (cosmetic, not root-caused past the handoff site).
+10338 -71
View File
File diff suppressed because it is too large. Load diff
+11 -11
View File
@@ -1,14 +1,14 @@
# GoldenEye 007 — game-behaviour reference
# GoldenEye 007 — game-behavior reference
Community-documented account of how the retail N64 game is *supposed* to
behave: the combat/AI/stealth model, difficulty scaling, per-level objective
sets, timers, weapon data, and the original game's known quirks and glitches.
**Status of this document.** This is secondary source material, not ground
truth. The decompiled game code in `src/` is authoritative for behaviour (see
truth. The decompiled game code in `src/` is authoritative for behavior (see
`AGENTS.md`). Use this doc to know *what correct looks like* when playtesting
the port — which objectives a level must expose, what a timer value should be,
whether a weird behaviour you just saw is an original-game quirk or a port
whether a weird behavior you just saw is an original-game quirk or a port
regression.
**Sources** (subjective commentary stripped; mechanical/factual data only):
@@ -20,7 +20,7 @@ regression.
Distilled from `goldeneye_007_reference.md` (the full raw walkthrough dump);
the walkthrough's per-stage tactical routes were dropped as not
behaviour-defining.
behavior-defining.
---
@@ -78,11 +78,11 @@ Relative to the lower tiers:
never alert.
- **Security cameras**: if a camera acquires the player it raises an alert
that **spawns black-clad guards** armed with DD44s and KF7s. Shoot the
camera centre for a one-shot kill.
camera center for a one-shot kill.
- **Retreating** breaks pursuit — most guards stop following after a set
distance, letting the player reload / heal / reposition.
### AI line-of-sight & animation quirks (exploitable, i.e. defining behaviour)
### AI line-of-sight & animation quirks (exploitable, i.e. defining behavior)
- A guard behind a **wall, low barrier or railing cannot shoot** the player,
even point-blank — it must path around the obstacle. **Exception:** a guard
@@ -97,7 +97,7 @@ Relative to the lower tiers:
## 3. Player state
| System | Behaviour |
| System | Behavior |
|---|---|
| Health | restored by health packs; no regen. 00 Agent pool ≈ 8 KF7 hits (§1). |
| Body armour | separate absorbing layer; fixed pickup locations (e.g. by the Jungle drone guns, start of Antenna Cradle). Nearly absent on 00 Agent. |
@@ -223,10 +223,10 @@ scientist casualties.
## 7. Weapons — data table
Clip / max-capacity / behaviour. (Balance and capacity numbers to cross-check
Clip / max-capacity / behavior. (Balance and capacity numbers to cross-check
against the decomp's weapon tables.)
| Weapon | Type | Clip | Max | Behaviour notes |
| Weapon | Type | Clip | Max | Behavior notes |
|---|---|---|---|---|
| PP7 | pistol | 7 | 800 | default sidearm |
| PP7 (Silenced) | pistol | 7 | 800 | no alert on fire |
@@ -284,7 +284,7 @@ noted. **All Guns** cheat unlocks everything.
| Object | Rule |
|---|---|
| Red alarm boxes | shoot to complete "neutralize all alarms"; a guard reaching one spawns reinforcements |
| Security cameras | destroy (centre = one-shot); acquisition raises the black-clad alert |
| Security cameras | destroy (center = one-shot); acquisition raises the black-clad alert |
| Sentry guns | fixed turrets (Bunker 2 striped corridor, Depot); destroy to pass |
| Drone guns | remote turrets (Jungle, Control Center, Water Caverns, Aztec); some pairs must be destroyed together |
| Locks / grates | shootable (e.g. the 4-lock vent grate at the end of Surface) |
@@ -344,7 +344,7 @@ faithful; if you see one while testing, it is probably *not* a port bug.
Ourumov has no objective use, but "firing" the key (action button) near the
rocket opens the giant launch doors. Otherwise both items are inert.
- **Cut content left in levels**: the Dam has the unused geometry of a
cancelled boat mission out on the water; the Train's rear region past
canceled boat mission out on the water; the Train's rear region past
Alec/Trevelyan leads nowhere; the Facility's starting vent has a second path
blocked purely cosmetically and shootable ceiling glass.
+78
View File
@@ -0,0 +1,78 @@
# GitHub preview video — runbook
The README embeds `docs/media/goldeneye-gh-preview.mp4` (the gif of the same
name is too heavy for GitHub; keep it only if someone wants a local copy).
It's a ~32 s, 8-clip montage of gameplay cut from ShadowPlay session
recordings.
## Tooling
`scratchpad/` is gitignored, so `make-preview.py` is a **local** tool (it
hardcodes local video paths anyway) — this doc is the tracked artifact. If it
goes missing, the commands below describe exactly what to rebuild. Run with
plain `python`.
Working files (per-clip mp4s, concat list, palette, `state.json`) go to
`scratchpad/.preview-work/` — disposable, safe to wipe (but then re-run
`build` after any edit).
```sh
python scratchpad/make-preview.py list # montage index -> source session + time
python scratchpad/make-preview.py shift <i> <±sec> # nudge a clip within its source video
python scratchpad/make-preview.py set <i> <sec> # absolute source time
python scratchpad/make-preview.py replace <i> <.10|.12|.18|.19> # new random spot in a session
python scratchpad/make-preview.py regen --seed N # full random redo (same clips/lengths)
python scratchpad/make-preview.py build # re-concat + re-render from state.json
```
Every edit command rebuilds mp4 + gif automatically. A full rebuild takes
~30–50 s; a single-clip shift is the same cost (concat dominates). Budget
one command per step if running under a timeout — cut+concat and the two gif
passes together fit in ~55 s.
## Source material
`C:\Users\james\Videos\NVIDIA\Build-pc\new\` — ShadowPlay recordings
(3072x1728 @ 60 fps, h264), no usable audio track (cut with `-an`). The four
sessions and their tags are hardcoded in `SESSIONS` at the top of the script;
add a new session there (tag, filename suffix, duration) to use it.
## Rules learned from review rounds
- **Never start a clip in the first ~60 s** of a session: desktop / boot /
menus. (`MIN_START`)
- **Bad zones** — source times that show non-gameplay; random picks avoid
them and `replace` should too. Currently known (in `BAD_ZONES`):
- `.10` @ ~395–425 s: file-select menu
- `.10` @ ~1290–1420 s: watch close-up (appears twice in that session)
Add new ones here whenever a review round finds another.
- **Clip length** 4 s is the sweet spot; 5 s starts to drag, <3 s flickers.
- **Same-session spacing**: two clips from one session ≥90 s apart, or they
read as a repeat of the same scene.
- When told "roll forward/back ~N seconds", use N ± a few s and prefer a
high-motion moment in that window if you have keyframe stats (see below);
otherwise a flat offset is fine.
- If a pick lands on a menu/loading screen, step it forward again rather
than picking a new random time — the user's "a bit later/earlier" almost
always means the *same* scene, different part.
## Output spec (don't change casually)
- mp4: 1920x1080 @ 30 fps, libx264 crf 20, `+faststart`, no audio. ~25 MB for 32 s.
- gif: 512 px wide, 12 fps, two-pass palette (`palettegen` then
`paletteuse=dither=bayer:bayer_scale=3`). ~28 MB — local use only.
## Optional: motion data to find "cool" moments
`scratchpad/analyze-sessions.py` dumps per-keyframe YAVG stats for the
session files; the diff between consecutive keyframes is a decent proxy for
"something is happening". If you need to find the coolest moment in a time
window, maximize it — but note plain random picks with spacing constraints
have been preferred by the reviewer over signal-optimized cuts, so use this
only when asked for "something cool".
## ffmpeg notes (Windows)
No system ffmpeg; the script falls back to the `imageio-ffmpeg` static
binary (`pip install imageio-ffmpeg`). Pass paths as forward-slash or bare
filenames with `cwd=` set — drive-letter colons and backslashes break
filter-graph argument parsing.
+99 -9
View File
@@ -220,25 +220,115 @@ offer to create it).
`docs/building.md` says it needs region-specific offsets; the code says
otherwise. Generate `e` and `j` in CI and diff against a
locally-ROM-generated copy to confirm before committing them.
2. **Does `d43_emit.py` really use `scripts/filelist.u.csv` for all three
regions?** It hard-codes `FILELIST = "scripts/filelist.u.csv"` even when
`REGION` is `pal-final`/`jpn-final`. Either model file offsets are
US-identical across regions (plausible — models live in obseg which may not
shift) or PAL/JP model conversion is currently untested. Confirm before
promising PAL/JP support in the release.
3. **Launcher vs self-re-exec vs three shortcuts** — pick one UX.
4. **Splash UI for the first-run convert** — reuse the SDL window / a simple
regions?** — *Resolved: yes, and that is a real bug.* Both emitters
hard-code the NTSC filelist regardless of `REGION`: `FILELIST =
"scripts/filelist.u.csv"` at `tools_pc/d43_emit.py:47` and
`tools_pc/d69_emit.py:56`, so `pal-final`/`jpn-final` runs read NTSC byte
offsets out of the wrong ROM — decompression garbage or `find_row()`
misses. The per-region CSVs already exist in the repo with the identical
shape (`rom_offset,size,asset_path,flag,flag`): `scripts/filelist.e.csv`
(32,843 bytes) and `scripts/filelist.j.csv` (37,947 bytes), just with bare
names instead of NTSC's `.bin`-suffixed ones — and `find_row()`
(`d43_emit.py:89-93`) already tries multiple name variants, so it would
work against any of the three CSVs if pointed at the right one. The fix is
small: pick `scripts/filelist.{u,e,j}.csv` from `REGION` instead of
hard-coding `.u`. **Remaining real unknown:** `d43_emit.py` also reads
every `assets/**/modelFileHeader.inc.c` (512 files, e.g.
`assets/obseg/chr/armourguard/modelFileHeader.inc.c:2`) to get NS/NT — arg
4 and arg 8 of the `MODELFILEHEADER(...)` macro — which it uses in
`build_nodes()` (`d43_emit.py`, ~lines 125-157) to compute the model root
offset `R0 = 4*NS + 12*NT`. Those headers are committed decomp data, one
file per model dir, with no region-suffixed variants; whether NS/NT
actually differ between NTSC/PAL/JP ROMs for the same model cannot be
verified from files in this repo alone (it would need an actual PAL or JP
ROM to check). If they don't differ, the CSV fix above makes `d43`/`d69`
region-correct on its own; if they do, that is a second, currently
invisible bug.
3. **Where does Part B's converter write, so `pcmodels.c` / `pccg.c` find it?**
— *Resolved.* Both loaders resolve the region from the ROM country byte at
offset `0x3E` via the same `Country→region` map (`'E'`→`"ntsc-final"`,
`'P'`→`"pal-final"`, `'J'`→`"jpn-final"`;
`pcmodelsRegionForCountry()` at `port/src/pcmodels.c:66-74`, same pattern
in `port/src/pccg.c:59-67`), then search the prefixes `$S/`, `$E/`, `./`
in order (`pcmodels.c:79`; `pccg.c` similar) through `sysResolvePath()`
(`port/src/system.c:207-259`: on Windows `$S/` → `./data` if it exists,
else `<exedir>/data`; `$E/` → `<exedir>/`). So a native first-run
converter in `port/src/pcconvert/` must write exactly, for region `R`:
`data/pcmodels-R/pcmodels.bin` + `data/pcmodels-R/manifest.csv`
(`pcmodels.c:100,109`) and `data/pccg-R/pccg.bin` +
`data/pccg-R/manifest.csv` (`pccg.c:92,101`). The manifest is a CSV with
header `name,offset,size`, decimal-parsed via `strtol`
(`pcmodels.c:185-186`, `pccg.c:176-177`), row cap 1024 for pcmodels / 256
for pccg.
4. **Launcher vs self-re-exec vs three shortcuts** — pick one UX.
5. **Splash UI for the first-run convert** — reuse the SDL window / a simple
progress line, or a pre-SDL Win32 dialog? ~5 s is short enough that a log
line + busy cursor may suffice.
5. **Non-Windows.** Part B's C converter is portable; the launcher and
6. **Non-Windows.** Part B's C converter is portable; the launcher and
bundling are Windows-only today. Out of scope but the C port keeps the
door open.
6. **Signing.** Unsigned exes + a "reads your ROM" first-run step will trip
7. **Signing.** Unsigned exes + a "reads your ROM" first-run step will trip
SmartScreen. Not blocking, but worth a release-notes note.
8. **How much metadata does Part B's CMake codegen actually have to
generate?** — *Resolved: far less than the original plan assumed.* The
emitters read three kinds of committed metadata:
(a) `scripts/filelist.u.csv` (and now `.e`/`.j` per question 2) — plain CSV,
~33-41 KB each; readable from disk at runtime, no embedding needed.
(b) `assets/obseg/file_resource_table.inc.c` (38,056 bytes, 884 lines) —
used only to extract an ordered filename list via regex; a C port can read
the already-compiled `file_resource_table[]` symbol directly at link time
instead of parsing or embedding it.
(c) `assets/**/modelFileHeader.inc.c`, 512 files — **this is the one real
codegen artifact**: the C port needs a build-time-generated table of 512
entries mapping model symbol → (NS, NT), the two args of the
`MODELFILEHEADER(...)` macro that `d43_emit.py` actually uses. Everything
else the scripts use — the N64_REC/PC_REC record-size tables, ADDR_OPS
marker bytes, `d69_emit.py`'s `TILESIZES[11]` array — are small fixed
constants that can just be hand-transcribed into the C port, not
codegen'd. Bottom line: the CMake codegen step Part B calls for is
narrowly the 512-entry nsnt table, not a general metadata-embedding
system.
## Suggested milestone ordering
1. Part A (3-region matrix + pre-generated `romassets` + launcher) — days.
2. Interim frozen `ge007-convert.exe` wired to first-run — days, optional.
3. Part B C port of `d43`/`d69` + embedded metadata + output diffing — weeks.
4. Drop the interim binary; single-binary drop-in experience complete.
## Part B follow-up backlog (was GitHub issue #85, folded here 2026-09-25)
Follow-up to #6, which shipped in v0.2.0 as an NTSC-U-only drop-in (PR #84). The issue was closed on GitHub and its scope is tracked here now. It has three independent workstreams.
### 1. PAL/JP asset pipeline repair (data + build)
`scripts/filelist.e.csv` and `filelist.j.csv` contain naming defects that make PAL/JP conversion impossible from this tree (D258: no PAL/JP PC build has ever been producible). The repair path has been verified but not applied:
- **PAL:** rename 31 model rows to the exact `file_resource_table.inc.c` symbols. Examples: `Pbridge_console` ×6 → `Pbridge_console1aZ…3bZ`, and `Pchrgrenadelauncher` → `PchrgrenadelaunchZ`. Greedy sequence alignment proves the mapping: 512×512 exact 1:1, offsets non-decreasing, and zero size anomalies against US.
- **JP:** rename the mislabeled font row @3044276 from `fontZurichBold_fontchartable.bin` to `fontBankGothic_fontchartable.bin`. The contiguity invariant confirms it: the Gothic kerning table @3043600+676 ends exactly at this row, and the size matches the US Gothic chartable.
- **Then:**
- `gen_romassets.py e/j` must run clean without a ROM.
- Commit `romassets_e.s` and `romassets_j.s`.
- Replace the hardcoded `filelist.u.csv` in the d43/d69/d88 emitters with a region→suffix map.
- Remove the NTSC-U-only gate in `prepare-assets.py`.
- **Needs a PAL and/or JP ROM** for final byte-level verification of at least one region. Only NTSC is available locally.
### 2. Multi-region release layout
- Give each region its own OUTPUT_NAME (`ge007-ntsc-final` / `-pal-final` / `-jpn-final`) plus a default alias.
- Add a region re-exec guard in `romdata.c` (~50 lines): if the country byte at 0x3E doesn't match the build, re-exec the sibling exe.
- CI builds all 3 regions sequentially, and the bundle scripts package all 3 exes.
### 3. C port of the emitters (optional, removes PyInstaller)
Port the d43/d69/d88 emit logic to C. That removes the frozen-converter dependency entirely, because the converter becomes part of the engine binary. It's a 2–4 week task, and PD's `filemodel.c` is the closest analogue. It isn't required for PAL/JP: the frozen Python path works for all three regions once #1 lands.
**Acceptance:** a user with a PAL or JP ROM gets the same one-step drop-in experience as NTSC does today.
Binary file not shown.

After

Width:  |  Height:  |  Size: 910 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 531 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 523 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 668 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 621 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 893 KiB

+13
View File
@@ -0,0 +1,13 @@
new_name,original,video_session,snapshot_time
shot-01.jpg,v040/dam0.png,v0.4.0 attract pass 2026.09.28,
shot-03.jpg,v040/runway.png,v0.4.0 attract pass 2026.09.28,
shot-04.jpg,v040/surface 1.png,v0.4.0 attract pass 2026.09.28,
shot-09.jpg,v040/frigate 0.png,v0.4.0 attract pass 2026.09.28,
shot-11.jpg,v040/surface 2.png,v0.4.0 attract pass 2026.09.28,
shot-16.jpg,v040/statue 0.png,v0.4.0 attract pass 2026.09.28,
shot-20.jpg,v040/archives 0.png,v0.4.0 attract pass 2026.09.28,
shot-21.jpg,v040/cradle 0.png,v0.4.0 attract pass 2026.09.28,
shot-24.jpg,v040/aztec 0.png,v0.4.0 attract pass 2026.09.28,
shot-27.jpg,v040/control 0.png,v0.4.0 attract pass 2026.09.28,
shot-28.jpg,v040/streets 0.png,v0.4.0 attract pass 2026.09.28,
shot-29.jpg,v040/jungle.png,v0.4.0 attract pass 2026.09.28,
1 new_name original video_session snapshot_time
2 shot-01.jpg v040/dam0.png v0.4.0 attract pass 2026.09.28
3 shot-03.jpg v040/runway.png v0.4.0 attract pass 2026.09.28
4 shot-04.jpg v040/surface 1.png v0.4.0 attract pass 2026.09.28
5 shot-09.jpg v040/frigate 0.png v0.4.0 attract pass 2026.09.28
6 shot-11.jpg v040/surface 2.png v0.4.0 attract pass 2026.09.28
7 shot-16.jpg v040/statue 0.png v0.4.0 attract pass 2026.09.28
8 shot-20.jpg v040/archives 0.png v0.4.0 attract pass 2026.09.28
9 shot-21.jpg v040/cradle 0.png v0.4.0 attract pass 2026.09.28
10 shot-24.jpg v040/aztec 0.png v0.4.0 attract pass 2026.09.28
11 shot-27.jpg v040/control 0.png v0.4.0 attract pass 2026.09.28
12 shot-28.jpg v040/streets 0.png v0.4.0 attract pass 2026.09.28
13 shot-29.jpg v040/jungle.png v0.4.0 attract pass 2026.09.28
Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

+208 -21
View File
@@ -1,32 +1,219 @@
---
title: GoldenEye 007 PC Port
description: A native PC port of the original Nintendo 64 GoldenEye 007, built from decompiled source — and a case study in AI-agent collaboration on a large low-level codebase.
# v0.4.0: status bumped from v0.3.0; Honest status replaced by the v0.4.0
# release-notes known issues (rainbow/water/z-fight/cameras/trees/muzzle/
# stretched-widescreen/gunshot items all fixed; D335b F10-overlay stretch is
# the new widescreen residual). Description kept short (Bing's 160 limit).
description: >-
A native PC port of GoldenEye 007 (Nintendo 64, 1997), built from
decompiled source with a software RSP. v0.4.0 for Windows, Linux and
Steam Deck.
---
# GoldenEye 007 PC Port
A native PC port of _GoldenEye 007_ (Rare, 1997, Nintendo 64), compiled from
the [GoldenEye 007 decompilation](https://github.com/n64decomp/007): the
original N64 game running from reconstructed source, not the Xbox 360
remaster. The N64's graphics coprocessor (RSP) is emulated in software; every
other hardware surface (video, audio, input, timers, save storage) is shimmed
in a dedicated `port/` layer, following the architecture of the
[Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark), the same
Rare "Indy" engine family, one hardware generation apart.
A work-in-progress **native PC port of the original 1997 Nintendo 64
_GoldenEye 007_**, compiled from the game's
[decompiled source](https://github.com/n64decomp/007) and running the N64's
graphics coprocessor in software. It follows the architecture of the
[Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark).
[Download](#download) · [News](#news) · [See it running](#see-it-running) · [Honest status](#honest-status) · [Documentation](#documentation)
It is also a **research project on AI-agent collaboration in a large,
unfamiliar, low-level codebase**: two coding agents — a local open-weight model
(Qwen 3.8) on a single RTX 5090, and Claude / Claude Code — driven by one person
part-time through ~230 translation units of unmodified big-endian MIPS game
code, handing work back and forth through shared written notes.
**Status: v0.4.0 (2026-09-28) — fully playable, with a small set of known
caveats.** The full single-player campaign runs at a steady 60 fps and is
completable end to end (all 20 missions, plus the ending-credits sequence,
playtested on Windows, Linux and Steam Deck). It is the most complete release
to date: **native widescreen** (on by default — undistorted world at your
display's aspect, 4:3 menus pillarboxed), a **complete aim system** (N64 or
centred PC style, for mouse *and* controller), **in-game key rebinding** with
a GEPD-style default layout, **crosshair customization**, **rumble-pak
haptics** on supported gamepads, and a **rebuilt options overlay** with
fine-tuning rows (frame cap, MSAA, draw/LOD distance, HUD scale). The most
common defect classes from earlier releases — particle colour drift, water
seams, z-fighting, billboard trees, muzzle flashes, the front-end Nintendo
logo, gunshot SFX — are fixed in this version; what remains is a short list,
under [Honest status](#honest-status).
**Status:** Phase 2 of 4 (rendering). Boots, renders the full intro and front
end, loads all 21 solo missions. No audio yet; not playable end to end.
**This is a pre-1.0 release, not a finished product** — v1.0 is the target
for a polished, feature-complete build; expect missing features and the
occasional breaking change until then. See the
[README's Roadmap section](https://github.com/jkdansereau/goldeneye-pc-port#roadmap)
for direction (PAL/JP support, controller-button rebinding, LAN multiplayer
under consideration, and more).
- **[Project repository and README](https://github.com/jkdansereau/goldeneye-pc-port)** — build instructions, requirements, status, how it compares to the Xbox 360 recompilation projects.
<p align="center">
<img src="media/goldeneye-gh-preview.gif" width="70%"
alt="~24 s gameplay montage from live play sessions">
<br><em>~24 s gameplay montage from live v0.4.0 play sessions (Runway tank,
Dam, Caverns, Aztec, Bunker&nbsp;2, Surface&nbsp;2), running in the port.</em>
</p>
## News
- **2026-09-28** — **v0.4.0**: native widescreen, a complete aim system for
mouse and controller, in-game key rebinding, crosshair customization,
rumble-pak haptics, a rebuilt options overlay, and a broad fidelity-fix
pass (water, particles, billboard trees, front-end logo, gunshot SFX, a
true stable 60 fps). [Release notes](https://github.com/jkdansereau/goldeneye-pc-port/releases/tag/v0.4.0) ·
[downloads](#download).
- **2026-09-20** — **v0.3.0**: the first release with the complete campaign
playable end to end at 60 fps on Windows, Linux, and Steam Deck.
[Release notes](https://github.com/jkdansereau/goldeneye-pc-port/releases/tag/v0.3.0).
- **2026-09-04 → 2026-09-16** — **v0.1.0 – v0.2.2**: the alpha and beta
cycle — build chain, software RSP, first rendered frames, front end, and
per-level stabilization across the campaign.
## Download
| Platform | Bundle | Notes |
|---|---|---|
| **Windows** (x86_64) | [win64.zip](https://github.com/jkdansereau/goldeneye-pc-port/releases) | Engine + runtime DLLs + the one-time asset tool. |
| **Linux** (x86_64) / **Steam Deck** | [linux tarball](https://github.com/jkdansereau/goldeneye-pc-port/releases) | SDL2 is bundled, so it runs as-is on any distro, and sideloads onto a Deck with nothing installed. |
**Bring your own ROM.** Both bundles contain no ROM and no game assets: you
supply your own GoldenEye 007 N64 ROM (the
[Requirements table](https://github.com/jkdansereau/goldeneye-pc-port#requirements)
has the region filenames and SHA-1s). This release supports the NTSC-U (US)
ROM; PAL and JP are on the roadmap. Then: unpack, drop the ROM in `data/`,
and launch; the first run generates the derived assets automatically (no
Python or other tooling needed). The full steps are in the
[Quick start](https://github.com/jkdansereau/goldeneye-pc-port#quick-start);
pre-built releases are legal to distribute precisely because they're useless
without a ROM you already own.
## See it running
<p align="center">
<img src="img/shots/shot-28.jpg" width="45%" alt="Streets, rendered by the port (v0.4.0 intro attract)">
<img src="img/shots/shot-01.jpg" width="45%" alt="Dam, rendered by the port (v0.4.0 intro attract)">
</p>
<details>
<summary><strong>Full gallery</strong>: 12 in-engine captures from a v0.4.0 intro-attract pass (3072×1728 widescreen, Sep 2026)</summary>
<p align="center">
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-01.jpg" width="100%" alt="In-engine: Dam (v0.4.0 intro attract)">
<div><small>Dam</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-03.jpg" width="100%" alt="In-engine: Runway (v0.4.0 intro attract)">
<div><small>Runway</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-04.jpg" width="100%" alt="In-engine: Surface (v0.4.0 intro attract)">
<div><small>Surface</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-09.jpg" width="100%" alt="In-engine: Frigate (v0.4.0 intro attract)">
<div><small>Frigate</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-11.jpg" width="100%" alt="In-engine: Surface 2 (v0.4.0 intro attract)">
<div><small>Surface 2</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-16.jpg" width="100%" alt="In-engine: Statue (v0.4.0 intro attract)">
<div><small>Statue</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-20.jpg" width="100%" alt="In-engine: Archives (v0.4.0 intro attract)">
<div><small>Archives</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-21.jpg" width="100%" alt="In-engine: Cradle (v0.4.0 intro attract)">
<div><small>Cradle</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-24.jpg" width="100%" alt="In-engine: Aztec (v0.4.0 intro attract)">
<div><small>Aztec</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-27.jpg" width="100%" alt="In-engine: Control (v0.4.0 intro attract)">
<div><small>Control</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-28.jpg" width="100%" alt="In-engine: Streets (v0.4.0 intro attract)">
<div><small>Streets</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-29.jpg" width="100%" alt="In-engine: Jungle (v0.4.0 intro attract)">
<div><small>Jungle</small></div>
</div>
</p>
</details>
More captures may land here as playtesting continues.
## Play it, or take it apart
- **Try it yourself**: grab a bundle above, bring your own ROM, and play the
campaign. It's an early cut for exactly this: if something breaks, an
[issue](https://github.com/jkdansereau/goldeneye-pc-port/issues) with what
you were doing is genuinely useful.
- **Read the code**: the game logic in `src/` is unmodified decompilation;
every hardware surface (video, audio, input, save storage, and the
software-RSP renderer) is shimmed in the MIT-licensed `port/` layer.
[Internals](internals.md) is the map; [Porting notes](porting-notes.md) is
the catalogue of N64→PC bug classes hit along the way, a good read even if
you never touch the code.
- **Mod it**: the port layer, build system and `tools_pc/` helpers are MIT
licensed and yours to extend: new video options, input tweaks, your own
asset sidecar. [CONTRIBUTING](https://github.com/jkdansereau/goldeneye-pc-port/blob/main/CONTRIBUTING.md)
has the ground rules that keep it faithful to the original game.
- **AI disclosure**: development here was agentic — Claude Pro plus a local
open-weight model on a single RTX 5090, as of August–September 2026. This
project is as much a study of *that process* as it is a port: whether
current LLMs can carry a codebase like this, and what actually goes wrong
along the way. Game codebases have historically been a rough fit for
LLMs — large, stateful, hardware-adjacent, unforgiving of a subtly wrong
memory layout — more so for older local models, so this is a useful data
point on where that stands now, for better or worse. Judge the result for
yourself. I'm one person doing this in my spare time, not a team. See
[the full write-up](dev/agentic-development.md) for the setup, timeline,
handoff workflow, and an honest assessment of what did and didn't work.
## How it works
The R4300 game code is compiled completely unmodified; the decompilation's
control flow is ground truth. The N64's Reality Signal Processor, the graphics
coprocessor that builds and executes each frame's display list, is emulated in
software: the port interprets the GBI stream the game emits and translates it
to OpenGL, bypassing the RDP entirely. Everything else that would touch N64
hardware (video, audio, input, timers, save storage) is shimmed in a small
dedicated layer, following the architecture of the Perfect Dark PC port from
the same Rare engine family. Full detail: [Internals](internals.md); the bug
catalogue: [Porting notes](porting-notes.md).
## Honest status
- On Facility, if gas leaks during Ourumov's monologue he can pause for up to
~10 s before resuming the scripted shootout — a latent race that exists in
the N64 original too (where it softlocks permanently); the port detects and
auto-recovers it. (D318)
- With native widescreen on, the F10 options overlay stretches with the
window instead of pillarboxing like the front-end menus (legible; cosmetic;
the F10 *Native widescreen* toggle restores the old stretched frame
throughout). The world/HUD widescreen rendering itself is correct. (D335b)
- The front-end Rareware logo shows a subtle texture-filtering artifact
(the Nintendo logo and legal page are clean). Cosmetic only. (D75)
- **`All unlocked` is experimental**: the fresh-install corruption case is
fixed, but saves made while it is on are not guaranteed recoverable by
switching it off — complete a level normally first, and back up
`data/ge007.eep` before enabling it. (D387)
- No macOS or ARM support. Keyboard/mouse rebinding shipped in v0.4.0;
controller-button rebinding is not supported yet.
The full list, with root causes and fix status: the
[release notes](https://github.com/jkdansereau/goldeneye-pc-port/releases)
and the [finding log](https://github.com/jkdansereau/goldeneye-pc-port/tree/main/docs/dev).
## Documentation
- **[The two-agent development case study](dev/agentic-development.md)** — goal, setup, timeline, the handoff workflow, and an honest assessment of what did and didn't work.
- **[Development process](dev-process.md)** — how work was scoped, partitioned, and budgeted across agents; the finding-log discipline.
- **[Internals](internals.md)** — architecture, the software RSP-emulation approach, GoldenEye-vs-Perfect-Dark engine differences, the phased plan.
- **[Porting notes](porting-notes.md)** — the recurring Nintendo 64 → PC bug classes hit during the port, with fixes.
- **[Game-behaviour reference](dev/game-behavior-reference.md)** — how the retail N64 game is meant to behave: combat/AI model, difficulty scaling, per-level objectives, timers, weapon data, and the original game's known quirks.
- **[Building](building.md)** — full build and asset-extraction guide.
- [The two-agent development case study](dev/agentic-development.md): goal, setup, timeline, the handoff workflow, and an honest assessment of what did and didn't work.
- [Development process](dev-process.md): how work was scoped, partitioned, and budgeted across agents; the finding-log discipline.
- [Internals](internals.md): architecture, the software RSP-emulation approach, GoldenEye-vs-Perfect-Dark engine differences, the phased plan.
- [Porting notes](porting-notes.md): the recurring Nintendo 64 → PC bug classes hit during the port, with fixes.
- [Building](building.md): full build and asset-extraction guide.
+66 -59
View File
@@ -1,15 +1,22 @@
# GoldenEye 007 — PC Port: architecture &amp; plan
---
title: Internals
description: Architecture behind the PC port; the software RSP-emulation approach, GoldenEye-vs-Perfect-Dark engine differences, and the phased development plan.
---
## GoldenEye 007 PC Port: architecture & plan
> This began as the pre-implementation research note and is kept as the
> architecture reference. Sections 1–9 describe the design; section 10 lists
> external references. The phased plan in section 8 is largely done through
> Phase 2 — for current status see the [README](../README.md) and
> [`dev/LEVEL-STATUS.md`](dev/LEVEL-STATUS.md); for the blow-by-blow finding
> log see [`dev/findings.md`](dev/findings.md).
> Phase 2; for current status see the
> [README](https://github.com/jkdansereau/goldeneye-pc-port#status) and
> [`dev/LEVEL-STATUS.md`](https://github.com/jkdansereau/goldeneye-pc-port/blob/main/docs/dev/LEVEL-STATUS.md);
> for the blow-by-blow finding log see
> [`dev/findings.md`](https://github.com/jkdansereau/goldeneye-pc-port/blob/main/docs/dev/findings.md).
This document captures the research done to plan a PC port of
GoldenEye 007, based on the decompilation this repository is forked from
([`n64decomp/007`](https://github.com/n64decomp/007)). It is modelled on the
([`n64decomp/007`](https://github.com/n64decomp/007)). It is modeled on the
approach used by the
[Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark),
which ports the same Rare "Indy" engine (a later revision) to modern
@@ -21,18 +28,18 @@ platforms.
1. [Executive summary](#1-executive-summary)
2. [What the PD port actually does (the reference architecture)](#2-what-the-pd-port-actually-does-the-reference-architecture)
— [2.1 Emulate the RSP, bypass the RDP](#21-the-key-architectural-insight-emulate-the-rsp-bypass-the-rdp)
· [2.1 Emulate the RSP, bypass the RDP](#21-the-key-architectural-insight-emulate-the-rsp-bypass-the-rdp)
· [2.2 How the RSP is invoked](#22-how-the-rsp-is-invoked)
· [2.3 Build system](#23-build-system)
· [2.4 PD-port copy-candidate audit](#24-pd-port-copy-candidate-audit-session-2026-08-22)
3. [GoldenEye decomp: what we're porting](#3-goldeneye-decomp-what-were-porting)
— [3.1 Entry / main loop](#31-entry--main-loop)
· [3.1 Entry / main loop](#31-entry--main-loop)
· [3.2 The two libultra trees](#32-the-two-libultra-trees-srclibultra-and-srclibultrare)
4. [N64 hardware surfaces that must be shimmed](#4-n64-hardware-surfaces-that-must-be-shimmed)
5. [GE-specific graphics differences vs PD (the RSP work)](#5-ge-specific-graphics-differences-vs-pd-the-rsp-work)
6. [Audio](#6-audio)
7. [Input & saves](#7-input--saves)
8. [Recommended plan](#8-recommended-plan) — phases 0–4
8. [Recommended plan](#8-recommended-plan); phases 0–4
9. [Open questions / risks](#9-open-questions--risks)
10. [References](#10-references)
@@ -63,7 +70,7 @@ platforms.
* **Saves** are **EEPROM-based** (controller EEPROM via `osEeprom*`), not
Memory Pak. The PFS/motor code in `joy.c` is only for accessory (Rumble Pak
/ Memory Pak) detection and can be stubbed. Phase 4 is therefore a
file-backed EEPROM plus no-op PFS/motor shims — simpler than a full Memory
file-backed EEPROM plus no-op PFS/motor shims; simpler than a full Memory
Pak emulation.
---
@@ -120,7 +127,7 @@ R4300 builds a Gfx display list
The PD port **emulates the RSP in software** (`fast3d/gfx_pc.cpp`) and
**bypasses the RDP entirely**: the software RSP translates GBI commands
directly into OpenGL calls. The R4300 game code is run natively and is
unaware of the difference — it just builds display lists and "starts the
unaware of the difference; it just builds display lists and "starts the
RSP" as usual.
The frame flow (from `port/src/pdsched.c` + `port/src/video.c`):
@@ -155,7 +162,7 @@ The PC build is **CMake** (the N64 build stays Make + IDO). Dependencies:
* `src/game/*.c` (all of it)
* a curated list of `src/lib/*.c` (the engine helpers that are
platform-independent: mema, memp, model, music, snd, vi, rdp, etc.)
* `src/lib/ultra/audio/*.c` (libaudio — runs fine on the CPU)
* `src/lib/ultra/audio/*.c` (libaudio; runs fine on the CPU)
* `src/lib/ultra/gu/*.c` (gu matrix helpers)
* `port/src/*.c` + `port/src/*.cpp` + `port/fast3d/*.cpp`
@@ -169,38 +176,38 @@ for this project (see CONTRIBUTING.md): same Rare "Indy"/Bond engine family, and
port layer solves the same problem classes we are currently hitting. Audited
against our remaining work items:
**Direct copy candidates (Phase 3/4 — our stubs were scaffolded to be replaced
**Direct copy candidates (Phase 3/4; our stubs were scaffolded to be replaced
by these):**
| PD file | Lines | Replaces our | Notes |
|---|---|---|---|
| `port/src/mixer.c` | 722 | 31-line stub | libaudio→SDL final mixer; same RareAL family. Adapt to GE's `audi.c` wiring (sample rate/buffering) |
| `port/src/input.c` | 1551 | 48-line stub | SDL_GameController backend: hotplug, rumble, keyboard fallback. Remap the button table to GE's scheme (C-buttons/Z-trig differ from PD) |
| `port/src/fs.c` | 294 | 81 lines | File-backed save dir (`--savedir`, `$S` path expansion) — foundation for Phase 4 EEPROM/PFS file backing (ours are no-op stubs today) |
| `port/src/fs.c` | 294 | 81 lines | File-backed save dir (`--savedir`, `$S` path expansion); foundation for Phase 4 EEPROM/PFS file backing (ours are no-op stubs today) |
| `port/src/audio.c` | 75 | 70 lines | Near-identical; diff when Phase 3 starts |
**Reference implementations (Phase 2 — the D32/D37/D43 class).** PD's port
**Reference implementations (Phase 2; the D32/D37/D43 class).** PD's port
layer has a `port/src/preprocess/` module (~4,125 lines): a per-ROM-segment
`preprocessfunc` table in their `romdata.c` that converts N64-layout asset data
to PC-native layout *at load time* — a systematic solution to the problem class
to PC-native layout *at load time*; a systematic solution to the problem class
we have been fixing ad-hoc per asset type.
- `filemodel.c` (1,114) — the direct **D43** analogue: same `PROMOTE` idiom,
- `filemodel.c` (1,114); the direct **D43** analogue: same `PROMOTE` idiom,
**same vma 0x5000000**. NOT a drop-in copy (GE's `ModelRoData` union + opcode
set differ — PD has stargunfire/headspot/gundl node types), but the node-walk
set differ; PD has stargunfire/headspot/gundl node types), but the node-walk
order, placement/alignment choices, and record handling are Rare-validated
against near-identical data: adapt rather than design from scratch.
- `filebg.c` (524) — BG tables (an expected D43 follow-on fault).
- `segaudio.c` (362) — audio bank-tree conversion; cross-check for our D37
- `filebg.c` (524); BG tables (an expected D43 follow-on fault).
- `segaudio.c` (362); audio bank-tree conversion; cross-check for our D37
`romdataFixupAudioBank`.
- `filelang.c`, `filetiles.c`, `filepads.c`, `filesetup.c`, `gbi.c`,
`segfonts.c` — the remaining asset types we will hit.
`segfonts.c`; the remaining asset types we will hit.
**Not copyable:** GE-specific `ModelRoData` record map; fast3d CC/RM
correctness (GE's custom GBI modes vs `gmain.s` do not exist in PD); PD extras
(`config.c`, `optionsmenu.c`, `mpsetups.c`, `mod.c`) — options menu, MP setup,
(`config.c`, `optionsmenu.c`, `mpsetups.c`, `mod.c`); options menu, MP setup,
MOD player; not part of 1:1 fidelity.
**Caveats:** copy **port-layer files only** — their decomp was modified for PC
**Caveats:** copy **port-layer files only**; their decomp was modified for PC
(e.g. `types.h` keeps N64 offset comments while using real pointers); our
non-negotiables are unchanged. Same family ≠ identical format: validate every
copied conversion per-field against GE headers + ROM ground truth (the standing
@@ -214,19 +221,19 @@ D32 procedure).
US/EU/JP ROMs. Game code lives in `src/game/` (~242 files) plus engine
files in `src/`.
* **Engine layout** (same family as PD):
* `src/sched.c` — RCP scheduler (`osCreateScheduler`, `__scExec`,
* `src/sched.c`; RCP scheduler (`osCreateScheduler`, `__scExec`,
`__scYield`). Structurally near-identical to PD's `src/lib/sched.c`.
* `src/game/rsp.c` — builds the Gfx display list, wraps it in an `OSScTask`,
* `src/game/rsp.c`; builds the Gfx display list, wraps it in an `OSScTask`,
and starts it via the scheduler (`rspGfxTaskStart`).
* `src/fr.c` — frame/VI management, video modes (`osViSetMode`).
* `src/audi.c`, `src/snd.c`, `src/music.c` — libaudio-based sound + music.
* `src/joy.c` — controllers, **EEPROM** save handling (via `joyGamePak*`
* `src/fr.c`; frame/VI management, video modes (`osViSetMode`).
* `src/audi.c`, `src/snd.c`, `src/music.c`; libaudio-based sound + music.
* `src/joy.c`; controllers, **EEPROM** save handling (via `joyGamePak*`
-> `osEeprom*`), plus PFS/motor code for accessory (Rumble Pak / Memory
Pak) detection only.
* `src/init.c` — `init()` -> `mainproc()` -> `bossEntry()` (game entry).
* `src/init.c`; `init()` -> `mainproc()` -> `bossEntry()` (game entry).
**Compiled for the PC** (provides `mainproc()`); its N64-only `init()` is
stubbed (see A3).
* **Custom RSP microcode:** `rsp/graphics/gmain.s` (1545 lines) — a modified
* **Custom RSP microcode:** `rsp/graphics/gmain.s` (1545 lines); a modified
fast3d. This is the RSP-side code that consumes the display list. For the PC
port we do **not** run this; we replace it with the software RSP. But it is
the authoritative reference for which GBI commands GE actually emits.
@@ -258,23 +265,23 @@ overlap.
What the PC build must do with each:
* **`src/libultra/audio/*.c`** — standard libaudio (AL). Runs on the CPU;
* **`src/libultra/audio/*.c`**; standard libaudio (AL). Runs on the CPU;
**compile** (the game's `audi.c`/`snd.c`/`music.c` depend on it).
* **`src/libultrare/audio/*.c`** (`drvrNew.c`, `env.c`, `reverb.c`) — Rare's
* **`src/libultrare/audio/*.c`** (`drvrNew.c`, `env.c`, `reverb.c`); Rare's
audio "New" driver. `synthesizer.c` calls `alSaveNew`/`alAuxBusNew`/
`alMainBusNew`/`alLoadNew`/`alResampleNew`/`alEnvmixerNew`/`alEnvmixerParam`/
`alFxParam`, all defined here. Runs on the CPU; **compile**.
* **`src/libultra/gu/*.c`** — matrix helpers (`guMtxF`, `guOrtho`, ...).
* **`src/libultra/gu/*.c`**; matrix helpers (`guMtxF`, `guOrtho`, ...).
**Compile**.
* **`src/libultrare/io/vitbl.c`** — defines `osViModeTable` (a data table),
* **`src/libultrare/io/vitbl.c`**; defines `osViModeTable` (a data table),
referenced ~15× by `fr.c`. **Compile** (it's data, not a driver). This is the
*only* `io/` file we compile.
* **All other `src/libultra/io/*.c` and `src/libultrare/io/*.c`** — hardware
* **All other `src/libultra/io/*.c` and `src/libultrare/io/*.c`**; hardware
drivers (PI, SI, SP, DP, VI, controller, PFS, motor, EEPROM, ...). **Exclude
and shim** in `port/src/libultra.c`. Notably `pfsinit.c`/`pfsisplug.c`
(`osPfsInit`/`osPfsIsPlug`) and `motor.c` (`osMotor*`) are called by
`joy.c` only for accessory detection, so they are shimmed as no-ops (see §7).
* **`src/libultra/os/*.c` + `src/libultrare/os/*.c`** — OS core (threads,
* **`src/libultra/os/*.c` + `src/libultrare/os/*.c`**; OS core (threads,
message queues, timers, cache, TLB). **Exclude and shim** in
`port/src/libultra.c` (+ `n64stubs.c` for the boot/TLB/FPU symbols).
@@ -289,7 +296,7 @@ What the PC build must do with each:
| Subsystem | GE API used | Port strategy (from PD) |
|-----------|-------------|--------------------------|
| RSP (coproc 0) | `osSpTaskLoad/StartGo/Yield`, display lists | Software RSP (`fast3d`) — **main work** |
| RSP (coproc 0) | `osSpTaskLoad/StartGo/Yield`, display lists | Software RSP (`fast3d`); **main work** |
| RDP (coproc 1) | `osDpSetStatus`, `osDpSetNextBuffer`, render modes | Bypassed; OpenGL backend |
| VI (video) | `osViSetMode`, `osViVSyncCallback`, `osViWaitVSync` | SDL2 window + GL context + frame pacing |
| AI (audio) | `osAiSetFrequency/SetNextBuffer/GetLength` | SDL audio device + queue |
@@ -305,32 +312,32 @@ What the PC build must do with each:
GE's `include/gbi_extension.h` defines the custom GBI surface. Compared to the
PD port's `fast3d`, the deltas are:
1. **`G_TRI4`** — draws up to 4 triangles in one command using 4-bit vertex
1. **`G_TRI4`**; draws up to 4 triangles in one command using 4-bit vertex
indices (0–15). A memory-saving optimization (Rare packed 4 tris into the
slot normally used for `G_TRI2`).
* **Status: already supported** by the PD `fast3d` (`gfx_sp_tri4`, and the
PD game itself uses it). ✅
2. **`G_SETTEX`** (`0xc0`) — "use texture from bank": selects a texture by
2. **`G_SETTEX`** (`0xc0`); "use texture from bank": selects a texture by
bank/tile with detail/mipmap type, min-level, detail-id and texture-id.
Emitted only by `gsSPUseTexture` (`include/gbi_extension.h:193`).
* **Status: appears UNUSED.** `gsSPUseTexture` is defined but never called
anywhere in the game code or the RSP ucode (`rsp/graphics/gmain.s`), and
`G_SETTEX` appears nowhere outside its `#define`. The game uses the
standard texture commands (`gSPTexture`, `gSPTextureL`,
`gSPTextureRectangle`, `gSPTextureRectangleFlip`) — all already handled
`gSPTextureRectangle`, `gSPTextureRectangleFlip`); all already handled
by the PD `fast3d`. **If this holds (verify the decomp is complete),
Phase 2 shrinks to verifying the custom CC/RM modes and the PD fast3d
may work largely as-is.** Keep a `G_SETTEX` decode path as a safety net,
but it is no longer the headline task.
3. **Custom color-combiner modes** — `G_CC_MODULATEIFADE`, `G_CC_FADE`,
3. **Custom color-combiner modes**; `G_CC_MODULATEIFADE`, `G_CC_FADE`,
`G_CC_DECALFADE`, `G_CC_BLENDRGBFADEA`, etc. (fade/blend variants).
* **Status: handled generically** by the fast3d color-combiner -> GLSL
shader compiler (`gfx_cc.cpp`), which compiles arbitrary CC equations.
Needs verification that each GE mode produces correct GLSL.
4. **Custom render mode** — `G_RM_CUSTOM_AA_ZB_XLU_SURF` (AA_ZB_XLU with
4. **Custom render mode**; `G_RM_CUSTOM_AA_ZB_XLU_SURF` (AA_ZB_XLU with
`Z_UPD`).
* **Status: handled** by the render-mode -> GL state mapping; verify.
5. **`gDPLoadTLUT06/07`** — custom TLUT load variants.
5. **`gDPLoadTLUT06/07`**; custom TLUT load variants.
* **Status: minor**, map onto the existing `G_LOADTLUT` path.
**Conclusion:** the PD `fast3d` is a strong starting point. `G_SETTEX` appears
@@ -340,7 +347,7 @@ safety net. Everything else (matrices, lights, fog, tri1/tri4, texture load,
sync/flush) is shared.
> Note: GE's on-hardware RSP ucode is `rsp/graphics/gmain.s`. We do not run it
> on PC, but it is the ground truth for the exact command encodings GE emits —
> on PC, but it is the ground truth for the exact command encodings GE emits;
> use it to validate the software RSP's command decoding.
---
@@ -348,7 +355,7 @@ sync/flush) is shared.
## 6. Audio
* GE uses **libaudio (AL)** on the R4300 (`src/audi.c`, `src/snd.c`,
`src/music.c`) — identical architecture to PD. Sound synthesis (ADPCM
`src/music.c`); identical architecture to PD. Sound synthesis (ADPCM
decode, mixing, FX) runs on the CPU; the RSP audio ucode is a thin
pass-through to the AI DMA.
* The PD port runs libaudio natively and routes the mixed output through
@@ -363,7 +370,7 @@ sync/flush) is shared.
* **Controllers:** `src/joy.c` uses `osContInit` / `osContStartReadData`.
The port maps SDL2 keyboard/mouse/gamepad into the N64 `OSError`/controller
structs. PD implements 1964GEPD-style and Xbox-style bindings — a good
structs. PD implements 1964GEPD-style and Xbox-style bindings; a good
default for GE too (fire=Z, aim=R, etc.).
* **Saves:** GE saves are **EEPROM-based**, not Memory Pak. The actual save
I/O (`src/game/file2.c`) uses `joyGamePakProbe/LongRead/LongWrite`, which
@@ -379,35 +386,35 @@ sync/flush) is shared.
## 8. Recommended plan
### Phase 0 — Scaffolding (this change)
### Phase 0: Scaffolding (this change)
* Add `port/` directory structure + headers + stub sources.
* Add a top-level `CMakeLists.txt` (+ `cmake/` modules) for the PC build,
parallel to the N64 `Makefile`.
* This document + `port/README.md`.
### Phase 1 — Boot to a window
### Phase 1: Boot to a window
* Port `system.c`, `fs.c`, `romdata.c`, `config.c`, `main.c`.
* Port `video.c` + a minimal `fast3d` (clear color, present).
* Goal: load `ge007.u.z64`, open a window, draw a clear color.
### Phase 1.5 — Boot to first frame (ABI reconciliation)
### Phase 1.5: Boot to first frame (ABI reconciliation)
* Get mainThread through all of `bossInitMainthreadData()` → `bossEntry()` →
`bossMainloop()` and render frame 1. (Done — D34–D58; see §G.)
* Recurring mechanism: **ROM-data struct ABI/layout fixes** (the D32 pattern) —
`bossMainloop()` and render frame 1. (Done; D34–D58; see §G.)
* Recurring mechanism: **ROM-data struct ABI/layout fixes** (the D32 pattern);
a 32-bit-pointer struct read as a 64-bit-pointer struct misaligns; convert
embedded pointer fields to `u32` + cast at use sites (PD ground truth), then
verify the load-time rebase yields valid DRAM addresses. See §H for the
step-by-step procedure and the standing "non-negotiable #2 ABI exception."
* Goal: title screen / first level actually drawn on the GL surface.
### Phase 2 — RSP / rendering
### Phase 2: RSP / rendering
* Bring in the PD `fast3d` (gfx_pc / gfx_opengl / gfx_cc / gfx_sdl2).
* Verify the custom CC/RM modes against `gmain.s`; add a `G_SETTEX` decode
path only if it turns out to be used (see §5 — it appears unused).
path only if it turns out to be used (see §5; it appears unused).
* Port `pdsched.c` (GE variant) to drive the software RSP.
* Goal: render the title screen / first level.
### Phase 3 — Audio + input
### Phase 3: Audio + input
* Port `audio.c`, `mixer.c`, `mod.c`; wire libaudio output to SDL.
* **Decide the ASP strategy** (see §11 C2): if audio runs the `aspMain`
microcode, wire `aspMainTextStart`/`aspMainDataStart` to the real microcode
@@ -415,7 +422,7 @@ sync/flush) is shared.
* Port `input.c` (keyboard/mouse/gamepad).
* Goal: playable with sound.
### Phase 4 — Saves + polish
### Phase 4: Saves + polish
* File-backed EEPROM (saves); PFS/motor already stubbed as no-ops.
* Widescreen, mouse-look, FPS options, config file, crash handler.
* Goal: feature-parity with the original + QoL extras.
@@ -424,21 +431,21 @@ sync/flush) is shared.
## 9. Open questions / risks
1. **`G_SETTEX` (low priority)** — appears unused (`gsSPUseTexture` is never
1. **`G_SETTEX` (low priority)**; appears unused (`gsSPUseTexture` is never
called). If a future decomp revision or a missed call site shows it is used,
reverse the texture-bank selection (bank -> image, tile, detail/mipmap type)
from `gmain.s` + `tex.c` / `initmttex.c`. Not currently the main unknown.
2. **Custom CC correctness** — the GLSL compiler must handle every fade/blend
2. **Custom CC correctness**; the GLSL compiler must handle every fade/blend
mode GE uses; validate per-mode against the original. This is now the main
rendering unknown.
3. **Scheduler fidelity** — GE's `sched.c` has speed-graph / profiling hooks
3. **Scheduler fidelity**; GE's `sched.c` has speed-graph / profiling hooks
(`speedgraphMarkerHandler`) and a slightly different task model than PD;
the port scheduler must reproduce the gfx/audio interleaving and VSync
pacing.
4. **Decomp completeness** — the decomp is WIP; some functions may still be
4. **Decomp completeness**; the decomp is WIP; some functions may still be
stubs/`unk_`. The PC port inherits whatever state the decomp is in. Track
the upstream `n64decomp/007` and rebase.
5. **Endianness / alignment** — GE, like PD, is big-endian N64 code with
5. **Endianness / alignment**; GE, like PD, is big-endian N64 code with
unaligned accesses; the PD port handles this with careful casts and
`-fno-strict-aliasing`. Reuse those flags.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 MiB

+1110 -124
View File
File diff suppressed because it is too large. Load diff
+109
View File
@@ -0,0 +1,109 @@
---
title: Security & fidelity status
description: What a release actually installs (no networking, no telemetry, no ROM or assets)
and how faithfully the port tracks the original N64 game's logic.
---
# Security & fidelity status
This page answers two questions plainly: *what does installing this put on
your machine*, and *how faithfully does the port actually reproduce the
original N64 game's logic*. Both were reviewed end-to-end on 2026-09-15 and
re-verified for the v0.3.0 release bundles on 2026-09-18 (bundle contents,
no-networking source check, and the asset converter). The full engineering
record behind each item is in the project's finding log
([`docs/dev/findings.md`](dev/findings.md)).
## What a release actually installs
Each release (`goldeneye-pc-port-*-win64.zip` / `-linux-x86_64.tar.gz`)
contains, and only contains: the game engine executable, a fixed set of
standard runtime libraries (SDL2, zlib, and the MinGW/Linux runtime — no
third-party downloads), the one-time ROM-to-PC asset converter, a README,
and license texts. The build scripts hard-fail if a ROM image or an
oversized blob ends up in the bundle. **No ROM, no game assets (textures,
audio, models, levels, text) are ever shipped** — you supply your own
legally-owned copy.
At runtime, the engine:
- has **no networking of any kind** (no sockets, no HTTP, checked directly
against the source),
- has **no telemetry, crash reporting, or analytics** of any kind,
- **never touches the Windows registry** and **never requests elevated
permissions**,
- writes save data, config, and derived ROM assets only inside its own
folder — never to `%APPDATA%`, `Program Files`, or any system location.
The one-time asset converter (`ge007-convert` / `prepare-assets.py`) — the
part of the bundle most likely to draw suspicion, since it's the thing that
reads your ROM and writes new files — has no network access at all,
verifies your ROM against a fixed table of known retail GoldenEye 007
hashes before touching it (an unrecognized file is rejected outright), and
writes only inside the bundle folder. Its full source ships right next to
the compiled version in every release if you'd like to read it yourself.
See [`SECURITY.md`](../.github/SECURITY.md) for why the binaries are
unsigned and what that means in practice (a SmartScreen prompt on first
run, and why some antivirus engines flag the converter specifically).
## Building from source
Cloning and building pulls in only standard, well-known packages: MSYS2
`pacman` packages on Windows, `apt` packages on Linux — no `npm`, no
`cargo`, nothing downloaded at CMake configure time, no git submodules, and
no prebuilt binaries checked into the repository. **One real gap**: the CI
build installs `pyinstaller` from PyPI without pinning a version, which is
used to freeze the release's asset-converter tool. It's a widely-used
packaging tool, and what it packages is entirely in-repo, stdlib-only
Python — but an unpinned dependency means a future PyInstaller release
could change what ships without a corresponding diff in this repository.
Pinning it is a tracked follow-up.
One development-only backdoor exists in source builds: the `GE_DEBUG_UNLOCKALL`
environment variable (`src/game/file2.c`) pre-unlocks all cheat options at boot.
It is off unless explicitly set, changes no shipped behavior, and is documented
here so the env-var surface stays fully accounted for.
## Fidelity to the original N64 game
The port's non-negotiable rule is that decompiled game logic is never
changed — only genuine N64-hardware dependencies get a PC-side
replacement, in the `port/` layer. This was audited directly:
- **Region build macros** exactly mirror the original N64 Makefile's macro
sets for all three regions — verified line-by-line.
- **The audio/library file classification** (which original files compile
vs. get PC-shimmed) exactly matches the project's own ground-truth
manifest — no silent gaps, no silent duplicates.
- **The one shipped game-logic-adjacent change** (a portal-culling
float-precision fix, D271) is `#ifdef PORT`-gated, documented in the
finding log, and moves *toward* matching N64 behavior, not away from it.
Every other deviation from the decompiled source is one of the documented
32→64-bit pointer-width ABI corrections described above.
**One issue, found and fixed before v0.3.0 shipped**: the port's
random-number generator (`port/src/random.c`) was documented as a bit-exact
port of the N64's PRNG, but wasn't — a shift-operation helper didn't match
the real MIPS64 instruction semantics it was meant to mirror. This was
independently confirmed by hand-tracing the assembly against the C port,
tracked as finding D284, and fixed before the release (verified clean
across a full level sweep — all 20 missions plus the ending sequence —
no crashes). It affected loot placement, AI
behavior variance, and replay-state determinism from the very first random
draw, in every release before v0.3.0. Because of the fix, RNG-derived
output — including the save-file CRC — changed, which meant existing save
files and any previously recorded replays no longer validate; v0.3.0's
release notes describe the automatic save migration that handles this. We'd
rather say this plainly than let a "verified bit-exact" claim stand
uncorrected, both when it was wrong and now that it's fixed.
Everything else checked (the documented ABI/pointer-width exceptions used
for the 32-to-64-bit transition, a sample of the finding log's own claimed
fixes against the actual current code) matched what's documented.
---
*This page is kept current alongside future releases. Its source review is a
manual, human-reviewed process today — see the project roadmap for plans to
partially automate the mechanical parts of it.*
+12
View File
@@ -269,6 +269,16 @@ typedef short ENVMIX_STATE[40];
* Macros to assemble the audio command list
*/
#ifdef PORT
/* PC port (D199): the N64 RSP audio ucode that would execute this acmd list
* never runs on PC (port/src/ucode.c). Following the Perfect Dark PC port's
* macro-swap pattern (docs/dev/AUDIO-PLAN.md), each aXxx macro below is
* replaced with an immediate call into port/src/mixer.c's software
* implementation instead of packing RSP command words. The N64 branch below
* is untouched. */
#include "mixer.h"
#else /* !PORT */
#define aADPCMdec(pkt, f, s) \
{ \
Acmd *_a = (Acmd *)pkt; \
@@ -402,6 +412,8 @@ typedef short ENVMIX_STATE[40];
_a->words.w1 = (unsigned int) d; \
}
#endif /* PORT */
#endif /* _LANGUAGE_C */
#endif /* !_ABI_H_ */
+9
View File
@@ -47,8 +47,17 @@ void gfx_set_target_fps(int);
void gfx_set_texture_filter(enum FilteringMode mode);
void gfx_set_mipmap_filter(enum MipmapFilteringMode mode);
void gfx_set_fix_mip_textures(int on);
void gfx_set_detail_base_tile(int on); /* D236: sample the base image of a DETAIL binding */
void gfx_set_wrap_fix(int on);
void gfx_set_anisotropy_level(int level); /* 1 = off; clamped to GL max */
void gfx_set_safe_area_crop(int on); /* crop the N64 TV-overscan safe-area margin instead of showing it as black bars */
/* On-window pixel rect (top-left origin) the full VI logical canvas (0,0)-
* (SCREEN_WIDTH, SCREEN_HEIGHT) currently maps to, honoring the safe-area
* crop above -- for inverting a window mouse click into logical 2D UI space
* (see port/src/optionsoverlay.c, D316). */
void gfx_get_ui_screen_rect(int32_t *outX, int32_t *outY, int32_t *outW, int32_t *outH);
void gfx_texture_cache_clear(void);
int gfx_texture_cache_count(void); /* D235 */
void gfx_texture_cache_delete(const uint8_t *orig_addr);
void gfx_texture_cache_delete_range(const uint8_t *start, const uint8_t *end);
int gfx_create_framebuffer(uint32_t width, uint32_t height, int upscale, int autoresize);
+99 -9
View File
@@ -510,11 +510,27 @@ static struct ShaderProgram* gfx_opengl_create_and_load_new_shader(uint64_t shad
}
}
/* D266: CVG_X_ALPHA ("texedge") modelled on the RDP's actual semantics,
* keyed on the alphacompare field. The inherited line quantized to
* opaque/discard at a hardcoded 0.19: it kept texels the N64 discards and
* forced survivors fully opaque, defeating the ALPHA_CVG_SEL blend.
* GE's Surface 1 IA4 tree strip (D236/D265) rendered as a wall because of
* it. Now: AC_DITHER -> dithered discard; AC_THRESHOLD -> discard only
* fully-transparent; AC_NONE -> discard below 0x80 and let survivors keep
* their alpha for blending. (AC_DECAL never sets SHADER_OPT_TEXTURE_EDGE,
* see gfx_pc.cpp.) */
if (cc_features.opt_texture_edge && cc_features.opt_alpha) {
append_line(fs_buf, &fs_len, " if (texel.a > 0.19) texel.a = 1.0; else discard;");
if (cc_features.opt_noise) {
append_line(fs_buf, &fs_len,
" if (texel.a < ((random(vec3(floor(gl_FragCoord.xy * noise_scale), float(frame_count))) + 1.0) / 2.0)) discard;");
} else if (cc_features.opt_alpha_threshold) {
append_line(fs_buf, &fs_len, " if (texel.a < 1.0 / 256.0) discard;");
} else {
append_line(fs_buf, &fs_len, " if (texel.a < 0.5) discard;");
}
}
if (cc_features.opt_alpha && cc_features.opt_noise) {
if (cc_features.opt_alpha && cc_features.opt_noise && !cc_features.opt_texture_edge) {
append_line(fs_buf, &fs_len,
" texel.a *= floor(clamp(random(vec3(floor(gl_FragCoord.xy * noise_scale), float(frame_count))) + "
"texel.a, 0.0, 1.0));");
@@ -696,12 +712,27 @@ static void gfx_opengl_select_texture(int tile, GLuint texture_id, bool linear_f
static void gfx_opengl_upload_texture(const uint8_t* rgba32_buf, uint32_t width, uint32_t height, bool gen_mipmaps) {
#ifdef PORT
/* GE_TEXDUMP: PPM-dump every uploaded texture (first N) for B2/D161 triage. */
if (getenv("GE_TEXDUMP")) {
/* GE_TEXDUMP: PPM-dump every uploaded texture (first N) for B2/D161 triage.
* D250: this runs on every texture upload -- cache the getenv() like the
* gfx_pc.cpp import_texture() sites (uncached getenv() measured at ~50%
* of the hot render thread's CPU time via a live profile). */
static int ge_texdump_gl = -1;
if (ge_texdump_gl < 0) ge_texdump_gl = getenv("GE_TEXDUMP") != NULL;
if (ge_texdump_gl) {
static int td = 0;
if (td < 400 && width && height && width < 4096 && height < 4096) {
static int td_fire = 0;
/* D219: the 400-cap fills up during ordinary level load, long before
* a player-triggered explosion. Never suppress the 16x14 RGBA fire
* particle images (assets/oddtextures.c IMAGE_FIRE_N via
* globalDL_0x078..0x9a8) -- that's the one shape we actually need. */
const bool is_fire_shape = (width == 16 && height == 14);
if ((is_fire_shape || td < 400) && width && height && width < 4096 && height < 4096) {
char nm[128];
snprintf(nm, sizeof nm, "texdump/t%03d_%ux%u_mip%d.ppm", td++, width, height, (int)gen_mipmaps);
if (is_fire_shape) {
snprintf(nm, sizeof nm, "texdump/fire%03d_%ux%u_mip%d.ppm", td_fire++, width, height, (int)gen_mipmaps);
} else {
snprintf(nm, sizeof nm, "texdump/t%03d_%ux%u_mip%d.ppm", td++, width, height, (int)gen_mipmaps);
}
FILE* f = fopen(nm, "wb");
if (f) {
fprintf(f, "P6\n%u %u\n255\n", width, height);
@@ -1064,19 +1095,63 @@ static void gfx_opengl_init(void) {
static void gfx_opengl_on_resize(void) {
}
/* #92 perf probe (GE_PERFSTAT=1): GPU time per frame via GL_TIME_ELAPSED,
* double-buffered so reading never stalls the pipeline. Skipped when the
* driver lacks timer queries. Tells a low-end reporter's log whether frames
* are GPU-bound (render/MSAA/filtering cost) or CPU-bound. */
static int s_gpuq_on = -1;
static GLuint s_gpuq[2];
static int s_gpuq_idx = 0, s_gpuq_valid[2] = { 0, 0 };
static double s_gpuq_ms = 0.0;
static int s_gpuq_n = 0, s_gpuq_frames = 0;
static void gfx_opengl_start_frame(void) {
frame_count++;
if (s_gpuq_on < 0) {
s_gpuq_on = getenv("GE_PERFSTAT") != NULL && glGenQueries != NULL &&
glGetQueryObjectui64v != NULL && glBeginQuery != NULL;
if (s_gpuq_on) {
glGenQueries(2, s_gpuq);
fprintf(stderr, "PERFSTAT gpu timer on: %s | %s\n",
(const char *)glGetString(GL_RENDERER), (const char *)glGetString(GL_VERSION));
}
}
if (s_gpuq_on == 1) {
if (s_gpuq_valid[s_gpuq_idx]) {
GLuint avail = 0;
glGetQueryObjectuiv(s_gpuq[s_gpuq_idx], GL_QUERY_RESULT_AVAILABLE, &avail);
if (avail) {
GLuint64 ns = 0;
glGetQueryObjectui64v(s_gpuq[s_gpuq_idx], GL_QUERY_RESULT, &ns);
s_gpuq_ms += (double)ns / 1.0e6;
s_gpuq_n++;
}
}
glBeginQuery(GL_TIME_ELAPSED, s_gpuq[s_gpuq_idx]);
}
}
static void gfx_opengl_end_frame(void) {
glFlush();
if (s_gpuq_on == 1) {
glEndQuery(GL_TIME_ELAPSED);
s_gpuq_valid[s_gpuq_idx] = 1;
s_gpuq_idx ^= 1;
if (++s_gpuq_frames % 300 == 0 && s_gpuq_n > 0) {
fprintf(stderr, "PERFSTAT gpu=%.2fms/frame (avg over %d frames)\n", s_gpuq_ms / s_gpuq_n, s_gpuq_n);
s_gpuq_ms = 0.0;
s_gpuq_n = 0;
}
}
}
/* Frame capture (dev tool, env GE_PCDUMP) — see gfx_opengl.h. TEMP D70: read
* the default framebuffer (the window back buffer; the scene renders there
* directly at 1:1 window size) and write a P6 PPM. */
extern "C" bool gfx_opengl_pcdump_enabled(void) {
return getenv("GE_PCDUMP") != NULL;
static int cached = -1;
if (cached < 0) cached = getenv("GE_PCDUMP") != NULL;
return cached != 0;
}
extern "C" bool gfx_opengl_dump_bound_fbo(uint32_t width, uint32_t height, const char* path) {
@@ -1155,6 +1230,20 @@ static void gfx_opengl_update_framebuffer_parameters(int fb_id, uint32_t width,
width = max(width, 1U);
height = max(height, 1U);
/* macOS GL 4.1 core: asking for more samples than GL_MAX_SAMPLES (e.g. 8x)
* makes the multisample renderbuffers incomplete and the screen goes dark
* (glReadPixels 0x506). Clamp to what the driver reports. */
if (msaa_level > 1) {
static GLint maxSamples = -1;
if (maxSamples < 0) {
glGetIntegerv(GL_MAX_SAMPLES, &maxSamples);
if (maxSamples < 1) maxSamples = 1;
}
if ((GLint)msaa_level > maxSamples) {
msaa_level = (uint32_t)maxSamples;
}
}
if (gfx_framebuffers_enabled) {
glBindFramebuffer(GL_FRAMEBUFFER, fb.fbo);
@@ -1251,7 +1340,8 @@ void gfx_opengl_resolve_msaa_color_buffer(int fb_id_target, int fb_id_source) {
glBindFramebuffer(GL_READ_FRAMEBUFFER, fb_src.fbo);
glBlitFramebuffer(0, 0, fb_src.width, fb_src.height, 0, 0, fb_dst.width, fb_dst.height, GL_COLOR_BUFFER_BIT,
GL_NEAREST);
glBindFramebuffer(GL_FRAMEBUFFER, current_framebuffer);
/* current_framebuffer is an INDEX into framebuffers[], not a GL name; fb 0 is the window's default framebuffer (name 0). */
glBindFramebuffer(GL_FRAMEBUFFER, current_framebuffer == 0 ? 0 : framebuffers[current_framebuffer].fbo);
glEnable(GL_SCISSOR_TEST);
}
@@ -1315,7 +1405,7 @@ void gfx_opengl_copy_framebuffer(int fb_dst, int fb_src, int left, int top, bool
glBlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, GL_COLOR_BUFFER_BIT, GL_NEAREST);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffers[current_framebuffer].fbo);
glBindFramebuffer(GL_FRAMEBUFFER, current_framebuffer == 0 ? 0 : framebuffers[current_framebuffer].fbo);
glReadBuffer(GL_BACK);
+1357 -54
View File
File diff suppressed because it is too large. Load diff
+9
View File
@@ -29,6 +29,15 @@ struct TextureCacheKey {
// (gDPLoadBlock mip chains vs partial loads); include the size so a
// truncated first import cannot poison later full imports.
uint32_t size_bytes;
// D217: for CI (palettized) textures, a hash of the decoded rdp.palette
// content at import time. GE assembles weapon / character model DLs in
// scratch arena RAM and reissues gDPLoadTLUT with different palette
// *content* from a repeated source address, so
// {texture_addr, palette_addrs, palette_index, size_bytes} alone collides
// across materials/frames -> a CI tile takes a stale cache HIT decoded
// against a different palette. Recomputed only in gfx_dp_load_tlut(), so
// there is no per-texel or per-draw cost. Zero for non-CI textures.
uint32_t palette_hash;
bool operator==(const TextureCacheKey&) const noexcept = default;
+2
View File
@@ -16,6 +16,8 @@ void gfx_sdl_make_context_current(void);
/* Release the GL context from the calling (host main) thread after init, so
* the game's scheduler thread can bind it per frame (see gfx_sdl2.cpp). */
void gfx_sdl_release_context(void);
/* D344: finish queued GL work and unbind the context (render thread, quit). */
void gfx_sdl_park_for_exit(void);
/* Refresh the cached drawable size after a window resize (called by the
* host thread's event pump). */
+28 -4
View File
@@ -9,6 +9,8 @@
#include "gfx_screen_config.h"
#include "input.h"
extern "C" void videoRequestQuit(const char *why); // port/src/video.c (D344)
static SDL_Window* wnd;
static SDL_GLContext ctx;
static SDL_Renderer* renderer;
@@ -319,7 +321,9 @@ static void gfx_sdl_handle_events(void) {
} else if (event.key.keysym.sym == SDLK_F4 && (event.key.keysym.mod & KMOD_ALT)) {
// D145: Alt+F4 quits; bare ESC no longer does (it is the
// menu "back" key -- see port/src/video.c / input.c).
exit(0);
// D344: request, don't exit(): this is the render thread,
// mid-frame. It parks at the frame's end; the host exits.
videoRequestQuit("Alt+F4 (render pump)");
} else if (event.key.keysym.sym == SDLK_ESCAPE && !event.key.repeat) {
// WI-1: this render-thread pump and port/src/video.c's
// host-thread pump both drain the same SDL queue, so either
@@ -345,7 +349,7 @@ static void gfx_sdl_handle_events(void) {
event.window.windowID == SDL_GetWindowID(wnd)) {
// We listen specifically for main window close because closing main window
// on macOS does not trigger SDL_Quit.
exit(0);
videoRequestQuit("window closed (render pump)"); // D344
} else if (event.window.event == SDL_WINDOWEVENT_FOCUS_LOST) {
inputSetMouseGrab(0); // free + show cursor on alt-tab (also handled by video.c's pump; whichever dequeues it)
} else if (event.window.event == SDL_WINDOWEVENT_FOCUS_GAINED) {
@@ -353,7 +357,7 @@ static void gfx_sdl_handle_events(void) {
}
break;
case SDL_QUIT:
exit(0);
videoRequestQuit("quit event (render pump)"); // D344
break;
}
}
@@ -372,6 +376,20 @@ extern "C" void gfx_sdl_make_context_current(void) {
}
}
/* D344: called on the render thread when a quit was requested, at a frame
* boundary: wait for the GPU to drain everything this process queued, then
* unbind the context, so process teardown never lands inside the driver. */
extern "C" void gfx_sdl_park_for_exit(void) {
if (ctx && SDL_GL_GetCurrentContext() == ctx) {
typedef void (SDLCALL *FinishFn)(void); // = APIENTRY on Windows
FinishFn finish = (FinishFn)SDL_GL_GetProcAddress("glFinish");
if (finish) {
finish();
}
SDL_GL_MakeCurrent(NULL, NULL);
}
}
/* Release the GL context from the calling (host main) thread. WGL only allows
* a context to be current on one thread at a time, and while another thread
* still holds it, wglMakeContextCurrent fails with "The requested resource is
@@ -409,7 +427,13 @@ static inline void sync_framerate_with_timer(void) {
// We want to exit a bit early, so we can busy-wait the rest to never miss the deadline
left -= 15000UL;
if (left > 0) {
sysSleep(left);
/* D250: `left` is in 100ns units (qpc_to_100ns); sysSleep() takes
* microseconds. Passing it unconverted slept ~10x too long every
* time this branch fired (e.g. an intended 2ms sleep became a real
* ~20ms one), silently eating the rest of the frame budget whenever
* per-frame work left little slack -- a resolution/GPU-independent
* frame-pacing bug, not a real render cost. */
sysSleep((uint32_t)(left / 10));
}
do {
+5 -3
View File
@@ -23,12 +23,14 @@ void audioDestroy(void);
/* Number of samples (stereo s16 frames) currently queued. */
s32 audioGetSamplesBuffered(void);
/* D204/F1: emulate the N64 AI_LEN_REG -- bytes remaining in the buffer the
* DAC is *currently* playing, NOT the whole queue depth. src/audi.c does
* arithmetic on this value that only holds inside the N64's range. */
u32 audioGetAiLengthBytes(void);
/* Queue the next block of mixed samples (len is in bytes). */
void audioSetNextBuffer(const s16 *buf, u32 len);
/* Called at the end of each frame to flush the queued buffer to the device. */
void audioEndFrame(void);
#ifdef __cplusplus
}
#endif
+17
View File
@@ -0,0 +1,17 @@
/*
* audiotrace.h — serialized trace writer for the GE_AUDIOTRACE / WIRE probes.
*
* D202/M-70 (PORT): the probe sites live in TWO threads — sndPlaySfx runs on
* the game thread, the sndp event handlers ([EVT]/[VOICE+]/[STOP-EVT]) and
* load.c's [PASTEND] run on the audio thread. Each site used to open its own
* unbuffered FILE* on the same path; concurrent fprintf() calls interleaved
* MID-LINE and corrupted records, which M-69's offline matcher misread as
* dropped voice allocations. One FILE* per path plus a spinlock around
* vfprintf makes each line atomic.
*/
#ifndef PORT_AUDIOTRACE_H
#define PORT_AUDIOTRACE_H
void geTracePrintf(const char *path, const char *fmt, ...);
#endif /* PORT_AUDIOTRACE_H */
+13
View File
@@ -0,0 +1,13 @@
/* d318bprobe.h -- D318/D320 boundary probe (M-199 step 2).
*
* Env-gated per-tick ground-truth logger for the aim-hold / derail /
* anim-pin state machine of chrlvTickAttack + chrlvTickAttackCommon.
* Read-only; inert without GE_D318B. TEMP: remove when D318/D320
* conclude (same class as d320repro.c).
*/
#ifndef D318BPROBE_H
#define D318BPROBE_H
void d318bProbeTick(void);
#endif /* D318BPROBE_H */
+20
View File
@@ -0,0 +1,20 @@
#ifndef _ENVFLAG_H_
#define _ENVFLAG_H_
/* Cached env-var probes for hot paths (D250/D302: an uncached getenv() per
* node/poly/audio event is a measurable per-frame cost on Windows). Each
* expansion site gets its own static, read once on first use; the env is
* fixed for the process lifetime, so this is exact. The TU must already have
* a real getenv() prototype (see D324). GCC/Clang statement expressions. */
#define GE_ENVFLAG(name) __extension__({ \
static int _ge_envflag = -1; \
if (_ge_envflag < 0) _ge_envflag = getenv(name) != NULL; \
_ge_envflag; })
#define GE_ENVSTR(name) __extension__({ \
static int _ge_envstr_init = 0; \
static const char *_ge_envstr; \
if (!_ge_envstr_init) { _ge_envstr = getenv(name); _ge_envstr_init = 1; } \
_ge_envstr; })
#endif
+33
View File
@@ -0,0 +1,33 @@
#ifndef PORT_FLOATVTX_H
#define PORT_FLOATVTX_H
/* D245 (M-201): port-only full-precision vertex load for the sky/water fans.
*
* On N64, sky.c builds these triangles directly as RDP edge/texture
* coefficients with 32-bit S/T, so a horizon-scale quad (the IsWater ocean
* spans ~300,000 S10.5 units, ~9,400 texels) is exact. Routed through an
* ordinary gSPVertex, both its texture coords (s16 tc: +-1,024 texels) and its
* positions (s16 ob, divided by a fan-wide wScale to fit camera-space w up to
* ~2e5) had to be quantised -- visible as water/sky jitter that slides with
* the view. G_FLOATVTX_EXT hands fast3d the clip-space position and S/T as
* floats instead; fast3d already stores every loaded vertex as floats.
*
* w0 = G_FLOATVTX_EXT << 24 | dest_index << 16 | count, w1 = PortFloatVtx *.
* s/t are in Vtx.tc units (S10.5, before gSPTexture scaling). */
#define G_FLOATVTX_EXT 0x47
typedef struct PortFloatVtx {
float x, y, z, w; /* clip space */
float s, t; /* tc units */
unsigned char r, g, b, a;
} PortFloatVtx;
#define gSPFloatVertexExt(pkt, ptr, n, v0) \
do { \
Gfx *_g = (Gfx *)(pkt); \
_g->words.w0 = ((uintptr_t)G_FLOATVTX_EXT << 24) | \
(((uintptr_t)(v0) & 0xff) << 16) | ((uintptr_t)(n) & 0xffff); \
_g->words.w1 = (uintptr_t)(ptr); \
} while (0)
#endif
+35
View File
@@ -0,0 +1,35 @@
#ifndef PORT_FRONTOPTIONS_H
#define PORT_FRONTOPTIONS_H
/*
* D343: PC options as a GE front-end screen, MENU_PC_OPTIONS
* (port/src/frontoptions.c).
*
* Hooks:
* front.c the five MENU_* dispatch sites : frontOptionsMenu{Init,Update,
* Interface,Draw} and the debug name (all #ifdef PORT)
* front.c constructor_menu05_fileselect : optionsFileSelectLabel() draws the
* file-select entry label and enters MENU_PC_OPTIONS on a click
* video.c / input.c : F10 / Select don't open the overlay on this screen
*/
#include <PR/ultratypes.h>
#include <PR/gbi.h>
#ifdef __cplusplus
extern "C" {
#endif
void frontOptionsMenuInit(void);
void frontOptionsMenuUpdate(void);
void frontOptionsMenuInterface(void);
Gfx *frontOptionsMenuDraw(Gfx *DL);
Gfx *optionsFileSelectLabel(Gfx *gdl); /* file-select hook */
int frontOptionsBlocksOverlay(void); /* 1 while MENU_PC_OPTIONS is up */
#ifdef __cplusplus
}
#endif
#endif /* PORT_FRONTOPTIONS_H */
+49
View File
@@ -0,0 +1,49 @@
#ifndef _HUDASPECT_H_
#define _HUDASPECT_H_
/* D335 (WIDESCREEN-FOV-PLAN Option B, Phase 3): HUD alignment under native
* widescreen (D334). The world is projected at the window aspect, but 2D/HUD
* elements are laid out in the game's 4:3 logical canvas, which fast3d
* stretches to the window. Wrapping a HUD draw in an aspect mode makes fast3d
* map its x undistorted (x scaled by (4/3)/window_aspect) and anchor it to
* the left edge, the right edge, or the centre -- the PD port's
* G_ASPECT_{LEFT,RIGHT,CENTER}_EXT extra-geometry-mode, which GE's fast3d
* already decodes (gfx_pc.cpp gfx_sp_extra_geometry_mode /
* gfx_update_aspect_mode). Game-includable: no SDL, no fast3d headers.
*
* PORT_HUD_ASPECT emits only while native widescreen is on and the window is
* wider than 4:3; otherwise the display list is byte-identical to before. */
#define GE_HUD_ASPECT_NONE 0x00u
#define GE_HUD_ASPECT_LEFT 0x10u /* G_ASPECT_LEFT_EXT (port/fast3d/gbiex.h) */
#define GE_HUD_ASPECT_RIGHT 0x20u /* G_ASPECT_RIGHT_EXT */
#define GE_HUD_ASPECT_CENTER 0x30u /* G_ASPECT_CENTER_EXT */
#define GE_HUD_ASPECT_MASK 0x70u /* G_ASPECT_MODE_EXT */
#define GE_EXTRAGEOMETRYMODE_EXT 0x3a /* G_EXTRAGEOMETRYMODE_EXT */
/* The decoder does extra_mode = (extra_mode & ~clear) | set with
* clear = ~(w0 & 0xFFFFFF): passing ~MASK as the low 24 bits clears exactly
* the aspect bits and leaves the other extra-geometry flags alone. */
#define gSPHudAspectEXT(pkt, mode) \
gDma0p((pkt), GE_EXTRAGEOMETRYMODE_EXT, (mode), ((~GE_HUD_ASPECT_MASK) & 0xFFFFFFu))
extern float portNativeAspect(void); /* port/src/video.c */
/* Only for windows wider than 4:3: in a narrower window a centred 4:3 HUD
* would be wider than the window and crop, so there the HUD keeps the canvas
* mapping (squeezed, never cropped). 1.3334 so exact 4:3 emits nothing. */
#define PORT_HUD_ASPECT(gdl, mode) \
do { if (portNativeAspect() > 1.3334f) { gSPHudAspectEXT((gdl)++, (mode)); } } while (0)
/* D226: HUD span scale. PORT_HUD_SCALE(gdl, ax, ay) scales every rectangle
* drawn after it (glyphs, message boxes, ammo icons) about the anchor
* (logical screen px) by Game.HudScale %, until PORT_HUD_SCALE_END. At 100%
* nothing is emitted (byte-identical). Decoded as G_HUDSCALE_EXT in fast3d. */
#define GE_HUDSCALE_EXT 0x46
extern s32 portHudScalePercent(void); /* port/src/video.c */
#define gSPHudScaleEXT(pkt, scale256, ax4, ay4) gDma0p((pkt), GE_HUDSCALE_EXT, ((u32)(ax4) << 16) | ((u32)(ay4) & 0xFFFFu), ((u32)(scale256) & 0xFFFFu))
#define PORT_HUD_SCALE(gdl, ax, ay) do { s32 _hp = portHudScalePercent(); if (_hp != 100) { gSPHudScaleEXT((gdl)++, _hp * 256 / 100, (ax) * 4, (ay) * 4); } } while (0)
#define PORT_HUD_SCALE_PCT(gdl, pct, ax, ay) do { s32 _hp = (pct); if (_hp != 100) { gSPHudScaleEXT((gdl)++, _hp * 256 / 100, (ax) * 4, (ay) * 4); } } while (0)
#define PORT_HUD_SCALE_END(gdl) do { if (portHudScalePercent() != 100) { gSPHudScaleEXT((gdl)++, 256, 0, 0); } } while (0)
#endif
+41 -7
View File
@@ -12,6 +12,7 @@
*/
#include <PR/ultratypes.h>
#include <SDL.h> /* API types (SDL_GameControllerButton/Axis); cf. video.h */
#ifdef __cplusplus
extern "C" {
@@ -43,28 +44,61 @@ unsigned inputComputePad(int idx, signed char *stick_x, signed char *stick_y);
* also suspends mouse-aim reads until re-grabbed. No-op if the mouse is
* disabled in config. */
void inputSetMouseGrab(int on);
/* D287: apply queued mouse-mode/cursor changes; host (window) thread only. */
void inputApplyMouseRequests(void);
/* WI-1 click-to-lock cursor capture (Input.MouseCaptureMode = 1). The host
* event pump calls inputNotifyClick() when a mouse button goes down inside the
* game window (arms + locks the cursor), and inputReleaseCapture() when ESC is
* pressed (frees it; returns 1 if it consumed the key). inputMouseCaptureActive()
* is true when capture mode is on and the cursor is currently free -- the pump
* uses it to decide whether a click should be swallowed rather than passed on. */
/* WI-1 click-to-lock cursor capture. The host event pump calls
* inputNotifyClick() when a mouse button goes down inside the game window
* (arms + locks the cursor), and inputReleaseCapture() when ESC is pressed
* (frees it; returns 1 if it consumed the key). */
void inputNotifyClick(void);
int inputReleaseCapture(void);
int inputMouseCaptureActive(void);
/* F10 options overlay: force the OS cursor free + visible while the overlay
* owns the mouse. Safe to call every poll. */
void inputSuspendForOverlay(void);
/* Raw gamepad state for controller idx (the F10 options overlay's gamepad
* navigation). Unaffected by the overlay's pad-swallow, which happens at our
* logic layer, not SDL's. No pad open -> 0. */
int inputPadButton(int idx, SDL_GameControllerButton b);
short inputPadAxis(int idx, SDL_GameControllerAxis a);
/* Queue a mouse-wheel weapon-cycle input (one short A-button press). Sign is
* ignored -- GE only cycles forward on a bare A edge. */
void inputPostWheel(int notches);
/* v0.4.0 M3: re-derive the keyboard binds after an F10 change to
* Input.CrouchMode or a future binding capture (optionsoverlay hook).
* Also drops the latched-crouch state. Scheduler thread only. */
void inputBindingsApply(void);
/* D384/D385: Primary/Secondary each accept a key or Mouse 1..5.
* Legacy extra ini tokens remain on disk but are ignored until action edit. */
#define INPUT_BIND_SLOTS 2
#define INPUT_BIND_MOUSE(button) (SDL_NUM_SCANCODES + (button))
const char *inputBindingSlot(const char *key, int slot);
int inputBindingSetSlot(const char *key, int slot, int code);
int inputBindingResetKey(const char *key);
/* D345(b): 1 while the 1:1 menu pointer owns cursor_h/v_pos (in a menu,
* abs pointer available, mouse used within the re-assert window). Port
* screens that teleport the crosshair should skip their snap when set. */
int inputMenuPointerLive(void);
/* Re-enumerate gamepads after a hotplug (SDL_CONTROLLERDEVICEADDED/REMOVED). */
void inputRescanPads(void);
/* D401: Rumble Pak -> real gamepad haptics. The osMotor* shims in
* libultra.c (game's src/joy.c rumble state machine) route here.
* idx is the N64 controller channel (0..3; out-of-range is a no-op).
* strength is 0..1, time seconds; 0/0 stops an active rumble. The scale
* is the single global Input.RumbleScale config value (0 = silent),
* applied to every pad. */
int inputRumbleSupported(int idx);
void inputRumble(int idx, f32 strength, f32 time);
f32 inputRumbleGetScale(void);
void inputRumbleSetScale(f32 v);
#ifdef __cplusplus
}
#endif
+61 -11
View File
@@ -2,16 +2,21 @@
#define PORT_MIXER_H
/*
* Audio mixing.
* Modelled on the PD port's port/include/mixer.h.
* Audio mixing — software implementation of GE's RSP audio ucode opcodes.
*
* On the N64 the RSP audio ucode does the final mix / sample-rate handling
* before the AI DMA. Since the RSP is emulated in software here, this module
* performs that final processing on the CPU and hands the result to audio.c.
* include/PR/abi.h's aXxx macros normally pack RSP command words into an
* Acmd list for the audio ucode (aspMain, never run on PC — port/src/ucode.c)
* to execute later. Under #ifdef PORT, abi.h includes this header instead,
* which redefines every aXxx macro to call an Impl function here immediately
* — same macro-swap trick as the Perfect Dark PC port (docs/dev/AUDIO-PLAN.md).
*
* NOTE: the exact responsibilities depend on how GE's libaudio output is
* wired (see src/audi.c). This is a placeholder interface to be refined in
* Phase 3 (audio).
* GE uses the classic IDO libaudio ABI (verified against every call site in
* src/libultra/audio and src/libultrare/audio/{env,reverb}.c — NOT PD's
* differently-shaped "naudio New" ABI). Several opcodes (aADPCMdec,
* aResample, aEnvMixer, aLoadBuffer, aSaveBuffer) take only a state/DRAM
* pointer; their DMEM source/dest addresses and byte counts come from the
* most recent aSetBuffer call(s), tracked here as persistent context —
* see the field comments on MixerCtx in mixer.c.
*/
#include <PR/ultratypes.h>
@@ -23,9 +28,54 @@ extern "C" {
void mixerInit(void);
void mixerDestroy(void);
/* Process one frame of audio. `in` is the libaudio-mixed buffer, `frames`
* the number of stereo frames. Output is queued via audioSetNextBuffer. */
void mixerProcess(const s16 *in, u32 frames);
/* Software RSP-audio-ucode opcode implementations (port/src/mixer.c). */
void aSetBufferImpl(u32 flags, u16 i, u16 o, u16 c);
void aClearBufferImpl(u16 addr, u32 count);
void aLoadBufferImpl(u32 dramAddr);
void aSaveBufferImpl(u32 dramAddr);
void aDMEMMoveImpl(u16 in, u16 out, u32 count);
void aLoadADPCMImpl(u32 count, u32 dramAddr);
void aSetLoopImpl(u32 stateAddr);
void aADPCMdecImpl(u32 flags, u32 stateAddr);
void aResampleImpl(u32 flags, u16 pitch, u32 stateAddr);
void aInterleaveImpl(u16 l, u16 r);
void aMixImpl(u32 flags, u16 gain, u16 in, u16 out);
void aSetVolumeImpl(u32 flags, u16 v, u16 t, u16 r);
void aEnvMixerImpl(u32 flags, u32 stateAddr);
void aPoleFilterImpl(u32 flags, u16 gain, u32 stateAddr);
void aSegmentImpl(u32 seg, u32 base);
#undef aSetBuffer
#undef aClearBuffer
#undef aLoadBuffer
#undef aSaveBuffer
#undef aDMEMMove
#undef aLoadADPCM
#undef aSetLoop
#undef aADPCMdec
#undef aResample
#undef aInterleave
#undef aMix
#undef aSetVolume
#undef aEnvMixer
#undef aPoleFilter
#undef aSegment
#define aSetBuffer(pkt, f, i, o, c) aSetBufferImpl((u32)(f), (u16)(i), (u16)(o), (u16)(c))
#define aClearBuffer(pkt, d, c) aClearBufferImpl((u16)(d), (u32)(c))
#define aLoadBuffer(pkt, s) aLoadBufferImpl((u32)(s))
#define aSaveBuffer(pkt, s) aSaveBufferImpl((u32)(s))
#define aDMEMMove(pkt, i, o, c) aDMEMMoveImpl((u16)(i), (u16)(o), (u32)(c))
#define aLoadADPCM(pkt, c, d) aLoadADPCMImpl((u32)(c), (u32)(d))
#define aSetLoop(pkt, a) aSetLoopImpl((u32)(a))
#define aADPCMdec(pkt, f, s) aADPCMdecImpl((u32)(f), (u32)(s))
#define aResample(pkt, f, p, s) aResampleImpl((u32)(f), (u16)(p), (u32)(s))
#define aInterleave(pkt, l, r) aInterleaveImpl((u16)(l), (u16)(r))
#define aMix(pkt, f, g, i, o) aMixImpl((u32)(f), (u16)(g), (u16)(i), (u16)(o))
#define aSetVolume(pkt, f, v, t, r) aSetVolumeImpl((u32)(f), (u16)(v), (u16)(t), (u16)(r))
#define aEnvMixer(pkt, f, s) aEnvMixerImpl((u32)(f), (u32)(s))
#define aPoleFilter(pkt, f, g, s) aPoleFilterImpl((u32)(f), (u16)(g), (u32)(s))
#define aSegment(pkt, s, b) aSegmentImpl((u32)(s), (u32)(b))
#ifdef __cplusplus
}
Loaded 100 of 244 files, more files were not shown because too many files have changed in this diff. Show more