264 Commits
Author SHA1 Message Date
popux 0740036903 Tank fix 2026-10-23 22:56:06 +02:00
popux a336ab507e Glass transparency fix 2026-10-23 20:56:05 +02:00
popux 4e8ea4ea96 Mouse sensibility tweaks 2026-10-23 20:38:59 +02:00
popux 98cd3b6b22 Gyro fix 2026-10-23 16:01:53 +02:00
popux 3cefb1d4e5 Look ahead 2026-10-23 10:11:40 +02:00
popux 0e767334f7 Draw order more fixes 2026-10-22 19:10:56 +02:00
popux 6c183d9434 Draw order 2026-10-22 19:00:41 +02:00
popux fd7800cf80 Unbreakable glass decals fix 2026-10-22 18:44:16 +02:00
popux 06856ed973 Water decal improvement 3 2026-10-22 18:27:58 +02:00
popux fbc8911238 Water decal improvement 2 2026-10-22 18:20:50 +02:00
popux 3154efa79f Water decal improvement 2026-10-22 18:06:02 +02:00
popux af9a3c19e6 Fix Water decals 2026-10-22 18:05:41 +02:00
popux dd88c8ee28 Fix mac build 2026-10-22 16:24:11 +02:00
popux aa8c2754d9 Fix corpses crash 2026-10-22 16:23:47 +02:00
popux 5787e3abbc Fix respawn 2026-10-22 16:23:41 +02:00
popux 342e825c73 Corpses 2026-10-22 13:08:39 +02:00
popux f3b74e9c7c Decal fix 2026-10-21 20:31:03 +02:00
James Dansereau 5b86a9bbeb v0.6.0: release notes, ROADMAP and findings for D953-D955 (maintainer Deck-confirmed) 2026-10-13 22:09:22 -04:00
James Dansereau e70fffffcd D953: a wider Field of view no longer pulls model LOD, prop fog fade and fog visibility closer
c_lodscalez is built from the rendered FOV (D222), so Video.FovScale moved every model LOD switch,
the prop fog fade and the fog-range visibility test closer (x0.58 at 150); half-fogged props such
as the Surface windows went fully invisible and flickered while turning. All three now use the
game's own FOV (portLodScaleOwnFov); identity at the default FOV.
2026-10-13 22:09:22 -04:00
James Dansereau a77bd4e03d D955: at 90+ fps, object motion and the sky are blended between ticks
Guards, doors and props moved at the 60 Hz tick inside a 90 Hz picture. Each object's joint
matrices are now blended, keyed by (Model*, joint) from a game-thread snapshot and rebased onto
the new camera; a chr blends together with its held weapons and hat; room-origin changes, failed
gates and split-screen stay exact. The CPU-projected sky is re-projected to the in-between camera.
Destroyed objects (world-space matrices) stay exact. Port layer only; porting-notes D46.
2026-10-13 22:09:22 -04:00
James Dansereau b7280e5386 D954: at 90+ fps, props (guards, doors, objects, effects) follow the blended camera
In-between presents drew props at the tick's camera while rooms used the blended one, so props
jumped against the world whenever the camera moved (doors and guards flickering at 90 fps, never
at 60). The props' plain perspective now gets the room camera blend (P * R^-1 * Rb) from
bgLevelRender up to the first-person gun. Port layer only (gfx_pc.cpp, interpportal.c).
2026-10-13 22:09:22 -04:00
James Dansereau 7d3abbf813 v0.6.0: D952 fix in; macOS black-window known issue removed from notes, ROADMAP, user guide and the macOS download README 2026-10-13 14:08:17 -04:00
James Dansereau a9feb0960f D952: macOS black window when started with the display asleep - VSync through CGL, not SDL3's display-link wait 2026-10-13 14:02:56 -04:00
James Dansereau af5e75ccbf Release notes: one line per paragraph and bullet (GitHub release pages render every newline as a line break) 2026-10-13 13:19:37 -04:00
James Dansereau 971ff2b2bd v0.6.0 docs accuracy pass: credits-crash and exit wording, known-issues table, download README save import and menu paths, D951 2026-10-13 13:14:59 -04:00
James Dansereau ac8b0f38df Download README: frame rates above 60 and the macOS ROM check 2026-10-13 11:25:57 -04:00
James Dansereau 61e45ed085 Release notes: link the user guide and about page at the v0.6.0 tag, not the release branch 2026-10-13 10:57:17 -04:00
James Dansereau c3e64329c4 README: SEO pass (Apple Silicon in the tagline, ultrawide, descriptive alt and link text, Steam Deck and refresh-rate FAQ entries) 2026-10-13 10:53:11 -04:00
James Dansereau 1ca5eb197e Docs: README, release notes and user docs restructured for v0.6.0
- README in the shape of the top decomp/recomp ports: title, one-paragraph
  pitch, latest-release link, a single no-assets line; then Requirements,
  Quick start, Controls, What this port offers, FAQ (new), Status, Roadmap,
  Building, Documentation, Contributing, Credits, Legal. 852 -> ~370 lines.
- Moved, not removed: docs/user-guide.md (full controls, feature list,
  settings and files, ROM check, macOS setup, updating, emulator saves,
  uninstalling, troubleshooting) and docs/about.md (release history, status
  in detail, how it was made with the models/tools table, how it works, the
  comparison with the XBLA projects).
- Release notes: upgrade and code-signing callouts, Highlights, Downloads,
  Getting started, Added/Fixed/Changed without internal finding labels, a
  shorter known-issues list, Tested on, Thanks, Verify (incl. attestation).
- ROADMAP: v0.7.0 rows for the macOS first-launch black window and the pixel
  gate vs the Auto frame-cap default. building.md: ROM table link.
2026-10-13 10:47:31 -04:00
James Dansereau cdf43d0140 v0.6.0: license texts in every download; README/NOTICE wording pass
- The Windows and Linux bundles shipped without the GCC runtime and SDL2
  license texts when the build machine lacked them (CI's MSYS2 has no
  share/licenses/gcc-libs; CI's Linux image has no dpkg). bundle-win.sh,
  bundle-linux.sh and package-macos.sh now fall back to in-tree copies:
  tools_pc/dist/GPL-3.0.txt, GCC-RUNTIME-EXCEPTION.txt, SDL2-LICENSE.txt.
- NOTICE / README: say the repository and downloads carry no ROM data or
  original assets and that the C source is a reconstruction (not "no
  Nintendo code"); describe the license-text sources.
- README: "What this port offers"; split-screen for up to 4 players; the
  few game-code changes keep N64 behavior or are opt-in; neutral wording
  for the comparison with the XBLA recompilation projects (rows about their
  status and motives removed); verification approach stated; release notes
  drop the 165/240 testing caveat.
2026-10-13 09:58:48 -04:00
James Dansereau 162691a3f5 v0.6.0: issue fixes and v0.7.0 roll-ins before release
Issue fixes:
- D643 (#145 b/c, #149): Original control style is a strict virtual N64 pad
  (native buttons in stage/menu/watch/tank, per-seat Input.N64Pad remap,
  Input.N64StickFeel)
- D605 + D645 (#145 a): Invert look reaches the original styles; in Original
  it is the game's own Look up/down, synced with the F10 row
- D606 (#144): optional always-visible mission timer
- D644 (#146): IA4 decodes as the RDP's I3:A1 (file-select crest drew on a
  dark square)
- D581 (#151): open on the real primary monitor (SDL display 0) and centre
  every window placement in the display's global bounds; the earlier
  "display at (0,0)" rule still picked the left monitor
- D580 (#153, #156): runtime check of the propDef type/end-of-list fix
- D646: F10 dropdown popup stays inside the settings box

Frame rate (#148):
- D578: Auto follows the display up to 240 (4 presents per sim tick, the
  renderer's ceiling); 165 and 240 in the Frame rate cap row

Rolled in from release/v0.7.0:
- D632: release the GL context + window before exit (BSOD 0x119)
- D626: Windows per-monitor DPI awareness
- D639: exclusive fullscreen uses the desktop mode
- verify.sh: a frame-size mismatch fails; demobatch/flicker_scan harness

Release gate, licenses, docs:
- NOTICE: macOS packaging and pi/ coverage; runtime libraries bundled in
  the downloads (SDL2/SDL3, zlib, MinGW runtime, macOS GCC runtime)
- macOS package ships SDL3, GCC COPYING.RUNTIME and the GPL-3.0 text
  (tools_pc/dist/GPL-3.0.txt)
- README: release date 2026-10-13, "Why play this port", Legal (Apple,
  runtime licenses), frame-rate text; site date tokens
- tools_pc/release_gate.sh: one-command win/deck/mac release checks
- release notes, findings D578/D580/D581/D643-D646 + index, porting notes
2026-10-13 09:37:19 -04:00
James Dansereau 884b8fb8a5 ROADMAP: v0.6.0 wrap-up accuracy — #95 merged (record merge 903095c9 + f87f4bcd), origin/macos + ci/selfhosted-fork-runners deleted from GitHub, #131 closed (scaffold parked park/ci-selfhosted-sweep-131), #124 tentatively v0.7.0 triage, #108 addressed as D573 2026-10-10 12:36:56 -04:00
James Dansereau c4ba655227 AGENTS: rule 7 — squashes/rebuilds must not erase contributor PRs (record merges, Forgejo backup + tree-diff gate + CI re-run before force-push; PR-base handling) 2026-10-10 12:20:37 -04:00
James Dansereau cd4902dfe6 ROADMAP: v0.7.0 picks (alt-tab, DPI awareness, exclusive fullscreen, Linux parity gate, Deck interp goldens, self-hosted runner) 2026-10-10 12:16:27 -04:00
James Dansereau 5d9ecb495a Docs: alt-tab known issue in the v0.6.0 notes; README macOS bullet (download exists) 2026-10-10 12:16:27 -04:00
James Dansereau 2ff406b9a8 GE-ENV-PROBES: rows for the v0.6.0 interpolation, Deck harness and macOS probes 2026-10-10 12:16:27 -04:00
3f73a18db1 docs: v0.6.0 README, release notes, site pages, social card, version 0.6.0
Co-authored-by: f1zz1ec0ke <220638765+f1zz1ec0ke@users.noreply.github.com>
Co-authored-by: danturn <7792186+danturn@users.noreply.github.com>
Co-authored-by: JunielKatarn <4507319+JunielKatarn@users.noreply.github.com>
Co-authored-by: italoarruda <3868006+italoarruda@users.noreply.github.com>
Co-authored-by: dolent <16023813+dolent@users.noreply.github.com>
2026-10-10 12:16:27 -04:00
James Dansereau 218cb90231 docs: findings D573-D614, porting notes, ROADMAP (v0.5.1 renumbered to v0.6.0, D585 parked, known issues) 2026-10-10 12:16:27 -04:00
James Dansereau 130ebc456b dev tooling: LLM session discipline (dev-process section 8) and the pi/ subagent package 2026-10-10 12:16:27 -04:00
James Dansereau b0d4abbfdf golden: Windows reference set re-baselined (D596) 2026-10-10 12:16:26 -04:00
James Dansereau 62c56cf886 ci+tools: macOS arm64 release package (scripts/package-macos.sh) + macos-build CI job, Mach-O symbol tooling, Deck levelbench/deckrunner harness, verify.sh ini extras 2026-10-10 12:16:26 -04:00
859a777d61 feat+fix: frame interpolation above 60 fps (D578, D583, D584; rework of #137), experimental macOS on Apple Silicon (#95, #88; D586-D604, D607-D613), issue fixes #150 (D579), #151 (D581), #152 (D582), #153 (D580), #108 (D573), ge007.log (D576), update-check fix, HUD-150 sliders, preset scope (D440); interpolation dev switches stripped for release
Co-authored-by: f1zz1ec0ke <220638765+f1zz1ec0ke@users.noreply.github.com>
Co-authored-by: danturn <7792186+danturn@users.noreply.github.com>
Co-authored-by: JunielKatarn <4507319+JunielKatarn@users.noreply.github.com>
Co-authored-by: italoarruda <3868006+italoarruda@users.noreply.github.com>
2026-10-10 12:16:26 -04:00
James Dansereau 903095c9c8 Merge pull request #95 from danturn/macos-arm64-gcc
Experimental macOS on Apple Silicon (record merge).

The content of this PR shipped in v0.6.0 via the macos-arm64 integration
line (D585-D613) and is carried by the commit above the merge in this
history; the release line uses maintainer squashes for its own work, so
this merge records the PR against that history. See the PR for the full
diff, and D600/D601-era notes for the port_addr decision made with the
author. Intel (x86_64) status: see issue #154.
2026-10-10 12:16:13 -04:00
James Dansereau fda76b6983 Merge pull request from jkdansereau/goldeneye-pc-port/docs/post-v050
Post-v0.5.0 docs: Pages markdown fix, credit line, ROADMAP
2026-10-07 12:07:09 -04:00
James Dansereau 0698524e87 Post-v0.5.0 docs: Pages markdown fix, credit line, release-notes template, ROADMAP/D572
- Pages: the two cblock sections render as markdown under GitHub Pages'
  kramdown (markdown="1"); the Download block's unparsed `<file>` had swallowed
  every heading below it. Gallery alt text says 3840x2160.
- Credit line (README "How it was made" + social preview card): several coding
  agents directed by one maintainer, with a handful of contributors; card
  findings count 430.
- Release-notes template: drop the stale header-stills comment and the Aztec
  re-check-owed note.
- ROADMAP: Windows display-scaling (not DPI-aware) item
  D572: CI-tarball verification (glibc floor, X220 + Deck fresh
  install, 21/21 pixel gate on both).
2026-10-07 12:04:04 -04:00
James Dansereau 56b574cb1c Merge pull request from jkdansereau/goldeneye-pc-port/release/v0.5.0
release/v0.5.0: v0.5.0 split-screen, options menu, N64 accuracy and Deck fixes
2026-10-07 10:50:51 -04:00
James Dansereau 5c43e69b23 Docs media: ~29 s looping v0.5.0 gameplay montage (GIF, 400x226 @ 15 fps, 15.8 MB) replaces the README stills strip; split-screen still is now 2-player 2026-10-07 10:39:21 -04:00
James Dansereau 10651a88d1 Docs media: v0.5.0 stills from 4K captures (12 level intros at max settings + split-screen and options-menu feature shots); README strip, release-notes header and site gallery use them 2026-10-07 09:49:32 -04:00
James Dansereau c45dece72f CI: linux-build container job runs steps under bash (D572) 2026-10-07 08:43:27 -04:00
James Dansereau 9f1cfe909e CI: Linux bundle built in the Steam Runtime sniper container (glibc floor 2.38 -> 2.31), SDL2 2.30.0 from source (D572) 2026-10-07 08:39:34 -04:00
James Dansereau 1d75c4c849 Docs: v0.5.0 release date 2026-10-07; Linux tarball needs glibc 2.38+ (README requirements, release-notes Known issues) 2026-10-07 08:24:16 -04:00
James Dansereau 44f526c3e8 eep_convert.py: no nested same-quote f-string (Python < 3.12 SyntaxError; ships in the bundles) 2026-10-06 22:23:44 -04:00
James Dansereau 4875fd4343 bundle-win.sh: package build-pc/ge007.x86_64.exe first, not a leftover renamed exe 2026-10-06 22:15:28 -04:00
James Dansereau 0a80d19333 Release notes: final build is 21/21 on all three platforms (Windows re-run on a quiet desktop) 2026-10-06 22:13:12 -04:00
James Dansereau e67c419628 Docs: ROADMAP D565 row describes the shipped Fog distance floor; release-note gate line matches the final runs 2026-10-06 22:07:42 -04:00
James Dansereau b5f1c18152 Icon: in-game sight red, pixel-art style, for exe, window and favicon (D571) 2026-10-06 21:59:17 -04:00
James Dansereau 107f151530 Crosshair: clamp the sight texture when scaled, no speck under the bottom bar (D570) 2026-10-06 21:32:33 -04:00
James Dansereau 3c3ba4b5fd F10: rename to "Crosshair always on" (D436), off by default; tip and docs 2026-10-06 21:27:20 -04:00
James Dansereau 4ab2b96a54 Docs: crosshair when not aiming (#123, D436) and PD-style crosshair colour (D569) in README, release notes and ROADMAP 2026-10-06 21:23:01 -04:00
James Dansereau afd86cd7f8 Merge PR #123 from dolent: opt-in crosshair when not aiming (D436)
Video.CrosshairPersistent, F10 Game page after Show crosshair. Merged with
the 2026-10-01 review conditions: both display-mode presets set it off,
tip added, gunDrawSight change is draw-only (Rule 2 sign-off by the
maintainer, 2026-10-06). Default off is pixel-identical (Dam gate PASS).
2026-10-06 21:22:17 -04:00
James Dansereau fadc0d6d33 F10: dropdown popup stays on screen (D568); Crosshair color is Original/Custom with 0-255 sliders, PD style (D569) 2026-10-06 21:20:22 -04:00
James Dansereau 1d15e5695a v0.5.0 final pass: AI and Deck fixes (D565-D567), docs and site refresh
Fixes:
- D565: AI awareness no longer follows two player render settings. Fog
  distance is floored at 100 (below it the shortened clip hid guards from
  AI on-screen checks); the D466 gameplay fog cull is rescaled to the game's
  own FOV (identity at defaults).
- D566: Steam Deck Game Mode no longer shows the on-screen keyboard at
  launch (SDL text input stopped; screen-keyboard hint off).
- D567: opening PC Options from the front end with a pad no longer drags
  the game's crosshair to the top-left corner.

Docs and site:
- README News/Status/Features/controls brought up to date; the unpublished
  v0.4.1 work is folded into v0.5.0 everywhere; stats to v0.5.0.
- Release notes reordered: player-facing highlights first, finding labels
  and verification at the bottom.
- Site status rewritten; favicon is now the exe icon (make_icon.py writes
  docs/favicon.ico).
- ROADMAP: AI review results, update-check follow-up, stale rows closed.

Verified: Windows + Linux builds; Windows pixel gate Surface 1 PASS;
Deck build deployed for the maintainer's Game Mode checks.
2026-10-06 21:06:48 -04:00
James Dansereau 4b710728bf v0.5.0 playtest fold-in: fog and near-plane clip, crash fixes, polish (D540-D564)
Deck/Windows playtest fixes: fog rework (D540) and RSP-style CPU near-plane
clip with per-vertex N64 fog (D543), with fog recomputed on near-plane-clipped vertices as the RSP does,
so the haze around the player stays steady (D553); colour-combiner clamp for banded walls
(D548); rocket and thrown-item stack overflows (D545, D549); menu
double-trigger mitigation (D541); Dam ending first-shot camera swivel
restored by removing an old port look-at workaround (D552); settings standardisation (D546), live
All unlocked (D547), F10 mouse (D544); static window title and the project
mark (the in-game crosshair shape, site gold) as window/taskbar icon and
favicon (D550). Reference frames re-based
on Windows, Linux and Steam Deck (A/B-confirmed); capture_p7.sh runs on
Windows. Opt-in update check, off by default (D551: one HTTPS request to
GitHub per launch when enabled; winhttp.dll loaded only then; the tag CI
job checks the tag against GE007_VERSION). Release notes, ROADMAP,
internals and security docs updated; tree-hygiene pass.

Refs: D540 D541 D543 D544 D545 D546 D547 D548 D549 D550 D551 D552 D553 D554 D555 D556 D557 D558 D559 D560 D561 D562 D563 D564
2026-10-06 18:35:44 -04:00
James Dansereau d7b9371afb Docs: public copy, site presentation, release-gate review (D533, D534)
Public-copy language pass, homepage restructure and Documentation rename,
404 page, dead-anchor and permalink fixes, release-gate sign-off, and the
pre-release review fixes of the 2026-10-05 batch (probe-doc drift,
build-path hygiene, verify.sh and capture safety).

Refs: D533 D534
2026-10-06 18:35:44 -04:00
James Dansereau 452fa1025a Golden gate: Linux baselines, canonical save, gate hardening (D521-D532)
Linux reference-frame set for all 21 levels and the cross-platform golden
rule (D521); golden tooling made non-destructive (D524); the gate pins a
canonical in-tree save (D529) and the PRNG seed (D532); the capture tool's
per-level re-pin bug fixed and the Linux set re-captured under the
canonical save (D530, D531); no-arg verify.sh exits non-zero. README and
docs visual/consolidation passes; the 15 MB preview gif replaced by stills.

Refs: D521 D523 D524 D525 D526 D527 D528 D529 D530 D531 D532
2026-10-06 18:35:43 -04:00
James Dansereau aa5a1b81fe v0.5.0: settings UI, control styles, save import, documentation
PD-style F10 menu with six groups, checkbox/slider/dropdown controls (D504),
wording pass and real-unit sliders (D505-D509), overlay/FPS-counter window
scaling (D510, D512), small settings (D511), control scheme Ext/Original
(D513) with the game's own N64 styles (D516-D518), emulator-format save
import on first launch (D514), build arch tag (D515), converter argv quoting
and sidecar size check, one PC options UI (D519) with by-eye follow-ups,
antialiased overlay sliders (D520), README settings paths, ROADMAP trim,
D441 tier-1 all-level sweep (21/21 pass).

Refs: D503-D520 D349 D441
2026-10-06 18:35:43 -04:00
James Dansereau daa2522511 D489-D502: settings persistence, collision, A/B harnesses, D441 census
F10 pad slider persistence (D489), glass collision hull (D490), A/B input
harnesses (1964 virtual pad, GE_INPUTSCRIPT, live one-pad launcher), adjacent
global struct reads (D491), vibration percent, D441 complete pointer-width
census with high-arena tier-1 build, fix batches 1-5 and an ABI warning
ratchet, playtest verification of D493-D502.

Refs: D489 D490 D491 D492-D502 D441
2026-10-06 18:35:42 -04:00
James Dansereau 4b38c2566e D478-D490: crash log, session start, performance, fidelity, regions
Crash-log snprintf (D478), start-mp folder (D479), shader pre-warm (D480),
render worker thread (#92 low-end 45 to 60 fps, D481), lighter first-launch
defaults (D482), sniper crouch/zoom fixes (D483-D485, #136), Rareware text
TMEM swap (D75), D173 residual, turret/explosion fidelity (D486, #126),
21-level Windows golden rebase with a two-tier gate, PAL and JP convert/build/
boot (D258) with PAL frame height (D487) and osTvType (D488).

Refs: D478 D479 D480 D481 D482 D483 D484 D485 D486 D75 D173 D258 D487 D488 #136 #126
2026-10-06 18:35:42 -04:00
James Dansereau 88bcef32a3 Performance work D473-D477; scaffolding batches 3-4
Uncached getenv per vertex batch (D473), fast3d micro-optimisations (D474),
hitch diagnosis (D475), CI4 palette-bank cache key (D476), spike counters,
CI8 palette churn measurement (D477). Dead diagnostic probes removed.

Refs: D473 D474 D475 D476 D477 GE_PERFSTAT
2026-10-06 18:35:41 -04:00
James Dansereauanditaloarruda b735190fd9 Input and audio features D469-D472
Gamepad presets and per-seat rebinding (D469, building on
italoarruda's #109), master volume and output
device (D470), controller-family button names and player LEDs (D471), F10
owner-pad input and widescreen pillarbox polish (D472).

Refs: D469 D470 D471 D472

Co-authored-by: italoarruda <3868006+italoarruda@users.noreply.github.com>
2026-10-06 18:35:41 -04:00
James Dansereauanditaloarruda 9dd98bdb20 Gameplay and rendering fixes D463-D468; D441 pointer-width sites
Zero-line tile texrect guard and texture-import skip (D463), stage-bank red
zone checker (D464), Dam/Caverns water slide (D465, #127), extended draw
distance no longer changing gameplay (D466, #125), player matrix pointers and
Model/PROMOTE/vtxstore rebase (D457 c/d), AI view clamp for ultrawide
(D468), fire-sound path audit (D433, D467 #130), italoarruda's #107 pointer-width
sites (two sites, credited).

Refs: D463 D464 D465 D466 D467 D468 D457 D433 #125 #127 #130 #107

Co-authored-by: italoarruda <3868006+italoarruda@users.noreply.github.com>
2026-10-06 18:35:40 -04:00
James Dansereau 00fdd94c8d 2-4 player split-screen (#99): D416-D419, D422, D428, D448, D450
Virtual seats, per-seat camera and input routing, aim modes, hot-plugged
controllers reopening via a coalesced rescan (D450, dolent's PR #122,
merged as the original commit). Rule-2
sign-off for D417.

Refs: #99 D416 D417 D418 D419 D422 D428 D448 D450 #122
2026-10-06 18:35:40 -04:00
James Dansereau 41d690f7ca 32-to-64-bit pointer-width fixes D451-D462; scaffolding removal batch 1
Tick-based auto-fire gate (D451), knife keyframe pointers (D454), font/text
slots (D456), memp arena start (D453), exe-address truncation census (D455),
global bank/RDRAM and background slots (D457 a/b), item-slot allocation
stride (D461), vtxstore table stride (D462). Mechanical, semantics-preserving
ABI/layout edits only. Diagnostic probes removed (batch 1).

Refs: D451 D453 D454 D455 D456 D457 D461 D462 D408 D294
2026-10-06 18:35:39 -04:00
James Dansereau ad3856892a v0.4.1 sync: build and CI fixes, settings presets D440-D447, outside PRs
build-pc.sh Windows temp-dir re-exec fixes; CI pin actions and no-cache ROM
handling; original-N64 and Deck presets; D440 preset labelling; D442 All
unlocked as a RAM/query-time override; D443 settings values (MSAA 16x,
Restart game); D444 front-end Previous/Next paging; D447 Original-aspect
pillarbox/letterbox. Outside contributions by dolent: PRs #120 and #121 (relabelled D445,
D446), merged as dolent's original commits.

Refs: D440 D441 D442 D443 D444 D445 D446 D447 D387 D283 #120 #121
2026-10-06 18:35:39 -04:00
James Dansereau 811e9a62cf Bug-fix pass for reported issues #114-#119 (D424-D434)
Recoil byte order, crouch-tank interactions, auto-fire cadence in ticks,
hipfire turret tracking, light-fixture Gfx index parsing, hit-type byte
reads. Rule-2 sign-offs recorded for D424 and D427. Documentation re-triaged
onto a single tracker (docs/ROADMAP.md).

Refs: D424 D425 D426 D427 D428 D430 D431 D432 D434 #114 #115 #116 #117 #118 #119
2026-10-06 18:35:38 -04:00
Dan Turner 7b08a04f54 ci: macOS arm64 build + port_addr self-test on PRs into macos
Adds a GitHub-hosted macos-15 (Apple Silicon) workflow for pushes and PRs
into the macos branch: build with Homebrew GCC, once with PORT_ADDR_STRICT
off and once on, then run the ROM-free address-model self-test.

- main.c: new --selftest flag, handled right after portAddrInit() and
  before crashInit/config/ROM/SDL. Exits 0 if the self-test passed, 1 if
  not. Listed in --help.
- port_addr.c/.h: portAddrSelfTest() now returns its result and
  portAddrSelfTestPassed() exposes it; no change to normal runs.
- macos.yml: contents: read only, no secrets, no ROM, persist-credentials
  off, so it is safe for fork PRs. The strict entry also requires the
  GE_ADDRSTRICT_SELFTEST report, proving the validator is live.
- docs: building.md CI note; plan M4 row no longer lists macOS CI as
  outstanding (ROM sweeps stay with the self-hosted job in #131).
2026-10-04 11:21:37 +01:00
dolent 92986a07a9 docs: D420 findings entry (crouched-rifle firing animation table: linker adjacency)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit d954dfb581)
2026-10-04 11:18:18 +01:00
dolent 82e20c4815 fix: D420 -- crouched-rifle firing animation never resolved on PC (linker adjacency) -> SIGSEGV in MP matches
crouched_rifle_firing_animation_group1 (chr.c) has one entry and no
terminator; on the N64, .data is in source order, so
crouched_rifle_firing_animation_groupA follows it and the single
initResolveAnimGroupTable pass over group1 also resolves groupA (len 2).
GCC places groupA below group1, so groupA[0] kept the raw anim offset
0xB84 (PTR_ANIM_fire_kneel_left_leg). playerTick (bondview2.c:11021) passes
it to modelSetAnimation for a player's crouched rifle fire
(firing_animation_groups[1][5]) -> SIGSEGV in modelConstrainOrWrapAnimFrame
(fault 0xb88). Crouching rifle guards also saw len 1 instead of 2.

#ifdef PORT: one array {right_leg, left_leg, terminator} (the N64 layout),
groupA = &group1[1] (chr.h). ABI/layout exception, porting-notes D5 class
(D164 precedent). Verified: 4-seat repro (M16, Complex, frame 5030) runs
5 min x3 SH-identical, no crash; gdb scan of all firing_animation_groups
entries: 1 bad pointer before, 0 after; P1 corpus (1324 recordings) verdicts
identical; solo/couch frames byte-identical; netplay 2P/4P identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 8595a06edd40ca22ac9ba4955b13a9ce61064475)
(cherry picked from commit 8e5d4bcd5a)
2026-10-04 11:17:57 +01:00
Dan Turner 9e473faa6c macos/arm64: review fixes — NULL-safe portAddrIsInWindow, drop duplicate MSAA clamp and wrong terminator
- port_addr.h: portAddrIsInWindow() now rejects the low 64 KiB of the
  window. With PORT_ADDR_BASE == 0 that range contains NULL; the previous
  version let NULL through on Windows/Linux, regressing langGet()'s
  cleared-bank guard (the old numeric bound rejected < 0x10000). Matches
  the old bounds on every platform. Self-test covers NULL, the low guard,
  DRAM and above-window.
- gfx_opengl.cpp: revert to origin/macos. upstream already clamps MSAA to
  GL_MAX_SAMPLES (D405 / #106); the branch's second clamp and its comments
  were a duplicate. MACOS-MSAA-CLAMP finding removed.
- chr.c: drop the branch's crouched_rifle_firing_animation_group1 {0}
  terminator. GCC places groupA below group1, so it never resolved groupA
  and did not fix the crouched-rifle crash. Replaced by #120 (next commit).
- boss.c: guard the rspReplyMsg re-base with #ifdef PORT (the file has no
  PORT_N64PTR identity fallback for the N64 build).
- findings.md: correct the objDeform vtxslot write-up (host pointers, not
  u32 N64 addresses), give accurate numbers for unguarded src/ edits, and
  record why the terminator was replaced.
2026-10-04 11:17:53 +01:00
Dan Turner aebce4013e macos/arm64: relabel findings to descriptive tags, use portAddrIsInWindow, document src/ edits
Owner review follow-ups on #95:

- Findings labels: replace the branch's D409-D421 (which clash with
  upstream's D-range) with descriptive MACOS-<TOPIC> tags across
  findings.md (section F rows + tail entries), findings-index.csv,
  porting-notes.md, MACOS-ARM64-PLAN.md, CMakeLists.txt, building.md and
  the code comments. Real D-numbers get assigned at merge.
- gen_findings_index.py: accept non-D descriptive tags as labels so the
  generated index stays complete while the branch is unmerged (D-rows
  unchanged).
- language.c: convert the last hardcoded numeric address bound in the
  boot path to portAddrIsInWindow(), per the branch rule (address
  plausibility checks must not use numeric bounds).
- Document the src/ edits the owner flagged as unwritten: the two
  vtxstore_allocate() callers and objDeform() (both the PORT_N64PTR(0)
  -> NULL class, under MACOS-BLOODSTAIN), the crouched-rifle table
  terminator (MACOS-CAST-CENSUS), and an honest note on which widened
  sites are #ifdef PORT and which follow the existing unguarded house
  style.
- Drop the stray .DS_Store from history (it was added in the base commit
  and only deleted at the tip).

No behaviour change: PORT_ADDR_BASE == 0 everywhere but macOS, and the
one logic edit (language.c) is an identity there. Build + window +
port_addr self-test ALL PASS on arm64.
2026-10-04 10:58:12 +01:00
Dan Turner 4d0abb1a39 macos/arm64: renumber findings onto v0.4.0 (D409–D421) + restore docs
After rebasing the macOS/arm64 branch onto v0.4.0 (main was at D408, and had
already consumed D323–D349), the branch's findings collided. Renumber the 13
macOS findings D294–D307 -> D409–D421 (main's own D29x/D30x references left
untouched), and restore the findings.md entries + porting-notes §F section
that the merge dropped.

- findings.md: 13 §F/§H rows (D409–D421) + their tail sections.
- findings-index.csv: regenerated (--check clean).
- porting-notes.md: §F macOS section restored + Darwin/Mach-O bullets renumbered.
- building.md / GE-ENV-PROBES.md / CMakeLists.txt / code comments: D294–D307 -> D409–D421.

Drops the obsolete reticule-jitter finding (D336): v0.4.0 fixed it as D332
(folded from PR #96).
2026-10-04 10:50:01 +01:00
Dan Turner 3785dcad33 macos: fix the pointer-truncation crash family, add PORT_ADDR_STRICT (D301-D308)
Census-driven: rebuilt the compiled set with -Wint-to-pointer-cast
-Wpointer-to-int-cast -Wint-conversion, giving 276 actionable sites, triaged
in three disjoint file groups. Group A (206 warnings) and most of C were
already-correct round trips; these are the real bugs.

- D301: chrCreateBloodStain() crashed on guard gunfire. Two defects —
  portN64ToHost(0) returned base+0 instead of NULL (defeating the decomp's
  `if (p != NULL)` failure checks), and Vertex.CollisionRelatedNode (a u32 vma
  kept 32-bit by PROMOTE32) was read with a bare cast.
- D302: six more truncations. Four are the "twin function" pattern — one of a
  near-duplicate solo/MP pair converted, the other missed. Includes bg.c:5213,
  where a truncated loop bound meant bgRoomCalcBB() never ran its body on
  arm64, leaving EVERY room in every level with an inverted bounding box.
  Silent, not a crash. Possible relevance to open D249.
- D303: texReadUncompressed() aligned its destination through u32. Latent —
  a probe over a full level boot showed the path is unreachable on PC
  (textures come from the pre-converted sidecars).
- D306: MSAA=8 produced a completely black screen. GL_MAX_SAMPLES is 4, and
  nothing clamped the request, so the renderbuffer was never allocated.
  video.c's own comment claimed MSAA "snaps down to the highest supported
  level"; it did not. Also fixed two index-vs-GL-name bugs in the same file.
- D308: the watch's Control page crashed. options.c cast a stack-local table
  pointer to (s32) before passing it as WatchContButtonPositions*, and
  watchRenderControllerOpaque forwarded the truncated value. Verified A/B
  against the user's exact repro — fault address byte-identical.
- D305: PORT_ADDR_STRICT, a debug gate (off by default, compiled out) that
  validates every u32->host conversion against the regions the port maps and
  reports offenders with a backtrace. Zero reports across 6 levels, 5 of them
  under full-control input.

Also records D307 (reticule jitter in aim mode) in the finding log as OPEN,
with the cause identified from live captures and no code change — an attempted
fix was reverted for not working.

Verified: clean build, 21/21 sweep, plus a targeted repro per fix.
2026-10-04 10:50:01 +01:00
Dan Turner 498be832c2 macos: packaging, sweep tooling, and two runtime fixes (D299, D300)
- tools_pc/bundle-mac.sh: a signed, double-clickable GoldenEye.app. Homebrew's
  libstdc++/libgcc_s and SDL2 are not system libraries, so they are bundled
  and rewritten to @executable_path/@loader_path. Note sdl2-compat (Homebrew's
  `sdl2`) finds its SDL3 backend through a RELATIVE rpath baked into the dylib,
  so SDL3 ships too and that rpath is repointed — copy it without that and
  sdl2-compat raises a modal error dialog from its initialiser and the app
  hangs before main(). prepare-assets/ goes in Contents/Resources/: codesign
  treats a directory under Contents/MacOS/ as nested code and fails the bundle.
- tools_pc/level_sweep_mac.sh: per-level crash-log sweep, symbolicated with
  atos, with a rendered-pixel check so "runs but renders nothing" is caught.
- D299: sndPlaySfx() rejected EVERY sound on arm64 — all in-level SFX were
  silent, while music played (separate player). Its guard predated the shifted
  window: `sp >= 0x400000000ULL` (16 GiB) is correct on x86_64 (DRAM at
  0x70000000) and rejects everything on arm64 (DRAM at ~0x1000_707b_5740).
  Fixed with a portAddrIsInWindow() helper rather than absolute bounds.
- D300: cleanupObjects() did `u32 *obj = (u32)g_CurrentSetup.propDefs`,
  truncating a host pointer to 32 bits. Crashed at stage unload — only
  reachable once a level actually ends.

Verified: 21/21 on both the no-input and full-control sweeps; SFX voices
allocate on every level.
2026-10-04 10:50:01 +01:00
Dan Turner d4cc71af0a macos: native Apple Silicon (arm64) support — address model, boot chain, 21/21 levels
Adds native arm64 macOS support on top of #88's Intel macOS layer.

The one structural change arm64 needs is an address model. The port realises
the N64's 32-bit address space *as host addresses* — DRAM at 0x70000000, cart
at 0x10000000, plus ~1700 linker-absolute symbols — which is only sound while
host addresses fit in 32 bits. Native arm64 cannot map below 4 GiB
(__PAGEZERO; a shrunken pagezero is SIGKILLed at exec; PIE is mandatory;
MAP_32BIT fails). So the whole space is relocated to a fixed host base,
PORT_ADDR_BASE (16 TiB), with portN64ToHost / portHostToN64 / PORT_N64PTR as
the conversion chokepoints. PORT_ADDR_BASE is 0 on every other platform, so
all of this is an identity there.

That relocation then surfaces a class of latent 64-bit bugs that are invisible
at base 0 — any place the decomp round-trips a pointer through a 32-bit
integer. The src/ changes here are that class: pointer-width/layout only, no
behaviour change, N64 branch textually unchanged, each documented in
docs/dev/findings.md (D298/M2).

Also: Mach-O absolute-symbol generation at the same base, #pragma weak ->
symbol equates, __x86_64__ -> PLATFORM_64BIT, Darwin crash handler/ucontext,
shm_open DRAM, the AppKit main-thread guard, and the sidecar generation that
was the real boot blocker.

Verified: 21/21 solo levels boot, render and run crash-free.
2026-10-04 10:50:01 +01:00
James Dansereau 39d4f8d088 Merge pull request #132 from jkdansereau/fix/macos-errno-include
romdata: include <errno.h> after ultra64.h (Linux build fix on macos)
2026-09-30 10:06:29 -04:00
James Dansereau 12a814a4e4 romdata: include <errno.h> after ultra64.h (fixes the Linux build on macos)
#88 added <errno.h> to the POSIX include block, ahead of ultra64.h.
glibc defines errno as a macro, (*__errno_location ()), which rewrites
the N64 `u8 errno;` fields in PR/os.h (OSContStatus, OSContPad,
OSContRamIo) and fails with "field '__errno_location' declared as a
function". Include it after ultra64.h/bondtypes.h instead. Nothing in
this file reads those struct fields; errno is only read by the mmap
failure diagnostic. No change on Windows.
2026-09-30 10:02:08 -04:00
James Dansereau c415315797 Merge PR #88 (JunielKatarn: native Intel macOS) into macos
#88's original commit (62b21054) conflicts with current main. The same
change, rebased onto main by danturn with the conflicts resolved, is
carried as ae1884cc (same author) at the base of #95. This merge keeps
that resolved tree exactly, so the tree is identical to ae1884cc, and
brings in #88's original head so the PR is recorded as merged.
2026-09-30 09:36:34 -04:00
Julio C. Rocha ae1884cc05 Implement macOS (Intel) support 2026-09-30 10:09:35 +01:00
dolentandClaude Fable 5.1 17891cc468 feat: D436 -- persistent crosshair, opt-in (Video.CrosshairPersistent; F10 > Gameplay > HUD > "Crosshair when not aiming")
The native sight is drawn at crosshair_angle outside aim mode too: one
masked condition in gunDrawSight (#ifdef PORT, beside the D373/D381
hooks) ignores GUNSIGHTREASON_NOTAIMING when the option is on; every
other reason and the MP menu still hide it. Default off is byte-identical
(frames 600/1200/1800); hipfire on/off differ only in the sprite region;
aim mode on/off identical. Findings D436.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-29 13:26:21 -07:00
dolentandClaude Fable 5.1 05af6d4e0d fix: D435 -- replugged controllers never reopened (render-thread event pump swallowed hot-plug events)
SDL_CONTROLLERDEVICEADDED / REMOVED are dequeued by whichever of the two
event pumps polls first; fast3d's gfx_sdl_handle_events had no case for
them and dropped them, so inputRescanPads never ran after a replug (the
stale handle later aborted in malloc). Both pumps now post a request and
inputUpdate() consumes it on the pad-reading thread. Findings D435.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-29 13:26:20 -07:00
dolentandClaude Fable 5.1 d954dfb581 docs: D420 findings entry (crouched-rifle firing animation table: linker adjacency)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-29 13:26:20 -07:00
dolent 4705b0e4e8 fix: D430 -- stale per-texture anisotropy on recycled GL ids made watch text layout-dependent
gfx_opengl_set_sampler_parameters set GL_TEXTURE_MAX_ANISOTROPY only for
mipmapped samplers and never reset it. GL ids freed by
gfx_texture_cache_delete_range are recycled in unordered_map order (keyed by
texture address), so a non-mipmapped glyph could inherit a world texture's
4x anisotropy depending on the binary's data layout -> +-1-2 per channel on
the MP pause-menu text in couch 2P (frame 2400). Now always written (level
when mipmapped, 1 otherwise). #ifdef PORT, render-only.

Verified: couch 2P frames 2300-2500 fix vs fix+3KB array 0/21 differ
(before: 13/21); base vs fix: couch 2P 0/21, solo -level_09 f1800 and couch
4P f2400 byte-identical; netplay 2P SH identical (2 671 ticks).
Findings D430 + index row, porting note.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit ec5057f026259f35d4075833e89bf6fd0b6c467f)
2026-09-29 13:26:20 -07:00
dolent 8e5d4bcd5a fix: D420 -- crouched-rifle firing animation never resolved on PC (linker adjacency) -> SIGSEGV in MP matches
crouched_rifle_firing_animation_group1 (chr.c) has one entry and no
terminator; on the N64, .data is in source order, so
crouched_rifle_firing_animation_groupA follows it and the single
initResolveAnimGroupTable pass over group1 also resolves groupA (len 2).
GCC places groupA below group1, so groupA[0] kept the raw anim offset
0xB84 (PTR_ANIM_fire_kneel_left_leg). playerTick (bondview2.c:11021) passes
it to modelSetAnimation for a player's crouched rifle fire
(firing_animation_groups[1][5]) -> SIGSEGV in modelConstrainOrWrapAnimFrame
(fault 0xb88). Crouching rifle guards also saw len 1 instead of 2.

#ifdef PORT: one array {right_leg, left_leg, terminator} (the N64 layout),
groupA = &group1[1] (chr.h). ABI/layout exception, porting-notes D5 class
(D164 precedent). Verified: 4-seat repro (M16, Complex, frame 5030) runs
5 min x3 SH-identical, no crash; gdb scan of all firing_animation_groups
entries: 1 bad pointer before, 0 after; P1 corpus (1324 recordings) verdicts
identical; solo/couch frames byte-identical; netplay 2P/4P identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 8595a06edd40ca22ac9ba4955b13a9ce61064475)
2026-09-29 13:23:47 -07:00
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
Julio C. Rocha 62b2105458 Implement macOS (Intel) support 2026-09-17 00:35:30 -07:00
512 changed files with 52701 additions and 6940 deletions

No files matched your search

+2 -3
View File
@@ -7,9 +7,8 @@ body:
value: |
Thanks for filing a report. First, please check:
- The port is **Phase 2 of 4** — rendering is incomplete and many
cosmetic defects are already tracked in
[`docs/dev/GRAPHICS-BACKLOG.md`](https://github.com/jkdansereau/goldeneye-pc-port/blob/main/docs/dev/GRAPHICS-BACKLOG.md).
- Known open issues (rendering, audio, platform) are tracked in
[`docs/ROADMAP.md`](https://github.com/jkdansereau/goldeneye-pc-port/blob/main/docs/ROADMAP.md).
Search open issues and that file before filing.
- Only the **Windows / MSYS2 MINGW64** build is tested. Linux and
macOS are best-effort — build breaks there are expected.
+1 -1
View File
@@ -4,7 +4,7 @@ contact_links:
url: https://github.com/jkdansereau/goldeneye-pc-port/blob/main/docs/building.md
about: Read this first for build/extraction problems — most are covered here.
- name: Known rendering defects
url: https://github.com/jkdansereau/goldeneye-pc-port/blob/main/docs/dev/GRAPHICS-BACKLOG.md
url: https://github.com/jkdansereau/goldeneye-pc-port/blob/main/docs/ROADMAP.md
about: Cosmetic / rendering issues already known and tracked. Check before filing.
- name: Security vulnerability
url: https://github.com/jkdansereau/goldeneye-pc-port/security/advisories/new
+58 -2
View File
@@ -1,12 +1,66 @@
# Security Policy
This project is a non-commercial, fan-made research port. It has no server
component and ships no binaries. The realistic security surface is:
component and no telemetry, and no networking unless you turn on the opt-in
update check (`Game.CheckUpdates`, off by default), which makes one HTTPS
request to GitHub's releases API per launch (GitHub sees that request and
your IP, as with any web request). On Windows it loads the system
`winhttp.dll` from System32 only then; on Linux/Steam Deck it runs the
system `curl`. It never downloads or installs anything. Each release ships
prebuilt Windows and Linux binaries, but never a ROM or any game asset — you
supply those yourself from a copy you legally own. The realistic security
surface is:
- the PC port executable parsing your own ROM and asset files at load time
(a malformed ROM/asset could in principle crash it or worse), and
- the build/extraction scripts and CI workflow.
A full walkthrough of what a release actually installs on your machine, and
what a from-source build pulls in, is in
[`docs/security.md`](../docs/security.md).
### Why the binaries are unsigned
The Windows `.exe` and the Linux build are **not code-signed** — this is a
free, single-developer, non-commercial hobby project with no code-signing
budget, not a sign of anything wrong with the binary. Two things you may
run into as a result:
- **Windows SmartScreen** may show "Windows protected your PC" on first
run. Click "More info" → "Run anyway."
- Some antivirus engines flag `ge007-convert.exe` (the one-time ROM-asset
converter bundled in the release) specifically, because it's packaged
with PyInstaller's `--onefile` mode — a well-known trigger for heuristic
AV engines industry-wide, unrelated to what this project's code actually
does. The game launches this converter itself on the first run, when the
derived asset folders are missing, so an AV prompt can appear at that
moment rather than when you unpack the release. If you want to verify what
it does, its full source is `prepare-assets.py`, shipped right next to it in
the same bundle folder. The build pins the PyInstaller version and the CI
actions it uses (by commit SHA), so what ships is reproducible from this
repository.
### Verifying a release build (build provenance)
From **v0.5.0** on, the tag-only CI job attaches an in-toto SLSA
build-provenance attestation to each release artifact (the zip/tarball and
its `.sha256` sidecars). The attestation is signed by GitHub's OIDC key for
*this repository's* workflow running the tagged commit. It does not replace a
code signature — but it proves that the exact file you downloaded was produced
by this repo's CI from the release tag, and nothing else. Verify any
downloaded artifact with:
```sh
gh attestation verify goldeneye-pc-port-0.6.0-win64.zip \
--repo jkdansereau/goldeneye-pc-port
```
(use the name of the file you downloaded, e.g.
`goldeneye-pc-port-0.6.0-linux-x86_64.tar.gz` on Linux). A passing check confirms both
the file's digest and its provenance; a tampered or foreign file fails.
Releases before v0.5.0 have no attestations — for those, the per-file
`.sha256` sidecars and a from-source build are the available checks.
## Reporting a vulnerability
Please **do not** open a public issue for a security problem.
@@ -38,4 +92,6 @@ errors, and anything requiring a ROM or assets we do not distribute.
## Supported versions
Only the tip of the default branch is supported. There are no releases yet.
Only the tip of the default branch and the
[latest release](https://github.com/jkdansereau/goldeneye-pc-port/releases/latest)
are supported.
+1 -1
View File
@@ -10,7 +10,7 @@
- name: rendering
color: 5319e7
description: Graphics / fast3d / RSP output defect (see docs/dev/GRAPHICS-BACKLOG.md)
description: Graphics / fast3d / RSP output defect (see docs/ROADMAP.md)
- name: audio
color: 1d76db
+94 -195
View File
@@ -1,221 +1,120 @@
## GoldenEye 007 PC Port <version>
# GoldenEye 007 PC Port — v<version>
<p align="center">
<img src="https://github.com/jkdansereau/goldeneye-pc-port/raw/v0.3.0/docs/media/goldeneye-gh-preview.gif" width="480"
alt="~12 s gameplay montage from live v0.3.0 play sessions (no audio track)">
<img src="https://github.com/jkdansereau/goldeneye-pc-port/raw/v<version>/docs/img/shots/feature-splitscreen.jpg" width="340" alt="2-player split-screen multiplayer in the port">
<img src="https://github.com/jkdansereau/goldeneye-pc-port/raw/v<version>/docs/img/shots/feature-options.jpg" width="340" alt="The PC options menu: custom crosshair colour sliders">
</p>
> The full single-player campaign runs at a steady 60 fps with audio (music +
> SFX) playing throughout, on Windows and Linux including Steam Deck, and it
> is completable end to end: the whole campaign has been playtested through
> all 21 missions (Agent difficulty). It's an early public cut: all 21 solo
> missions load and run crash-free, but the known issues below are real;
> feedback is very welcome. **This is a pre-1.0 release, not a finished
> product** — expect rough edges and missing features until v1.0; see the
> [README's Roadmap section](https://github.com/jkdansereau/goldeneye-pc-port#roadmap)
> for where this is headed.
> [!NOTE]
> **Updating?** Your progress and settings carry over. Unpack the new version into a new folder and copy your ROM, `ge007.eep` and `ge007.ini` into its `data/` folder. Leave the old `pcmodels-*` and `pccg-*` folders behind; the new version makes its own.
### What's new since v0.2.2
> [!NOTE]
> The downloads aren't code-signed. On Windows, if SmartScreen appears, click **More info**, then **Run anyway**. On macOS, see [Getting started](#getting-started).
- **Bond is fixed in cutscenes** — the most visible remaining glitch in
prior releases: Bond no longer floats or glitches out during cutscenes,
his animation no longer breaks or repeats, and the camera correctly
tracks him through scripted sequences including all three Dam end-of-level
abseil shots (D243). Root cause: a 32-bit-era struct-size constant in the
cutscene body-model setup never accounted for 64-bit pointer widening,
corrupting the model's own animation-timing fields on creation. Fixing
that also fixed the associated camera shake, which had previously been
worked around with a camera-freeze hack that's no longer needed. **Two
related reports also cleared up as a side effect**: Bond's third-person
model floating above the ground at level start, and death animations
sinking him into the floor, both shared an underlying mechanism with this
bug — Bond now ends up in the right place the large majority of the time,
with only a small positional drift on the rare occasion he's off (D173/
D292).
- **Particle colors mostly fixed**: bullet-impact sparks and lingering
smoke/explosion residue no longer cycle through a rainbow palette (D219) —
root cause was a hardcoded raw-memory-offset read into the spark's stored
color that landed on the wrong bytes once the containing struct grew from
64-bit pointer widening. A second, separate cause was found and fixed in
explosion rendering itself (an out-of-bounds vertex-buffer write
corrupting adjacent particle data). This fix is real and stays in, but the
rainbow effect can still occur intermittently on any platform, not every
time and not on every level — a second cause hasn't been found yet; see
Known Issues.
- **Automatic widescreen FOV scaling**: the game now scales its field of
view to match your display's aspect ratio automatically, instead of
requiring the old manual FOV-scale slider. Toggle off in the F10 overlay
(`Widescreen Auto`) if you prefer the original 4:3 framing. The scaled
vertical FOV is clamped to a 20°–160° range as a guard against degenerate
values on unusual aspect ratios. (This is FOV scaling, not a native 16:9
world/HUD re-render — see Known Issues.)
- **TV overscan artifacts removed**: the black bars top/bottom and the thin
pixel strips left/right of the gameplay view are gone (D247/D246) — both
were the original N64 TV-safe-area margin, invisible on a CRT under real
overscan but visible on a PC monitor. New `Video.SafeAreaCrop` toggle
(default on) if you want the original framing back.
- **Two more crash fixes**: a campaign-halting crash at the end of Control
Center on 00 Agent difficulty, which would then crash on every subsequent
relaunch until the save was reset (D295); and a crash selecting most save
slots with `Skip Intro` enabled (D299).
- **The elusive complete freeze is root-caused and fixed.** Players had hit
a total, non-self-recovering lockup (most recently when returning from
Caverns to the intro screen) that was real but never reproduced on demand.
It turned out to be a latent defect in the original game's own AI command
stream — a record type the engine mis-measures, which our level-teardown
timing can trigger and the N64 never presents. Fixed with a minimal change
under the project's game-code exception process (D309/D310); a second,
unrelated freeze mechanism found during the same playtest is now guarded
as well (D311).
- **A NULL-pointer guard was added** around a save-data lookup that two
players hit with an identical crash address (GitHub #87, D301) — this
should close that crash, though we weren't able to reproduce the exact
trigger condition ourselves to confirm it end-to-end.
- **Facility's execution softlock is caught and recovered from**: a rare
race in the original game's own AI script (present on the N64 too) could
leave Ourumov frozen mid-execution after Objective C, with no way out short
of reloading. A port-side watchdog now detects the exact unrecoverable
state and re-seeds the attack so the scene plays out as authored; it
re-arms once the scene has cleared, so replaying Facility later in the
same session is still covered (D318).
- **A Steam Deck crash is fixed**: an audio-thread bug could crash the game
early in a level (`sndHandleEvent`'s sound-preemption scan could
dereference a pointer without checking it was valid first). Live-verified
crash-free on real Steam Deck hardware, including with `All unlocked`
toggled on.
- **Mouse input and F10 controls fixed**: mouse input was reworked to
follow the Perfect Dark PC port's architecture — slow mouse motion is no
longer barely recognized and fast motion no longer saturates almost
immediately (D300, `Input.MouseDirectLook`, on by default). The separate
aim-speed and turn-speed sliders — which could be set to inconsistent
values against each other — are now a single `Mouse sensitivity` control
with a wider range. Also fixed three F10 menu bugs found along the way:
clicking a row could unexpectedly scroll the whole list, a click near
certain rows could silently activate a different option than the one you
clicked (most noticeably, clicking `All unlocked` could instead trigger
`Quit to desktop`), and the panel's bottom row no longer duplicates
whatever item is currently selected (D251).
- **Random-number generation now matches the N64 original exactly** (D284) —
a shift-operation bug in the PRNG had desynced the PC's random stream from
N64's since the very first release, affecting loot placement, AI variance,
and other randomized elements. **This changes the random sequence from
every prior build**, so old input recordings/replays will diverge, but
your save files are unaffected: **a migration shim automatically upgrades
any save written by an older build the first time you load it** (D297) —
no manual action needed.
- **Assorted texture/rendering fixes**: an incorrect IA4 texture-format
decode and an overly-tight near-plane portal-culling guard were both
corrected (D266/D271) — reduces some texture-edge and visibility artifacts
on affected levels.
## Highlights
### Known issues
- **Frame rates above 60.** Pick 90, 120, 144, 165 or 240, or **Auto** to match your display (up to 240). The game still runs at its original speed; the extra frames are blended between its ticks, for the camera, the level, the sky, and the guards, doors and objects moving in it. A rework of f1zz1ec0ke's #137.
- **macOS on Apple Silicon (experimental).** A new download for M1 and later Macs, based on danturn's #95. Tested on one M3 Mac so far; reports are welcome.
- **Original controls are now original (#145, #149).** With *Control style* set to Original, your pad behaves as an N64 controller, and *Invert look* works in every original style.
- **Fixes:** geometry vanishing up close (#150), the first launch opening on the wrong monitor (#151), the end-credits crash on Linux and macOS (#152) and a crash when a guard spawns with a hat (#153, #156).
- **Also:** an optional mission timer (#144), the file-select crest fixed (#146), and a fix for a Windows blue screen on exit.
- The front-end **Nintendo logo renders as two white blobs**, and the
Rareware logo is close but its texture filtering looks off (D75).
- **Particle colors: a real fix landed this release, but the rainbow effect
can still occur intermittently.** One genuine, reproducible cause (an
out-of-bounds vertex-buffer write in explosion rendering) is fixed. A
second, still-unidentified cause remains, on both Windows and Steam Deck —
it doesn't reproduce every time or on every level, so treat this as
improved, not fully resolved (D252).
- **Some muzzle flashes draw an extra, erroneous long flash straight up from
the gun** (seen on the M16, among others), on top of the normal, correctly
drawn flash. Cosmetic only (D303).
- **Surface 1: the 2D billboard trees near the start render as a solid wall
of tree texture** instead of discrete sprites (D236). Under active
investigation across ten+ passes; no fix yet.
- **No native widescreen render**: v0.3.0's automatic FOV scaling (above)
expands the *field of view* for 16:9+ displays, but the world geometry and
HUD are still authored/laid out for 4:3 — this is not yet a true
edge-to-edge 16:9 render.
- **No controller rebinding UI, and no macOS or ARM builds.**
- **Distant geometry can drop out on the biggest open levels** (Streets,
Egyptian) at default FOV — a culling/LOD issue that sometimes
self-corrects as you keep moving (D249).
- **Gunshot SFX can sound off during sustained/rapid fire**: cadence can
drift from the N64 original's rate, and PP7/AK47 fire can occasionally go
silent under heavy automatic fire near another looping sound (e.g. an
alarm klaxon) (D240/D241).
- **`All unlocked` is highly experimental — do not enable it until you have
at least one save written.** Complete a level normally first (e.g. Dam on
Agent), then turn the toggle on. Enabling it on a brand-new install with
no prior save can still cause silent audio and odd right-mouse-aim
behavior (D259/D257). (The Steam Deck crash fix above addresses a
separate, now-fixed symptom that had also been associated with `All
unlocked` use; this audio/aim caveat is unrelated and still open.)
- Water on `IsWater` levels shows a moving seam between two patterns (D245).
- **Occasional z-fighting on some levels' geometry** (D308) — a Dam intro/
truck-wheel instance showing odd transparent-looking areas has also been
seen and may be related (D306). Minor, cosmetic; not investigated for
this release.
- **In-level security camera props can occasionally end up facing backwards**
(the wrong direction) on some levels, seen on Bunker — this is the
gameplay security-camera object, not the player's own view (D307). Not
investigated for this release.
- **A rare, self-clearing visual quirk**: on an occasional animation
transition, Bond's model can briefly render out of place. It has no effect
on gameplay or your save, and it clears on its own or by re-entering the
level; a safeguard added this release guarantees it can never affect game
stability (D311).
- **Two collision oddities found in final playtesting**: throwable items
(mines, grenades) can occasionally clip through walls in certain spots
(D312), and bullet impacts don't appear on some surfaces — doors and
windows are unaffected (D313). Both are queued for the next release; if
you hit either, an issue with the level and spot helps a lot.
### Downloads
## Downloads
| File | Platform |
|---|---|
| `goldeneye-pc-port-<version>-win64.zip` | Windows x86-64 |
| `goldeneye-pc-port-<version>-linux-x86_64.tar.gz` | Linux x86-64 (incl. Steam Deck) |
| `goldeneye-pc-port-<version>-win64.zip` | Windows 10/11, 64-bit |
| `goldeneye-pc-port-<version>-linux-x86_64.tar.gz` | Linux x86-64 (glibc 2.31+), including Steam Deck |
| `goldeneye-pc-port-<version>-macos-arm64.tar.gz` | macOS 14+ on Apple Silicon (experimental) |
Each contains the engine executable, a README, license texts, and the
`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.
Each download is ready to run: it carries its runtime libraries, a README, the license texts and the one-time asset tool. **No ROM and no game assets are included; you need your own US GoldenEye 007 ROM.**
### Running it
## Getting started
You supply your own **GoldenEye 007 N64 ROM** (`.z64`, big-endian) that you
legally own: the **NTSC-U (US)** release is what this build supports (PAL / JP
are on the roadmap). No Python, no toolchain, nothing to install.
1. Unpack the download.
2. Make a `data/` folder next to the executable and put your ROM in it as `ge007.ntsc-final.z64`.
3. Start the game. The first start takes a few extra seconds while it prepares the game files from your ROM. **F10** (controller: **Select**) opens the options menu.
1. Unpack the archive.
2. Make a `data/` folder next to the executable and put the ROM in it, named
`ge007.ntsc-final.z64`.
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:** do the steps on the Deck, then add the executable as a non-Steam game.
**Steam Deck:** sideload the unpacked folder (USB or a file manager), do steps
2–3, then add the executable to Games → *Add Game* as a non-Steam game.
**macOS:** the app isn't notarized, so run this once in Terminal before the first launch, then start `./ge007.aarch64` from the folder.
**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.
```
xattr -dr com.apple.quarantine goldeneye-pc-port-<version>-macos-arm64
```
Full steps are in the bundled `README.md`.
Or download it in Terminal, which skips that step:
### Verify the download
```
curl -L https://github.com/jkdansereau/goldeneye-pc-port/releases/download/v<version>/goldeneye-pc-port-<version>-macos-arm64.tar.gz | tar xz
```
Without Terminal: after macOS blocks the first launch, open System Settings → Privacy & Security and click **Open Anyway** next to the `ge007.aarch64` message.
The [user guide](https://github.com/jkdansereau/goldeneye-pc-port/blob/v0.6.0/docs/user-guide.md) covers controls, settings, emulator saves and troubleshooting.
## What's new
### Added
- **Frame rate cap:** Auto, 90, 120, 144, 165 and 240 join 30 and 60 (F10 → *Video*). Auto is the default on fresh installs; an existing `ge007.ini` keeps its value. Above 60 the extra frames are blended between game ticks (no effect on a 60 Hz display): the camera, the level and the sky, and each guard, door and object along its own motion. This adds one tick of latency; a fast camera turn or a big change on screen shows a full frame instead. The *Original N64* display mode keeps 60.
- **macOS Apple Silicon download** (experimental), plus source builds with Homebrew's GNU `gcc`.
- **Original control style as an N64 controller:** every press sends the real N64 button in levels, menus, the watch and the tank. Default layout: South = A, West = B, right trigger = Z, left trigger / RB = R, LB = L, right stick = C-buttons. Each button can be moved per player in `ge007.ini` (`Input.N64Pad.A = a`, ...), and `Input.N64StickFeel` picks the N64's linear stick range.
- **Mission timer** (#144): an optional always-visible timer, off by default.
- **`ge007.log`:** every run is logged next to the ini; the previous run is kept as `ge007.prev.log`.
### Fixed
- Geometry vanishing when you stand against a wall (#150).
- With a wider *Field of view*, objects in fog (such as the windows on Surface) no longer flicker or vanish while you turn, and models keep their detail out to the normal distance.
- The first launch opening on the wrong monitor (#151).
- A crash during the end credits on Linux and macOS (#152).
- macOS: a black window that never started the game when it was launched while the display was asleep.
- A crash when a guard spawns wearing a hat (#153, #156).
- The file-select crest drawing on a dark square (#146).
- *Invert look* now works in every original control style and stays in sync with the watch's *Look up/down* (#145).
- A Windows blue screen (0x119) when the game exits, seen on an NVIDIA GPU.
- The Cradle catwalk shadow flicker, and stale pixels beside the widescreen picture on front-end screens.
- Sound and text pointer checks that rejected valid pointers (#108, with italoarruda).
- The Linux/macOS update check failing on a large response.
- F10 slider labels being cut off at HUD scale 150.
### Changed
- Windows: per-monitor DPI awareness, so fullscreen on a scaled 4K display is native resolution.
- F10 dropdowns stay inside the settings box.
- The crosshair look (colour, size, style) is no longer reset by the *Display mode* presets.
- The old front-end *Velocity* pointer mode is removed.
## Known issues
| Issue | Workaround |
|---|---|
| On the Steam Deck at 90 fps, firefights in the centre of Bunker 1 and 2 can drop a few fps, and Jungle can feel slightly less smooth | Set the frame cap to 60 for those levels |
| Cradle's turret explosions may draw wrongly | None yet |
| On Windows, the picture can look hazy for a moment after alt-tabbing back | It settles on its own |
| PAL and JP ROMs aren't supported yet | Use the US (NTSC-U) ROM |
| Saves from v0.4.0 and earlier can hold fake unlocks from *All unlocked* | Keep a backup of `data/ge007.eep` from before you used it |
The full list is the [known-issues table](https://github.com/jkdansereau/goldeneye-pc-port/blob/main/docs/ROADMAP.md#known-issues).
## Tested on
The release downloads themselves, from a fresh folder with only the ROM: all 21 solo levels on Windows (at 120 fps), the Steam Deck (Desktop and Game Mode, at 90), macOS on Apple Silicon, and a 2011 Linux laptop with Intel HD 3000 graphics (at 60).
## Thanks
f1zz1ec0ke (#137, the frame-interpolation design this release reworks), danturn (#95, macOS), Julio C. Rocha (#88, the first macOS groundwork), italoarruda (#108), and everyone who reported the issues above.
## Verify the download
```
sha256sum -c goldeneye-pc-port-<version>-win64.zip.sha256
sha256sum -c goldeneye-pc-port-<version>-linux-x86_64.tar.gz.sha256
shasum -a 256 -c goldeneye-pc-port-<version>-macos-arm64.tar.gz.sha256 # macOS
gh attestation verify <file> --repo jkdansereau/goldeneye-pc-port # build provenance
```
### 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).
Non-commercial fan preservation/research project; not affiliated with any
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.
Built on the [GoldenEye 007 decompilation](https://github.com/n64decomp/007) and modelled on the [Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark). A non-commercial fan project, not affiliated with any rights holder. Developed with AI coding agents directed by one maintainer; see [About the project](https://github.com/jkdansereau/goldeneye-pc-port/blob/v0.6.0/docs/about.md#how-it-was-made).
+9 -11
View File
@@ -55,11 +55,9 @@
.tab span { writing-mode:vertical-rl; text-orientation:mixed;
font-family:'Arimo',sans-serif; font-weight:700; font-size:15px;
letter-spacing:4px; color:#3b3b33; transform:rotate(180deg); }
.reticle { position:absolute; right:34px; top:246px; width:44px; height:44px; z-index:5; }
.reticle::before, .reticle::after { content:""; position:absolute; background:#c62828; }
.reticle::before { left:50%; top:-6px; width:3px; height:56px; margin-left:-1.5px; }
.reticle::after { top:50%; left:-6px; height:3px; width:56px; margin-top:-1.5px; }
.reticle i { position:absolute; inset:4px; border:3px solid #c62828; border-radius:50%; }
/* the project mark: in-game default crosshair proportions (ring outer r 18,
bars reach ~1.43x r with an open centre ~0.24x r); see tools_pc/make_icon.py */
.reticle { position:absolute; right:30px; top:242px; width:52px; height:52px; z-index:5; }
/* the parchment card */
.card { position:absolute; left:40px; top:40px; right:96px; bottom:40px;
@@ -124,12 +122,12 @@
<div class="rep">REPORT:</div>
<div class="rows">
<div class="statusline">Status: <span class="status">RELEASED v0.2.0 &mdash; WINDOWS &middot; LINUX &middot; STEAM DECK</span></div>
<div class="statusline">Status: <span class="status">RELEASED v0.6.0 &mdash; WINDOWS &middot; LINUX &middot; STEAM DECK</span></div>
<div class="stats">
<span class="k">Statistics:</span><span>All 21 solo missions &mdash; full campaign completed in playtesting</span>
<span></span><span>~230 game-code units, compiled unmodified</span>
<span></span><span>~22k lines C/C++ &nbsp;&middot;&nbsp; ~10k Python (port layer + tools)</span>
<span></span><span>~250 bugs root-caused &nbsp;&middot;&nbsp; two coding agents, one human, part-time</span>
<span class="k">Statistics:</span><span>All 20 solo missions &mdash; 60 to 144 fps &middot; macOS (experimental)</span>
<span></span><span>~210 game-code units, compiled unmodified</span>
<span></span><span>~34k lines C/C++ &nbsp;&middot;&nbsp; ~10k Python (port layer + tools)</span>
<span></span><span>465 findings logged &nbsp;&middot;&nbsp; AI coding agents + a handful of contributors</span>
</div>
</div>
@@ -142,6 +140,6 @@
<div class="tab"><span>NEXT</span></div>
<div class="tab"><span>PREVIOUS</span></div>
</div>
<div class="reticle"><i></i></div>
<svg class="reticle" viewBox="-26 -26 52 52" aria-hidden="true"><g fill="none" stroke="#c62828" stroke-width="3"><circle r="16.5"/><path d="M0-26V-4.5M0 4.5V26M-26 0H-4.5M4.5 0H26"/></g></svg>
</body>
</html>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 597 KiB

After

Width:  |  Height:  |  Size: 597 KiB

+188 -34
View File
@@ -75,7 +75,7 @@ jobs:
# self-hosted per the 2026-09-12 standing preference.
windows_selfhosted: ${{ steps.runner.outputs.selfhosted }}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
fetch-depth: 0
@@ -120,7 +120,7 @@ jobs:
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
@@ -171,40 +171,71 @@ jobs:
# here is a "does it compile + link" signal only, not a runtime claim.
# Not a required check yet; flip it to required once it is reliably green.
#
# Toolchain: ubuntu-24.04 + gcc-14. The port is developed on MinGW GCC 16;
# pinning a recent GCC here keeps the first bring-up focused on real
# portability gaps rather than a decade of accumulated old-GCC pedantry.
# (A "does it build on the GCC a typical distro ships" pass can come later.)
# Toolchain: Steam Runtime 3 "sniper" SDK container (Debian 11 base,
# glibc 2.31) + its gcc-14. Building against the old glibc is what sets the
# tarball's floor: the ubuntu-24.04 build needed glibc 2.38 (C23
# __isoc23_* symbols), so it refused to start on Ubuntu 22.04 / Debian 12.
# The image is pinned by version AND digest.
# SDL2 is built from source (2.30.0, the version the 24.04 build bundled
# and every Linux/Deck test used): the SDK's own libSDL2 is sdl2-compat,
# which needs SDL3 at runtime. pip is bootstrapped from a hash-pinned
# get-pip.py: Debian 11's security archive no longer serves python3-pip.
linux-build:
runs-on: ubuntu-24.04
container:
image: registry.gitlab.steamos.cloud/steamrt/sniper/sdk:3.0.20260805.254768@sha256:1c33c507bc75d012e77df5727f93b0d5b8c3f7c8d4142ba5f7a16882cc92e014
# A container job's default `run` shell is sh, not bash (the packaging
# step's ${GITHUB_SHA::7} is a bashism).
defaults:
run:
shell: bash
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
# touched port/ files miss cache -- near-total hit rate. CCACHE_DIR is
# set in a step: in a container job ${{ github.workspace }} is the
# host path, not the container's.
CCACHE_MAXSIZE: 500M
SDL2_VERSION: 2.30.0
SDL2_SHA256: 36e2e41557e0fa4a1519315c0f5958a87ccb27e25c51776beb6f1239526447b0
GET_PIP_SHA256: 95c0ae79ccf9ac1e47e4187f49081d6ae1f45997afd4549e14f37337cbcfd766
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential gcc-14 g++-14 cmake python3 \
python3-pip libsdl2-dev zlib1g-dev libgl1-mesa-dev ccache patchelf
echo "CCACHE_DIR=$GITHUB_WORKSPACE/.ccache" >> "$GITHUB_ENV"
apt-get update
apt-get install -y patchelf
curl -fsSL -o /tmp/get-pip.py https://bootstrap.pypa.io/pip/3.9/get-pip.py
echo "$GET_PIP_SHA256 /tmp/get-pip.py" | sha256sum -c
python3 /tmp/get-pip.py --quiet
- name: Build SDL2 ${{ env.SDL2_VERSION }} from source
run: |
curl -fsSL -o /tmp/sdl2.tar.gz \
"https://github.com/libsdl-org/SDL/releases/download/release-$SDL2_VERSION/SDL2-$SDL2_VERSION.tar.gz"
echo "$SDL2_SHA256 /tmp/sdl2.tar.gz" | sha256sum -c
tar xzf /tmp/sdl2.tar.gz -C /tmp
cmake -S "/tmp/SDL2-$SDL2_VERSION" -B /tmp/sdl-build -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=/usr/local -DSDL_STATIC=OFF -DSDL_TEST=OFF
cmake --build /tmp/sdl-build -j"$(nproc)"
cmake --install /tmp/sdl-build
ldconfig
- name: Restore ccache
uses: actions/cache@v4
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ env.CCACHE_DIR }}
key: ccache-linux-${{ hashFiles('CMakeLists.txt') }}-${{ github.sha }}
path: .ccache
key: ccache-linux-sniper-${{ hashFiles('CMakeLists.txt') }}-${{ github.sha }}
restore-keys: |
ccache-linux-${{ hashFiles('CMakeLists.txt') }}-
ccache-linux-
ccache-linux-sniper-${{ hashFiles('CMakeLists.txt') }}-
ccache-linux-sniper-
- name: Regenerate ROM-asset symbol file (US)
run: python3 scripts/gen_romassets.py u
@@ -216,8 +247,8 @@ jobs:
# 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 -c "import PyInstaller, sys; sys.exit(PyInstaller.__version__ != '6.22.3')" 2>/dev/null \
|| python3 -m pip install --quiet pyinstaller==6.22.3
python3 -m PyInstaller --onefile --noconfirm --clean \
--name ge007-convert \
--distpath tools_pc/dist/prepare-assets \
@@ -254,7 +285,7 @@ jobs:
tools_pc/bundle-linux.sh "$V"
- name: Upload bundle
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: goldeneye-pc-port-linux-x86_64
path: |
@@ -263,6 +294,93 @@ jobs:
if-no-files-found: error
retention-days: 14
# Experimental macOS (Apple Silicon) build. Compiles + links with Homebrew's
# real GNU GCC (Apple Clang cannot compile the decomp's Plan 9 struct
# extensions) and packages the bundle with scripts/package-macos.sh: the
# non-system dylibs (sdl2-compat + SDL3, libstdc++, libgcc_s) are copied
# next to the exe with @executable_path install names, re-signed ad-hoc.
# The bundle is not notarized; the README inside it gives the one-line
# `xattr -dr com.apple.quarantine .` unblock. Like linux-build this is a
# "compiles + links + packages" signal, not a runtime claim. Not a required
# check. Mirrors the maintainer's local build-mac configuration (Unix
# Makefiles, RelWithDebInfo, newest Homebrew gcc-N).
macos-build:
runs-on: macos-14
env:
CCACHE_MAXSIZE: 500M
HOMEBREW_NO_AUTO_UPDATE: "1"
HOMEBREW_NO_INSTALL_CLEANUP: "1"
HOMEBREW_NO_ANALYTICS: "1"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install dependencies
run: |
echo "CCACHE_DIR=$GITHUB_WORKSPACE/.ccache" >> "$GITHUB_ENV"
brew install gcc cmake sdl2 sdl3 ccache
ls "$(brew --prefix)"/bin/gcc-[0-9]* "$(brew --prefix)"/bin/g++-[0-9]*
- name: Restore ccache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: .ccache
key: ccache-macos-arm64-${{ hashFiles('CMakeLists.txt') }}-${{ github.sha }}
restore-keys: |
ccache-macos-arm64-${{ hashFiles('CMakeLists.txt') }}-
ccache-macos-arm64-
- name: Regenerate ROM-asset symbol file (US)
run: python3 scripts/gen_romassets.py u
# Same frozen first-run converter as linux-build. The runner's python3
# is Homebrew's (PEP 668, no system pip installs), so use a venv.
- name: Freeze asset converter (PyInstaller)
run: |
python3 -m venv "$RUNNER_TEMP/venv"
"$RUNNER_TEMP/venv/bin/pip" install --quiet pyinstaller==6.22.3
"$RUNNER_TEMP/venv/bin/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
- name: Configure (ntsc-final)
run: |
GCC="$(ls "$(brew --prefix)"/bin/gcc-[0-9]* | sort -t- -k2 -n | tail -1)"
GXX="$(ls "$(brew --prefix)"/bin/g++-[0-9]* | sort -t- -k2 -n | tail -1)"
echo "using $GCC / $GXX"
cmake -S . -B build-mac -DROMID=ntsc-final -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_C_COMPILER="$GCC" -DCMAKE_CXX_COMPILER="$GXX"
- name: Build (ntsc-final)
run: cmake --build build-mac -j"$(sysctl -n hw.ncpu)"
- name: ccache stats
run: ccache -s
- name: Show binary
run: |
ls -la build-mac/ge007* || true
file build-mac/ge007* || true
otool -L build-mac/ge007.aarch64
- name: Package macOS bundle
run: |
if [ "${GITHUB_REF_TYPE}" = "tag" ]; then
V="${GITHUB_REF_NAME}"
else
V="0.0.0-ci.${GITHUB_SHA::7}"
fi
bash scripts/package-macos.sh build-mac dist "$V"
- name: Upload bundle
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: goldeneye-pc-port-macos-arm64
path: |
dist/*.tar.gz
dist/*.tar.gz.sha256
if-no-files-found: error
retention-days: 14
# Real Windows (MSYS2 MINGW64) build — the only platform the port is
# developed and tested on. This compiles and LINKS the full executable.
#
@@ -288,7 +406,7 @@ jobs:
CCACHE_DIR: ${{ github.workspace }}/.ccache
CCACHE_MAXSIZE: 500M
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
@@ -300,7 +418,7 @@ jobs:
# 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
- uses: msys2/setup-msys2@ec48f7c5447b3140e2b088413ae3a55687bccb6e # v2.33.0
with:
msystem: MINGW64
release: ${{ needs.changes.outputs.windows_selfhosted != 'true' }}
@@ -316,7 +434,7 @@ jobs:
run: tools_pc/check-selfhosted-deps.sh
- name: Restore ccache
uses: actions/cache@v4
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ env.CCACHE_DIR }}
key: ccache-windows-${{ hashFiles('CMakeLists.txt') }}-${{ github.sha }}
@@ -335,9 +453,9 @@ jobs:
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 -c "import PyInstaller, sys; sys.exit(PyInstaller.__version__ != '6.22.3')" 2>/dev/null || {
pacman -S --noconfirm --needed mingw-w64-x86_64-python-pip
python -m pip install --quiet --break-system-packages pyinstaller==6.22.3
}
python -m PyInstaller --onefile --noconfirm --clean \
--name ge007-convert \
@@ -349,6 +467,12 @@ jobs:
- name: Build (ntsc-final)
run: ./build-pc.sh ntsc-final
# D441 ratchet: fail if the per-file count of 64-bit pointer/int ABI
# warnings rises relative to tools_pc/abi_baseline.json. Re-baseline
# locally with `python tools_pc/abi_ratchet.py build-pc --update`.
- name: ABI warning ratchet (D441)
run: python tools_pc/abi_ratchet.py build-pc
- name: ccache stats
run: ccache -s
@@ -366,7 +490,7 @@ jobs:
tools_pc/bundle-win.sh "$V"
- name: Upload bundle
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: goldeneye-pc-port-win64
path: |
@@ -380,30 +504,60 @@ jobs:
# -level_09 soak, a golden-frame diff) would run on a self-hosted runner
# instead. Not planned.
# Tags only. Attach the packaged Windows + Linux bundles to a DRAFT
# Tags only. Attach the packaged Windows + Linux + macOS bundles to a DRAFT
# pre-release so a human can do the ROM smoke-test on each platform before
# publishing. This job — and only this job — gets contents: write.
# publishing. This job — and only this job — gets contents: write, and it
# is also the only job that attests build provenance (id-token: write +
# attestations: write; attest-build-provenance needs both).
release:
if: github.ref_type == 'tag'
needs: [ changes, validate, windows-build, linux-build ]
needs: [ changes, validate, windows-build, linux-build, macos-build ]
runs-on: ubuntu-22.04
permissions:
contents: write
id-token: write
attestations: write
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/download-artifact@v7
# The pushed tag must match the version baked into the exe (the update
# check compares against it); bump GE007_VERSION in CMakeLists.txt first.
- name: Check tag matches GE007_VERSION
run: |
ver=$(sed -n 's/^set(GE007_VERSION "\([^"]*\)").*/\1/p' CMakeLists.txt)
if [ -z "$ver" ] || [ "${GITHUB_REF_NAME}" != "v${ver}" ]; then
echo "tag ${GITHUB_REF_NAME} != v${ver} (CMakeLists.txt GE007_VERSION)"; exit 1
fi
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: goldeneye-pc-port-win64
path: dist
- uses: actions/download-artifact@v7
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: goldeneye-pc-port-linux-x86_64
path: dist
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: goldeneye-pc-port-macos-arm64
path: dist
# SLSA build provenance for the release artifacts (in-toto, signed by
# GitHub's OIDC key for this repo's workflow at the tagged commit).
# Anyone can check a downloaded file:
# gh attestation verify <file> --repo jkdansereau/goldeneye-pc-port
- uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0
with:
subject-path: |
dist/*.zip
dist/*.zip.sha256
dist/*.tar.gz
dist/*.tar.gz.sha256
- name: Create or update draft pre-release
env:
GH_TOKEN: ${{ github.token }}
@@ -412,7 +566,7 @@ jobs:
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
# (bundle-{linux,win}.sh, package-macos.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}"
+43 -77
View File
@@ -1,7 +1,7 @@
# 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.
# self-hosted runner, 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
@@ -9,16 +9,19 @@
# 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).
# - ROM -> copied every run from a seed dir on the runner:
# {runner_home}/_rom-seed/, or the repo variable
# GE_ROM_SEED_DIR. Bootstrap: put ge007.ntsc-final.z64,
# pcmodels-ntsc-final/ and pccg-ntsc-final/ there once.
# Integrity is re-verified every run against the committed
# ge007.u.sha1 (repo-tracked US ROM hash).
# The ROM is deliberately NOT stored in actions/cache (it
# used to be, key ge-rom-data-ntsc-v1). Cache entries live on
# GitHub's side, and pull_request workflows, including ci.yml
# runs from fork PRs, can restore caches created on the base
# branch, so a PR could read the ROM out of the cache. (The
# old save never actually persisted on this box, so no entry
# was ever created; checked 2026-09-30.)
# - 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
@@ -37,8 +40,8 @@
# 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/.
# - The ROM + sidecars live only in the runner's local seed dir (never in
# actions/cache); 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.
@@ -67,7 +70,7 @@ concurrency:
cancel-in-progress: false
env:
ROM_CACHE_KEY: ge-rom-data-ntsc-v1
GE_ROM_SEED_DIR: ${{ vars.GE_ROM_SEED_DIR }}
jobs:
build-and-sweep:
@@ -79,7 +82,7 @@ jobs:
CCACHE_DIR: ${{ github.workspace }}/.ccache
CCACHE_MAXSIZE: 500M
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
@@ -90,7 +93,7 @@ jobs:
# 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
- uses: msys2/setup-msys2@ec48f7c5447b3140e2b088413ae3a55687bccb6e # v2.33.0
with:
msystem: MINGW64
release: false
@@ -101,71 +104,26 @@ jobs:
- 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"
# --- ROM + sidecars: runner-local seed dir (never actions/cache) ------
# {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'
# The seed dir is the only copy the workflow uses; it is never deleted.
- name: Stage ROM + sidecars (runner seed dir)
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
}
seed="${GE_ROM_SEED_DIR:-$(dirname "$(dirname "$RUNNER_WORKSPACE")")/_rom-seed}"
for f in ge007.ntsc-final.z64 pcmodels-ntsc-final pccg-ntsc-final; do
[ -e "$seed/$f" ] || {
echo "missing $seed/$f" >&2
echo "One-time bootstrap: copy ge007.ntsc-final.z64, pcmodels-ntsc-final/" >&2
echo "and pccg-ntsc-final/ into $seed (or set the repo variable GE_ROM_SEED_DIR)." >&2
exit 2
}
done
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 }}
echo "staged ROM store from $seed"
- name: Verify ROM integrity (committed US sha1)
run: |
@@ -179,7 +137,7 @@ jobs:
# 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
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ env.CCACHE_DIR }}
key: ccache-selfhosted-${{ hashFiles('CMakeLists.txt') }}-${{ github.sha }}
@@ -197,8 +155,16 @@ jobs:
# 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)
env:
INPUT_LEVELS: ${{ inputs.levels }}
run: |
[ -n "${{ inputs.levels }}" ] && export SWEEP_LEVELS='${{ inputs.levels }}'
if [ -n "$INPUT_LEVELS" ]; then
if ! printf '%s' "$INPUT_LEVELS" | grep -Eq '^[A-Za-z0-9]+:[0-9]+( [A-Za-z0-9]+:[0-9]+)*$'; then
echo "levels input must be space-separated Name:NN entries, e.g. 'Dam:33 Facility:34'" >&2
exit 1
fi
export SWEEP_LEVELS="$INPUT_LEVELS"
fi
tools_pc/verify.sh sweep --json > sweep.log 2>&1
rc=$?
cat sweep.log
@@ -223,7 +189,7 @@ jobs:
# Console logs + verdict only — no frames, no ROM bytes (leak rules).
- name: Upload sweep report
if: always()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: selfhosted-sweep-${{ github.run_number }}
path: |
+25
View File
@@ -170,6 +170,9 @@ 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/
@@ -188,3 +191,25 @@ 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-*/
# Sweep capture output dirs and PPM dumps: regenerable framebuffer captures
# (capture.sh OUTDIR), useful on disk, never in the tracked tree.
tools_pc/sweep-captures/cap-*/
tools_pc/sweep-captures/**/*.ppm
# Stray full CPython install that landed in repo root (2026-09-28);
# referenced by no project tooling. Never commit.
/Python/
docs/media/*.mp4
# mod-loader tool build artifacts
/tools_pc/modpatch/modpatch.exe
# demo harness data (tools_pc/demobatch.sh): personal recordings + run output
demos/
demobatch/
levelbench/
+168 -17
View File
@@ -5,9 +5,37 @@
- `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). **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).
- **Open work / known issues:** `docs/ROADMAP.md` — the single tracker; add new items there, never start a new backlog/roadmap file.
- **Current status:** the README "Status" section (`docs/dev/LEVEL-STATUS.md` is a historical per-level sweep record). 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.
## Public site pages (`docs/`) — presentation layer, not a source of truth
The repo's homepage and sub-pages are published by GitHub Pages; the local
preview is `node scratch/build_preview.mjs site` → `scratch/_site/` (served
with `node scratch/serve_preview.mjs 8777`). The site is a **presentation
layer over the reference docs, kept in a separate area, and allowed to go
stale** — never let it poison a work item:
- **Pure site pages** — `docs/index.md`, `documentation.md`,
`the-software-rsp.md`, `the-asset-pipeline.md`, `framerate-and-pacing.md`,
`input-and-aim.md`, `saves.md`, `security.md`, `fidelity.md` —
summarise the ROADMAP / README / finding log / release notes. **Do not
consult them as work information, cross-check against them, or treat a
mismatch with `docs/ROADMAP.md` or the finding log as a bug** (that is
the expected, independent-staleness case). Update them only when the task
says so, and source every fact from the ROADMAP / README / finding log —
never the other way round. The footer "last updated" date in
`docs/_layouts/default.html` is the same class of site-only detail.
- **Shared files** — `docs/internals.md`, `porting-notes.md`,
`building.md`, `dev-process.md`, `dev/agentic-development.md` are
authoritative working docs that the site publishes verbatim; edit them
normally as part of dev work and the site picks up the change for free.
Keep the split intact: no working notes migrate into pure site pages, and
never edit a shared doc merely to make the site read better — if the site is
stale, fix the site page (or the fact upstream, as its own task).
## Non-negotiables
1. **N64 build untouched.** `Makefile`, `tools/`, `rsp/`, `ld/` belong to the N64 build. Never modify them for the PC port.
@@ -16,6 +44,60 @@
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.
## Tree hygiene & release gate
Standing rules for anything that lands in the tracked tree or in a push. These
exist because the repo is public, MIT-licensed, forked, and its history is
mostly agent-authored.
1. **No local machine paths.** Tracked files carry no local absolute paths and
no usernames. Use the placeholder convention: `<repo>` = this working
directory, `<repos>` = the local source-repos root, `<games>` / `<videos>` =
local media roots, `<python>` = the local CPython install, `<temp>` = a
writable native temp dir (legend at the top of
`docs/dev/HANDOFF-ARCHIVE.md`). Tools must not default to a machine-specific
path — take the path as an argument and error if it is missing.
2. **No contributor emails in prose.** Credit people by name plus the PR/issue
that carries the change ("co-authored with X (#123)"), never an inline
`<email>` trailer inside findings text or docs. Commit-trailer emails must
be noreply forms, never a contributor's real address.
3. **Every vendored third-party component is declared in `NOTICE`** with its
license and holder — the "Third-party code vendored into the port layer" and
"Vendored build tooling" lists. GPL sources must ship their license text
in-tree (`tools/gzipsrc/COPYING`). MIT covers only our original work; never
relicense inherited code and never re-license the repo (forks depend on the
MIT `LICENSE` staying put).
4. **Screenshots are game content.** A framebuffer capture is a derivative work
of Nintendo/MGM art regardless of which engine drew it. Keep repo imagery
documentary and small (bug shots, previews — downscaled), and never commit
bulk ROM-derived captures: capture output dirs are gitignored
(`tools_pc/sweep-captures/cap-*/`, `tools_pc/sweep-captures/**/*.ppm`); the
only bulk imagery in the tree is `tools_pc/golden/**/*.png`
(63 frames × 3 platforms, 189 total, nested per level at
`tools_pc/golden/<level>/{win,linux,deck}/`; the `deck` set added
2026-10-05 with maintainer approval), which exists because `verify.sh`
consumes it. Downloads ship no game content at all.
5. **Batch, don't spam.** Accumulate approved doc/tree fixes across a session
into ONE commit; no per-item branches.
6. **Release gate.** Before pushing a release: the PII scrub and legal/copyright
review reports (gitignored, `docs/dev/notes/`) must be run and clean, and the
README Legal section must name every rights holder the game's trademarks
implicate. Rewriting pushed history (squash) needs explicit maintainer
consent, and any such report states the remote + branch it applies to.
7. **Squashes and history rebuilds must not erase contributor PRs.** Release
lines may squash *our own* work, but every contributor PR whose content
shipped gets a zero-diff record merge at its timeline position (merge
commit with the PR head as second parent, tree unchanged) so the PR shows
as merged and the contributor's commits stay reachable in public history
with their authorship. If the PR's base is a stale internal branch, merge
it there via `gh pr merge` so the badge is true; GitHub refuses a base
change to a branch that already contains the head. Superseded/reworked
PRs are never merged — comment + let the author close. Before force-
pushing a rewritten release branch: back up the pushed tip to Forgejo
(`backup/<branch>-pre-rebuild-<date>`), pass a byte-identical tree-diff
gate against the old tip, re-run CI, and note the rebuild in any open
issue that tells people to build the branch.
## Critical files
| File | Role |
@@ -23,7 +105,7 @@
| `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); 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/src/` | Shims: `libultra.c` (OS API; also shims the scheduler's hardware leaf calls and runs the software RSP inline — the game's real `src/sched.c` is compiled), `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`. |
@@ -39,26 +121,95 @@ 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 a relocated MSYS2
root — `<msys>` in the placeholder legend — so its path 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).
**FIXED 2026-09-30 (`fix/build-pc-tmp-reexec`):** the heredoc `.ps1`
was byte-identical to a hand-extracted copy — not line endings. Three
real bugs: (a) the TMP probe's inline `cmd //c "... \"path\" ..."` never
ran (msys re-escapes embedded quotes as `\"`, which cmd rejects), so
*every* build took the re-exec, and it only tested that TMP existed;
(b) the `.ps1`'s toolchain dirs were derived from `command -v cmake`,
which in Git Bash/agent shells is a pip cmake (no MSYS2 toolchain) —
the hand-run worked because it passed explicit `C:\msys64\...` paths;
(c) `/tmp` + `cygpath` disagree when Git Bash and MSYS2 `usr/bin` are
both on PATH, so the `.ps1` could be written to one `/tmp` and
powershell pointed at the other (it then exits 0 having done nothing).
Now: the MSYS2 `mingw64/bin` is located and validated (override:
`GE_MSYS2_ROOT`) and prepended to PATH; the probe is a `.cmd` file that
tests TMP *writability*; all native-facing files live in the build dir
with paths from bash's own `pwd -W`. Verified from an agent Git Bash
shell: fresh + incremental builds, both on the direct path and with a
forced unwritable TMP (`TMP=C:\Windows\`) through the re-exec.
**FIXED 2026-09-30 (`fix/build-pc-reexec-noop`): the re-exec exited rc=2
with the diag log ending `cmake ran LASTEXITCODE=[]`.** From a shell with
MSYS2 `usr/bin` first on PATH, powershell inherits a stripped ~13-var env
with `PATHEXT=.CPL` and no ComSpec/TMP/TEMP, so `& cmake.exe` silently
launches nothing. The `.ps1` now restores PATHEXT and ComSpec. In that
shell the re-exec itself is legitimately needed (no TMP), so the probe
is correct. Verified: MSYS2-PATH shell, forced `TMP=C:\Windows\`, and a
no-change incremental run all exit 0 with a fresh exe.
**FIXED 2026-10-04 (`fix/build-arch-tag`, D515):** a FRESH build dir
configured through the re-exec cached `CMAKE_HOST_SYSTEM_PROCESSOR ""` /
`CMAKE_SYSTEM_PROCESSOR ""` (the stripped env also drops
`PROCESSOR_ARCHITECTURE`, which CMake reads for the host processor — same
class as PATHEXT/ComSpec), so `cmake/TargetArch.cmake` produced an empty
arch tag: `Target arch: (64bit=FALSE)`, binary named `ge007..exe`
(a correct 64-bit build; only the name was wrong — found during T7, D513).
The `.ps1` now restores `PROCESSOR_ARCHITECTURE` from the machine
environment (fallback `AMD64`; `SystemRoot`/`windir` likewise) and logs
the four vars to the diag log; `cmake/TargetArch.cmake` falls back to
`${CMAKE_C_COMPILER} -dumpmachine` when `CMAKE_SYSTEM_PROCESSOR` is empty
and FATAL_ERRORs instead of emitting an untagged binary. Existing build
dirs are unaffected (value already cached); a dir configured before the
fix picks up x86_64 on its next in-place reconfigure.
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`.
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`, `n64stubs.c`, `random.c`, or `ucode.c`.
2. **Duplicates.** No symbol defined twice across the compiled set (watch `sp_*` stacks, `rmon*`, `os*` shims, segment markers).
3. **Syntax.** Every touched file must parse; `./build-pc.sh` is the final word.
Run `/linkcheck` for this sweep. Record new findings in `docs/dev/findings.md` §F/§H style (next `Dxx` label after the last used) and add the label to the §F index.
## Phase status (summary — see README + `docs/dev/findings.md` for detail)
## Phase status
- **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 →
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
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.
Do not keep a status summary here — it goes stale. Open work, known issues
and decisions owed: **`docs/ROADMAP.md`** (the single tracker). Current
release state: the README "Status" section.
+10 -2
View File
@@ -16,6 +16,8 @@ Context is scarce. Load by tier; do not blind-read whole files.
`docs/HANDOFF.md` (current state + next task + environment — a rolling
local working file; may be absent in a fresh clone, in which case read
the README "Status" section instead) — read fully;
`docs/ROADMAP.md` (the single tracker of open work — read the section
relevant to the task);
`docs/porting-notes.md` (recurring bug classes) — skim the section
headers, read the classes relevant to the task.
- **Tier 2 — on demand only, do NOT read start-to-finish:**
@@ -41,6 +43,12 @@ Having a concrete "next step" from the handoff is NOT license to skip this.
2. Skim the README "Status" section for where the project stands.
3. If you may dispatch a subagent this session, read `docs/dev-process.md`
NOW — not at dispatch time.
4. Tree hygiene applies to anything you commit — see AGENTS.md §Tree hygiene &
release gate: no local absolute paths or usernames in tracked files (use
`<repo>`/`<repos>`/`<games>`/`<videos>`/`<python>`/`<temp>`), no contributor
emails in prose (name + PR number), every vendored third-party license
declared in `NOTICE`, no bulk ROM-derived captures in the tree (capture
dirs are gitignored), and one batched commit per session — not one per fix.
## Dispatch preflight — EVERY Agent/Task spawn, no exceptions
@@ -74,7 +82,7 @@ Full template: `docs/dev-process.md` -> "Investigation-brief template".
## Local Qwen dispatch (cost-free, data-local worker)
A local Qwen3.8-27B is wired in via the `delegate-local` MCP server (see
`C:\Users\james\Source\Repos\20260902-qwen38claudepair`). Use it as a
`<repos>\20260902-qwen38claudepair`). Use it as a
*subagent whose compute runs on-machine* for scoped, checkable work — the lead
still owns judgment, planning, and the verification gate.
@@ -91,7 +99,7 @@ still owns judgment, planning, and the verification gate.
proxy (`litellm/run.ps1`) + Unsloth Studio both up.
- **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`):
harness's own `_run_bash` (the local `claude-code-delegate-local` harness dir):
`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
+260 -23
View File
@@ -23,13 +23,30 @@ list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake)
# declaration (no members) and rejects base-member access. So prefer GCC.
# (GCC does hard-error on the one struct that declares a member with the same
# name as an inherited one — CCTVRecord's `pad` — see docs/internals.md.)
# An explicit -DCMAKE_C_COMPILER=... always wins.
if(NOT DEFINED CMAKE_C_COMPILER)
find_program(_GE007_GCC NAMES gcc)
# An explicit -DCMAKE_C_COMPILER=... or a CC environment variable always wins.
if(NOT DEFINED CMAKE_C_COMPILER AND "$ENV{CC}" STREQUAL "")
if(CMAKE_HOST_APPLE)
# Homebrew (gcc-N) and MacPorts (gcc-mp-N) install real GNU GCC with a
# version suffix; /usr/bin/gcc is Apple Clang and does not support the
# Plan 9 struct extensions required by the decomp's `inherits` pattern.
find_program(_GE007_GCC
NAMES gcc-16 gcc-mp-16 gcc-15 gcc-mp-15 gcc-14 gcc-mp-14 gcc-13 gcc-mp-13
gcc-12 gcc-mp-12 gcc-11 gcc-mp-11 gcc)
else()
find_program(_GE007_GCC NAMES gcc)
endif()
if(_GE007_GCC)
set(CMAKE_C_COMPILER "${_GE007_GCC}" CACHE FILEPATH "C compiler" FORCE)
get_filename_component(_GE007_GCC_DIR "${_GE007_GCC}" DIRECTORY)
find_program(_GE007_GXX NAMES g++ PATHS "${_GE007_GCC_DIR}" NO_DEFAULT_PATH)
if(CMAKE_HOST_APPLE)
find_program(_GE007_GXX
NAMES g++-16 g++-mp-16 g++-15 g++-mp-15 g++-14 g++-mp-14 g++-13 g++-mp-13
g++-12 g++-mp-12 g++-11 g++-mp-11 g++
PATHS "${_GE007_GCC_DIR}" NO_DEFAULT_PATH)
else()
find_program(_GE007_GXX NAMES g++
PATHS "${_GE007_GCC_DIR}" NO_DEFAULT_PATH)
endif()
if(_GE007_GXX)
set(CMAKE_CXX_COMPILER "${_GE007_GXX}" CACHE FILEPATH "CXX compiler" FORCE)
endif()
@@ -37,17 +54,48 @@ if(NOT DEFINED CMAKE_C_COMPILER)
endif()
if(APPLE)
# CMAKE_OSX_ARCHITECTURES is normally EMPTY here (the build targets the host
# arch), so an arm64 Mac used to fall into the 10.13 (Intel) floor and ld
# warned that the brew-gcc aarch64 runtime libs and the generated .s objects
# carry a newer floor than the linked one. D609 (macos branch): also consult
# the host machine when no explicit -arch list was requested. CMake's own
# CMAKE_HOST_SYSTEM_PROCESSOR is EMPTY on macOS (its `uname -p` probe
# returns "unknown" there - same class as D515's stripped-env arch probe),
# so ask `uname -m` directly. 11.0 is Big Sur, the first arm64 release.
if(CMAKE_OSX_ARCHITECTURES MATCHES "arm64")
set(CMAKE_OSX_DEPLOYMENT_TARGET "11.0" CACHE STRING "" FORCE)
else()
set(CMAKE_OSX_DEPLOYMENT_TARGET "10.13" CACHE STRING "" FORCE)
set(_GE007_OSX_FLOOR "11.0")
elseif(CMAKE_OSX_ARCHITECTURES STREQUAL "")
# No /bin/uname on macOS, and CMake's execute_process looks the name up in
# PATH - so pass the bare name.
execute_process(COMMAND uname -m OUTPUT_VARIABLE _GE007_UNAME_M
RESULT_VARIABLE _GE007_UMRC)
if(_GE007_UMRC EQUAL 0)
string(STRIP "${_GE007_UNAME_M}" _GE007_UNAME_M)
if(_GE007_UNAME_M MATCHES "^(aarch64|arm64)")
set(_GE007_OSX_FLOOR "11.0")
else()
set(_GE007_OSX_FLOOR "10.13")
endif()
endif()
endif()
if(NOT DEFINED _GE007_OSX_FLOOR)
set(_GE007_OSX_FLOOR "10.13")
endif()
set(CMAKE_OSX_DEPLOYMENT_TARGET "${_GE007_OSX_FLOOR}" CACHE STRING "" FORCE)
unset(_GE007_OSX_FLOOR)
message(STATUS "macOS deployment target floor: ${CMAKE_OSX_DEPLOYMENT_TARGET}")
endif()
# ASM is needed for the generated absolute ROM asset symbols
# (port/src/romassets_<region>.s, see scripts/gen_romassets.py).
project(ge007 C CXX ASM)
# Debug gate: validate every u32 -> host-pointer conversion against the regions
# the port actually maps, and report offenders at the conversion site with a
# backtrace. Off by default (zero cost); enable with -DPORT_ADDR_STRICT=ON.
# Catches the MACOS-SFX-BOUND through MACOS-TEXALIGN truncated/never-rebased-pointer family early.
option(PORT_ADDR_STRICT "Validate N64->host address conversions at runtime" OFF)
include(TargetArch)
include(FindPkgConfig)
@@ -93,6 +141,21 @@ else()
endif()
message(STATUS "Target arch: ${TARGET_ARCH} (64bit=${TARGET_IS_64BIT})")
# Host base of the N64 address window (port/include/portaddr.h, D607). Must be
# set BEFORE add_definitions below, and AFTER target_architecture(): D609 showed
# CMAKE_SYSTEM_PROCESSOR/CMAKE_HOST_SYSTEM_PROCESSOR are EMPTY on macOS, so the
# gate uses TARGET_ARCH (the D609 pagezero gate uses the same test).
# 0 on Windows/Linux; 16 TiB on arm64 macOS (overridable with
# -DPORT_ADDR_BASE=... for tooling, e.g. an ASan build whose shadow region
# overlaps 16 TiB). Fed to the compiler (-DPORT_ADDR_BASE) and to
# scripts/gen_macho_syms.py --base for the romassets absolute symbols, so the
# cart pairing (romdata.c CART_HOST vs romassets_*.s values) agrees.
if(APPLE AND TARGET_ARCH MATCHES "^aarch64$")
set(PORT_ADDR_BASE 0x100000000000 CACHE STRING "Host base of the N64 address window (arm64 macOS)")
else()
set(PORT_ADDR_BASE 0x0 CACHE STRING "Host base of the N64 address window")
endif()
if(WIN32)
set(TARGET_OS "windows")
elseif(APPLE)
@@ -149,6 +212,7 @@ 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(GE007_VERSION "0.6.0") # bump on every release
set(ORIGIN_URL "https://github.com/jkdansereau/goldeneye-pc-port")
configure_file(
@@ -163,7 +227,18 @@ configure_file(
# the toolchain include dir from the compiler location (MinGW/MSYS2 layout:
# bin/../include) and generate an absolute-path include.
get_filename_component(_PORT_GCC_DIR "${CMAKE_C_COMPILER}" DIRECTORY)
get_filename_component(PORT_HOST_STRING_H "${_PORT_GCC_DIR}/../include/string.h" ABSOLUTE)
if(APPLE)
execute_process(
COMMAND xcrun --show-sdk-path
OUTPUT_VARIABLE _PORT_MACOS_SDK OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET)
if(NOT _PORT_MACOS_SDK)
message(FATAL_ERROR "Cannot locate the macOS SDK; install the Xcode Command Line Tools")
endif()
set(PORT_HOST_STRING_H "${_PORT_MACOS_SDK}/usr/include/string.h")
else()
get_filename_component(PORT_HOST_STRING_H "${_PORT_GCC_DIR}/../include/string.h" ABSOLUTE)
endif()
configure_file(
"${CMAKE_SOURCE_DIR}/port/include/hoststring.h.in"
"${CMAKE_BINARY_DIR}/port/include/hoststring.h" @ONLY)
@@ -172,7 +247,11 @@ configure_file(
# the include path and shadows the system header; libstdc++'s <thread>/gthr
# headers in the fast3d C++ TUs need the real <sched.h> (cpu_set_t,
# sched_yield). Both MinGW and Linux keep <sched.h> at bin/../include.
get_filename_component(PORT_HOST_SCHED_H "${_PORT_GCC_DIR}/../include/sched.h" ABSOLUTE)
if(APPLE)
set(PORT_HOST_SCHED_H "${_PORT_MACOS_SDK}/usr/include/sched.h")
else()
get_filename_component(PORT_HOST_SCHED_H "${_PORT_GCC_DIR}/../include/sched.h" ABSOLUTE)
endif()
configure_file(
"${CMAKE_SOURCE_DIR}/port/include/hostsched.h.in"
"${CMAKE_BINARY_DIR}/port/include/hostsched.h" @ONLY)
@@ -182,7 +261,11 @@ configure_file(
# one C++ TU (fast3d): MinGW's own sec_api/stdlib_s.h does `#include
# <stdlib.h>`, which resolves to the N64 header via -I. The shim routes C++
# TUs to the host header by absolute path (same trick as hoststring.h).
get_filename_component(PORT_HOST_STDLIB_H "${_PORT_GCC_DIR}/../include/stdlib.h" ABSOLUTE)
if(APPLE)
set(PORT_HOST_STDLIB_H "${_PORT_MACOS_SDK}/usr/include/stdlib.h")
else()
get_filename_component(PORT_HOST_STDLIB_H "${_PORT_GCC_DIR}/../include/stdlib.h" ABSOLUTE)
endif()
configure_file(
"${CMAKE_SOURCE_DIR}/port/include/hoststdlib.h.in"
"${CMAKE_BINARY_DIR}/port/include/hoststdlib.h" @ONLY)
@@ -212,8 +295,13 @@ add_definitions(
-D_LANGUAGE_C=1
-DAVOID_UB=1
-DPAL=${VERSION_IS_PAL}
-DPORT_ADDR_BASE=${PORT_ADDR_BASE}
${REGION_DEFS}
)
if(PORT_ADDR_STRICT)
add_definitions(-DPORT_ADDR_STRICT=1)
message(STATUS "PORT_ADDR_STRICT enabled: N64->host conversions are validated at runtime")
endif()
if(TARGET_IS_64BIT)
add_definitions(-DPLATFORM_64BIT=1)
endif()
@@ -237,7 +325,9 @@ add_compile_options(
# runs the same way; the genuinely understood cases are still fixed properly
# under #ifdef PORT (D189). A hardened Linux build would need the whole class
# swept first — tracked in docs/dev/findings.md.
add_compile_options(-fno-stack-protector)
if(NOT GE_STACK_PROTECTOR_ALL)
add_compile_options(-fno-stack-protector)
endif()
# Keep frame pointers: dbghelp's StackWalk64 (crash handler + thread dumps)
# cannot read DWARF unwind info on MinGW, so the EBP chain is the only way to
# get a real backtrace of a hung/crashed game thread.
@@ -287,14 +377,18 @@ add_compile_options($<$<COMPILE_LANGUAGE:CXX>:-Wno-unused-function>)
# automatic base-pointer conversion the idiom relies on). C only — the decomp
# is C; fast3d C++ does not use `inherits`.
if(NOT MINGW)
add_compile_options($<$<COMPILE_LANGUAGE:C>:-fplan9-extensions>)
# GCC 14 promoted several long-standing C leniencies to hard errors
# (int<->pointer for a known prototype, incompatible-pointer arg types,
# implicit int/function). The WIP decomp still trips these (e.g.
# append_text_picked_up's vestigial u8* params in propobj.c). -fpermissive
# downgrades them to warnings — the C counterpart of the -Wno-error lines
# above, and the documented path for building older C against GCC 14+.
add_compile_options($<$<COMPILE_LANGUAGE:C>:-fpermissive>)
# GCC-only flags: the macOS default compiler is Apple clang (via the
# /usr/bin/gcc c99 shim), which rejects both of these.
if(CMAKE_C_COMPILER_ID STREQUAL "GNU")
add_compile_options($<$<COMPILE_LANGUAGE:C>:-fplan9-extensions>)
# GCC 14 promoted several long-standing C leniencies to hard errors
# (int<->pointer for a known prototype, incompatible-pointer arg types,
# implicit int/function). The WIP decomp still trips these (e.g.
# append_text_picked_up's vestigial u8* params in propobj.c). -fpermissive
# downgrades them to warnings — the C counterpart of the -Wno-error lines
# above, and the documented path for building older C against GCC 14+.
add_compile_options($<$<COMPILE_LANGUAGE:C>:-fpermissive>)
endif()
endif()
# ASLR must be OFF: dram_syms.s defines absolute symbols (cfb_16 @ 0x70000000,
@@ -328,6 +422,30 @@ elseif(UNIX AND NOT APPLE)
# relocations are fine (unlike a 0x140000000 base, which needs mcmodel=large).
# This is the ELF analogue of the Windows build's fixed 0x140000000 base.
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-Ttext-segment=0x20000000")
elseif(APPLE)
# Mach-O executables normally reserve [0, 4 GiB) as __PAGEZERO. The x86_64
# (Intel) port needs fixed mappings at 0x10000000 (ROM), 0x70000000 (s32-safe
# DRAM) and 0x80000000 (KSEG0 mirror), so shrink the guard to 64 KiB and keep
# null/low-pointer protection only that far (#88).
#
# D609: on arm64 macOS that flag is FATAL AT EXEC, not at link time. The
# Apple-Silicon kernel enforces the 4 GiB guard region (SVE): an image whose
# __PAGEZERO is smaller than 0x100000000 is SIGKILL'd by the kernel during
# exec, before dyld runs - no output, no crash report, no unified-log entry
# at unprivileged levels, and lldb refuses the image ("Malformed Mach-o file").
# Reproduced with a 6-line C program: `gcc probe.c -Wl,-pagezero_size,0x10000`
# exits 137 (128+9); the same program with the default pagezero exits 7. The
# arm64 port does not need the shrink at all - portaddr.h already shifts the
# whole N64 window up by PORT_ADDR_BASE = 16 TiB, so every required mapping
# (ROM, DRAM V1/K0) lives far above the guard; verified with the same minimal
# probe (shm_open + the two MAP_FIXED mmaps at 0x100070000000 / 0x100080000000
# alias correctly and exit 7). So: shrink on x86_64 only, default on arm64.
if(TARGET_ARCH MATCHES "^aarch64$")
message(STATUS "arm64 macOS: keeping the default 4 GiB __PAGEZERO (arena is at PORT_ADDR_BASE)")
else()
set(CMAKE_EXE_LINKER_FLAGS
"${CMAKE_EXE_LINKER_FLAGS} -Wl,-pagezero_size,0x10000")
endif()
endif()
# port/shim first: PC-only shadow headers (bondtypes.h, bondconstants.h,
@@ -342,6 +460,7 @@ include_directories(
"${CMAKE_SOURCE_DIR}/src/game"
"${CMAKE_SOURCE_DIR}/src/libultra"
"${CMAKE_SOURCE_DIR}/src/libultra/audio"
"${CMAKE_SOURCE_DIR}/src/libultra/gu"
"${CMAKE_SOURCE_DIR}/port/include"
"${CMAKE_BINARY_DIR}/port/include"
)
@@ -395,6 +514,8 @@ if(NOT EXTRA_LIBRARIES)
set(EXTRA_LIBRARIES dbghelp -static-libstdc++ pthread)
elseif(WIN32)
set(EXTRA_LIBRARIES dbghelp -static-libstdc++)
elseif(APPLE)
set(EXTRA_LIBRARIES stdc++ m pthread)
else()
set(EXTRA_LIBRARIES stdc++ m dl pthread)
endif()
@@ -414,14 +535,91 @@ file(GLOB_RECURSE SRC_FAST3D "${CMAKE_SOURCE_DIR}/port/fast3d/*.cpp"
# scripts/gen_romassets.py). Defines every obseg/ramrom/music label and the
# ge007.ld segment markers as 0x10000000 + rom_offset, so `&symbol` / romCopy()
# work once romdata.c maps the .z64 at 0x10000000.
set(SRC_ROMASSETS "${CMAKE_SOURCE_DIR}/port/src/romassets_${ASSET_REGION}.s")
if(NOT EXISTS ${SRC_ROMASSETS})
message(FATAL_ERROR "Missing ${SRC_ROMASSETS} — run: python3 scripts/gen_romassets.py ${ASSET_REGION}")
set(SRC_ROMASSETS_INPUT "${CMAKE_SOURCE_DIR}/port/src/romassets_${ASSET_REGION}.s")
if(NOT EXISTS ${SRC_ROMASSETS_INPUT})
message(FATAL_ERROR "Missing ${SRC_ROMASSETS_INPUT} — run: python3 scripts/gen_romassets.py ${ASSET_REGION}")
endif()
# Absolute symbols inside the reserved N64-DRAM region (port/src/dram.c):
# cfb_16 (replaces src/cfb.c) and _bssSegmentEnd (mempool start marker).
set(SRC_DRAM_SYMS "${CMAKE_SOURCE_DIR}/port/src/dram_syms.s")
set(SRC_DRAM_SYMS_INPUT "${CMAKE_SOURCE_DIR}/port/src/dram_syms.S")
# D441 high-arena test flavour: relocate the DRAM arena for test builds
# (tier 1 = 0x90000000). Build option only — the default build is unchanged.
# GE_HIGHARENA_BASE=0x90000000 ./build-pc.sh ntsc-final
# or -DGE_HIGHARENA_BASE=0x90000000 at configure time. The value must be a
# C/asm integer literal; it overrides PORT_DRAM_V1_BASE in portaddr.h for
# C, CXX and ASM (dram.c's mapping and the dram_syms.S absolute symbols both
# follow it).
set(GE_HIGHARENA_BASE "" CACHE STRING "Override PORT_DRAM_V1_BASE for test builds (high-arena flavour)")
if(NOT GE_HIGHARENA_BASE)
set(GE_HIGHARENA_BASE "$ENV{GE_HIGHARENA_BASE}")
endif()
if(GE_HIGHARENA_BASE)
message(STATUS "GE_HIGHARENA_BASE = ${GE_HIGHARENA_BASE}")
set(_d441_arena_def "-DPORT_DRAM_V1_BASE=${GE_HIGHARENA_BASE}")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${_d441_arena_def}")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${_d441_arena_def}")
set(CMAKE_ASM_FLAGS "${CMAKE_ASM_FLAGS} ${_d441_arena_def}")
endif()
# Apple: the two absolute-symbol files are converted to Mach-O spelling
# (scripts/gen_macho_syms.py: .global -> .globl with a leading underscore,
# ELF-style directives dropped). dram_syms.S is C-preprocessed first: it
# pulls the arena bases from portaddr.h (PORT_ADDR_BASE + offset; the header
# self-detects the 16 TiB arm64 base from its own __ASSEMBLER__ branch, no
# -D needed), and the Mach-O assembler does not run the C preprocessor on
# .s input. Only the high-arena test flavour passes a -D override.
if(APPLE)
find_package(Python3 REQUIRED COMPONENTS Interpreter)
# The generated .s files land in a build-dir subdir; CMake creates dirs for
# rules with C/C++ outputs only, so make it explicitly (the cc -E -o below
# fails with "No such file or directory" otherwise).
file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/port/src")
set(SRC_ROMASSETS "${CMAKE_BINARY_DIR}/port/src/romassets_${ASSET_REGION}_macho.s")
set(SRC_DRAM_SYMS_EXPANDED "${CMAKE_BINARY_DIR}/port/src/dram_syms_expanded.s")
set(SRC_DRAM_SYMS "${CMAKE_BINARY_DIR}/port/src/dram_syms_macho.s")
add_custom_command(
OUTPUT "${SRC_ROMASSETS}"
COMMAND "${Python3_EXECUTABLE}"
"${CMAKE_SOURCE_DIR}/scripts/gen_macho_syms.py"
--base "${PORT_ADDR_BASE}"
"${SRC_ROMASSETS_INPUT}" "${SRC_ROMASSETS}"
DEPENDS "${SRC_ROMASSETS_INPUT}"
"${CMAKE_SOURCE_DIR}/scripts/gen_macho_syms.py"
VERBATIM)
set(_dram_syms_pp_def "")
if(GE_HIGHARENA_BASE)
set(_dram_syms_pp_def "-DPORT_DRAM_V1_BASE=${GE_HIGHARENA_BASE}")
endif()
# D607: the COMMAND list must stay one CMake argument per argv element:
# quoting the whole "cc -E -I..." as ONE string works on Windows (cmd
# parses the command line) but /bin/sh on macOS/Linux treats the entire
# string as a single filename ("No such file or directory").
add_custom_command(
OUTPUT "${SRC_DRAM_SYMS_EXPANDED}"
COMMAND "${CMAKE_C_COMPILER}" "-E"
"-I${CMAKE_SOURCE_DIR}/port/include"
${_dram_syms_pp_def}
"${SRC_DRAM_SYMS_INPUT}"
"-o" "${SRC_DRAM_SYMS_EXPANDED}"
DEPENDS "${SRC_DRAM_SYMS_INPUT}"
"${CMAKE_SOURCE_DIR}/port/include/portaddr.h"
VERBATIM)
add_custom_command(
OUTPUT "${SRC_DRAM_SYMS}"
COMMAND "${Python3_EXECUTABLE}"
"${CMAKE_SOURCE_DIR}/scripts/gen_macho_syms.py"
"${SRC_DRAM_SYMS_EXPANDED}" "${SRC_DRAM_SYMS}"
DEPENDS "${SRC_DRAM_SYMS_EXPANDED}"
"${CMAKE_SOURCE_DIR}/scripts/gen_macho_syms.py"
VERBATIM)
set_source_files_properties(${SRC_ROMASSETS} ${SRC_DRAM_SYMS_EXPANDED} ${SRC_DRAM_SYMS}
PROPERTIES GENERATED TRUE)
else()
set(SRC_ROMASSETS "${SRC_ROMASSETS_INPUT}")
set(SRC_DRAM_SYMS "${SRC_DRAM_SYMS_INPUT}")
endif()
# Game code — compiled unmodified.
file(GLOB_RECURSE SRC_GAME "${CMAKE_SOURCE_DIR}/src/game/*.c")
@@ -452,6 +650,39 @@ file(GLOB SRC_LIBULTRARE_AUDIO "${CMAKE_SOURCE_DIR}/src/libultrare/audio/*.c")
# gu matrix helpers.
file(GLOB SRC_GU "${CMAKE_SOURCE_DIR}/src/libultra/gu/*.c")
# GCC for Darwin crashes while lowering the decomp's ELF-style
# `#pragma weak alias = target` directives. Compile generated copies with
# those directives removed, then provide their aliases in Mach-O assembly.
set(SRC_MACHO_ALIASES "")
if(APPLE)
set(_MACHO_GENERATED_SOURCES "")
set(_MACHO_WEAK_INPUTS
"${CMAKE_SOURCE_DIR}/src/game/lv.c"
"${CMAKE_SOURCE_DIR}/src/game/objective_status.c"
"${CMAKE_SOURCE_DIR}/src/game/spectrum.c"
"${CMAKE_SOURCE_DIR}/src/libultra/gu/sinf.c"
"${CMAKE_SOURCE_DIR}/src/libultra/gu/cosf.c")
foreach(_INPUT IN LISTS _MACHO_WEAK_INPUTS)
get_filename_component(_NAME "${_INPUT}" NAME)
set(_OUTPUT "${CMAKE_BINARY_DIR}/port/src/macho_${_NAME}")
add_custom_command(
OUTPUT "${_OUTPUT}"
COMMAND "${Python3_EXECUTABLE}"
"${CMAKE_SOURCE_DIR}/scripts/strip_weak_pragmas.py"
"${_INPUT}" "${_OUTPUT}"
DEPENDS "${_INPUT}" "${CMAKE_SOURCE_DIR}/scripts/strip_weak_pragmas.py"
VERBATIM)
list(APPEND SRC_MACHO_ALIASES "${_OUTPUT}")
list(APPEND _MACHO_GENERATED_SOURCES "${_OUTPUT}")
list(REMOVE_ITEM SRC_GAME "${_INPUT}")
list(REMOVE_ITEM SRC_GU "${_INPUT}")
endforeach()
list(APPEND SRC_MACHO_ALIASES
"${CMAKE_SOURCE_DIR}/port/src/macho_weak_aliases.s")
set_source_files_properties(${_MACHO_GENERATED_SOURCES}
PROPERTIES GENERATED TRUE)
endif()
# VI mode table (DATA, not a driver): fr.c references osViModeTable ~15x.
# This is the only libultra/io file we compile — the rest are hardware
# drivers that get shimmed in port/src/libultra.c.
@@ -569,6 +800,7 @@ set(SRC
${SRC_LIBULTRARE_LIBC}
${SRC_ASSETS}
${SRC_GU}
${SRC_MACHO_ALIASES}
${SRC_ENGINE}
)
@@ -581,6 +813,11 @@ target_link_libraries(ge007 ${LIBS})
set_target_properties(ge007 PROPERTIES OUTPUT_NAME "${BIN_NAME}")
if(WIN32)
set_target_properties(ge007 PROPERTIES WIN32_EXECUTABLE TRUE)
# Explorer/taskbar icon: the original crosshair mark (cmake/icon/ge007.rc,
# a static resource script; the ge007.ico it references is generated by
# tools_pc/make_icon.py). Compiled by the resource compiler; ge007.ico is
# resolved relative to this .rc file.
target_sources(ge007 PRIVATE cmake/icon/ge007.rc)
endif()
message(STATUS "Output binary: ${BIN_NAME}")
+19
View File
@@ -32,6 +32,25 @@ keep the port faithful to the original game.
clean configure + build for `ntsc-final` and a crash-free run of one level
(`./build-pc/ge007.x86_64 -level_09`).
5. **No game data, ever.** Never commit, upload or attach a ROM or anything
extracted from one (textures, audio, models, level data, in-game text,
the generated `data/pcmodels-*` / `data/pccg-*` folders) in a pull
request or an issue. A small cropped screenshot of a glitch is fine.
## Bug reports
Open an [issue](https://github.com/jkdansereau/goldeneye-pc-port/issues/new/choose)
with:
- `ge007.crash.log` if the game crashed (in the folder you launched from),
- the output of `ge007.x86_64 --version` (or the release version you
downloaded), your OS, and your GPU and driver version,
- your ROM's SHA-1 (Windows: `certutil -hashfile <rom> SHA1`, Linux:
`sha1sum <rom>`),
- what you did, what happened, and what you expected.
**Never attach the ROM itself.**
## Style
- C/C++ formatting follows `.clang-format` and `.editorconfig` in the repo
+36 -4
View File
@@ -10,6 +10,9 @@ repository:
* the port layer port/
* the PC build system CMakeLists.txt, cmake/, build-pc.sh
* PC-port tooling tools_pc/
* macOS packaging scripts/package-macos.sh, scripts/gen_macho_syms.py,
scripts/strip_weak_pragmas.py
* agent-development tooling pi/ (not part of the game build)
* documentation docs/, README.md
Third-party code vendored into the port layer keeps its own license:
@@ -18,6 +21,33 @@ Third-party code vendored into the port layer keeps its own license:
libultraship / sm64-port lineage; see port/fast3d/LICENSE.txt.
* port/fast3d/glad/ - generated OpenGL loader (glad), public domain / MIT.
Runtime libraries bundled in the downloads (not in this repository) keep
their own licenses; each license text ships in the download's licenses/ folder:
* SDL2 (all platforms) and SDL3 (macOS, loaded by sdl2-compat) - zlib license.
* zlib - zlib license.
* Windows: the MinGW-w64 runtime (libwinpthread, libgcc, libstdc++).
* macOS: the GCC runtime (libgcc_s, libstdc++) - GPL-3.0 with the GCC
Runtime Library Exception 3.1. The GPL-3.0 text the macOS package ships
is tools_pc/dist/GPL-3.0.txt.
The license texts the downloads carry come from the build environment where
it provides them, otherwise from tools_pc/dist/ (GPL-3.0.txt,
GCC-RUNTIME-EXCEPTION.txt, SDL2-LICENSE.txt).
Vendored build tooling keeps its own license (none of it is compiled or linked
into the PC port; it belongs to the N64 decompilation build):
* tools/ido5.3_recomp/rabbitizer/ - disassembler library (rabbitizer), MIT,
(c) 2022 Decompollaborate; license text in that directory's LICENSE file.
* tools/armips.cpp - assembler (armips), MIT, (c) 2009-2020 Kingcom.
* tools/extractor/puff.c, puff.h - puff-style inflate, zlib-style license,
(c) Mark Adler.
* tools/gzipsrc/ - gzip 1.x sources, GPL-2.0-or-later, (c) 1992-1993
Jean-loup Gailly; the GPL-2.0 text is tools/gzipsrc/COPYING. These sources
are build tooling only: they are never linked into the port, and they are
not part of any download package.
It does NOT cover:
* src/, include/, assets/, Makefile, tools/, rsp/, ld/, and the other
@@ -34,11 +64,13 @@ It does NOT cover:
assets -- textures, audio, models, level geometry, in-game text -- are
NOT included and are extracted from your own ROM at build time.
NO NINTENDO / RARE CONTENT IS DISTRIBUTED HERE
---------------------------------------------
NO ROM DATA OR ORIGINAL ASSETS ARE DISTRIBUTED HERE
--------------------------------------------------
This repository contains NO copyrighted ROM data, game assets (textures,
models, levels, audio, text), or Nintendo/Rare code. Building a playable
This repository contains NO ROM data and none of the original game assets
(textures, models, levels, audio, text), and no original Nintendo/Rare
binaries. The C source under src/ is the decompilation community's
reconstruction of the game's code (see above). Building a playable
executable requires a GoldenEye 007 Nintendo 64 ROM that you legally own;
the build extracts the required assets from that ROM on your own machine.
+282 -384
View File
@@ -1,393 +1,264 @@
# GoldenEye 007 PC Port
<p align="center">
<img src="docs/img/icon/ge007-icon.png" width="112" alt="GoldenEye 007 PC Port">
</p>
[![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)
![license](https://img.shields.io/badge/license-MIT-green)
<h1 align="center">GoldenEye 007 PC Port</h1>
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.
<p align="center">
The original Nintendo 64 GoldenEye 007 (1997) as a native game for Windows,
Linux, Steam Deck and macOS (Apple Silicon).
</p>
**v0.3.0** is out for Windows and Linux (including Steam Deck, where the
Linux bundle sideloads as-is) and runs the full campaign at a steady 60 fps
with known rough edges ([Status](#status)). Free to download, build on and
modify (you bring the ROM).
The GoldenEye 007 PC Port is a native port built from the game's own
reconstructed source code (the [GoldenEye 007 decompilation](https://github.com/n64decomp/007)),
in the same family as the [Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark).
It is not an emulator: the game is compiled for your PC and runs from your own
ROM, with native widescreen, 60 fps or more (up to 240 on fast displays),
mouse and keyboard or a modern controller, split-screen for up to 4 players,
and an in-game options menu.
**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 rough edges,
missing features, and breaking changes between versions. See
[Status](#status) for what works today and [Roadmap](#roadmap) for where
this is headed.
### [Download the latest release](https://github.com/jkdansereau/goldeneye-pc-port/releases/latest)
**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).
**This repository and its downloads contain no ROM data and none of the
original game assets. You need your own GoldenEye 007 ROM (US version) to play.**
<p align="center">
<img src="docs/media/goldeneye-gh-preview.gif" width="64%"
alt="~12 s gameplay montage from live play sessions">
<br><em>All in-engine, running in the port — a ~12&nbsp;s gameplay montage
from live v0.3.0 play sessions.</em>
alt="GoldenEye 007 running natively on PC: the Facility, the Dam sniper rifle and split-screen multiplayer">
<br><em>Recorded in the port: the Facility intro, the Archives, the Dam
sniper rifle, Silo, a 2-player match and the Dam ending.</em>
</p>
## Download
## Table of contents
| 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.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) caveats first: v0.3.0 has known rough edges,
listed plainly there.
## Status
**v0.3.0 - playable, with known rough edges.** The full single-player
campaign is completable end to end (all 21 missions, Agent difficulty,
playtested), at a steady 60 fps; all 21 solo missions load, render and run
crash-free, verified on Windows, Linux and real Steam Deck hardware. 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 21 solo missions load, render and are crash-free (full campaign
playtested end to end at Agent difficulty); steady 60 fps
(software RSP off the presentation critical path); full audio: in-level music and SFX; keyboard + mouse (click-to-lock, proportional aim mode, a single
simplified sensitivity control) and a modern dual-stick controller layout;
automatic widescreen FOV scaling; 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 (resolution, frame cap, MSAA,
filtering, FOV, sensitivity); Windows and Linux, including Steam Deck.
**Known issues:**
- Particle colours: a real fix landed (a corrupted-vertex-buffer bug), but
the rainbow effect can still occur intermittently on either platform, not
every time or on every level — a second cause hasn't been found yet
(D252).
- Some muzzle flashes draw an extra, erroneous long flash straight up from
the gun (seen on the M16, among others), overlaid on the normal, correctly
drawn flash. Cosmetic only (D303).
- Water levels show a moving seam between two water patterns (D245).
- Occasional z-fighting on some levels' geometry (D308) — a Dam intro/
truck-wheel instance showing odd transparent-looking areas may be related
(D306). Minor and cosmetic.
- In-level security camera props (not the player's own view) can
occasionally end up facing backwards on some levels, seen on Bunker
(D307).
- Rarely, Bond's model can briefly render out of place after an animation
transition — purely visual: no effect on gameplay or saves, and it clears
on its own or by re-entering the level (D311).
- 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).
- Some front-end 3D models are off: the spinning Nintendo logo renders as two
white blobs and the Rareware logo's texture filtering looks wrong (D75).
- Surface 1's 2D billboard trees render as a solid wall of tree texture
instead of discrete sprites (D236). Under active investigation.
- **Widescreen is stretched, not native**: on non-4:3 windows the whole
frame (world and HUD) is stretched horizontally to fill your display —
about 33% wider than original at 16:9, like a 4:3 video in a TV's "stretch"
mode. Automatic FOV scaling keeps the framing comfortable and gameplay is
completely unaffected (all game logic runs in world space), but shapes are
subtly wider than on the N64 — most visible on round objects (barrels, the
radar). A distortion-free native widescreen render is on the roadmap.
- Distant geometry can drop out on the biggest open levels (Streets,
Egyptian) at default FOV — a culling/LOD issue that sometimes
self-corrects as you keep moving (D249).
- Gunshot SFX can sound off during sustained/rapid fire: cadence can drift
from the N64 original's rate, and PP7/AK47 fire can occasionally go silent
under heavy automatic fire near another looping sound (D240/D241).
- **`All unlocked` is highly experimental — don't enable it until you have
at least one save written** (complete a level normally first, e.g. Dam on
Agent). Enabling it on a brand-new install with no prior save can still
cause silent audio and odd right-mouse-aim behavior (D257/D259/D281).
- Assorted further cosmetic defects are tracked in
[`docs/dev/GRAPHICS-BACKLOG.md`](docs/dev/GRAPHICS-BACKLOG.md).
- No macOS or ARM support; no controller rebinding UI.
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:
unzip, 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, MSAA 4, and 150% draw/LOD
distance (the authored N64 fade distances read short on the close-up 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.
- **Native widescreen** — a distortion-free render at your display's aspect
(correct-aspect projection and a wide-frame HUD), replacing today's
stretched 4:3 frame + FOV compensation.
- **Controller rebinding UI**, 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 and mouse feel;
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:
~3.5 weeks, ~430 commits, 200+ bugs logged and tracked to resolution.
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.3.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](#requirements)
- [Quick start](#quick-start)
- [Controls](#controls)
- [What this port offers](#what-this-port-offers)
- [FAQ](#faq)
- [Status](#status)
- [Roadmap](#roadmap)
- [Building from source](#building-from-source)
- [Documentation](#documentation)
- [Contributing and forking](#contributing-and-forking)
- [Credits](#credits)
- [Legal](#legal)
## Requirements
You need a GoldenEye 007 (Nintendo 64) ROM that you legally own, in
big-endian (`.z64`) format, matching one of:
- **Windows:** Windows 10 or 11, 64-bit. Nothing to install: the download
carries its own runtime libraries and the one-time asset tool.
- **Linux and Steam Deck:** 64-bit x86 Linux with glibc 2.31 or newer (Ubuntu
20.04+, Debian 11+, Fedora 32+, SteamOS). SDL2 is bundled.
- **macOS (experimental):** an Apple Silicon Mac (M1 or later) on macOS 14 or
newer.
- **Graphics:** OpenGL 3.0 (practically any PC GPU from the last fifteen
years). A 2011 laptop with Intel HD 3000 graphics holds 60 fps.
- **Input:** keyboard and mouse, or an Xbox, PlayStation, Switch or other
SDL-supported controller; split-screen needs one controller per extra player.
| Region | ROMID | ROM filename (in `data/`) | SHA-1 |
|--------|-------|---------------------------|-------|
| NTSC-U (US) | `ntsc-final` | `ge007.ntsc-final.z64` | `abe01e4aeb033b6c0836819f549c791b26cfde83` |
| PAL (EU) | `pal-final` | `ge007.pal-final.z64` | `167c3c433dec1f1eb921736f7d53fac8cb45ee31` |
| NTSC-J (JP) | `jpn-final` | `ge007.jpn-final.z64` | `2a5dade32f7fad6c73c659d2026994632c1b3174` |
## Quick start
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)).
1. Download the build for your system from
[Releases](https://github.com/jkdansereau/goldeneye-pc-port/releases/latest)
and extract it. On Windows, don't run the exe from inside the zip.
2. Make a `data/` folder next to the executable and put your ROM in it as
`ge007.ntsc-final.z64` (US version; [check your ROM](docs/user-guide.md#whats-a-rom)).
3. Launch the game. The first start takes a few extra seconds while it
prepares the game files from your ROM.
- **Windows:** if SmartScreen appears (the exe isn't code-signed), click
**More info**, then **Run anyway**.
- **Steam Deck:** add the executable as a non-Steam game. It starts in
native 1280x800 fullscreen.
- **macOS:** run `xattr -dr com.apple.quarantine <folder>` once in
Terminal, then start `./ge007.aarch64`
(see the [macOS setup details](docs/user-guide.md#macos-apple-silicon-experimental)).
4. Play. **F10** (controller: **Select**) opens the options menu.
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,
plus the decompilation's own build dependencies (an IRIX MIPS toolchain via
`qemu-irix`, used only for the one-time asset extraction). See
[`docs/building.md`](docs/building.md) for the full walkthrough and the
asset-extraction details.
### Windows (MSYS2)
```sh
# in the MINGW64 shell
pacman -S mingw-w64-x86_64-toolchain mingw-w64-x86_64-SDL2 \
mingw-w64-x86_64-zlib mingw-w64-x86_64-cmake \
mingw-w64-x86_64-python make git
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 # NTSC-U; PAL/JP engine builds compile, but their asset sidecars are not yet producible (issue #85)
```
### Linux
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
git clone https://github.com/jkdansereau/goldeneye-pc-port.git
cd goldeneye-pc-port
# extract assets (docs/building.md), then:
./build-pc.sh ntsc-final
```
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:
`./build-pc/ge007.x86_64`.
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
## Controls
| Action | Keyboard / mouse | Controller |
|---------------------|--------------------------|---------------|
| Move / strafe | `W` `A` `S` `D` / arrows | Left stick (or D-pad) |
| Move / strafe | `W` `A` `S` `D` | Left stick (or D-pad) |
| Move / turn | Arrow keys | - |
| 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 |
| Next weapon | Mouse wheel up | RB |
| Previous weapon | Mouse wheel down | LB |
| Fire | Left mouse | Right trigger |
| Aim mode | Right mouse / `LShift` | Left trigger / LB |
| Use / interact | `E` | A |
| Reload | `R` | X |
| Crouch (toggles) | `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 (D-pad/stick + A/B navigate, Start closes) |
| Options menu | `F10` | Select |
The controller layout follows the modern dual-stick scheme used by the
console re-releases (left stick move, right stick look, triggers fire/aim,
shoulders cycle weapons).
Everything is rebindable in the options menu. Rare's Xbox control styles are
presets, and the **Original** control style turns your pad into an N64
controller. The [user guide](docs/user-guide.md#controls) covers control
styles, rebinding and split-screen.
Mouse sensitivity, Y-inversion and the aim/turn split are tunable in the
`[Input]` section of `ge007.ini`.
## What this port offers
## How it works
- **Real 60 fps, not a speed hack.** The game's logic counts time in 60 Hz
ticks. On the N64 it only drew a frame every 2–3 ticks (20–30 fps); here it
draws every tick, so you get 60 fps with the game, physics and AI running
at the N64's own speed. There is no CPU overclock or frame-skip patch involved,
and the few spots in the game that behaved differently at one tick per frame
are fixed to match the N64. Above 60 (90 to 240, or Auto), extra
frames are blended between ticks for smoothness, at the cost of one tick of
latency.
- **Aiming that doesn't feel floaty.** *Aim style* picks the behavior.
**Centered** pins the crosshair in the middle, and the mouse or the right
stick turns the camera directly, like a modern shooter. **Original** keeps
the N64's crosshair that moves a little before the camera follows. The stick
has separate horizontal and vertical sensitivity, separate deadzones for each
stick, look smoothing and a trigger threshold. The mouse has raw input, an aim
curve, smoothing, and separate turn and aim-mode sensitivities.
- **Dual analog and mouse and keyboard, built in.** No emulator remapping. The
right stick looks, the triggers fire and aim, and every button is rebindable,
for the keyboard, the mouse and each controller. Rare's Xbox control styles are
presets. If you want the N64 exactly, the **Original** control style turns the
pad into a strict virtual N64 controller.
- **The whole game.** All 20 solo missions and the credits, with every
difficulty, 007 mode and the cheats, plus split-screen multiplayer for up to
4 players. Your emulator saves (1964, Project64) load directly.
- **Faithful by construction.** It is the game's own reconstructed source
compiled natively, not an emulator and not a remake, so levels, guards and
weapons behave the way the N64 cartridge does. The few changes to game code
either keep that N64 behavior where the PC would otherwise differ (frame
rate, widescreen, compiler) or are opt-in extras, and the **Original N64**
display preset switches every visual improvement off at once.
- **Modern display options:** native widescreen and ultrawide at any aspect ratio, high
resolutions, MSAA, adjustable FOV and draw distance, and HUD scaling.
- **Windows, Linux, Steam Deck and (experimental) macOS** from one download
page, with an in-game options menu on F10 or the pad's Select button.
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).
Bots, online play, mod support and PAL/JP ROMs are on the
[Roadmap](#roadmap).
The [user guide](docs/user-guide.md#feature-list) has the full feature list.
## FAQ
**Is this an emulator?**
No. The game's code is compiled natively for your PC, the same way the
Perfect Dark PC port works. Graphics go through OpenGL, and the N64 hardware
is replaced by a small port layer.
**Do I need the game?**
Yes. You need a ROM of the US (NTSC-U) version, dumped from a cartridge you
own. PAL and Japanese ROMs are planned.
**Does it play like the original?**
Yes. Levels, guards and weapons run on the game's own logic at its original
speed. The **Original N64** display preset and the **Original** control style
give you the N64 look and controls whenever you want them.
**Does it run on the Steam Deck?**
Yes. The Linux download is the Deck build: add it as a non-Steam game and it
starts in native 1280x800 fullscreen at up to 90 fps, with the options menu
fully usable on the controller.
**Does it support ultrawide and high refresh rates?**
Yes. Native widescreen works at any aspect ratio, including ultrawide, and the
frame rate goes up to 240 fps on fast displays (Auto matches your monitor).
**Is there online play, bots or mod support?**
Not today. LAN play, bots and mod support are on the [roadmap](#roadmap) as
opt-in extras.
**Can I use HD textures?**
Not in this release. A texture-pack loader is planned for 1.0.
**How is this different from the XBLA recompilation projects?**
Those projects statically recompile the unreleased Xbox 360 remaster, a
separate version of the game. This project ports the original N64 game from
its source code. See [About the project](docs/about.md#how-this-differs-from-the-other-goldeneye-pc-projects).
**Can I keep my saves?**
Yes. Your progress is a plain file, `data/ge007.eep`. Saves from 1964 or
Project64 load directly (see [using an emulator save](docs/user-guide.md#using-an-emulator-save)),
and updating keeps your save and settings (see [updating](docs/user-guide.md#updating)).
**Was AI used to build it?**
Yes. The port was developed with AI coding agents directed by one maintainer.
Releases are checked by comparing captured frames from all 21 solo levels on
Windows, crash sweeps on four platforms, and play-testing.
[How it was made](docs/about.md#how-it-was-made) has the full story.
**Something went wrong.**
See [Troubleshooting](docs/user-guide.md#troubleshooting), or
[open an issue](https://github.com/jkdansereau/goldeneye-pc-port/issues/new/choose)
with `ge007.crash.log`, your OS and GPU. Never attach the ROM.
## Status
**v0.6.0 is the current release.** The whole campaign, the end credits and
split-screen multiplayer are playable on Windows, Linux, Steam Deck and macOS.
The project is pre-1.0, so expect new features and the occasional change
between versions. Known issues and workarounds are in the
[known-issues table](docs/ROADMAP.md#known-issues);
[About the project](docs/about.md#status-in-detail) has the testing details and
the [release history](docs/about.md#release-history).
## Roadmap
The goal is a **feature-complete, faithful** port: every mode and region the
N64 cartridge ships, with modern options on top that can always be switched
back to the original look and feel. Next up:
- **PAL and JP ROM support** in the release packages
([#85](https://github.com/jkdansereau/goldeneye-pc-port/issues/85)).
- **A notarized macOS build**, Intel Macs and ARM Linux.
- **A texture-pack loader** for HD texture packs.
- **The 1.0 sign-off:** a full campaign playthrough and a split-screen session
on every platform that ships.
After 1.0, opt-in extras that are always off by default: LAN play, bots,
co-op and enhanced visuals. The full tracker is
[`docs/ROADMAP.md`](docs/ROADMAP.md). If something matters to you, open an
issue; it helps prioritize.
## Building from source
You need CMake 3.16+, a C/C++ toolchain, SDL2, zlib, OpenGL and Python 3, plus
a one-time asset extraction from your ROM. On Windows, build in the MSYS2
MINGW64 shell; on macOS, use Homebrew's GNU `gcc`.
```sh
git clone https://github.com/jkdansereau/goldeneye-pc-port.git
cd goldeneye-pc-port
# extract assets from your ROM first (see docs/building.md), then:
./build-pc.sh ntsc-final
```
CMakeLists.txt PC build (parallel to the decomp's Makefile, which is untouched)
build-pc.sh configure + build helper
src/ include/ the decompilation (game + libultra) - compiled unmodified
port/
fast3d/ software RSP -> OpenGL
src/ port layer (main, OS shims, video, audio, input, fs, ...)
include/ port-facing headers
tools/ Makefile the N64 build + asset extraction (from the decomp; do not modify)
tools_pc/ PC-port helper + analysis scripts
docs/ see below
```
[`docs/building.md`](docs/building.md) has the full walkthrough for Windows,
Linux and macOS.
## Documentation
Key docs are also published as a site:
The docs are also published as a site:
<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/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, 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. |
| [User guide](docs/user-guide.md) | Controls, settings and files, ROM check, macOS setup, updating, emulator saves, troubleshooting. |
| [About the project](docs/about.md) | Release history, testing status, how it was made, how it works. |
| [Roadmap](docs/ROADMAP.md) | Open work and the known-issues table. |
| [Building](docs/building.md) | Full build and asset-extraction guide. |
| [Internals](docs/internals.md) | Architecture and the software RSP. |
| [Porting notes](docs/porting-notes.md) | The recurring N64-to-PC bug classes, with fixes. |
| [Security](docs/security.md) and [Fidelity](docs/fidelity.md) | What a release installs, and how closely it tracks the original game's logic. |
| [`docs/dev/`](docs/dev/) | The engineering record: the finding log and development process. |
## Contributing and forking
Issues and pull requests are welcome, and so are forks: the port layer, build
system and `tools_pc/` are MIT-licensed and exist to be extended. Read
[`CONTRIBUTING.md`](CONTRIBUTING.md) first: it has the ground rules that keep
the port faithful, what to check before a pull request, and what a fork is
asked to keep.
## Credits
@@ -431,46 +302,69 @@ This port is a thin layer on a large amount of other people's work.
Kids decompilation write-up), the agent-workflow practices in
[`docs/dev-process.md`](docs/dev-process.md) are adapted from it.
**Contributors**
- **dolent** (#120, #121, #122, #123), **italoarruda** (#106, #107, #108, #109),
**danturn** (#95 macOS, #96), **Julio C. Rocha** (#88 macOS Intel groundwork),
**f1zz1ec0ke** (#137 frame interpolation), **JosephAHK** (#133), and
**MST246** (the #125 investigation), plus everyone who filed issues.
**Consultation**
- **f1zz1ec0ke** ([GitHub](https://github.com/f1zz1ec0ke)): LLM consultation on model
tuning, agent harnesses, and agentic strategy throughout the port's development.
tuning, agent harnesses, and agentic strategy throughout the port's development
(and the author of the #137 frame-interpolation design noted above).
**Tools and models used to develop the port**
- Qwen 3.8 (Alibaba Qwen team), run locally from the
[`unsloth/Qwen3.8-27B-GGUF`](https://huggingface.co/unsloth/Qwen3.8-27B-GGUF)
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).
The models and tools used to develop the port are listed in
[About the project](docs/about.md#tools-and-models-used-to-develop-the-port).
## 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:
GitHub. It is **not affiliated with, endorsed by, or sponsored by** Nintendo,
Rare, Microsoft, Valve, Apple, MGM, Danjaq, EON Productions, or any rights holder in
GoldenEye or James Bond. "GoldenEye 007", "007", "James Bond" and related
marks belong to their respective owners; they are used here only to say which
game this port runs. No official logos, box art, or marketing assets are used.
- **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
[GoldenEye 007 decompilation](https://github.com/n64decomp/007) and inherits
its contents unmodified (see [`NOTICE`](NOTICE) for what that includes).
- 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).
- 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.
### What the download contains
This project is **not affiliated with, endorsed by, or sponsored by** Nintendo,
Rare, Microsoft, MGM, Danjaq, EON Productions, or any rights holder in
GoldenEye or James Bond. If you are a rights holder with a concern, open an
issue and it will be addressed.
- **The engine:** the `port/` layer plus the compiled decompilation.
- **Runtime libraries:** SDL2 (and SDL3 on macOS) and zlib under the zlib
license, the MinGW runtime on Windows, and the GCC C++ runtime on macOS
(GPL-3.0 with the GCC Runtime Library Exception); their licenses travel in
the download.
- **The one-time asset tool**, which reads your ROM on your machine.
It contains **no ROM and no game assets of any kind**. Any build, yours or a
release, is useless without a ROM you supply.
### What happens on your computer
Textures, audio, models, level data and in-game text are extracted from a ROM
*you already own*, on *your* machine, the first time you start the game. The
port has no network access unless you turn on the opt-in *Check for updates* option (`Game.CheckUpdates`, off by default). When on, it makes one HTTPS request per launch to GitHub's releases API to look for a newer release (GitHub sees the request and your IP, as with any web request), never downloads or installs anything, and never uploads anything.
### This repository
The repository is a fork of the public
[GoldenEye 007 decompilation](https://github.com/n64decomp/007) and inherits
its contents (see [`NOTICE`](NOTICE) for what that includes). The few
port-specific edits inside the decompiled code are marked `#ifdef PORT`.
The project mark / favicon is original *generated* art -- a generic version
of the game's default aim cross (a circle with N/S/E/W lines crossing its
edge), no game screenshots or logos in it.
### Your part
- Only use a ROM that you dumped from a cartridge you own.
- Never ask for, link to or share ROMs or extracted game files in issues,
discussions or pull requests.
If you are a rights holder with a concern, open an issue and it will be
addressed.
## License
@@ -478,3 +372,7 @@ 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.
---
*v0.6.0 · 2026-10-13 · <a href="https://github.com/jkdansereau/goldeneye-pc-port/releases">Releases</a>*
+5 -3
View File
@@ -197,7 +197,7 @@
// These are compile time extern constants (so not "really" const), but used the same
// as the defines above, which is to index into ptr_animation_table.
#if defined(__x86_64__)
#if defined(__x86_64__) || (defined(__aarch64__) && defined(__APPLE__))
/* D34 (docs/dev/findings.md): on N64 this segment links at VMA 0
* (ge007.ld), so &ANIM_DATA_x is the record's offset within the
* animation_data segment and game code re-bases it with
@@ -207,7 +207,9 @@
* image base, set by romdataInit); (s32)&ANIM_DATA_x then equals
* PTR_ANIM_x exactly. Address-only: the record data itself is romCopy'd
* from ROM into ptr_animation_table, and assets/animationtable_data.c is
* not compiled into the PC image. Never dereference these lvalues. */
* not compiled into the PC image. Never dereference these lvalues.
* D608: the arm64 macOS image loads at the 16 TiB window base
* (PORT_ADDR_BASE) — same re-basing class, mechanical guard widening. */
extern unsigned char *g_pc_animdata_base;
#define ANIM_DATA_empty (g_pc_animdata_base[0x0u])
#define ANIM_DATA_idle (g_pc_animdata_base[0x1cu])
@@ -572,7 +574,7 @@ extern s32 ANIM_DATA_credits_natalya_kissing;
extern s32 ANIM_DATA_helicopter_cradle;
extern s32 ANIM_DATA_plane_runway;
extern s32 ANIM_DATA_helicopter_takeoff;
#endif /* __x86_64__ (D34) */
#endif /* __x86_64__ / arm64 macOS (D34, D608) */
Regular → Executable
+221 -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,226 @@ 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.
# ---------------------------------------------------------------------------
# Locate the MSYS2 MinGW64 toolchain dir (cmake.exe + gcc.exe, inside an
# MSYS2 install). Whatever `cmake`/`gcc` happen to be first on PATH is not
# trustworthy: Git Bash's /mingw64 has no toolchain, and a pip-installed
# cmake in a Python Scripts dir can shadow the MSYS2 one. GE_MSYS2_ROOT
# (e.g. C:\msys64) overrides the search.
_ge_find_mingw_bin() {
local c root
root="${GE_MSYS2_ROOT:-}"
[ -n "${root}" ] && root="$(cygpath -u "${root}" 2>/dev/null || echo "${root}")"
for c in \
${root:+"${root}/mingw64/bin"} \
"$(dirname "$(command -v gcc 2>/dev/null || echo /nonexistent/gcc)")" \
"$(dirname "$(command -v cmake 2>/dev/null || echo /nonexistent/cmake)")" \
"/mingw64/bin" \
"/c/msys64/mingw64/bin"; do
if [ -f "${c}/cmake.exe" ] && [ -f "${c}/gcc.exe" ] && [ -f "${c}/../../usr/bin/msys-2.0.dll" ]; then
(cd "${c}" && pwd)
return 0
fi
done
return 1
}
if [ -n "${MSYSTEM:-}" ] && [ -z "${GE_PC_BUILD_VIA_NATIVE:-}" ]; then
_mingw_bin="$(_ge_find_mingw_bin)" || {
echo "ERROR: could not find the MSYS2 MinGW64 toolchain (mingw64/bin with cmake.exe + gcc.exe)." >&2
echo "Install it (see docs/building.md) or point GE_MSYS2_ROOT at the MSYS2 root, e.g. GE_MSYS2_ROOT='C:\msys64'." >&2
exit 1
}
# Failure mode 3 (AGENTS.md): the toolchain's DLL dir must be on PATH,
# and ahead of any other cmake/gcc. Only mingw64/bin — NOT usr/bin:
# from a Git Bash shell that would swap in MSYS2's cygpath/mktemp,
# whose /tmp is a different Windows dir than this bash's /tmp.
export PATH="${_mingw_bin}:${PATH}"
# Every path that crosses to a native process lives in the build dir and
# is converted with bash's own `pwd -W` — never /tmp + cygpath, which
# silently disagree when two msys runtimes are on PATH (Git Bash + MSYS2:
# the .ps1 was written to one /tmp and powershell was pointed at the
# other; the TMP probe likewise always read "unusable").
mkdir -p "${BUILD_DIR}"
_bd_win="$(cd "${BUILD_DIR}" && pwd -W)"
_bd_win="${_bd_win//\//\\}"
_probe="${BUILD_DIR}/.ge007-tmpprobe"
rm -rf "${_probe}"; mkdir -p "${_probe}"
# The probe is a .cmd FILE: an inline `cmd //c "... \"path\" ..."` never
# worked — the msys argv conversion re-escapes embedded quotes as \"
# (which cmd does not understand), so neither ok.txt nor bad.txt was
# ever written and every build took the re-exec. It also tests that TMP
# is WRITABLE, not merely that it exists (the failure mode is TMP =
# C:\Windows\, which exists). %~dp0 = the probe's own dir: no paths
# cross the boundary. CRLF: cmd's goto/label scan is unreliable on LF.
sed 's/$/\r/' > "${_probe}/probe.cmd" <<'CMD'
@echo off
if "%TMP%"=="" goto bad
(echo x> "%TMP%\ge007-tmpprobe.tmp") 2>nul || goto bad
del "%TMP%\ge007-tmpprobe.tmp" 2>nul
echo ok> "%~dp0ok.txt"
exit /b 0
:bad
echo bad> "%~dp0bad.txt"
CMD
/c/Windows/system32/cmd.exe //c "${_bd_win}\\.ge007-tmpprobe\\probe.cmd" >/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)"
# Windows-side toolchain paths: from the validated MSYS2 mingw64/bin
# found above (NOT blindly from `command -v cmake`, which in a Git
# Bash / agent shell resolves to e.g. a pip-installed cmake and fed
# the .ps1 a bin dir with no MSYS2 toolchain — the "re-exec fails
# in-script, works by hand with explicit paths" bug). usr/bin first
# on the .ps1's PATH: mingw tools may need msys-2.0.dll from there.
_msys_root="$(cd "${_mingw_bin}/../.." && pwd -W)" # msys install root (bin -> mingw64 -> root)
_bin_w="$(cd "${_mingw_bin}" && pwd -W)"
_bin_w="${_bin_w//\//\\}"
_usrbin_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="${BUILD_DIR}/ge007-native-reexec.ps1"
_ps1_win="${_bd_win}\\ge007-native-reexec.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.
# Resolve BuildDir against the repo root, not the inherited cwd.
Set-Location $Here
$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"
# A PowerShell launched from an MSYS2 bash (the agent-shell case: MSYS2's
# usr/bin first on PATH, so `env bash` is MSYS2's bash) inherits a stripped
# ~13-var env with PATHEXT=".CPL" and no ComSpec. With that PATHEXT,
# `& cmake.exe` silently does NOTHING: no process, no output, $LASTEXITCODE
# stays $null (the rc=2 "did not execute" regression). Restore them.
$env:PATHEXT = '.COM;.EXE;.BAT;.CMD'
if (-not $env:ComSpec) { $env:ComSpec = 'C:\Windows\System32\cmd.exe' }
# D515: same class of bug as PATHEXT/ComSpec above — a stripped re-exec env
# can also drop PROCESSOR_ARCHITECTURE, which CMake reads to fill
# CMAKE_HOST_SYSTEM_PROCESSOR; empty value -> empty TARGET_ARCH -> "ge007..".
# Restore it from the machine environment (fallback 'AMD64'), and restore
# SystemRoot/windir the same way (standard Windows vars native tools expect).
if (-not $env:PROCESSOR_ARCHITECTURE) {
$env:PROCESSOR_ARCHITECTURE = [Environment]::GetEnvironmentVariable('PROCESSOR_ARCHITECTURE', 'Machine')
if (-not $env:PROCESSOR_ARCHITECTURE) { $env:PROCESSOR_ARCHITECTURE = 'AMD64' }
}
if (-not $env:SystemRoot) {
$env:SystemRoot = [Environment]::GetEnvironmentVariable('SystemRoot', 'Machine')
if (-not $env:SystemRoot) { $env:SystemRoot = 'C:\Windows' }
}
if (-not $env:windir) {
$env:windir = [Environment]::GetEnvironmentVariable('windir', 'Machine')
if (-not $env:windir) { $env:windir = 'C:\Windows' }
}
# 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
# D515 diag: the stripped re-exec env can also drop PROCESSOR_ARCHITECTURE,
# which CMake reads to fill CMAKE_HOST_SYSTEM_PROCESSOR (empty value ->
# empty TARGET_ARCH -> "ge007.."). One line, cheap, useful next time.
"env PROCESSOR_ARCHITECTURE=[$env:PROCESSOR_ARCHITECTURE] PROCESSOR_ARCHITEW6432=[$env:PROCESSOR_ARCHITEW6432] SystemRoot=[$env:SystemRoot] windir=[$env:windir]" | Out-File $diag -Append -Encoding ascii
$exe = "$Bin\cmake.exe"
"exe=[$exe] exists=[$(Test-Path -LiteralPath $exe)]" | Out-File $diag -Append -Encoding ascii
# A missing exe makes `& $exe` throw (caught below) and leaves LASTEXITCODE
# null, which used to surface only as an opaque "did not execute".
if (-not (Test-Path -LiteralPath $exe)) { Write-Host "cmake.exe not found at [$exe] (diag: $diag)"; exit 5 }
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
rm -rf "${_probe}"
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}"
+12
View File
@@ -6,6 +6,14 @@ function(target_architecture OUT_VAR)
# Derive from the cross-compiler target, e.g. x86_64-w64-mingw32-gcc
string(REGEX MATCH "^(i[3-6]86|x86_64|aarch64|arm|powerpc|wasm32)" _arch
"${CMAKE_C_COMPILER_TARGET}")
elseif(CMAKE_SYSTEM_PROCESSOR STREQUAL "")
# D515: a stripped native env (e.g. the build-pc.sh PowerShell re-exec
# before it restored PROCESSOR_ARCHITECTURE) leaves
# CMAKE_HOST_SYSTEM_PROCESSOR / CMAKE_SYSTEM_PROCESSOR empty. Ask the
# compiler instead of silently producing an untagged binary ("ge007..").
execute_process(COMMAND "${CMAKE_C_COMPILER}" -dumpmachine
OUTPUT_VARIABLE _dm OUTPUT_STRIP_TRAILING_WHITESPACE)
string(REGEX MATCH "^(i[3-6]86|x86_64|aarch64|arm|powerpc|wasm32)" _arch "${_dm}")
else()
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|amd64|AMD64)$")
set(_arch "x86_64")
@@ -24,5 +32,9 @@ function(target_architecture OUT_VAR)
set(_arch "i686")
endif()
if(_arch STREQUAL "")
message(FATAL_ERROR "TargetArch: could not determine the target architecture (CMAKE_SYSTEM_PROCESSOR empty and compiler -dumpmachine did not match); refusing to build an untagged binary -- the 'ge007..' bug, see D515.")
endif()
set(${OUT_VAR} "${_arch}" PARENT_SCOPE)
endfunction()
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

+4
View File
@@ -0,0 +1,4 @@
/* Windows executable icon (Explorer / taskbar).
* Original crosshair mark generated by tools_pc/make_icon.py (cmake/icon/
* ge007.ico) -- same art as the site favicon (docs/img/icon/). */
IDI_ICON1 ICON "ge007.ico"
+20
View File
@@ -0,0 +1,20 @@
---
title: 404
description: Page not found.
layout: default
permalink: /404.html
---
## 404
This page doesn't exist. The site's real pages are linked from the
nav: [home]({{ '/' | relative_url }}) ·
[documentation]({{ '/documentation' | relative_url }}) ·
[building]({{ '/building' | relative_url }}).
*Note for anyone following a moved link: the old "under the hood" page now
lives at [documentation]({{ '/documentation' | relative_url }}).*
<!-- Links use the | relative_url filter on purpose: a 404 page is served for
ANY unknown path, so plain relative links would resolve against the
requested (dead) URL instead of the site root. -->
+357
View File
@@ -0,0 +1,357 @@
# Roadmap and open items
**This is the single tracker for high-level work on the port.** If an item
is not here, it is either done or not planned. The README's *Known issues*
and *Roadmap* sections are short summaries of this file. Root-cause detail
for each `Dxx` lives in [`dev/findings.md`](dev/findings.md) (find it via
[`dev/findings-index.csv`](dev/findings-index.csv); that index is generated,
first-line-only, and can lag the entry tail, so read the entry).
Last full triage: 2026-09-29. Sources: the findings log, all GitHub issues,
PRs and releases, every local backlog/plan/roadmap note, `HANDOFF.md`, all
local and remote branches, and known community forks.
## How to use this file
- **One row per item.** Link the `Dxx` and/or GitHub `#issue`. Keep detail
in the finding, not here.
- **Status vocabulary:** `open` · `partial` · `verify` (fixed, a live check
is owed) · `parked` (work exists on a branch or patch, not merged) ·
`decision` (blocked on a maintainer call).
- **When an item closes, delete its row.** The history is kept in the
finding and in git. Do not strike rows through. Struck rows are what made
the old backlogs unreadable.
- **New work goes here first.** Do not start a new backlog, roadmap or
plan-of-record file. A design doc for one item is fine; link it from the
row and delete it when the item lands.
- Release scoping (which items go into which version) happens in the
release notes, not here.
- The **release gate** (what must be green before pushing / publishing a
release) is kept as a local working note — `docs/dev/notes/RELEASE-BLOCKERS-v050.md`
(gitignored, like the other P-pass reports). Keep it current; don't fork a
second copy of it into the tracked tree.
- **Known issues** (the player-facing subset) live in the table below. The
README links to it and does not duplicate it.
## Goal and definition of done
**Feature complete and faithful to the N64 original.** Every mode and
region the cartridge ships should work and behave 1:1 with the N64,
checked against an accurate emulator (1964/GEPD) or real hardware. On top
of that sits a **modern-remaster layer** (the bar set by Nightdive's
Quake/Turok remasters, the XBLA remaster and GE+/Dab's mod). It must never
change game logic by default, and it must always be possible to get back
to the original look and feel (see the *Original N64 preset*, D440).
The port is **feature complete** when all of these hold:
1. Every solo mission (all 20 plus the credits) is completable at every
difficulty, on Windows and Linux.
2. No crash on any level under normal play, on either platform.
3. Audio is complete and faithful: music, positional SFX, menu and weapon
sounds.
4. Cutscenes play correctly (camera, actor positions, timing).
5. AI locomotion and pacing match the N64.
6. Input feels like GoldenEye on a controller, and like a modern FPS on
mouse and keyboard.
7. Rendering has no visible defect versus 1964/GEPD at default settings,
apart from the port's deliberate modern defaults.
8. **Multiplayer (2–4 player split-screen) works.** It is the largest item
left (§1).
9. **PAL and JP ROMs work** (§1, pending a decision).
**Exit gate:** a full two-platform campaign playthrough at more than one
difficulty, plus a split-screen session, on a build with the §3
verification debt cleared.
Where things stand (2026-10-01): 1–7 hold for NTSC-U, with the small
accuracy gaps in §2. 8 is merged on `release/v0.4.1` (3P/4P with real pads
owed). 9 is open (Stage B). **Parity rule (2026-09-30):** every released
platform carries the same gate: a full 21-level sweep plus a campaign pass.
## Order of work (decided 2026-09-30)
Work continues on `release/v0.4.1`, **shipped as v0.5.0 (named 2026-10-03,
cut staged 2026-10-05, maintainer)** — the docs, golden set (both platforms,
63 frames each) and tooling are complete; the P11 squash + tag + push is owed
maintainer consent (push checklist: `dev/notes/PUSH-TASKS-v041.md`). There are
two stages:
**Stage A: now → the pre-1.0 build**
1. Clear the §3 verification debt.
2. Split-screen (§1): keep working and testing until it is good.
3. `All unlocked` rework (§5a).
4. D357 settings values (§5a), plus the restart button.
5. Outside PRs (§5a), in the proposed order once the maintainer approves it.
6. Pillarbox with the exact original aspect for the Original N64 preset (§5a).
7. Platform parity: re-base the golden baselines and extend them to both
platforms (§7), then run the gate on each platform.
8. Investigate and triage every §2 fidelity gap to fix or accept. Do not
leave any of them as "open, unexamined".
**Stage B: pre-1.0 build → 1.0**
1. PAL/JP (§1). It goes last on purpose, so that region-specific
regressions are easy to isolate and unwind.
2. macOS/ARM: merge the `macos-arm64` branch (§4), with the D441 census
(store-to-u32 tagging) and a high-arena test. (2026-10-08 decision: an
*experimental* arm64 macOS build ships in v0.6.0; the merge-to-main is
still the post-1.0 step here. Scope is Apple Silicon — the x86_64 path is
unverified and out of release scope.)
3. The exit gate on every shipped platform.
4. Remove the temporary scaffolding (§7).
**Next (decided 2026-10-03, after the Birdman64 comparison; local note `BIRDMAN64-LESSONS.md`):**
1. D441 census: **done 2026-10-03** (§2).
2. Fix the listed sites as mechanical, build-checked batches on a cheap
worker; the lead reviews every diff (§7). **Batches 1-5 done
2026-10-03.** Next: the tier-1 sweep over every level with a seeded
frame compare, then further batches from it.
3. Scripted-playthrough harness: moved to post-1.0 (maintainer
2026-10-04; revisit with newer models) (§7).
## Known issues
What a player will notice in the current release. **This table is the
single list:** the README links here instead of keeping its own copy. When
an issue is fixed and released, delete its row. When a new player-visible
issue is confirmed, add a row here (and its tracking row in the sections
below).
| Issue | Impact | Workaround | Tracking |
|---|---|---|---|
| PAL and JP ROMs aren't supported in release packages | NTSC-U only | Use an NTSC-U ROM. Both regions convert, build and boot from source (D258, D487, D488; see `docs/building.md`); packaging (per-region exe, CI matrix, converter region check) is still open | D258, #85, §1 |
| Changing Aspect ratio in a level (mostly Fill window to Original or a forced ratio) can briefly glitch the gun / hand model, rarely | Cosmetic, one-off | Change the ratio from the front-end PC Options, or accept it | D509 (frame hold fixed the common case; the rare remainder is logged, not investigated) |
| No Intel macOS or ARM Linux builds (the macOS download is Apple Silicon only, experimental and not notarized) | Platform | — | #88, #95, #101, #154, §4 |
| Steam Deck at 90 fps: centre firefights in Bunker 1/2 drop several fps; Jungle feels slightly less smooth than other levels | Brief FPS drops | Frame cap 60 for those levels | D583, D585 (present thread, v0.7.0), D614 |
| Cradle's turret explosions may draw wrongly | Cosmetic | None yet | Deck per-level pass (2026-10-10), not investigated |
| Windows: the picture can look hazy for a moment after alt-tabbing back; fast repeated alt-tabbing recovers slowly | Cosmetic, temporary | It settles on its own | D578, alt-tab row in §4 (v0.7.0) |
| Saves from v0.4.0 and earlier can hold fake unlocks from `All unlocked` | Save data | Not repaired automatically. Since v0.5.0 the option never writes the save; keep a backup of `data/ge007.eep` from before you used it | D442, D387 |
| The first frame of a level takes a little longer while its textures upload | Brief FPS-counter dip | None needed | D475, D480 |
| Far objects almost fully in fog are now hidden as on the N64, except in widescreen, where the wider view can still show a faint distant building edge (e.g. Surface's dish from the start area). 4:3 matches the N64 | Cosmetic | None; higher Draw/LOD distance shows more | D503, §5c |
| Windows: an occasional one-off render-thread stall can trip the "no frame rendered for N ms" watchdog (rare; observed once, during heavy `GE_`-debug I/O) | Brief one-off stall / recover | Normal play (no `GE_` dump probes) is unaffected; a flake, not a deterministic hang | D593 (root-cause owed: probe I/O cost vs a genuine render-thread hang) |
---
## 1. Feature completeness vs the N64 cartridge
| Item | Status | Refs | Notes |
|---|---|---|---|
| **2–4 player split-screen multiplayer** | verify | #99, D416–D423, D496 | Merged 2026-10-01. D417 Rule-2 approved. **Maintainer playtest 2026-10-03:** 2P and 3P with real Xbox and PS5 pads, handicaps, teams and game modes all good (D496 fixed an uninitialised weapon-text buffer). **Still owed:** 4P with real pads, pads-only mode, MP audio, D422 centred aim live on P2+ (`AimMode = 1`), D428 sway on P2+. |
| **PAL and JP ROM support** | partial | D258, D487, #85 | **2026-10-02: both regions convert, build and boot** (D258 byte-verified against real PAL and JP ROMs); both play Dam with no crash; NTSC-U unchanged. Build: `BUILD_DIR=build-pal ./build-pc.sh pal-final` / `build-jpn ... jpn-final` with `data/ge007.<region>.z64`. D487, D488 fixed (PAL frame height, TV type). **Open:** full runtime passes per region (levels, menus, audio, saves; PAL 50 Hz feel incl. the D427/D451/D486 gates; EU-only text); packaging (per-region exe names, CI matrix, converter region gate, #6). ROMs: `ge-port-reference/1964_GEPD_Edition/ROMs`. #85 needs reopening or a fresh tracker when this ships. |
| Drop-in ROM converter, remaining parts | decision | #6, [`dev/release-dropin-rom-plan.md`](dev/release-dropin-rom-plan.md) | NTSC-U drop-in shipped. Left: per-region output names and re-exec guard (with PAL/JP), native C emitters to replace the PyInstaller converter. |
| Cut content (TCRF weapons/items) | decision | [`dev/CUT-CONTENT-BACKLOG.md`](dev/CUT-CONTENT-BACKLOG.md) | Not part of the shipped game. Tier 0 already works via the debug/cheat menus. Tier 1 (config-toggle grants) is small and port-only. Tier 2/3 (placing props, re-enabling unused MP maps) need game data changes and are out of scope for this repo. **2026-10-09 (maintainer):** the Tier-2/3 items whose data is already in the ROM — the 8 cut MP maps and the extended MP roster (backlog §D/§F) — are in 1.0 scope as a mod-fork branch (the `front.c` table edits touch `src/game`, non-negotiable #2), shipped through the mod scaffolding (§5a). Prop placement stays out. |
## 2. Accuracy (behaviour differs from the N64)
| Item | Status | Refs | Notes |
|---|---|---|---|
| Third-person Bond offset residual | partial; intro puppet explained (2026-10-02) | D173, D292 | Mostly fixed. On the level-start puppet the variance is tick/PRNG-dependent idle timing (D13 class, faithful, no sign-off requested); one 1-frame first-tick Y transient is untraced. If the residual is seen elsewhere (cutscene body model), the maintainer should note where. |
| AI awareness follows two non-default render settings | fixed 2026-10-06 (D565: Fog distance floor 100; AI fog cull rescaled to the game's FOV) | D565, D466, D468, D540, D222 | AI review of v0.4.0..v0.5.0 (interpreter, movement/timing, visibility/combat): no regression at shipped settings (Draw 2.0x, Fog 1.0x, FOV 60, native widescreen). Two leaks of player render settings into AI awareness remain: **(1) `Video.FogDistance` below 100** shrinks the far clip (`bgfog.c fogLoadCurrentEnvironment`, clip = FarFog x min(Fog, Draw)). AI on-screen checks require the render `PROPFLAG_ONSCREEN` (`drawdistgameplay.c portPropGameplayOnScreen`), so guards beyond the shortened clip stop counting as on screen; at Draw 1.0x `portD466Active()` is off and sight range (`fogGetScaledFarFogIntensitySquared`) shrinks too. Effect: guards notice Bond / on-screen triggers fire later than the N64. Fix (shipped): `Video.FogDistance` is floored at 100 (`video.c` registration, so 50/75 in an ini clamp to 100 on load; F10 slider `uiMin` 100); 100-800 only extends, which D466 already keeps off gameplay. **(2) FOV other than 60 (`Video.FovScale` != 100, or native widescreen off at a wide aspect)**: the gameplay fog-cull `propobj.c portSub7F054C58Gameplay` uses `getPlayer_c_lodscalez()`, which grows with the rendered FOV (D222), so at a wider FOV far fogged positions drop out of the AI on-screen verdict sooner (narrower: later). Fix: divide out the port FOV factor (as `portFovYScaleFactor`), keeping the game's own zoom FOV changes. Minor at defaults: the D466 verdict reuses the extended pass's room screen bbox (`posIsOnScreen`); at Fog 1.0x the clip is the authored far, so the two passes match. D491 (room-list portal seeds) and D431 (light-fixture hit scoring) were checked: both restore N64 data reads and only move AI toward the N64. |
| Dam ending cutscene | done (2026-10-06) | D526, D552 | Railing/grate transparency faithful (D526); first-shot swivel restored (D552, maintainer-verified). |
| Title-screen attract demo runs too fast; F10 box rescales while it plays | backlogged (after v0.5.0, maintainer 2026-10-04) | D87 (ramrom file struct); timing: D13, D248, D427, D451, D486 | **Reported 2026-10-04 (live pass):** attract plays visibly too fast; not investigated. Lead: it is a ramrom demo replay whose recordings carry a per-frame tick count, which the port's 60 fps / 1-tick pacing may override. Rule out PC causes, then compare with 1964. Same pass: the F10 box briefly rescaled during the demo (not reproduced; candidate fix = overlay reads fast3d's native viewport, parked locally). |
| Sniper rifle: zoom level and speed vs 1964 | verify | #136, D484, D485, D494 | Crouch: **D494** (2026-10-03) makes the PC crouch key crouch/stand freely with the sniper, as GEPD does (superseding D483's freeze); maintainer-verified. Zoom level (D484, 20 -> 7 degrees) and speed (D485, tick-scaled, Rule-2) fixed 2026-10-02. **Owed:** 1964 A/B of zoom level and speed; close #136 after it. |
| Geometry disappears when hugging a wall (dark close-range smear) | verify (fixed 2026-10-09; golden sweep owed) | #150, D579, D543 | **Root cause (2026-10-09):** a v0.5.0 regression from D543, the CPU near-plane clip. It cut at the near plane (`z = -w`), but GL runs with `GL_DEPTH_CLAMP`, so geometry between the eye and the near plane is meant to be drawn (as on the N64; 1964 shows the wall). Hugging a wall put it in that band and the clip deleted it. Confirmed by one `GE_NEARCLIP=0` run (no see-through). **Fix:** the clip now cuts at the camera plane (`w = 0.01`), and clip-created vertices take fog at `max(z/w, -1)` so the D553 near-plane haze is unchanged. Maintainer-verified at the Dam gate in 16:9 with the floor haze intact. The earlier cull/ucode/guard-band investigation was a dead end (see D579 and dev-process §8 "Regression triage first"). **Owed:** one 21-level golden sweep. Diagnostics stripped (last one, the `src/game/bg.c` probe, removed 2026-10-10). The `any_w_neg` no-cull caused the Cradle catwalk flicker; mixed-w triangles now get the homogeneous facing test (D584), Dam gate wall hug re-checked clean. |
| Decal shadows flicker at high FPS (Dam, Cradle catwalks) | closed 2026-10-09 | D578 | Maintainer compared Dam against 1964 (GEPD): the shadow look there is essentially the same, so it is faithful, not a port bug. The flicker seen during the D578 switch testing is not considered an open issue. Reopen if the Cradle catwalk flicker shows up on the shipped default. |
| 120 fps + VSync on G-Sync/VRR: OLED brightness flicker (present cadence); Deck 90 smoothness | PC fixed 2026-10-09; Deck 90 good on most levels (per-level pass 2026-10-10, `feat/d583-deck90`, local); open items in the per-level row below | D583, D578 | PC: continuous present grid snapped to tick/N, 8-slot present ring, draw-time-based ready, present-before-draw (120.0 presents/s, 99%+ gaps 7-9.5 ms, OLED by eye OK). Deck: false turn-detection redraws removed, exact frames drawn once, turn softening applied at frame start, room scissor = union of replayed and game box, projections paired by nearest (draw peak 8-10 -> 3-5 ms, 88-90 presents/s; maintainer by eye OK); no post-swap glFinish under gamescope (present 3.3 -> 0.3 ms); set-change limit 60%. Not yet a smooth 90 in crowded fights (maintainer: "not perfect for the release yet"): set-change holds and turn-softening steps remain; cut detection replaced the matrix set-change check (crowd fights "good now"). Next: re-tune softening (Dam tunnel by-eye), per-level Deck pass, Dam truck brief flicker (Deck, 2026-10-09), Aztec M16 viewmodel flicker, rare dead-body flicker before fade-out, level-start load stutter, gamescope flip-time stats. PC 120 re-checked 2026-10-09 (120.0/s, 1% exact). Owed: VSync-without-G-Sync check, per-level Deck pass + 90 fps performance validation across levels (only Dam exercised). **2026-10-10 Deck Dam run (ac50865d):** 9.5-12 ms gaps 98-99.7% per 10 s window, 0-5 dropped, draw peak 2.1-6.3 ms, late softened (redrawn) 8-56 per 10 s, maintainer by eye: good, no tunnel holes. **Turn-softening re-tune closed as not needed** (the redraw count no longer costs presents). **2026-10-10:** per-level Deck pass done (next row); the older "Next" list above is superseded by it, except the still-unexplained Dam truck / Aztec M16 / dead-body flickers (captures owed), level-start stutter, gamescope flip-time stats and the VSync-without-G-Sync PC check. |
| Deck per-level pass findings (2026-10-10, maintainer, `ac50865d`) | open | D583, D578 | Overall very strong. (1) **Bunker 1/2 centre firefights drop 5-10 fps:** the log shows a ~70-frame run of 2-pass frames drawing 11.2-12.5 ms (~5.6 ms per pass, over the 11.1 ms 90 Hz budget); game side is idle (work ~0.14 ms), so this is draw cost, a real-optimization job (profile the pass), not pacing. **2026-10-10 measured (SLOW line counters):** ~550-800 batches per pass at ~6 us each (~4-5 ms per pass); texture lookups ~1.2 per batch, few misses; a softened restart made fight frames 3 passes. Refuted: same-texture rebinds (reverted). Landed: late-soften budget gate (no restart when n+1 passes exceed 3/4 tick; `nobudget` = old): 3-pass frames mostly gone, but drops remain (2-pass frames to ~13 ms; maintainer: still up to ~10 fps drops). **2026-10-10 late (D583 addendum "present scheduling"):** NOT draw cost: not CPU-bound (perf), not the GPU clock (1600 MHz manual: same), not the VBO upload (ring: within noise). 72% of the long present gaps are a due present stuck behind the single worker's next draw. **2026-10-10 review: present thread (D585) PARKED for v0.7.0** on local `wip/d585-present-thread` (opt-in, offscreen frames black): a second shared GL context + thread on every platform, macOS included, for one Deck scenario was too much risk for a point release. v0.6.0 ships with the Bunker fight drops as a known Deck issue. **2026-10-10 (D614):** texture-cache thrash measured in the fight (CI8 keys hashed leftover palette entries); fixing it cut re-imports 60-70% but did not change pacing: reverted, not the cause. (2) **Jungle/Egyptian read 90 fps but do not feel fully smooth:** motion histogram shows 3-10 hold+jump pairs per 10 s (~0.5-1 visible hitch/s) even with 0 dropped; next step is to log which decision (room-change exact, late exact, pre-soften) precedes each hold. **2026-10-10 hold-cause log (6d7aa851), Jungle:** every hold is an exact fallback (1-7 per 10 s), almost all REACH (a room the previous frame drew and this one dropped); `noreach` removed them with no visible holes in Jungle but did NOT change the feel (maintainer: "not significantly"; Egyptian and other levels feel better). So holds are not the Jungle issue and the pacing stats look like the good levels. Untested lead: content refreshed only at the game tick inside a 90 Hz blended view (the sky is always this frame's; objects not paired), most visible outdoors. Needs a focused look, not more switch rounds. (3) **Clip-through at long range (FIXED 2026-10-10):** Frigate terminal shadow through a wall, Statue Valentin through his crate; above 60 fps only. Cause: the D578 in-between depth-clip widening (`gfx_interp_conservative_clip`, heading-dependent depth remap). Gone on the Deck with `noclip`; now default OFF (`interp_clip_off`; `GE_INTERPCLIP_ON` / `clipon` opts back in). Owed: Dam distant-ridgeline check without it (the reason it was added; maintainer deferred it 2026-10-10). (4) **Cradle catwalk shadow flicker (FIXED 2026-10-10, D584):** a regression from D579 at every frame rate (bisected on the Deck); mixed-w triangles get the homogeneous facing test again. (5) **Cradle turret explosions may draw wrongly** (maintainer, 2026-10-10): noted only, not investigated. |
| Native widescreen: stale watch pixels at the sides of 4:3 menus (mission folder, file-select background, main menu after exiting from the watch) | fixed (2026-10-10, ac50865d; maintainer Deck check OK) | D579, D334 | Not interpolation (present with `allexact`). D579 (#150) stopped the per-frame canvas color clear to match N64 persistent VRAM and clears only the letterbox bars; with Native widescreen the canvas is the full 16:10 window, but 2D menus draw only their 4:3 core, so the canvas sides are never drawn or cleared and keep the watch. Fix direction: keep the no-clear where the game draws, clear the widescreen extension outside the 4:3 core (or on 2D/menu frames). Regression from D579 (this week), not in v0.5.0 releases. **Fix 2026-10-10 (`gfx_frame_body`, D579 follow-up):** outside a running stage (`current_menu != RUN_STAGE`) with native widescreen wider than 4:3, the canvas strips outside the centred 4:3 core are cleared to black before the DL, like the letterbox bars; in-stage frames keep the no-clear. |
| Dam: grate decals vanish behind a sliding edge, wall lighting shifts with view; guard tower window/doorway view of the outside glitches | closed 2026-10-09: faithful | D583 | Present with every present exact (`allexact`) and in v0.5.0; maintainer checked 1964 (GEPD): the exact same behaviour. Original-game portal/culling artefacts, not port bugs. Candidate for the opt-in fixes row below. |
| Opt-in fixes for original-game rendering bugs (QoL) | idea, post-1.0 (maintainer 2026-10-09) | D583 | Maintainer: a nice QoL feature would be optionally fixing artefacts the original game has, e.g. the Dam wall grates cut by a sliding portal edge and the guard-tower window/doorway outside view (D583 row above), and overlapping 2D transparent textures on Cradle sorting wrongly (also in the original). Must stay off by default (fidelity is the default) and live in `port/` or an explicit option; game-logic changes would need Rule-2 sign-off. |
| Exclusive fullscreen picks the wrong resolution with NVIDIA "OpenGL present method = Prefer native" | open (v0.7.0, maintainer 2026-10-10) | D583 | Seen 2026-10-09 while bisecting D583; borderless is fine. Probably exclusive mode switches to the ini Width/Height instead of keeping the desktop mode. Not investigated. |
| Deck golden captures are 1280x800 when interpolation is on | open (v0.7.0, maintainer 2026-10-10) | D578, D283 | With Auto FpsCap on the Deck's 90 Hz screen, GE_PCDUMP writes 1280x800 frames against the 640x480 Deck goldens, so every frame fails to compare (and the sweep prints PASS with "stems don't match", which is not a pass). **2026-10-11:** the false PASS is fixed: `framediff.py` exits 3 on a size mismatch and `verify.sh` reports it as REGRESSION ("not compared"); the capture-size fix itself is still owed. `verify.sh` now pins `Video.DeckPresetApplied = 1` (D283 preset). Fix: dump at the window size, or re-capture the Deck goldens once. |
| Crash on save slots 1/3/4, plus a host BSOD (fltmgr.sys) | decided | #94 | Needs a repro. Two maintainer retest requests (v0.3.0, v0.4.0) got no reply. Likely D299/D301 (save-slot crash, fixed) and D344 (BSOD on exit, fixed). **Decided 2026-10-01: close as stale when the release ships**, pointing at D299/D301/D344; no GitHub activity before then. |
| Authored mip chains not sampled for explicit-LOD textures | post-1.0 | D323 | Distant textures use driver mips instead of Rare's hand-authored chain (heaviest on Statue/Aztec). Parked as a fidelity enhancement. **Decided 2026-10-01:** after 1.0, together with the LOD-fraction approximation as one "real RDP LOD" project. |
| Approximations in the software RSP | post-1.0 | `gfx_pc.cpp` `G_CCMUX_LOD_FRACTION`; D323 | The LOD-fraction CC input is an eyeballed approximation, bundled with D323 as one "real RDP LOD" project after 1.0 (2026-09-30 triage). **WrapFix settled 2026-10-02:** the maintainer checked the Depot building ceilings against 1964 with the default `Video.WrapFix = 0` and they look right, so the default stays off (the opt-in knob remains; RC3/D167). |
| Latent 64-bit ABI hazards | partial | D441, #107, #108; AUDIT-M6 `struct player`/`struct hand` raw offsets (porting-notes §A1) | **State (2026-10-03, D441):** complete census (local `scratch/d441/`; many hits are intentional `(s32)&ANIM_DATA_*` offsets, D34); 17 files widened to `uintptr_t` under `#ifdef PORT`; 35 sign-extension sites fixed; CI ratchet `tools_pc/abi_ratchet.py` (baseline 389); arena windows centralised (`port/include/portaddr.h`); tier-1 high-arena build (`GE_HIGHARENA_BASE=0x90000000`) runs the menu and `-level_09` pixel-identical to default. D478: nothing live on the current layout; the rest would bite only on macOS/ARM/ASLR. **Sweep (2026-10-05):** tier-1 all-level sweep done, 21/21 PASS, no fix batches, ratchet unchanged (389), recipe in the D441 entry. **Owed:** tier 2 (above 4 GiB; u32 slots widened); store-to-u32 vs arithmetic-only tag per site (`portHostToN64` under macOS port_addr); `mema.c` widening (D453); `initanitable.c` heap slots; GE_DETERM wall-clock timers (D117 follow-up, blocks using determinism mode for frame gates). **#108:** widening the `sndPlaySfx` bound disables the guard everywhere; prefer a range check — **done in v0.6.0 as D573** (address-window check). |
| Port-memory vertex/pool bounds hardening | post-1.0 (decided; not a v0.5.0 blocker) | D545, D549 | D545 (rockets through floor, fixed) left one residual: ~1 dropped rocket in ~60 spawns (Archives, akimbo), consistent with the room-pool hypothesis (`bgBuildRoomVtxBounds` allocation failing leaves no bounds) but unproven; D549 (same stack-overflow class in grenade/knife/object throws) is fixed. Both shipped. Proposed hardening: a port-memory bounds table validating vtx/pool pointers against the known arena windows. |
## 3. Verification debt (fixed; a live check is owed)
A single campaign pass with these on the checklist would clear most of them.
| Item | Refs | Check |
|---|---|---|
| Presets: Original N64 / Port defaults | D440 | Press both rows in F10 and in the front-end PC Options with a mouse and a pad; draw distance applies from the next level load. `GE_OPTIONTREEPROBE` already fails on the base commit (separate look). |
| Probe strip (2026-09-30) | D245, D302, D318 | Frigate or Dam water/sky A/B against the previous build (the s16 sky path was removed); one Facility playthrough to confirm the D318 watchdog stays silent. |
| Infra fixes | D179, D515, build-pc.sh, crash.c, CI | 2026-09-30 re-exec no-op (PATHEXT) fixed; 2026-10-04 empty-arch-tag bug (D515) fixed headless (details in AGENTS.md) — confirm on your next fresh-dir build. **Owed:** `./build-pc.sh` from a genuine MSYS2 login shell; the next CI run (pinned actions, workflow input check). `GE_CRASHTEST=6` shows resolved frames. The romdata `VirtualAlloc` failure path now fails boot instead of falling back to the heap copy: confirm you want that. |
| Light-fixture hit-type reads (impact sound + sparks) | D434, #119 | Shoot fixtures on Bunker and Caverns. |
| Dam/Caverns water animation | D465, #127 | Fixed 2026-10-01 (16-byte `Gfx` raw-index write). Maintainer: still "slightly faster" than 1964 on Dam; measured rate equals N64 within 0.1%. Optional objective check: time ten shimmer cycles on both (expect ~26 s). |
| End credits after Cradle | #129 | Did not reproduce on `release/v0.4.1` 2026-10-01 (maintainer finished Cradle on Agent, credits played, clean exit). Close #129 as not reproducing when the release ships. |
| v0.4.1 batch (unreleased; fixes live on `release/v0.4.1`) | D424–D432; #114 fire rate, #115 tank-crush audio loop, #116 tank movement, #117 sway, #118 GL recoil, #119 light fixtures | All user-verified against GEPD/1964 on 2026-09-29. Close the issues when it ships. |
| FOV-scale edge culling | D222 | Max FOV, pan across NPCs at the screen edge. Also audit the FOV/aspect terms in `camIsPosInScreen` and `c_lodscalez` for the Surface dish (D503 residual). Maintainer decision owed: expected widescreen behaviour vs fix. |
| Draw/LOD 800% | D503 | Draw/LOD distance ceiling raised 400% → 800% (2026-10-04). Owed: at 600-800, check far-field z-fighting on Dam (tunnel), Surface and Jungle; on Aztec or Streets, grep the log for the D500 vtx-pool budget line. If z-fighting shows, lower the ceiling to 600 rather than touching depth precision. |
| Water pulse, Surface 2 | D229 | Frigate is verified. Only the Surface 2 by-eye check is owed. |
| Linux save/config path when launched outside the game dir | D256 | Save-flow check on Linux. |
| Settings/F10 batch | D356, D358, D361, D363, D374, D406 | Live accept of each item in the F10 overlay. (D377–D379 residuals were waived.) |
| Controller feel | CONTROLLER-INPUT-PLAN Wave A, D204 | Per-stick deadzone/smoothing feel on a pad and on Deck; D204 tempo by ear; `Input.RumbleScale=0` silences rumble. |
| Widescreen HUD at 21:9 | D334/D335 | One ultrawide HUD playthrough. |
| Older "owed" notes | D371, D372 | **Bookkeeping done 2026-10-01:** D150, D177, D264 ticked off against the 2026-09-28 campaign sign-off; D369 accepted in use. Still owed: D371 (GEPD key-layout preset + reload/crouch bindings: select the preset in F10 and play a level). (D372 low-end preset: no further effort; low-end work is real optimizations, see §4.) |
## 4. Platform
| Item | Status | Refs | Notes |
|---|---|---|---|
| macOS (Apple Silicon) / ARM64 Linux | open (target 0.9.0/1.0, maintainer 2026-10-02; **scope 2026-10-08: Apple Silicon only, experimental in v0.6.0**) | PRs #88, #95; #101 | Outside drafts, parked until after 1.0. **Address model decided (2026-09-30): port_addr (#95)**, a `portN64ToHost`/`PORT_N64PTR` chokepoint with a `PORT_ADDR_STRICT` validator. The integration branch is `macos-arm64` (renamed 2026-10-08 from `macos`; local + Forgejo mirror) with #88 (merged), #132 (merged) and #95 (**merged 2026-10-10** — record merge `903095c9` in the v0.6.0 line + base merge `f87f4bcd`; the stale GitHub `origin/macos` mirror was deleted 2026-10-10, its lineage stays reachable via the record merge); nothing merges to `main` before the post-1.0 pass. **Scope decision (maintainer 2026-10-08): ship Apple Silicon only** — the x86_64 macOS path (#88) stays in-tree (identity at `PORT_ADDR_BASE==0`) but was **never verified on Intel hardware** and is out of release scope; **arm64 macOS is planned as *experimental* in v0.6.0** (the first release carrying a macOS build; owed before the release: D602 diagnosis, D600/D601 live confirmations, and a release-notes experimental marker on the `macos-arm64` artifact). **#131 (self-hosted sweep scaffold) closed 2026-10-10** — its content shipped in the v0.6.0 CI; the fork-friendly rework is parked locally (`park/ci-selfhosted-sweep-131`, `2fb8e52e`) for the v0.7.0 self-hosted runner item. **Status 2026-10-08:** D590 fixed the deterministic Windows `load_bg_file` SIGSEGV (the D588 bug class on the full-width `bg_addr_t` — plain casts, 5/5 Dam runs clean; the D589 A/B's Win side is unblocked); **D589's rev32 is ROOT-CAUSED as D591**: the decode-time pool is 134/134 byte-identical Mac/Win (the writer side is clean — the “pool holds the rev32” line is retired), and the reversal happens at the *import* — `gfx_tex_source_is_c_array` (`port/fast3d/gfx_pc.cpp`) mixes host-space and N64-raw-space in its range test, so on arm64 macOS (`PORT_ADDR_BASE=16 TiB`) every DRAM/cart texture source mis-classifies as a “C-compiled array” and `gfx_tex_normalize_source` bswaps it (a clean rev32 of the whole wide-pixel pool — the D219/M-114 pink-HUD symptom class). Fix in `9c59cbe7` (compare the host pointer directly; port-layer only, no-op at `PORT_ADDR_BASE==0`); **Windows verified byte-identical (400/400 import `r*.bin` + PPM unchanged), Mac A/B now verified 2026-10-08** (clean arm64 build at `25f92e09`: `GE_TEXN carray=0` on all probe lines, Mac `raw8` matches the Win baseline — the rev32 is gone). **D589 is now RESOLVED: the "second cause" (D592) was CLOSED as a non-issue 2026-10-08.** The "rock striping" that D592 tracked turned out to be a **false positive of a crude vertical-high-frequency (VHF) metric flagging natural rock grain**, *not* a GL-layer artifact: a `GE_TEXSTATE` GL-state probe showed the effective sampler state **and** the NPOT mip level counts are **byte-identical Win/Mac** (the Apple M3/Metal backend builds the full NPOT mip chain; the aniso extension is present on both; the NPOT rock texture gets `min=trilinear, mag=linear, aniso=4` on both sides), a 0-pink scan confirmed **D591 held**, and the **maintainer's live Mac look showed the Dam rendering clean** (no visible striping, ammo counter not pink, F10 menu fine). The NPOT `glGenerateMipmap` guard + companion single-level sampler fix (`9bed5904` / `82dbea9c`) were **reverted as dead weight** (`b80dcd08`, which also strips the `GE_TEXSTATE` probe). The genuine cross-platform symptom (the pink HUD) was **D591 (fixed, `9c59cbe7`)**. **D594 (FIXED): macOS partial input** — `GE_KEYLOG` **disproved** the cross-thread-poll suspect (WASD + LMB reach both the main-thread pump and the scheduler-thread poll); the real gap was the `actHeld` mouse gate on `current_menu == RUN_STAGE` (a forced `-level_NN` start leaves `current_menu == INVALID (-1)`, so the mouse was never read) plus the macOS `reconcileGrab` `windowFocused` gate. Fixed by matching PD's input model (in-stage = RUN_STAGE *or* INVALID; macOS grab = PD's MLOCK_ON, no focus gate) — LMB fire now works on the Mac (trackpad: LMB fire, two-finger = RMB/aim, Shift = ADS). **D595 (FIXED): the arm64-only SIGSEGV that firing then exposed** — `initResolveAnimTable` truncated the live `ptr_animation_table` host pointer to `(s32)`, dropping the 16 TiB `PORT_ADDR_BASE` → every AI hit/stagger `struck_anim` resolved to bogus low memory → crash in `modelSetAnimFrame` on the first AI hit-reaction. Fixed by reading the base at full pointer width (§A1; identity on x86_64). Both are the last input-blockers to a playable arm64 macOS build. (maintainer visual-sanity package: `scratch/d589-dam/ab3-mac/{d591_sbs_win_mac.png,d591_diffmap.png,D591-AB-SUMMARY.txt}`. `tools_pc/abi_ratchet.py` baseline refreshed for the branch. **Integration into the v0.6.0 line done (2026-10-10):** #95's `crouched_rifle` hunk and its `D420` label were dropped at integration (our #120/D445 is Line truncated
| macOS first-launch black window (v0.7.0) | open | D581-era sweeps; logs in `docs/dev/notes/mac-first-launch-hang-2026-10-13/` (local) | **2026-10-13, v0.6.0 release download on the M3:** the cold first launch generated assets, reached the title stage (90) and then rendered no frame (`kernel heartbeat: no frame rendered`, frames=0, all game threads blocked); the next launch was clean. 1 hang in 16 launches that day (1 in 10 first launches), 0 in 7 after a reboot; neither a new binary alone nor asset generation alone reproduced it. 3 earlier sightings on 10-12. Lead: one timer mq in the heartbeat dump shows `valid=83746432/0`. Release notes carry a one-line "close and start again" tip. |
| Pixel gate vs the Auto frame-cap default (v0.7.0) | open | D578, `tools_pc/verify.sh` | The goldens (2026-10-02) predate frame blending; with the defaults-only ini the sweep now runs Auto, which blends at 120 on a 120 Hz panel, and 2-9 levels flag at random per run (worst 9.3%). The same levels pass with `VERIFY_INI_EXTRA` set to a `[Video]` block with `FpsCap = 60`. Pin the sweep to 60 or re-capture goldens on the blended path; until then run the release sweep with that extra ini. |
| Platform parity gate (Linux co-equal with Windows) | open (v0.7.0, maintainer 2026-10-10) | — | **Decided 2026-09-30:** yes. Every released platform carries the same gate: a 21-level sweep plus a campaign pass. Needs the golden baselines on both platforms first (§7). The last known Linux crash (D190) was closed as not-a-bug. |
| Low-end GPUs (~30 fps on Celeron/HD 400) | fixed, verify on N3060 class | D339, D481, D482, #92 | **D481 (2026-10-02):** fast3d ran inline on the scheduler thread, delaying retrace delivery so the tick gate skipped frames; the render worker took a Lenovo X220 (HD 3000) from 44-46 to 60.0 fps. **D482:** Atom/Celeron-class GPUs (HD 400-605, UHD 600/605, software renderers) get DrawDistance/LodDistance 100 and MSAA 1 once, where still at defaults. **D473:** uncached per-batch getenv fixed. Direction: real optimizations, not a preset. **Next only if N3060-class hardware still drops:** one `GE_PERFSTAT=1` run (`gpu=` vs `run=`), then a lighter renderer-keyed default (GPU) or fast3d per-batch cuts (CPU; local `PERF-HOTSPOTS.md`). |
| Statue/Cradle hitching (maintainer, 2026-09-30) | parked (not player-visible, 2026-10-02) | `GE_PERFSTAT` spike counters | **Diagnosed (D475, D477, D480):** no sustained load — the ~33 ms frames are the frame limiter skipping a retrace after a late gfx task (`boss.c`, D13-class timing); CPU bursts were CI-palette re-imports (ruled out, D477) and in-play shader compiles (fixed, D480). **Parked 2026-10-02:** a play session with the timeline probe felt no hitching. Reopen only if a visible hitch is reported (lever: pre-warm at level load). |
| Steam Deck preset skipped when first launched from Desktop Mode | verify | D283 | Fixed on `release/v0.4.1`: the Deck is detected by DMI hardware id (Valve Jupiter/Galileo; `STEAMOS` as a second signal) and the preset applies once per ini, only while Fullscreen/Maximized are untouched. Only a Windows build and a Linux `-fsyntax-only` check ran. **Owed: one run on a real Deck** (Desktop-Mode-created ini, then Game Mode; expect fullscreen 1280x800 and the log line `video: Steam Deck (DMI Valve ...)`). The Known-issues row goes when this ships. |
| Windows 7 | open | #98 | **Re-checked 2026-10-02: likely feasible around 1.0.** The exe imports no Windows 8+ functions and the bundled SDL2 2.32.10 still supports Windows 7. The real blocker is the first-run converter `ge007-convert.exe` (PyInstaller, Python 3.9+): the native C converter (#6 Part B) removes it, or a separate Python 3.8 build. Work: one Windows 7 VM test, then manual per-release checks (CI cannot test it). Supersedes the earlier 'won't-fix' call. |
| Alt-tab haze + fast alt-tab recovery | open (v0.7.0, maintainer 2026-10-10) | D578 | Maintainer 2026-10-10 (v0.6.0 build, Windows 120 Hz): alt-tabbing leaves a hazy picture; fast repeated alt-tab is wonky and recovers only after a while or more alt-tabbing. Not new in v0.6.0 (was an owed check). Known issue in the v0.6.0 notes. QoL fix: clean focus-loss/regain path (context/FBO state, present queue reset). |
| Deck: max Field of view feels choppier while turning | fixed in v0.6.0 2026-10-13 (D953 LOD/fade/visibility + D954/D955); maintainer Deck-confirmed (Surface windows) | D953 | At FOV 90 (menu max) turning feels worse than at 60, at 60 and 90 fps. Frame pacing measured identical at both FOVs. Lead: FovScale pulls every model LOD switch 1.73x closer, and depth-based LOD flips off-axis objects while turning (Dam turn probe: 0 flips at 60 deg, 400 at 90 deg, 120 with the fix candidate on `fix/d953-fov-lod`). Pass 2 (10-13 evening): prop fog fade and fog-range visibility fixed the same way; on Surface LOD/fade/visibility do not change while turning, so they are not the feel. Maintainer: objects "update not at the right speed" (object motion is not interpolated at 90, plan D955). **Next:** D955 Stage 0, and one Deck `GE_D193=1` run per FOV for 2-tick game steps. |
| Doors and guards flicker at 90 fps (not at 60) | fixed 2026-10-13 (D954); maintainer Deck-confirmed | D954 | In-between presents drew props (chrs, doors, effects) at the exact camera while rooms used the blended one, so props jumped against the world whenever the camera moved (Surface turn on Windows: 0.4-0.5 deg mean, up to 1.8 deg). Present in v0.6.0 at 90 fps. Fix in `gfx_pc.cpp`: props' plain perspective gets the room camera blend (`P * R^-1 * Rb`) up to the first-person gun. **Owed:** Deck at 90 fps, Surface doors/guards, and the gun must not move while turning. |
| Objects move at 60 Hz steps at 90 fps (props, doors, guards) | fixed 2026-10-13 (D955 + sky pass); maintainer Deck-confirmed (guards, doors); first-person weapon not blended yet | D955 | In-between presents drew objects at their tick pose (only the camera was blended). Now blended per joint, keyed by (Model*, joint) from a game-thread snapshot, rebased onto the new camera; exact on room-origin changes, failed gates, split-screen. Dam on Windows: 99.6% of object joint loads blended. **Owed:** Deck at 90: guards walking, gun in hand, hats, deaths, shadows, doors, Facility stall doors/sinks (the old ghost site), room boundaries. Vehicles later. |
| Windows display scaling: the game is not DPI-aware on scaled displays | fixed 2026-10-10 (D626); maintainer by-eye on a scaled display owed | D626 | Seen 2026-10-07 during the v0.5.0 4K captures: on a 3840x2160 display at 125% Windows gives the game a 3072x1728 surface and stretches it, so the picture is softer than native. `gfx_sdl2.cpp` sets `SDL_HINT_WINDOWS_DPI_AWARENESS` only when `allow_hidpi` is off; setting it (`permonitorv2`) on the HiDPI path too renders at native resolution. Workaround today: environment variable `SDL_WINDOWS_DPI_AWARENESS=permonitorv2`. |
## 5. Modern-remaster layer (port features beyond the N64)
**The bar:** feature parity with a Nightdive-style remaster and with
GE+/Dab's mod where it's practical. Shipped so far: native widescreen and
ultrawide, FOV, MSAA/anisotropic/texture-filter options (including N64
3-point), draw and LOD distance, VSync and a frame cap, show-FPS,
keyboard/mouse rebinding with a GEPD-style layout, pad deadzone,
sensitivity, smoothing, southpaw and triggers, rumble, crosshair and HUD
customisation, HUD scale, music/FX volume, no-hit-flash, SkipIntro,
All unlocked (see D387, D442), and the F10 overlay.
The modern defaults (native widescreen, overscan crop, 250% draw/LOD
distance, bilinear filtering) are **deliberate**. They are not a fidelity
defect.
Background research lives in the maintainer's local notes (gitignored):
`GEPORT-REFERENCE-DEEPDIVE.md`, `MODERN-PORT-FEATURES-BACKLOG.md`,
`MODERN-OPTIONS-PLAN.md` and `PD-LEGACY-SURVEY.md` under `docs/dev/notes/`.
### 5a. Before 1.0
**Renumbered 2026-10-10 (maintainer):** the planned v0.5.1 (frame rate above 60 + macOS) ships as **v0.6.0**, and the old v0.6.0 scope is now **v0.7.0**; labels below are updated, findings and commits keep the old names. **Order (maintainer, 2026-10-08; mod-start added 2026-10-09): v0.6.0 = frame rate above 60 (D578) + macOS (#95, local); then custom textures and mod support (the texture work likely shares the surface/geometry findings from D578). v0.7.0 ships the items marked v0.7.0 below (#145 control styles, native N64 pad support, the v0.5.0 issue triage, the README line, **mod support start — maintainer 2026-10-09: mods come sooner, in v0.7.0, not after 1.0**). They are the minimum for 1.0.** Everything marked v0.7.0 is open; none of it is claimed fixed or verified.
| Item | Status | Refs | Notes |
|---|---|---|---|
| Log file (next release) | done (v0.6.0) | maintainer 2026-10-06; D576 | **Done 2026-10-07 (D576):** `ge007.log` next to the ini, previous run kept as `ge007.prev.log`, 8 MiB cap. Was: write `ge007.log` next to the ini (console output is lost on a normal launch). Crosshair pointer done in v0.5.0 (D555). |
| F10 / PC Options menu: PD-style dialog refactor + watch-consolidation review | verify | D504-D512, `MENU-PD-ALIGNMENT-PLAN.md` (maintainer's local notes) | **Built 2026-10-04 (D504); follow-ups merged:** D505/D506/D507 (wording, real-unit sliders, descriptions); D508 forced aspect ratios (16:9 / 21:9); D509 2-frame hold after an aspect change (rare gun glitch = known issue); D510 overlay + FPS counter same size front end vs level; D512 `Game.HudScale` scales the F10 overlay. **Owed (live pass, `docs/dev/notes/MENU-LIVE-CHECKLIST.md`, launches A/B/C):** look-and-feel vs PD; mouse, keyboard and pad in a level and on the front end; HudScale 75/100/150; 21:9; display-mode round trip across a restart; split-screen 2P with the overlay open. **Gaps:** background blur (post-1.0, §5c); per-seat control scheme is #139 / D513. Watch consolidation: not cheaply feasible (parked as T5). |
| Defaults/presets consolidation: one `Display mode` switch | verify | D504, D440, `MENU-PD-ALIGNMENT-PLAN.md` §5 (local notes) | **Landed 2026-10-04 (D504):** the derived `__DisplayMode` row shows Modern / Original N64 / Custom (no ini key); F10 dropdown + left/right stepping, front end steps too; one reset row per dialog covering exactly its keys; editing a non-bundled key does not flip Display mode. **Decided 2026-10-03: tri-state**, flipping to Custom when the user edits a bundled key. **Owed (live):** display-mode round trip across a restart; read of the regrouped pages and wording (MENU-LIVE-CHECKLIST.md A1/A2/A4). |
| Control style Original shows the game's own N64 styles | verify | #139 follow-up, D516, D517 | **Landed 2026-10-04 (D516, merged @ 91595597):** "Controller style" row (Original only; the game's own 1.1-2.4 per seat, saved to the Bond file; front end reads "(in a mission)"); D498 presets hidden in Original. **D517:** hold-repeat on any Bond row dropped fps (EEPROM rewrite per detent); now staged like sliders. Live passes 2026-10-04 (maintainer): single player + split-screen 2P with different styles per seat. **Owed:** the 2.x second-pad pass (real second controller). |
| Pad layout preset per player (setting P2's no longer moves P1) | verify | D518, D498 follow-up | **Landed 2026-10-04 (D518):** `Input.PadPreset` kept as seat 1's key + new `Input.PadPreset2/3/4`; the F10/front-end preset row follows the Select-player seat. Headless GE_PADMAPTEST PASS; **live passes 2026-10-04** (P2 change leaves P1 alone; split-screen: each seat ran its own preset). |
| Control scheme selectable: Ext / Original control style | verify | #139, D513, D498, D166 | **Landed 2026-10-04 (D513, merged into `release/v0.4.1`).** `Input.ControlScheme` (row "Control style", Ext default / Original) on the Controller page, both UIs. Original leaves the game's own style alone; D498 presets set layout and stick swap only in Original; 2.x runs faithfully; global in v1, per seat with the Select Player work. **Owed:** pad pass in both modes, watch-selected 1.1-1.4, split-screen 2P with different styles, 2.x with a second pad. |
| Outside PRs #107–#109, #122–#124 | decided | #122 hot-plugged pads never reopen, #123 persistent crosshair (`gunfire.c`), #124 compact health/armour bars (`bondview2.c`); #107/#108/#109 (italoarruda) | **Decided 2026-10-01:** **#120/#121/#122** merged locally (#122 = D450); close on push. **#123**: merged 2026-10-06 into v0.5.0 (D436; presets set it off, tip, Rule-2 signed off; by-eye hipfire / sniper zoom / 2P owed). **#124**: deferred (ignores HudScale, overlaps status text, ultrawide anchoring, split-screen untested) — **tentatively on the v0.7.0 triage list (commented to the author 2026-10-10; blocker is ours: scope + golden re-baseline, so a rebase onto `release/v0.6.0` would make integration cheaper).** **#133**: duplicates D472; close as superseded on push. **#109** (pad rebinding): decline as-is; supersede with our pad presets (§5a), crediting the author (Co-authored-by). **#107**: folded into D441; close on push. **#108**: range check implemented in v0.6.0 as D573 (commented + credited 2026-10-10; superseded, author to close). |
| Gamepad presets: duplicate-binding warning, Xbox stick behaviours | open (after v0.5.0; documented in the README) | D469, D498, D518, D394 | D469 (pad action table, Jinx 1.1 default, Custom per-seat bindings), **D498** (Xbox 1.2 Christmas / 1.3 Frost / 1.4 Elektra) and the per-seat preset (**D518**) are done and maintainer-verified with pads. Not done (maintainer 2026-10-04: skip in game for v0.5.0, document only): a warning when two actions share a button; the Xbox "left stick aims the crosshair" and "D-pad copies the left stick" behaviours (D394). |
| Pad button names on a Nintendo pad | verify | D471; local note `PAD-MENU-AUDIT.md` | Audit done 2026-10-01 (no controller-only trap; D472 fixed G1-G3). D471 family button names + player LEDs: **PS5 DualSense verified 2026-10-03.** **Owed:** a Nintendo (Switch) pad check; the maintainer has no such pad, so this waits for one or for a user report. |
| Mute and auto-pause on focus loss; PNG screenshots to `screenshots/` | parked | D231, PR #56 (closed unmerged), `docs/dev/notes/parked/0001-QoL-*.patch` (local) | Deprioritised by the maintainer on 2026-09-25. F12 today is the dev PPM dump. |
| Frame rate above 60 (90/120/144 with interpolation); **v0.6.0** | verify | D578, #137 (f1zz1ec0ke), [`dev/UNLOCKED-FPS-PLAN.md`](dev/UNLOCKED-FPS-PLAN.md) | **Built 2026-10-07 (D578, rework of #137 by f1zz1ec0ke), branch `feat/interp-fps`:** `Video.FpsCap` 90/120/144 draws matrix-blended frames between game frames on displays faster than 60 Hz; exact-frame fallback for turns (default ON); room-set change forces an exact frame; auto (match display) cap default. Latest commit `06ab98af`: port-side portal-chain replay recomputes room scissors for the interpolated camera (verified 0 mismatches over 1,200 exact frames at alpha=1 on Dam; the deep tunnel chain and flicker are untested). **Goal: a fresh Steam Deck install defaults to 90.** **Remaining before merge into the `release/v0.6.0` line (all open):** (1) Dam tunnel holes / pop-in re-test on a Deck with `06ab98af`; (2) VSync-on re-test (74.5 presents/s seen earlier); (3) Windows 120 Hz check plus a default-60 golden regression; (4) alt-tab haze; (5) live 144 Hz and Deck OLED 90 Hz checks. Dam in-between flicker (2026-10-08, NOT FIXED, the blocker for shipping high FPS): eight Deck A/B variants (dclamp, projset, exactproj, clipfix, nodepth, nocull, fullsciss, plus the older noportal/nosciss/noclip/noreach) all still flicker, and the CPU emits the same triangle counts in blended and exact passes; the loss is per-pixel content, not missing triangles. Next: run `holecap` in the Dam tunnel gap (saves in-between vs exact images of a real hole) and read what the hole pixels are. Findings: D578 addenda; handoff: docs/HANDOFF.md. **State 2026-10-10 (supersedes the "Remaining before merge" list and the Dam-flicker note above, both resolved by D578 2026-10-09 / D583):** work lives on `feat/d583-deck90` (local, unpushed, on top of `feat/interp-fps`). PC 120 + VSync on G-Sync fixed; Deck 90 good on most levels. **Merged 2026-10-10 into `land/v051-integration`** (off `release/v0.6.0`, with `macos-arm64`). Known issues for the release notes: Bunker fight drops on the Deck (D585 parked), Jungle feel, Cradle turret explosions (uninvestigated). Open gates: Dam ridgeline check with the clip widening off, default-60 golden regression / 21-level golden sweep, alt-tab haze, 144 Hz live check, VSync-without-G-Sync PC check. Was: decision. Older note: the menu cap was {30, 60} (the ini accepts 0 = uncapped, but the sim ticks at the VI rate); the plan's Phase 0 is resolved by D248 and its Phase 1 audit and Phase 2 slice scope are written (2026-09-30). Must stay compatible with netplay determinism. |
| PD Extended Options parity: settings that need new dev work | done (reviewed 2026-10-04; two rows deferred) | `MENU-PARITY-SONNET-PLAN.md` §3 (local notes); pd_port `port/src/optionsmenu.c` @ 32a1cb9 | **Reviewed 2026-10-04 (maintainer decisions; local notes).** **Done:** D511 fullscreen mode, Center window, crosshair opacity and color by health (live check owed, MENU-LIVE-CHECKLIST.md A4/A5/B3). **Watch Screen size/Ratio:** stay on the watch, no PC rows; the game saves watch settings on watch close (D349 correction) and they don't fight Aspect ratio (README). **Deferred past v0.5.0:** mouse lock mode (Auto/Always/Off; aim lock itself = `Input.AimMode`, D337); per-seat controller device select (needs a stable pad id; MP input layer). **Closed:** edge deadzone = `Input.AimRange` (D338); crosshair sway N/A (GE has none; gun sway is D428); use key reloads, no toggle (Ext keeps D378's GEPD split, Control style Original gives the N64 behaviour); per-seat options/binds (pad binds already per seat, D469/D518; keyboard/mouse is one device); no Detail textures toggle (`Video.DetailBaseTile` is the D236 fix); N/A confirmed: HUD centering (D335b), glare, overexposure, GE64 muzzle flashes, MP death music, radial menu speed, language filter, crosshair speed. **Other rows:** analog movement/stick scale/crouch → #139, T7; tickrate/framerate limit → frame rate above 60. |
| One PC options UI: front-end "PC Options" opens the F10 overlay over the folder graphic | verify | D519, D343, D504, D510, D512 | **Landed 2026-10-04 (D519):** the file-select "PC Options" entry opens the F10 overlay over file select; the `MENU_PC_OPTIONS` screen and four old probes are gone; Bond-file rows and the watch chooser are keyed to file select. Headless checks pass. **Owed:** by-eye mouse, keyboard and pad pass on file select; profile chooser + Controller style with a real profile; footer text overlaps the Copy/Erase bar. |
| Emulator saves load directly in the game (no tool for import) | verify | D514, D492; `port/src/eepimport.c` called from `geEepromLoad`; D297 `legacycrc.c` same pattern | **Landed 2026-10-04 (D514).** Copying a 1964/Project64 `.eep` in as `data/ge007.eep` works: on first launch each region that validates in emulator format and not port format is converted in place (same rules as `eep_convert.py`); regions invalid in both are left to the game's wipe; port saves untouched; the original is backed up to `ge007.eep.emulator.bak`. Export stays with `eep_convert.py`. Headless verify 6/6 byte-identical to the reference. **Owed:** by-eye mission select after importing a real 1964 save. |
| DexDrive / GameFAQs `.n64` saves | idea (easy, post-1.0) | D514; sample `scratch/eepinv/gamefaqs_dexdrive_2001.eep` (local) | **Noted 2026-10-04.** Old GameFAQs GoldenEye saves are DexDrive Controller Pak dumps (`123-456-STD` header, then a 32 KB pak) whose note (game code `3BADD1E5`, 2 pages) holds the 512-byte cartridge EEPROM. Extracting it and zero-padding to 2048 bytes gives a valid emulator-format save (TenebrusoM sample: all 6 CRCs OK). Support = walk the note table, pull the note, pad, hand to the D514 importer (or an `eep_convert.py dexdrive-to-pc` subcommand). Not tested in game. |
| Title bar and window icon (no FPS counter; static "GoldenEye 007" title + GE-crosshair window icon) | done (v0.5.0 polish) | maintainer request 2026-10-17; D550; `port/src/video.c`, `port/fast3d/gfx_sdl2.cpp`, `cmake/icon/ge007.rc`; icon art = `docs/img/icon/ge007-icon.png` (`tools_pc/make_icon.py`) | **DONE 2026-10-17 (D550):** static title bar (live FPS stays on F10), `SDL_SetWindowIcon` from a generated 32x32 header, exe icon via `ge007.rc`. Original generated mark, no game imagery. Build-verified; maintainer live window/taskbar check owed. |
| Opt-in update check (`Game.CheckUpdates`, off by default) | verify | D551; `port/src/updatecheck.c` | **Landed 2026-10-06:** Game-page toggle; one HTTPS GET per launch to `/releases/latest` (no pre-releases); Windows loads `winhttp.dll` at runtime only. Live-verified (Win/Linux/Deck), security-reviewed. **Follow-up PR (2026-10-06):** (1) DONE 2026-10-07 (D575; offline harness `tools_pc/updatecheck_test.c` covers the edge-case half of (2); the live half is still owed). Was: the Linux/Deck `curl` path reads into a 64 KB buffer and treats `curl`'s exit code as failure, so a `/releases/latest` response over 64 KB (long notes, many assets) silently gives no result even though `tag_name` is in the first ~1.5 KB; raise the cap and accept a full buffer (v0.4.0's response is 27 KB, so v0.5.0 is safe). (2) Validation matrix: offline harness for `versionNewer`/`extractTag` edge cases; throwaway builds with `GE007_VERSION` set to 0.3.0/0.4.0 against the live endpoint, Windows + Deck; a pre-release never prompts; after a publish, an older-version build shows the F10 row and opens the releases page; network-off and setting-off runs. |
| Control style: Classic/Original gamepad styles do not match the N64 (#145) | open (v0.7.0; reported on v0.5.0) | #145, D516, D498, D469, D518, D394; README gamepad section | **Reported:** (a) Input "Invert look" (`port/src/input.c` `padLookInvertY`, applied only on the right-stick look path, ~lines 2797/2821) does not reach the left stick when an Original style that aims with the left stick (1.2) is active. **Hypothesis from code reading, not yet verified.** (b) Face buttons under "Classic" do not revert to the original roles (original: South = change weapon, West = action/reload; port: South = action, West = reload, North = change weapon). (c) Menu buttons differ (original: South accepts, West goes back; port: West + South accept, East goes back). (d) The in-game watch invert row looks locked (D564 removed the game's own Look up/down row). **Decision owed (maintainer):** should "Original" mean strictly the N64 face-button and menu roles, with the modern roles kept as a separate preset? Port-only fix. Cross-refs the Gamepad presets row above. |
| Native N64 pads (raphnet adapter) | open (v0.7.0; unverified) | v0.5.0 video review; vendored mupen `InputAutoCfg.ini` | Nothing raphnet-specific exists in the repo. The adapter should appear as an SDL joystick, but that is **unverified**. Work: verify with a real adapter or a user report (raw stick range ~+-80, octagonal gate, deadzone, C-buttons as buttons vs axes), then add a port-only N64-pad profile alongside the Xbox 1.2-1.4 styles. **Owed: a real adapter test (maintainer or a tester).** |
| v0.5.0 issue triage: #146 menu emblem, #147 gun sway part 2, #144 mission timer | open (v0.7.0 triage) | #146, #147, #144, D428 | **#146:** main-menu file-folder emblem texture corruption (4K, Fedora KDE); needs a repro and a 1964 comparison before any fix. **#147:** gun slides side to side but does not turn with the look direction (part 2 of the sway report, relates to D428); check against 1964 first. **#144:** optional PD-style mission timer; port-only QoL, small. Parked issues #113 (cheat toggles), #111 (bots), #110 (third-person), #105, #101, #100, #98 are unchanged and already decided/answered above. |
| README "what this is" line | open (v0.7.0, docs) | recurring viewer questions (v0.5.0 video comments) | Add a short, honest line to the README: a decomp-based native port, AI-assisted, with 1:1 fidelity as the goal. Answers the repeated vibe-coding doubts and comparisons with other GoldenEye ports. VR stays post-1.0 (reference row in *Community forks*). |
| Opt-in gameplay behaviour toggles (shoot-through, true rockets) | open (1.0, added 2026-10-09; **not started — no F10 row, no ini key, no port code; today's build is pure vanilla**) | §5b pattern; `gunfire.c`, `chrprop.c`; the 007 Plus source in the reference tree (**semantic reference only — it is a modded decomp, never a copy source**) | Port-only, default off; one batched Rule-2 sign-off (same pattern as the §5b rows). **Semantics (checked 2026-10-09 against the 007 Plus source in the reference tree):** shoot-through = the per-weapon `ObjectsShootThrough` stat (`gun.h:77`; `PROPFLAG2_SHOOTTHROUGH` `bondconstants.h:360`; penetration-limit checks `chrprop.c:2693/2822/2848`) — how many cover objects a shot passes through; mods retune the per-weapon table (e.g. Moonraker 6 vs 2; the dabs weapon-stat notes agree) and the disable toggle restores the vanilla values (RAM override, All-unlocked class). **True rockets — still open (2026-10-09):** the on-disk Plus tree's rocket handling matches vanilla (`propobj.c:4943`: fixed 100.0f player-prop / 2.0f enemy damage, owner-tagged self-damage, `unk8C` wall-bounce), so the scene's "true" semantics are not in that tree, nor in the GEPD bundle or the footstep docs; plausible candidates = AoE radius/splash retune or door-destruction behaviour — **confirm with the scene author before implementing**. **Implementation-risk note (maintainer 2026-10-09):** these are game-adjacent weapon-stat edits — fine-tune, don't guess: implement as minimal RAM-override toggles, diff-check against the vanilla values, verify with 1964 A/B + golden frames, one small commit per toggle, human review of the diff. **State 2026-10-09: nothing implemented. Work order:** (1) one-off stats-table dumper (vanilla + tuned per-weapon values, both ROMs); (2) the RAM-override + F10 row (one small commit, per the risk note); (3) 1964 A/B + golden-frame verify. True rockets waits on the scene-author answer for its semantics. F10 rows; the Original N64 preset keeps both off. |
| Mod/patch support: drag-and-drop mod loading + mod management + mod template scaffolding + a shipped example mod | open (v0.7.0 start — maintainer 2026-10-09: **mods come sooner, in v0.7.0, not after 1.0**; full scope stays the 1.0 floor; added 2026-10-09) | D413 (capacity-only Rule-2 exception, user-approved 2026-09-28 — recorded in the mod-loader worktree's AGENTS.md), `land/modloader-*` worktrees, §5d; dabs `CLAUDE-notes/mods.md` (local reference) | **Progress so far (the Goldfinger 64 campaign, 2026-09-28 → 2026-10-08, worktree `land/modloader-m1-backlog`) — this is a working mod-loader layer, not a starting point; do not undersell it:** the **`rommod` layer is live** — a 25.1 MB GF64 mod ROM loads over the 12.5 MB vanilla base (`rommod: mod 'goldfinger64' requested — 58 raw file rows, 598 sidecar rows` in the run logs; the standard pcmodels/pccg sidecar pools carry the extra content), with C2 ROM-patch decoding of the mod's patches. **M1 done** (`97fda74f`): GF64 **boots, renders (walls fixed), clean-quits** via the loader. **C2 done** (`8c325fc5`): the ROM-patch decoder CLI `tools_pc/modpatch/` — **branch-only, not yet merged to main**. **D413** (user-approved 2026-09-28): the capacity-only Rule-2 exception — raising compiled table/pool capacities (`rommodTextureCap()`, `g_MaxProps`, `STAN_MAX_ROOMS`) under `#ifdef PORT` with a provably-vanilla-identical gate; it is the mechanism that lets a mod exceed vanilla limits. **Asset-prep + verification:** the `modprepare` pipeline (decodes the vanilla RZ file table — 727 rows, all resolved → `base_filelist.csv`/`base_table.json`); a **70-run GF64-vs-vanilla A/B campaign** (per-system: chrs/heads, levels, fog, portals, props, textures, vis/cull/nocull, mem, dyn, shots, clean-quit; vanilla-gate runs proving no-mod identity; GF64 re-verification runs against mainline findings). This row = M1→: content-tier mods (maps, textures, models) loadable by drop onto the exe / a `mods/` folder, with a management list (enable/disable); custom patch support rides on the same path. **Design standard (maintainer 2026-10-09): the dabs mod system** — `mods/<name>/{files,segs,textures}` overlay dirs, single-overlay slot, live file swap (`modListApplySelection` / `romdataResetFiles`), restart path only for `segs/` mods. **Add (maintainer 2026-10-09):** mod-template scaffolding + docs (new mod = copy template into `mods/`) and a first example mod we ship that does something cool. **The starter/test mod is the gun-colour feature (maintainer 2026-10-09 — it does not stand alone as its own feature):** per-weapon tint + a texture swap, demonstrating the scaffold end-to-end (crosshair colour is the separate shipped D511 — don't conflate). **Precedent:** the drop-in ROM converter already loads a Goldfinger 64 ROM — maps work, the mod's custom logic is inert; closing that gap (weapon/script data) is the stretch goal. Acceptance case: GEPD footstep-patch-style mods (bodies staying down, footstep SFX, core tweaks); their feature set maps onto the §5b opt-in toggles. **v0.7.0 slice (maintainer 2026-10-09):** look at mods sooner — merge M1/C2 to main, start the M1→ management + scaffolding (dabs standard) in v0.7.0, with the GF64 boot/render as the acceptance case; template + example mod and custom-patch support complete by 1.0. |
| Cut-content restoration (mod tier): extended MP roster + the 8 cut MP maps | open (1.0; added 2026-10-09, bring sooner if easy) | [`dev/CUT-CONTENT-BACKLOG.md`](dev/CUT-CONTENT-BACKLOG.md) §D/§F | Everything is in the ROM data (backlog, verified): roster — `front.c:601+` `mp_chr_setup[]` holds ~40 named entries beyond the 8 selectable (ROSIKA, KARL, TERRORIST, BIKER, …); maps — `front.c:556-573` `multi_stage_setups[]` has 8 commented-out entries (Citadel/Frigate/Statue/Cradle/Aztec/Runway/Dam/Depot) and every one of those maps already boots standalone (`-level_XX`, geometry, AI setup and briefings intact). Tier 2/3: the table edits touch `src/game` → land on the mod-fork branch (non-negotiable #2) and ship through the mod scaffolding above; Tier-1 config-toggle grants stay port-side (backlog §2). **Maintainer 2026-10-09: in the 1.0 roadmap at minimum; if the `front.c` table edits + setup fixes turn out small, pull in earlier (v0.7.0).** **Assessed 2026-10-10 (read-only, `front.c`):** decision = *not* pulled in for v0.7.0. (a) Roster half is smaller than the row implies: `num_chars_selectable_mp` is already dynamic game logic (`front.c:4240` `unlock_all_mp_chars()` sets 0x40 via the existing debug/cheat unlock-all; `front.c:4986` sets 0x21 on Agent + Cradle-completed, else 8) — so an extended roster may need no `src/game` table edit at all: a Tier-1 port-only config toggle invoking the existing unlock path (live check owed: the 64-char select screen under the cheat) — the one-line "set the default to N" variant is the only `src/game` touch, and that stays mod-fork-only either way. (b) Maps half is the real cost: 8 entries are commented out with per-map state flags (`stat`/`crad` "works"; `azt` "needs setup"; `dest`/`runway`/`dam`/`depot` "xbla setup"; `Citadel` "old format") — uncommenting is trivial, the xbla/old-format/needs-setup fixes are not, and they touch `src/game` data handling → mod-fork branch. (c) v0.7.0 has no mod scaffolding yet (it lands with the 1.0 mod-fork), so there is no v0.7.0 ship path for the content itself; keep the item in 1.0 mod-fork scope and do the (a) live check opportunistically. |
| Motion/gyro aim (controller sensor) | open (1.0, added 2026-10-09) | — | Controller gyro look, opt-in, default off; sensitivity + calibration rows. **Scope note (2026-10-09):** gyro hardware exists only on PS4/PS5 pads (and the rare Switch Pro via adapter) — Xbox/XInput pads have no sensor, so the practical feature set is small; the API is SDL2 `SDL_GameControllerGetSensor` — **verify first on the PS5 DualSense (maintainer has one; the D471 row already used it as a controller test)** before building. Steam-Input-style gyro mapping stays a player-side workaround (the game is non-Steam, custom shortcut only — no Steamworks). |
| Localisation: JP/PAL ROM support first, then community language packs | open (1.0; reframed 2026-10-09) | §1 (PAL/JP row) | **Reframed 2026-10-09 (maintainer):** phase 1 = JP/PAL ROM support, already Stage B (§1). Phase 2 (later 1.0, community-driven) = a port-only text-override layer (`data/languages/<code>` packs overriding ROM strings) so people can add their own language packs if inclined; translation content is community-supplied (open-source-branching model). |
| Custom gun colour (ships as the example/starter mod — see the mod row above) | deferred (2026-10-09) | D511 (crosshair colour already shipped — a different thing) | Maintainer 2026-10-09: not a feature in its own right — it is the starter/test mod that demonstrates the mod scaffolding (per-weapon tint + texture swap); the hook (a tint pass in the model/fast3d stage) is designed when the mod row starts. |
### 5b. GE+ / Dab's-mod gameplay options (opt-in; touch game code)
| Item | Status | Refs | Notes |
|---|---|---|---|
| Disable knockback / hitstun, damage sound, endless death cam | post-1.0 | hooks at `chraction.c:2233`, `gunfire.c:3334`, `bondview2.c:8354` | Needs one batched Rule-2 sign-off. **Decided 2026-10-01:** after 1.0, all opt-in (default OFF), one batched Rule-2 sign-off. |
| Bodies stay where they fell | post-1.0 | — | **Decided 2026-10-01:** after 1.0, opt-in. |
| Port-native third-person camera | post-1.0 | #110 | The 007 Plus ROM patch itself is out of scope. Decide whether a port-native TP camera is in or out. **Decided 2026-10-01:** decide in/out after 1.0 with the other §5b options. |
### 5c. Enhanced visuals (optional; after feature-complete)
The XBLA/Nightdive-style "new look". None of it has started in this repo.
An outside fork has a large Original/Enhanced/Remaster render-mode stack
(see *Community forks* below). Adopting any of it is its own decision and
needs Rule-2 review.
| Item | Status | Notes |
|---|---|---|
| Post-process chain: FXAA/SMAA, bloom, depth of field, colour grade/LUT, CRT filter, brightness/gamma, colourblind filters, SSAO | post-1.0 | Build the framebuffer pass once. Every effect defaults OFF. **Decided 2026-10-01:** after 1.0; our own opt-in implementation (default OFF; the Original N64 preset restores the N64 look); thepont's fork is reference only. |
| Near-full fog reveal (D503): far objects about 97% fogged show as faint silhouettes with straight room-clip edges on 32-bit output (e.g. Surface's dish) | verify | **Snap landed 2026-10-04** (cutoff 247, not derived from measured vertex fog); dish residual = (C) widescreen frustum, moved to the §3 D222 row; by-eye pass across levels owed. **Decided 2026-10-03 (maintainer): option (a)**, snap fog at or above ~97% to full fog (port-only, fast3d vertex fog). Option (b) (N64 16-bit colour/dither output) stays a candidate for the Original N64 bundle. Measurements: findings D503. |
| Render scale / supersampling, integer scaling, "authentic 320×240" mode | post-1.0 | **Decided 2026-10-01:** after 1.0; our own opt-in implementation (default OFF; the Original N64 preset restores the N64 look); thepont's fork is reference only. |
| Dynamic/coloured lighting, dynamic shadows, water reflections | post-1.0 | Large. Consult the PD port first. #105 is about the fork's version of reflections, not ours. **Decided 2026-10-01:** after 1.0; our own opt-in implementation (default OFF; the Original N64 preset restores the N64 look); thepont's fork is reference only. |
| Path-traced render mode (opt-in, RTGL1) | post-1.0 | Experimental, for fun (maintainer, 2026-10-01): **deprioritised to after 1.0**. Plan in the maintainer's local notes: vendor MIT RTGL1 behind `port/rt/`, default OFF; fast3d stays the default backend and the fidelity ground truth; Phase 0 is a toolchain spike (RTGL1 under MSYS2/MINGW64). Nothing starts before 1.0. |
| HD texture packs: loader + texnum dumper; model packs | open (1.0; loader + dumper bumped 2026-10-09, was post-1.0; model packs stay post-1.0) | #100 | There is a staged design. Step 1 (texture identity registry) touches `src/game`, so it needs Rule-2 framing. **Bumped 2026-10-09:** loader + texnum dumper in 1.0 — the §5a order already puts custom texture work in the v0.6.0 sequence (sharing the D578 surface/geometry findings); the geometry/texture-lining fixes for pack support come first; model packs stay post-1.0; our own opt-in implementation (default OFF; the Original N64 preset restores the N64 look); thepont's fork is reference only. |
| Animation smoothing (keyframe slerp) | post-1.0 | **Decided 2026-10-01:** after 1.0; our own opt-in implementation (default OFF; the Original N64 preset restores the N64 look); thepont's fork is reference only. |
| F10 background blur + GUI-only texture filtering | post-1.0 | **Backlogged 2026-10-04 (maintainer): visual nice-to-have after 1.0.** Design done (local `MENU-BLUR-GUIFILTER-DESIGN.md`, ready Sonnet prompts in its §4). Blur: fast3d already decodes PD's framebuffer EXT opcodes and has the 16-tap kernel; only an emitter is missing (`port/fast3d/gbiex.h`); capture once on open (live capture too costly on HD 3000, D481). GUI filter: `Video.GuiTextNearest` point-samples texrects only. Port-only, default off. |
### 5d. Extras (nice to have)
In-game console · demo/replay record and playback (doubles as the
determinism harness, §7) · photo mode · benchmark mode · subtitles · HUD
opacity and a flash-intensity slider · a screen-shake slider that covers
every shake source (D181 only scales explosions) · F11 video recording ·
in-game patch notes and update check · mod loader (parked: branches
`land/modloader-*`, D413/D414; only the content tier M1/C3′ is viable here)
and a stage loader.
## 6. Multiplayer beyond the N64
| Item | Status | Refs | Notes |
|---|---|---|---|
| LAN netplay | open (1.0; bumped 2026-10-09, was post-1.0) | — | Depends on split-screen. Determinism groundwork is D117 (not achieved). **Decided 2026-10-01:** in scope after 1.0. **Bumped to 1.0 (2026-10-09):** single-screen view option alongside split-screen, spectator mode, and demo recording (doubles as the §7 determinism harness, §5d). Online (non-LAN) netplay is in scope via the client/server row below. |
| Online netplay: client/server (Zandronum-style) | open (1.0 if viable — tier 1 pre-1.0, narrow tier-2 core as the 1.0 stretch, else post-1.0 per the plan) | local note `dev/notes/ONLINE-CLIENT-SERVER-PLAN.md` (direction agreed 2026-10-07) | **Added 2026-10-09 (maintainer pivot; supersedes the blanket online exclusion in *Not planned*); maintainer 2026-10-09: wants it in 1.0 if viable — the viability gate is D117 determinism + the plan's tier-2 milestones, tier 1 (port-only) goes regardless.** Target: client/server (Quake/QuakeWorld→Source lineage) with a Zandronum-style feature set; shipped as a build variant in the main repo (`GE_ONLINE=ON` → `ge007-online` client + headless `ge007-server`; no forks); game-logic hooks fenced under `#ifdef GE_ONLINE` so faithful builds stay byte-identical (golden frames prove it; a fenced Rule-2 amendment owed, maintainer call). **Tiers:** 1 port-only (lobby + NAT, server config, master server + browser, quick match, rcon — pre-1.0); 2 the `GE_ONLINE` core (authoritative server, prediction/reconciliation, mid-match join, spectators — start narrow: one level, one scenario, 4 players; 1.0 stretch, post-1.0 if the gate slips); 3 later (more players, unlagged hit checks, server demos). References: the PD port `port-net` branch (ENet, host-authoritative — closest analogue, study first); Zandronum/Odamex design-only (GPL — copy design, never code). **The #141 security hold stands** (forensic review before reusing any of it). |
| MP bots | open (1.0; bumped 2026-10-09, was post-1.0; opt-in) | #111 | Not N64 (GoldenEye's MP is human-only; Simulants are Perfect Dark). birdturtle `simulants` is an outside prototype. **Decided 2026-10-01:** in scope after 1.0 (opt-in; not N64). **Bumped to 1.0 (2026-10-09):** opt-in AI/scripted bots for local MP. |
| Co-op campaign | open (1.0; bumped 2026-10-09, was post-1.0; opt-in) | — | Not N64. thepont `feature/coop` is an outside prototype. Listed under Not planned unless this changes. **Decided 2026-10-01:** in scope after 1.0 (opt-in; not N64). **Bumped to 1.0 (2026-10-09):** opt-in split-screen co-op campaign (007 Plus-style). |
| Cheats beyond the N64 cheat menu | post-1.0 | #113 | Only in scope as an opt-in toggle. **Decided 2026-10-01:** in scope after 1.0, opt-in toggles only. |
## 7. Tooling, infrastructure, security
| Item | Status | Refs | Notes |
|---|---|---|---|
| 1964-vs-port comparison harness | partial | `tools_pc/abpad/` | One millisecond timeline drives 1964 (virtual XInput pad) and the port (`GE_INPUTSCRIPT_MS`); both windows captured on one clock with side-by-side sheets, verified through intro skip, file select and folder open. Not frame-identical (1964 has no seed pin or frame dump). **Left:** in-level timelines (e.g. the D486 turret check); the two sides use different save files. |
| Scripted-playthrough harness | post-1.0 (maintainer 2026-10-04: not a good use of current LLMs, revisit with newer models) | `GE_INPUTSCRIPT`, D117 | Input script + `GE_PCDUMP` + crash-log check for unattended regression runs. Levers: Invincibility + Invisibility cheats, closed-loop waypoint steering, pinned seed. Spike: one Dam route x5, measure end-position spread, then go/no-go. |
| Cheap worker for mechanical, build-checked tasks | open | `delegate-local` MCP; memory of past Qwen issues | Birdman64's fast worker model wrote 44% of its commits while the lead planned and reviewed. Ours: pattern-applied edits (D441 fix batches, scaffolding strips, small tools), each gated by a clean build + a specific check + the lead's diff review, in the worker's own worktree. Local Qwen for edit-only tasks (it fabricates on reading tasks), or a hosted fast model via `delegate_to_provider` (the maintainer's cost call). |
| Golden baselines | partial | `tools_pc/verify.sh`, `tools_pc/golden/README.md`, D117/D522/D529/D531 | **2026-10-06: all three sets (win/linux/deck) re-based for D543/D546**, A/B-confirmed; history in the golden README. **Windows gate GREEN (2026-10-05 re-base):** 21/21 PASS honest sequential. The recipe pins the save CONTENT (D529: `verify.sh` installs the canonical in-tree save `tools_pc/golden/ge007.eep` per level) and per-level windows — `900-1500:300` on 14 levels, `1000-1400:200` on Surface 2/Streets/Depot/Cradle/Frigate, `1500-1700:100` on Dam — because the 900 stem sits inside the wall-clock-paced intro flyby (D117; dam measured 1.6–7.2 % run to run). No tol limit raised. **Linux set (D525 → D530 → D531):** D530's re-round ran under the box-local save (`capture_p7.sh`'s re-pin overwrote D529's canonical install); D531 fixed the tool and re-captured all 21 levels under the canonical eep (47/63 frames moved). **Cross-platform spread recomputed 2026-10-05** from the committed pairs (`framediff.py` win-vs-linux at the stems, tol 2): exact **0.380–5.120 %** on the 20 non-Cuba levels (silo's 900 stem is the outlier: cell mean 34.3, phash 10), structural **21/21 clean**, max phash 40 (cradle, at threshold); Cuba 1.357–12.663 % is the structural-tier-only level. **Pixel gate green on all three, final v0.5.0 build (2026-10-06):** win 21/21; linux 21/21 on the X220 (`DISPLAY=:1` makes the GL probe see the real GPU; one Jungle flyby-jitter miss passed twice solo); deck 21/21 (needs `XAUTHORITY` for the probe over ssh). |
| Flicker scan (A/B/A frame alternation) | parked (2026-10-03: tooling, nothing blocked on it; local spec `scratch/birdman64-rows-investigation.md` §4) | `GE_PCDUMP`, `GE_INPUTSCRIPT` | Dump **every** frame of a scripted run at low resolution and flag frame n when it differs from n-1 but matches n-2. For any old-vs-new pixel diff outside the golden gate, run a same-commit control first. Reference: Birdman64 `tools/flicker_scan.py`. |
| GE_ probe catalog (macos-track diagnostics: keep vs strip) | done 2026-10-08 (strip debt closed; the `GE_TEXDUMP` KEEP family stays in-tree) | [`dev/GE-PROBE-CATALOG.md`](dev/GE-PROBE-CATALOG.md) | The macOS track added env-gated, diagnostic-only `GE_` probes (all off by default, no behavior change when unset). **Keep:** the `GE_TEXDUMP` family (`GE_TEXN`/`GE_TEXDUMPROW`/`GE_TEXRAW` + the GL `.rgba32` upload dump — the standing cross-platform texture-triage tool, cached `getenv`). **Stripped already (2026-10-08, `b80dcd08`):** `GE_NPOT_NOGENMIP` (the D592 NPOT mip-guard, `9bed5904` + companion `82dbea9c`) and `GE_TEXSTATE` (the D592 GL-state probe, `de5a0fea`) — both removed when **D592 closed as a non-issue** (metric false-positive; see the D592 close-out). **Stripped 2026-10-08 (final probe-strip commit; on `macos-arm64`, pushed to Forgejo 2026-10-08):** `GE_TEXP` (the `src/game/image.c` pool-cursor dump — its removal returns that region to the N64 build), `GE_SIZEOF` (the `port/src/main.c` startup ABI-width canary, redundant with the `abi_ratchet.py` CI) and `GE_KEYLOG` (the `video.c`+`input.c` D594 input diagnostic) — 75 pure deletions, behavior-neutral, Mac headless re-verified (2P + solo, 0 crash logs). Stripping is a mechanical, behavior-neutral edit. |
| Release build-provenance attestation | verify | `.github/workflows/ci.yml` release job; `.github/SECURITY.md`; README Status section | **Implemented 2026-10-04:** SHA-pinned `actions/attest-build-provenance` attests `dist/*.zip[.sha256]` + `dist/*.tar.gz[.sha256]` in the tag-only release job (`id-token: write` + `attestations: write` on that job only). Backs SECURITY.md's "unsigned, but reproducible" stance with `gh attestation verify <file> --repo jkdansereau/goldeneye-pc-port`; documented in SECURITY.md and the README. **Owed:** the v0.5.0 tag build is the first real run; confirm the step passes and verification succeeds on a downloaded asset, then delete this row. |
| Self-hosted CI runner link failure | open (v0.7.0, maintainer 2026-10-10) | D244 | Needs a `pacman -Syu` on the runner (maintainer-only access). Release tag builds use GitHub runners, so it doesn't block a release. |
| Remove temporary scaffolding before a release | partial | [`dev/GE-ENV-PROBES.md`](dev/GE-ENV-PROBES.md), D302 | Passes 2026-09-30 to 2026-10-03 removed the closed-finding probe suites (registry in `GE-ENV-PROBES.md`, `gen_env_probes.py --check` passing); TEMP probes `GE_TESTSTYLE` / `GE_D516` removed 2026-10-04. **Left:** D104 (kept while D294 is in verify); the M-183/M-185 clamps were **deleted 2026-10-04** (`879f879a`, `model.c` pure decomp again; the M-189 root fix stays) and the cutscene re-check was covered by the 2026-10-05 Deck campaign playthrough on rc1 (level intros, endings incl. Facility and Dam, credits). |
| P12 U5 review of `tools_pc/**` (release-diff, B10) | partial | D531/D532/D533 | Reviewed 2026-10-05 (the unit that owed the P7 merge). **Blocker found + fixed (D531):** `capture_p7.sh`'s per-level re-pin restored the local save (see Golden baselines). **Should-fixes applied as D532 (tooling-only):** `verify.sh`'s `pin_ini_640x480` fails closed like `pin_eep`; `GOLDEN_SEED` hard-pinned with a warning for a stray `GE_RSEED`; `audiodebug.ps1`'s dead `-AB`/`-Old` switches removed. Box pixel verify done 2026-10-06 (see Golden baselines). |
| Findings log and doc size | partial | `tools_pc/docs_budget.py` | Budget checker (per-file and tier-1 token budgets; `--toc FILE`). `findings.md` is still about 2 MB (~500k tokens), so agents must keep using the index. Nothing blocks; trim only when a budget check fails. |
| Branch and PR housekeeping | open | — | 30 landed local branches pruned 2026-09-30 (tips in `prune-branches-2026-09-30.txt`, notes archive). **On hold by the maintainer (do not prune yet):** probe-only `d243-verify-local`, `investigate/d243-cutscene-race`, `park/d236-probes-m139`. Local `main` and `release/v0.4.1` are deliberately not pushed. PR #62 (mouse dt-decouple) stays held until mouse feel is fundamentally better. |
| Docs site: Web 1.0 phase 2 | decision | — | Phase 1 shipped. Phase 2 (centred serif page) is plan-only. |
| Docs site: press/SEO pass (2026-10-05) | partial | `docs/` site pages | Landed on the pure site pages + layout: real `<h1>` masthead, `color-scheme`/`theme-color`/`max-image-preview`/`rel=me`, ink-mute >=4.5:1, focus-visible + selection, 15px body, mobile padding; `date`/`modified_time` front matter on the pure pages. Home-page additions (pull-quote, Overview fact table, press section) **removed on review 2026-10-05** as duplicates. The five short topic notes (the-software-rsp, the-asset-pipeline, framerate-and-pacing, input-and-aim, saves) were dropped from the published set (files stay as working material). **Open:** the same front matter on the *shared* docs is deliberately untouched (authoritative working docs, not edited for site polish). |
| Agentic-development deep dive (doc) | queued — not yet (2026-10-05) | `docs/dev/agentic-development.md` | Write-up owed for the public case study: per-agent commit/work estimates (manual attribution pass over the session log, incl. D350-style commit-date divergences), a code-quality audit of the trial models (GPT-6 (Sol) et al.), and process feedback / findings. Deferred by the maintainer; nothing here blocks. |
| Docs site: split Security & fidelity -> two pages + v0.5.0 update pass (2026-10-05) | done (local, unpushed) | `docs/security.md`, `docs/fidelity.md` (was `security-and-fidelity-status.md`) | Split on review (the homepage pointer sat near Known issues and read as if there were security *issues*); both pages cross-linked and linked from the hub and index.md's Download section. Update pass 2026-10-05 against the `release/v0.4.1` tree: no network includes/symbols/HTTP literals in compiled `src/`+`port/`; no registry or library-loading calls; converter imports no network module; bundle manifests re-checked (allowlist + ROM/oversized-blob hard-fail); region macros re-compared vs the Makefile (all three regions); library classification re-checked (CMake globs libultra+libultrare audio, gu, `io/vitbl.c` only). Full game-logic audit not re-run; recent `src/game` edits are finding-gated. |
| Docs site: final sanity sweep + anchor fixes (2026-10-05) | done (local, unpushed) | `docs/porting-notes.md` (3 TOC hrefs), `docs/404.html` (permalink) | Audit of the 10-page published set (one h1 per page, no heading skips, in-page anchors resolve, unique titles/descriptions, og:image/canonical, JSON-LD, no local paths, served == built). Three `porting-notes.md` TOC hrefs had a double hyphen where kramdown emits one: corrected at source against the live ids. `docs/404.html` got `permalink: /404.html`. Preview-builder slug rule fixed to kramdown's. |
| LLM-agent dev-velocity workstream (model tiers + subagents + compaction hygiene) | decision — Tier 0/1 landed 2026-10-09; two approvals outstanding | `docs/dev-process.md` §8, `pi/` package, forensics of pi session `01a11ec6` (#150 cull-hunt evening) | Root cause of the #150 loss was model + discipline, not retrieval: a 4-bit-quantized 27B default model fired unvalidated render fixes at the maintainer for A/B (four consecutive failures); the 04:52Z compaction promoted an unvalidated hypothesis to "diagnosed" and post-compaction turns built on it. RAG/GitNexus evaluated and rejected (PolyForm license conflict, auto-writes AGENTS.md, wrong failure class; the repo's `findings-index.csv` is the domain-specific retrieval layer). **Landed per the 2026-10-09 maintainer instruction:** Tier 0 — `~/.pi/agent/settings.json` default = the ninfer backend's model (only one of ninfer/strata runs at a time, and ninfer is the fast one; today that is the coding-launcher 4-bit `qwen3.8-27b-nvfp4full` — the quality launcher `ninfer-quality.bat` serves the groupwise-int `qwen3.8-27b` as the strong local tier, a 30-second launcher restart + new pi session, not a backend switch); §8 rules (model tiers; data-before-edit; context reset after a 2nd failed maintainer A/B or a contested compaction; evidence-ledger compaction prompt); MODEL line in the brief template. Tier 1 — `pi install npm:pi-subagents@0.76.1` (pinned; audit in `docs/dev/notes/pi-subagents-audit.md`: MIT, no eval/telemetry, no install hooks, 310 js files) + tracked `pi/` package: four model-pinned subagent roles on ninfer (`build-runner`/`sweep-watcher`/`doc-sync` on 4-bit; `pairing` pinned `inherit` so the reasoning step follows the parent session's tier) + the `compaction-ledger` extension (evidence-ledger prompt; optional stronger summarizer via `GE_COMPACT_MODEL`). **Awaiting maintainer approval:** (1) `pi-subagents` as a third-party npm dependency; (2) the tier split itself — planning on `openrouter/deepseek/deepseek-v4-pro` (or `strata-coder-hard` with the local backend switched), execution on the standing ninfer backend (default = the model it serves, today the 4-bit `nvfp4full`). **Parked until after v0.7.0:** `pi-plans` trial (228★/2 contributors, exploratory benchmark, self-published evidence), `rpiv-ask-user-question`, Langfuse tracing, `pi-durable-subagents` (1★, unvetted); do NOT stack `billion-context` on the ledger extension (compaction overlap). **Workstream mapping:** first real runs on the v0.6.0 critical path — D578 Dam-tunnel flicker (`feat/interp-fps`; the `holecap` probe is `build-runner`-shaped) and D579 T-step (a)+(c) log pairing (the `pairing` agent's first job); build-runner + sweep-watcher support the v0.7.0 #145/native-pad campaigns. The ledger extension loads at session start, so its verification happens in a fresh session. |
## Community forks (not ours; reference only)
| Fork | What it has | Relevance |
|---|---|---|
| thepont/goldeneye-pc-port `feature/graphics-final-stack`, `fix/original-water-fidelity`, `feature/coop` | ORIGINAL/ENHANCED/REMASTER render mode, lighting, post-FX, bump/shadow/DoF, water reflections, vector HUD font, 2P co-op | Source of #105. Heavy `src/game` edits. **Their D323–D326 and "D329" labels clash with ours**, so relabel anything ported. |
| birdturtle `feature/local-mp-input`, `simulants` | Local MP restore; first MP bot | Cross-check for #99 and #111. |
| kaziema `vita` | PS Vita port | Platform reference. |
| mattymattmattmatt/goldeneye-pcvr-port `vr/stereo-headaim` | VR stereo + head aim | Platform reference. |
## Decisions owed (maintainer)
Decided 2026-09-30 and 2026-10-01 (all moved into the rows above): PAL/JP in Stage B; `All unlocked` as a RAM override; D357 approved with changes; platform parity gate; feature PRs get a design review first; port_addr for #88/#95; D417 approved; the gameplay-visibility policy (§2); §5b, §5c and §6 post-1.0 and opt-in; D433 code audit; D323 post-1.0; #94 closes as stale on release.
Nothing else owed right now (outside PRs decided 2026-10-01, see §5a).
## Not planned
Online matchmaking, leaderboards, anti-cheat, cross-play (client/server
netplay is in scope now — the §6 online row) · Steamworks, achievements, cloud
saves · HDR, frame
generation, stereo 3D (ray tracing only as the opt-in post-1.0 experiment
in §5c) · remake-scope assets (new models, music or voice) · new movement
mechanics (jump, roll, melee) · the 007 Plus ROM patch as such (#110) · a
per-user save folder (decided 2026-10-03: saves stay next to the game,
keeping the security doc's "never writes to `%APPDATA%`" promise) · a
first-launch controls card and controller toasts (decided 2026-10-03: new
UI for little gain; F10 is documented in both READMEs).
+40 -21
View File
@@ -1,13 +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
# v0.2.1 (Bing SEO): keep this SHORT -- jekyll-seo-tag splices it into the
# Keep this SHORT (Bing SEO): 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: N64 GoldenEye 007, decompiled for PC.
theme: jekyll-theme-cayman
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 (the dark "amber terminal" theme: a
# near-black starfield, a single amber accent, mono chrome over a sans body).
show_downloads: false
# Canonical site location - used by jekyll-seo-tag (canonical URLs, og: tags)
@@ -17,7 +19,7 @@ baseurl: /goldeneye-pc-port
# Link-preview card image for jekyll-seo-tag (og:image / twitter:image).
# Path is relative to the site root; the plugin prepends url + baseurl.
image: /img/attract-bunker1.png
image: /img/shots/shot-28.jpg
# Both are on the GitHub Pages plugin whitelist - no Gemfile needed.
# jekyll-seo-tag: per-page <title>/description/canonical/OpenGraph via {% seo %}
@@ -33,40 +35,57 @@ plugins:
# instead, so they resolve from the published pages too.
# ---------------------------------------------------------------------------
# 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.
# The site publishes a small curated set:
# index.md, documentation.md (the documentation hub), building.md, plus
# 404.html (themed not-found page),
# the four docs grouped behind that hub: internals.md, porting-notes.md,
# dev-process.md, dev/agentic-development.md, and
# security-and-fidelity-status.md (split on review 2026-10-05 into
# security.md + fidelity.md, linked from index.md's Download section and
# the hub).
# The five short topic notes (the-software-rsp.md, the-asset-pipeline.md,
# framerate-and-pacing.md, input-and-aim.md, saves.md) were dropped from the
# site on review 2026-10-05: the Perfect Dark port documents the same
# subjects and nothing here was significantly new; they stay in the repo as
# working material. Everything else under docs/ is working material - the
# finding log, level status, backlogs, plans, session notes, 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.
# NOTE: keep this list in sync as dev/ working docs are added; a new unlisted
# .md under docs/ is silently published to the site + sitemap.xml.
exclude:
- BACKLOG.md
- the-software-rsp.md
- the-asset-pipeline.md
- framerate-and-pacing.md
- input-and-aim.md
- saves.md
- HANDOFF.md
- ROADMAP.md
- SetupGuide.md
- StructureGuide.md
- StyleGuide.md
- dev/ACCURACY-AUDIT-2026-09-20.md
- dev/AUDIO-PLAN.md
- dev/AUDIT-M6-player-offsets.md
- dev/D172-PARTICLE-COLOR-RESEARCH.md
- dev/CONTROLLER-INPUT-PLAN.md
- dev/CUT-CONTENT-BACKLOG.md
- dev/D176a-SKY-NOTES.md
- dev/D356-SETTINGS-REGROUP-PLAN.md
- dev/D357-SETTINGS-VALUES-PLAN.md
- dev/GE-ENV-PROBES.md
- dev/GRAPHICS-BACKLOG.md
- dev/GEPD-INPUT-PLAN.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/notes
- dev/OPTIONS-MENU-PLAN.md
- dev/preview-video.md
- dev/QOL-INVENTORY.md
- dev/RUMBLE-PLAN.md
- dev/TEXTURE-GLITCH-ANALYSIS.md
- dev/UNLOCKED-FPS-PLAN.md
- dev/VIEWMODEL-RESEARCH.md
- dev/WIDESCREEN-FOV-PLAN.md
- dev/WATCH-SETTINGS-PLAN.md
- dev/findings-index.csv
- dev/findings.md
- dev/game-behavior-reference.md
+317 -237
View File
@@ -3,286 +3,366 @@
<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=IBM+Plex+Mono:wght@400;600;700&family=IBM+Plex+Sans:wght@400;600&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.
Site theme (replaces the 2026-09-28 "night desk" theme).
Look: a dark, minimal "amber terminal" -- near-black space background
(a faint starfield), a single amber accent (GoldenEye gold), a
console/mono identity for chrome (IBM Plex Mono) over a clean
readable sans body (IBM Plex Sans). Flat and calm: to reduce eye
strain and evoke the old-school web. Content is untouched; this is
a presentation layer only.
--------------------------------------------------------------------- */
: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: #050607; /* true near-black */
--bg-frame: #0c0d0f; /* the page frame */
--bg-panel: #121316; /* code / cards / tables */
--ink: #e6e4df; /* warm off-white */
--ink-soft: #a9a7a0; /* secondary text */
--ink-mute: #8a8a83; /* captions, footer (>=4.5:1 on --bg-frame:
the old #70706a measured 3.9:1 and failed
WCAG AA on the tagline + legal line) */
--line: #26282c; /* 1px borders */
--line-2: #3a3d44; /* stronger borders */
--accent: #e8a13a; /* GoldenEye gold (amber) */
--accent-hi: #ffca7a; /* link / accent hover */
--code-ink: #ffcf9e; /* code text (warm) */
--th: #15161a; /* table header fill */
--zebra: #0e0f12; /* alt row fill */
--measure: 46rem;
--mono: "IBM Plex Mono","Courier New",monospace;
--sans: "IBM Plex Sans",Verdana,Geneva,sans-serif;
}
@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;
color: var(--ink);
background: var(--bg);
font-size: 17px;
line-height: 1.7;
font-family: var(--sans);
font-size: 15px; /* 14px read as console chrome, but the
body is prose people actually read */
line-height: 1.6;
/* faint starfield (the old-school "space" tile, softened) */
background-color: var(--bg);
background-image:
radial-gradient(1px 1px at 22px 34px, rgba(255,255,255,.50), transparent),
radial-gradient(1px 1px at 120px 82px, rgba(255,255,255,.28), transparent),
radial-gradient(1.4px 1.4px at 68px 138px, rgba(232,161,58,.45), transparent),
radial-gradient(1px 1px at 172px 44px, rgba(255,255,255,.20), transparent),
radial-gradient(1px 1px at 92px 172px, rgba(255,255,255,.32), transparent);
background-size: 200px 200px;
-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);
}
.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 {
max-width: 52rem;
margin: 0 auto;
padding: 3rem 1.5rem 1rem;
color: var(--ink);
font-size: 17px;
}
.main-content p,
.main-content ul,
.main-content ol,
.main-content blockquote { max-width: var(--measure); }
.main-content h1,
.main-content h2,
.main-content h3,
.main-content h4 {
color: var(--ink);
font-weight: 600;
letter-spacing: -0.015em;
line-height: 1.3;
margin-top: 2.4rem;
}
.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; }
.main-content a {
color: var(--accent-ink);
a {
color: var(--accent);
text-decoration: underline;
text-decoration-color: rgba(183,121,31,0.4);
text-decoration-color: rgba(232,161,58,.45);
text-underline-offset: 2px;
}
.main-content a:hover {
a:hover { color: var(--accent-hi); text-decoration-color: currentColor; }
/* keyboard focus + selection: the amber thread again, so a focused
control is visible on the near-black ground (a11y / keyboard users) */
:focus-visible {
outline: 2px solid var(--accent-hi);
outline-offset: 2px;
}
::selection { background: var(--accent); color: #0b0b0d; }
/* ---- the centred "page" frame -------------------------------------- */
.frame {
max-width: 50rem;
margin: 20px auto 32px;
background: var(--bg-frame);
border: 1px solid var(--line-2);
box-shadow: 0 0 0 1px #000, 0 18px 50px -30px #000;
}
/* ---- banner -------------------------------------------------------- */
.banner { text-align: center; padding: 2.4rem 1rem 1.4rem; }
.banner .t {
margin: 0; /* it is an <h1> now (see the layout) */
font-family: var(--mono);
font-weight: 700;
font-size: 1.7rem;
letter-spacing: .05em;
color: var(--accent); /* brand name in goldeneye gold -- the amber
thread (masthead -> nav -> rule -> page title) */
}
.banner .tag { color: var(--ink-mute); font-size: .82rem; margin-top: .55rem; }
/* ---- top nav row --------------------------------------------------- */
.sitenav {
text-align: center;
padding: .5rem 1rem;
font-family: var(--mono);
font-size: .78rem;
letter-spacing: .05em;
}
.sitenav a {
color: var(--accent);
text-decoration-color: currentColor;
text-decoration: none;
padding: 0 .35rem;
}
@media (prefers-color-scheme: dark) {
.main-content a { color: var(--accent); }
.sitenav a:hover { color: var(--accent-hi); text-decoration: underline; }
.sitenav .sep { color: var(--ink-mute); }
/* the amber rule */
.rule { border: 0; height: 1px; background: var(--line); margin: 0; }
.rule.g {
background: linear-gradient(90deg, transparent, var(--accent), transparent);
height: 1px; opacity: .55;
}
.main-content blockquote {
color: var(--ink-mute);
border-left: 3px solid var(--accent);
padding: 0.2rem 0 0.2rem 1.1rem;
margin-left: 0;
font-style: normal;
}
/* ---- content ------------------------------------------------------- */
.doc-content { padding: 1.6rem 1.6rem .6rem; }
.doc-content p, .doc-content ul, .doc-content ol,
.doc-content blockquote { max-width: var(--measure); }
.main-content hr {
height: 1px;
background: var(--line);
border: 0;
margin: 2.5rem 0;
.doc-content h1 {
font-family: var(--mono);
font-size: 1.5rem;
font-weight: 700;
color: var(--accent);
letter-spacing: .02em;
margin: .2rem 0 1rem;
}
/* Lead paragraph on doc pages (the italic summary under the H1) */
.main-content > p > em:only-child {
font-style: normal;
color: var(--ink-soft);
.doc-content > h1:first-child { margin-top: .2rem; }
.doc-content h2 {
font-family: var(--mono);
font-size: 1.05rem;
font-weight: 600;
color: var(--ink);
letter-spacing: .03em;
margin: 2rem 0 .8rem;
padding-bottom: .35rem;
border-bottom: 1px solid var(--line-2);
}
.doc-content h2::before { content: "\25aa "; color: var(--accent); }
/* Page-title look: the 2026-09 SEO pass demoted page headings to h2
(the masthead owns h1; no duplicate page titles) -- see the SEO
conventions note in scratch/pr-seo-revise-prompt.md. The first
content h2 of each page gets the h1 look; presentation only, the
heading levels stay per-convention. (On the home page the masthead
itself is the title, so its first h2 -- Status -- leads as a section.) */
/* .flat-title (front matter on the home page): the masthead IS the
page title there, so the first h2 must read as a plain section --
otherwise masthead-h1 + amber first-h2 stack as a double header. */
.doc-content:not(.flat-title) > h2:first-of-type {
font-size: 1.5rem;
font-weight: 700;
color: var(--accent);
margin: .2rem 0 1rem;
padding-bottom: 0;
border-bottom: 0;
}
.doc-content h3 {
font-family: var(--mono);
font-size: .92rem;
color: var(--accent-hi);
margin: 1.6rem 0 .5rem;
}
.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;
font-size: 0.86em;
background: var(--bg-soft);
border: 1px solid var(--line);
border-radius: 4px;
padding: 0.12em 0.4em;
color: var(--ink);
.doc-content hr {
border: 0; height: 1px; background: var(--line-2); margin: 2rem 0;
}
.main-content pre {
background: var(--header-1);
.doc-content blockquote {
color: var(--ink-soft);
border-left: 3px solid var(--accent);
padding: .2rem 0 .2rem 1rem;
margin-left: 0;
}
/* ---- code ---------------------------------------------------------- */
.doc-content code {
font-family: var(--mono);
font-size: .86em;
background: var(--bg-panel);
border: 1px solid var(--line);
padding: .1em .4em;
color: var(--code-ink);
}
.doc-content pre {
background: var(--bg-panel);
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 {
background: transparent;
border: 0;
padding: 0;
color: #e6edf3;
font-size: 0.84rem;
.doc-content pre code {
background: transparent; border: 0; padding: 0;
color: var(--code-ink); font-size: .84rem;
}
/* ---- Tables ------------------------------------------------------- */
.main-content table {
border-collapse: collapse;
display: table;
width: 100%;
max-width: var(--measure);
margin: 1.5rem 0;
font-size: 0.92rem;
/* ---- tables -------------------------------------------------------- */
.doc-content table {
border-collapse: collapse; width: 100%;
max-width: var(--measure); margin: 1.5rem 0; font-size: .9rem;
}
.main-content table th,
.main-content table td {
.doc-content th, .doc-content td {
border: 1px solid var(--line); padding: .5rem .7rem; text-align: left;
}
.doc-content th {
background: var(--th); color: var(--accent);
font-family: var(--mono); font-size: .74rem;
letter-spacing: .05em; font-weight: 600;
}
.doc-content tr:nth-child(2n) td { background: var(--zebra); }
/* ---- console blocks ------------------------------------------------ */
/* Status / News / Download / Known issues panels (home page). Same
amber-terminal language as the code blocks above, but the content
keeps its semantic list/table markup (SEO + screen readers).
Site formatting rules (2026-10-05 PR pass + 2026-09 SEO conventions)
— keep these:
- Heading LEVELS follow the SEO pass: the page heading is h2 (the
masthead owns h1; no duplicate page titles), sections are h2,
h3 labels a real sub-topic only (e.g. the current-release line
under Status). The first content h2 gets the amber "page-title"
look via CSS only (.doc-content > h2:first-of-type) -- never by
promoting the markup to h1.
- h2 = the section, outside the panel.
- Directives ("always use the latest release") are .callouts inside
panels, or a .warn line (amber rule, ink text) where no panel
exists; never headings — headings label topics,
never emphasis.
- List intros ("what a player will notice…") stay plain paragraphs.
- Dated list-item leads ("2026-10-05 — v0.5.0") stay <strong> leads
inside the <li>, not headings.
- Headings must be single source lines: marked ends an ATX heading at
the line break, so a wrapped "### …" silently becomes a heading plus
a stray paragraph (Jekyll/other renderers differ — don't rely on
multi-line headings at all). */
.doc-content .cblock {
background: var(--bg-panel);
border: 1px solid var(--line);
padding: 0.5rem 0.75rem;
padding: 1.1rem 1.3rem .9rem;
margin: 1.6rem 0;
}
.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); }
.main-content img {
border-radius: var(--radius);
/* Expandable panels (<details>, e.g. the release history folded under
Status): same panel language as .cblock; the summary line is the
amber handle. The content is in the DOM either way -- collapsing is
presentation, not hiding from search. */
.doc-content details {
background: var(--bg-panel);
border: 1px solid var(--line);
padding: .3rem 1.3rem .8rem;
margin: 1.6rem 0;
}
.doc-content summary {
font-family: var(--mono);
font-size: .9rem;
font-weight: 600;
color: var(--accent);
letter-spacing: .03em;
cursor: pointer;
}
.cblock h3 { margin-top: 0; color: var(--ink); }
.cblock p, .cblock ul, details p, details ul {
font-family: var(--mono);
font-size: .88rem;
line-height: 1.6;
}
.cblock li, details li { margin: .5rem 0; }
.cblock li::marker, details li::marker { color: var(--accent); }
.cblock strong, details strong { color: var(--accent); font-weight: 600; }
.cblock code, details code { background: transparent; border: 0; padding: 0; }
.doc-content .callout {
border-left: 3px solid var(--accent);
padding: .15rem 0 .15rem .8rem;
margin: 0 0 1rem;
}
.doc-content .callout p { color: var(--ink-soft); }
/* Inline directive marker -- "Warning:" leading a sentence, amber like
the rest of the accent thread, no panel around it. */
.doc-content details .warn {
border-left: 3px solid var(--accent);
padding: .1rem 0 .1rem .7rem;
margin: .2rem 0 .9rem;
font-weight: 600;
}
.cblock table { background: transparent; margin: 1rem 0 .4rem; }
.cblock > p:last-child { margin-bottom: 0; }
/* ---- images -------------------------------------------------------- */
.doc-content img {
border: 1px solid var(--line); border-radius: 0; max-width: 100%;
height: auto; /* no squashed stills when a width= is set */
}
/* ---- 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;
/* ---- console-viewer carousel (home page media) --------------------- */
.viewer {
max-width: 38rem; margin: 1rem auto;
border: 1px solid var(--line-2);
background: var(--bg-panel);
font-family: var(--mono);
}
.site-footer a { color: var(--accent-ink); }
@media (prefers-color-scheme: dark) { .site-footer a { color: var(--accent); } }
@media (max-width: 42rem) {
.project-name { font-size: 1.9rem; }
.page-header { padding: 3rem 1rem 2.75rem; }
.main-content { padding-top: 2rem; }
.viewer .vbar {
display: flex; align-items: center; gap: .5rem;
padding: .4rem .6rem; border-bottom: 1px solid var(--line);
font-size: .72rem; color: var(--ink-mute); letter-spacing: .05em;
}
.viewer .vbar .dots { display: flex; gap: .3rem; }
.viewer .vbar .dots i {
width: 8px; height: 8px; border: 1px solid var(--line-2);
display: inline-block;
}
.viewer .vbar .dots i:nth-child(1) { border-color: var(--accent); }
.viewer .vbar .lbl { flex: 1; }
.viewer .vbar .idx { color: var(--accent); }
.viewer .vstage { position: relative; background: #000; aspect-ratio: 16 / 9; }
.viewer .vstage img { width: 100%; height: 100%; object-fit: contain; display: block; }
.viewer .vbtn {
position: absolute; top: 50%; transform: translateY(-50%);
background: rgba(6,7,8,.55);
border: 1px solid var(--line-2);
color: var(--accent);
font-family: var(--mono); font-size: 1.3rem; line-height: 1;
width: 34px; height: 52px; cursor: pointer; padding: 0;
}
.viewer .vbtn:hover { color: var(--accent-hi); border-color: var(--accent); }
.viewer .vbtn.prev { left: 0; }
.viewer .vbtn.next { right: 0; }
.viewer .vcap {
display: flex; justify-content: space-between; align-items: center;
padding: .45rem .6rem; border-top: 1px solid var(--line);
font-size: .74rem; color: var(--ink-soft); letter-spacing: .02em;
}
/* ---- 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 {
background: transparent;
border: 0;
color: var(--ink);
padding: 0;
/* ---- footer -------------------------------------------------------- */
.site-footer { text-align: center; padding: 1.2rem 1rem 1.6rem; }
.footer-stamp {
font-family: var(--mono); font-size: .78rem;
letter-spacing: .03em; color: var(--ink-soft);
margin: 0 0 .7rem;
}
.main-content pre.mermaid { text-align: center; overflow-x: auto; }
.main-content pre.mermaid:not([data-processed]) { visibility: hidden; }
.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: .72rem; line-height: 1.55;
margin: 0 auto; max-width: var(--measure);
}
@media (max-width: 40rem) {
.frame { margin: 8px; }
.banner { padding: 1.4rem .8rem 1rem; }
.banner .t { font-size: 1.35rem; }
.doc-content { padding: 1.1rem 1rem .5rem; }
.doc-content table { font-size: .82rem; }
.doc-content pre { padding: .7rem .8rem; }
}
</style>
<script type="module">
import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs";
// 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";
pre.textContent = code.textContent;
code.parentElement.replaceWith(pre);
});
mermaid.initialize({
startOnLoad: false,
securityLevel: "loose", // our own docs; needed for <br/> in node labels
theme: dark ? "dark" : "neutral",
});
mermaid.run({ querySelector: "pre.mermaid" });
</script>
+71
View File
@@ -0,0 +1,71 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Paint the browser chrome (scrollbars, form controls, new-tab cards)
dark before CSS loads, so the page never flashes white on mobile. -->
<meta name="color-scheme" content="dark">
<meta name="theme-color" content="#050607">
<!-- Indexing is NOT restricted: this only asks Google/Bing to show the
large image preview in search cards, which is what makes the
in-engine stills legible in a coverage roundup. -->
<meta name="robots" content="max-image-preview:large">
<!-- Favicon: the project's own crosshair mark (tools_pc/make_icon.py),
not a game still -- original art, no derivative-work exposure. -->
<!-- favicon.ico is a byte copy of cmake/icon/ge007.ico (the exe icon, with
the hand-tuned 16/24/32 px variants); a browser-downscaled 512 px PNG
washes out at tab size. Re-copy it after running make_icon.py. -->
<link rel="icon" href="{{ '/favicon.ico' | relative_url }}" sizes="16x16 24x24 32x32 48x48 64x64 128x128 256x256">
<link rel="apple-touch-icon" sizes="512x512" href="{{ '/img/icon/ge007-icon.png' | relative_url }}">
<!-- Entity disambiguation: the maintainer behind every page. -->
<link rel="me" href="https://github.com/jkdansereau">
{%- seo -%}
{%- include head-custom.html -%}
</head>
<body>
<div class="frame">
<!-- The masthead IS the page's h1: every page gets exactly one h1
(the brand), and the page's own heading starts at h2. A <div>
here left the document with no h1 at all, which is what search
and screen readers key on. Presentation only -- the CSS class
carries the look. -->
<header class="banner">
<h1 class="t">{{ site.title }}</h1>
<p class="tag">{{ site.description }}</p>
</header>
<nav class="sitenav" aria-label="Site navigation">
<a href="{{ '/' | relative_url }}">home</a><span class="sep">/</span>
<a href="{{ '/documentation' | relative_url }}">documentation</a><span class="sep">/</span>
<a href="{{ '/building' | relative_url }}">building</a>
</nav>
<div class="rule g"></div>
<!-- flat_title: this page's masthead IS its title, so the layout opts
out of the amber first-h2 "page-title" look (no double header).
Jekyll + the preview builder both honour this conditional. -->
<main class="doc-content{% if page.flat_title %} flat-title{% endif %}">
{{ content }}
</main>
<div class="rule g"></div>
<footer class="site-footer">
<div class="footer-stamp">
<a href="https://github.com/jkdansereau">maintained by jkdansereau</a>
&nbsp;&middot;&nbsp; last updated: 2026-10-13 &middot; v0.6.0
&nbsp;&middot;&nbsp; <a href="https://github.com/jkdansereau/goldeneye-pc-port">GitHub</a>
&nbsp;&middot;&nbsp; <a href="https://github.com/jkdansereau/goldeneye-pc-port/releases">releases</a>
</div>
<div 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.
</div>
</footer>
</div>
</body>
</html>
+225
View File
@@ -0,0 +1,225 @@
---
title: About the project
description: Release history, how the GoldenEye 007 PC port was made, how it works, and how it differs from the other GoldenEye PC projects.
---
# About the project
- [Release history](#release-history)
- [Status in detail](#status-in-detail)
- [How it was made](#how-it-was-made)
- [How it works](#how-it-works)
- [How this differs from the other GoldenEye PC projects](#how-this-differs-from-the-other-goldeneye-pc-projects)
## Release history
The full notes for each version are on the [Releases page](https://github.com/jkdansereau/goldeneye-pc-port/releases).
- **2026-10-13** — **v0.6.0** ([release notes](https://github.com/jkdansereau/goldeneye-pc-port/releases/tag/v0.6.0)):
- **Frame rates above 60.** `Video.FpsCap` now offers 90, 120, 144, 165 and
240, and an **Auto** setting (the new default on fresh installs) that
matches your display's refresh rate, up to 240. Frames between game ticks are blended from the
previous and current frame; the game still simulates at its original
rate. It costs one game tick of extra latency, does nothing on a 60 Hz
display, and the *Original N64* preset caps at 60. This is a rework of
f1zz1ec0ke's #137. I checked 120 Hz with VSync on a G-Sync display
on PC and 90 fps on the Steam Deck.
- **macOS on Apple Silicon (experimental).** There is now a `macos-arm64`
download (or build from source; see [Building](building.md)). Based
on danturn's #95 (with Julio C. Rocha's earlier Intel groundwork in
#88). Tested with a 21-level sweep and live sessions on one M3 Mac (the
one crash seen, in the end credits, is guarded in this release); the download is not notarized, so see [macOS](user-guide.md#macos-apple-silicon-experimental)
for the one-line unblock. Test reports are very welcome.
- **Fixes:** geometry vanishing when you press against a wall (#150),
the first launch opening on the wrong monitor (#151), a crash in the end
credits on Linux and macOS (#152), a crash when a guard spawns with a hat (#153, #156), a
sound/text pointer check that rejected valid pointers (#108), a Cradle
catwalk shadow flicker, stale pixels beside the widescreen picture on
front-end screens, and a black window on macOS when the game was started
with the display asleep.
- **Original controls (#145, #149):** with *Control style* set to Original,
a gamepad now behaves as an N64 controller (South = A, West = B, the game
decides what each does), and *Invert look* works in every original style.
Also an optional mission timer (#144), the file-select crest no longer
drawing on a dark square (#146), and a fix for a Windows blue screen on
exit.
- **Smaller changes:** the legacy front-end Velocity pointer mode is gone,
the Linux/macOS update check no longer fails on a large response, every
run is also logged to `ge007.log` next to the ini, and F10 slider labels
fit at HUD 150.
- **Known issues on the Steam Deck at 90 fps:** firefights in the centre of
Bunker 1 and 2 can drop several fps, and Jungle can feel slightly less
smooth than other levels. Cradle's turret explosions may draw wrongly
(reported, not yet investigated).
- **2026-10-07** — **v0.5.0** ([release notes](https://github.com/jkdansereau/goldeneye-pc-port/releases/tag/v0.5.0)), the first release
since v0.4.0; it includes the v0.4.1 work, which was never published on
its own:
- **Split-screen multiplayer for up to 4 players**, each player with their
own pad, controls and aim settings.
- **One options menu** for every setting, laid out like the Perfect Dark
port's, with the game's own crosshair as the mouse pointer. Pick the
game's own N64 control styles (1.1–2.4) per player, or the Xbox release's
presets with per-player pad rebinding; PlayStation and Nintendo pads show
their own button names.
- **Closer to the N64:** fog and haze (Surface 2's ground fog, the sky at
the horizon), automatic-weapon fire rate, sniper and camera zoom, Dam and
Caverns water, the Jungle boss fight's timing, guard visibility in
widescreen, and the Dam ending camera.
- **Fewer crashes and glitches:** a multiplayer crash with the crouched
rifle, rockets passing through the ground, and broken geometry around
Aztec's shuttle.
- **A steady 60 fps on low-end GPUs** (tested on an Intel HD 3000 laptop),
with lighter first-launch defaults on Atom/Celeron-class graphics.
- **Emulator saves load directly:** copy a 1964 or Project64 save in as
`data/ge007.eep` and it is converted on first launch.
- Also: more audio, video and control settings (master volume, audio device,
trilinear filtering, draw distance up to 800%, PC-friendly crouch and
look-ahead defaults), and controllers are picked up again when replugged.
- **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).
- **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.
## Status in detail
**Fully playable, with a small set of known caveats.** All 20 solo missions
and the end-of-campaign credits load, render and run crash-free. For v0.6.0
the release downloads themselves were tested on Windows, Steam Deck (Desktop
and Game Mode), macOS on Apple Silicon and a 2011 Linux laptop: a fresh
folder with only the ROM, a first launch, then all 21 solo levels. The new frame-rate path and the listed fixes were also checked by
playing on Windows and the Steam Deck. The full campaign was last played end
to end, at Agent difficulty on Windows, Linux and the Deck, for v0.4.0.
Split-screen was played live in 2P on every multiplayer map and in 4P on
Temple.
**Known issues:** see the **[known-issues table](ROADMAP.md#known-issues)**
(what you'll notice, impact, workarounds). The short version: NTSC-U ROMs
only in the release packages, and the Apple Silicon macOS download is
experimental and not notarized. On the Steam Deck at 90 fps, Bunker 1/2
centre firefights can drop several fps and Jungle can feel slightly less smooth
than other levels.
> ***All unlocked* never changes your save** since v0.5.0: it unlocks every
> level and cheat in memory only, and switching it off shows your real
> progress. v0.4.0 and earlier could write fake unlocks into the save, and
> those are not repaired; if you used it on an older build, keep a backup of
> `data/ge007.eep`.
What a release installs (no networking unless you turn on the opt-in update check, which is off by default and makes one HTTPS request to GitHub per launch; no telemetry, no ROM or game assets)
and how faithfully the port tracks the original game's logic:
[Security](security.md) and [fidelity](fidelity.md) status.
The binaries are not code-signed; from v0.5.0 on each release artifact also
carries a GitHub build-provenance attestation you can check with
`gh attestation verify <file> --repo jkdansereau/goldeneye-pc-port`
(see [SECURITY.md](../.github/SECURITY.md)).
## How it was made
The port was built by several coding agents directed by one maintainer, with
contributions from a handful of people (credited in the release notes).
A local open-weight model (`unsloth/Qwen3.8-27B-GGUF` on one RTX 5090, via the
[pi](https://github.com/earendil-works/pi) agent) did the groundwork: build, boot chain,
software-RSP integration, asset pipeline, first frames. **Claude Code**
(Sonnet 5 and Opus 5, now 5.5; Opus for the hardest bugs) joined for the collaborative phase:
the 21-level sweep, the ABI finding catalog, SDL input, front end. The two
handed work back and forth through shared written notes. Up to v0.5.0: 51
days (16 Aug – 6 Oct), 1,537 commits (the public history squashes the
v0.4.1–v0.5.0 work into 17), and 419 findings root-caused and logged
(`D1`–`D564`). Every change is checked by comparing captured frames from all
21 solo levels against reference images, crash sweeps on Windows, Linux,
Steam Deck and macOS, and play-testing; there is no unit-test suite.
The full write-up (timeline, handoff mechanism, effort breakdown, an honest
"what worked / what didn't"): [`docs/dev/agentic-development.md`](dev/agentic-development.md).
The workflow itself: [`docs/dev-process.md`](dev-process.md). To cite the
project or its findings: [`CITATION.cff`](../CITATION.cff) (GitHub's "Cite this
repository" menu).
### Tools and models used to develop the port
Development ran for most of its life on a single local model — **Unsloth's
Qwen 3.8 27B** — which did nearly all of the local (groundwork) work on its
own. In the final week that grew into a small menu, and the **current standard
is NInfer + Strata**; tools are chosen per task, with local models carrying
the high-volume work and hosted models the exception. The full record of the
models used, and the harnesses that drove them:
| Model | Ran as | How it was used |
|---|---|---|
| **Qwen 3.8 27B** ([Unsloth](https://huggingface.co/unsloth/Qwen3.8-27B-GGUF)) | `UD-Q4_K_XL`, one RTX 5090 | The primary local workhorse for the bulk of the port's development. |
| **Qwen 3.8 27B** via [NInfer](https://github.com/natpate/ninfer-windows) (Windows) | base, NVFP4 and NVFP4-full artifacts; MTP (DFlash2) spec-decode; one 5090 | Trialed in the final week, now half of the standard; the fast day-to-day local driver. |
| **Qwen 3.8 27B** ([ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-27B-GSQ-RCO-GGUF)) | `IQ3_S` (GSQ-RCO), larger context | Briefly trialed for larger-context workloads; shelved, little use since v0.2.0. |
| **Qwen 3.8 Flash-Next** via [Strata](https://github.com/Niko1221/Strata) | 125B MoE, `IQ1_M` (1-bit), one gaming GPU | The big-context local tier: a model that normally needs a server, run locally for the largest-context passes. |
| **Claude** (Anthropic) | Sonnet 5 / Opus 5, now **Sonnet 5.5 / Opus 5.5** (out in the last few weeks) | The orchestrator in the two-agent phase: held what the 27B couldn't close, took over on limit handoff; the 5.5 tier has driven the most recent work. |
| **GPT-6 (Sol)** (OpenAI), via [OpenRouter](https://openrouter.ai) | a frontier hosted model | Trialed. |
*Harnesses and tooling:*
- **[pi](https://github.com/earendil-works/pi)** — the local coding-agent
harness that drives the local models.
- **[Claude Code](https://github.com/anthropics/claude-code)** (Anthropic) —
the agentic harness that drove the Claude models above; the orchestrator in
the two-agent phase.
- **[claude-code-delegate-local](https://github.com/fegone/claude-code-delegate-local)** —
the MCP bridge that let Claude and the local models talk (delegating Claude's
subagents to the local engines).
## How it works
The game code in `src/` is the decompilation's, and its control flow is the
reference we preserve. Everything that would touch N64 hardware is redirected into
`port/`: a **software RSP** (`port/fast3d/`, adapted from the Perfect Dark
port) that interprets the graphics command stream (the GBI display list)
the game builds each frame and
emits OpenGL, bypassing the RDP; libultra OS shims (`port/src/libultra.c`,
game threads as real host threads) that let the game's own scheduler run,
with the software RSP on its own render thread as the RSP was its own chip;
and SDL2/OpenGL/filesystem backends for video, audio, input and storage.
Every port change inside `src/` is gated behind `#ifdef PORT` and
documented. They are few in kind, all narrow in scope: mechanical 32→64-bit
pointer/struct fixes (the pointer-width transition touches many structs, so
there are a lot of them), a few maintainer-approved timing fixes where
per-frame N64 code ran too fast at the PC's 60 fps (for example sniper zoom
and turret fire), the opt-in *All unlocked* hook, and env-gated diagnostic
probes tied to the bug catalogue (off by default, no behavior change). Where it diverges from the Perfect Dark port: GoldenEye's N64
serialized asset formats are converted in a separate step rather than
fixed up at load time — on first start for a download, or by the offline
Python converters in `tools_pc/` for a source build. Full detail:
[`docs/internals.md`](internals.md) and
[`docs/porting-notes.md`](porting-notes.md).
```
CMakeLists.txt PC build (parallel to the decomp's Makefile, which is untouched)
build-pc.sh configure + build helper
src/ include/ the decompilation (game + libultra); port edits are #ifdef PORT
port/
fast3d/ software RSP -> OpenGL
src/ port layer (main, OS shims, video, audio, input, fs, ...)
include/ port-facing headers
tools/ Makefile the N64 build + asset extraction (from the decomp; do not modify)
tools_pc/ PC-port helper + analysis scripts
docs/ see below
```
## How this differs from the other GoldenEye PC projects
This is a native port of the original 1997 Nintendo 64 game, built from its
reconstructed source code, the same lineage as the Perfect Dark PC port.
Other GoldenEye PC projects take a different approach: they statically
recompile the Xbox 360 XBLA remaster, a separate version of the game with its
own codebase. The two don't share code, and they target different versions
of the game.
| | This project | The XBLA recompilation projects |
|---|---|---|
| **What it ports** | The original **Nintendo 64** game (1997) | The **Xbox 360 XBLA** remaster |
| **How** | Decompilation-based source port: reconstructed C, compiled for the host | Static recompilation of the Xbox 360 executable |
| **Lineage** | [GoldenEye 007 decompilation](https://github.com/n64decomp/007) + [Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark) engine family | The Xbox 360 static-recompilation family |
+52 -12
View File
@@ -1,6 +1,6 @@
---
title: Building
description: Full build and asset-extraction guide for the GoldenEye 007 PC port, covering Windows (MSYS2) and Linux.
description: Full build and asset-extraction guide for the GoldenEye 007 PC port on Windows, Linux, and macOS.
---
## Building the PC port
@@ -18,7 +18,7 @@ Stages:
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](https://github.com/jkdansereau/goldeneye-pc-port#requirements)
[ROM table in the user guide](user-guide.md#whats-a-rom)
for accepted versions and hashes.
---
@@ -27,21 +27,48 @@ for accepted versions and hashes.
### Port build
> **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.
> **The Windows (MSYS2 MINGW64) path is the primary one.** Linux is compiled
> by CI and ships in the release bundle. **macOS on Apple Silicon is
> experimental as of v0.6.0**: the release has a `macos-arm64` download
> (`scripts/package-macos.sh` makes it on a Mac; CI runs it) that is not
> notarized, so clear the quarantine flag once before the first launch with
> `xattr -dr com.apple.quarantine <folder>`. This guide covers building it from
> source. On an arm64 Mac the build moves the N64 address window to a
> high host base (`PORT_ADDR_BASE`, set automatically); Windows and Linux builds
> are unaffected. The x86_64 macOS path (Julio C. Rocha, #88) is still in the
> tree but was never verified on Intel hardware.
| Need | Windows (MSYS2 MINGW64) | Debian/Ubuntu | macOS (Homebrew) |
|------|------------------------|---------------|------------------|
| toolchain | `mingw-w64-x86_64-toolchain` | `build-essential` | Xcode CLT / `gcc` |
| toolchain | `mingw-w64-x86_64-toolchain` | `build-essential` | Xcode CLT + Homebrew `gcc` |
| CMake | `mingw-w64-x86_64-cmake` | `cmake` | `cmake` |
| SDL2 | `mingw-w64-x86_64-SDL2` | `libsdl2-dev` | `sdl2` |
| zlib | `mingw-w64-x86_64-zlib` | `zlib1g-dev` | `zlib` |
| OpenGL | (in the toolchain) | `libgl1-mesa-dev` | (system) |
| Python 3 | `mingw-w64-x86_64-python` | `python3` | `python3` |
On macOS, install the build dependencies with:
```sh
brew install cmake gcc sdl2 zlib python3
```
The project requires real GNU GCC for the decomp's Plan 9 struct extensions.
Apple's `/usr/bin/gcc` is Clang and is not compatible. CMake automatically
selects Homebrew's versioned `gcc-N`/`g++-N` (or MacPorts' `gcc-mp-N`/`g++-mp-N`)
executables; set `CC`/`CXX` to pick a specific one. Delete the build directory
after switching compilers.
If GCC fails with `fatal error: _bounds.h: No such file or directory`, it was
built against a newer macOS SDK than the one CMake picked (the `xcode-select`
default). Point `SDKROOT` at the SDK GCC was built with (`gcc -v` shows it as
`--with-sysroot`), for example with MacPorts GCC 15:
```sh
SDKROOT=/Library/Developer/CommandLineTools/SDKs/MacOSX15.sdk \
CC=/opt/local/bin/gcc-mp-15 CXX=/opt/local/bin/g++-mp-15 ./build-pc.sh ntsc-final
```
### Asset extraction (decompilation toolchain)
The extraction scripts need `binutils-mips-linux-gnu` (or an equivalent MIPS
@@ -51,7 +78,7 @@ 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`](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`](https://github.com/jkdansereau/goldeneye-pc-port/blob/main/docs/SetupGuide.md).
alternatives (Docker) are in `SetupGuide.md`.
---
@@ -93,6 +120,15 @@ cmake -S . -B build-pc -DROMID=ntsc-final
cmake --build build-pc -j
```
On Windows, `build-pc.sh` also locates the MSYS2 MinGW64 toolchain itself
(so it works from Git Bash or a non-login shell too, even with another
`cmake` earlier on PATH) and, if native child processes would get an
unwritable `TMP`, re-runs the configure+build under PowerShell with a
writable one. If MSYS2 is not installed at `C:\msys64`, set
`GE_MSYS2_ROOT` (e.g. `GE_MSYS2_ROOT='C:\tools\msys64'` for a custom
install location). Re-exec logs:
`build-pc/ge007-native-reexec-*.log`.
For PAL/JP you must first generate that region's ROM-asset symbol file
(the US one is committed):
@@ -128,11 +164,15 @@ to d69's output):
```sh
python3 tools_pc/d43_emit.py ntsc-final # -> data/pcmodels-ntsc-final/{pcmodels.bin,manifest.csv} (~1.3 MB)
python3 tools_pc/d69_emit.py ntsc-final # -> data/pccg-ntsc-final/{pccg.bin,manifest.csv} (bg + stan)
python3 tools_pc/d88_emit.py ntsc-final --regen # appends the 21 per-level Usetup*Z stage-setup files -> ~3.6 MB
python3 tools_pc/d88_emit.py ntsc-final --regen # appends the 21 solo Usetup*Z + 13 multiplayer Ump_setup*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.
**PAL / JP note:** both regions convert, build and boot from source (finding
D258, fixed 2026-10-02; D487/D488 fixed the PAL frame height and TV type).
Run the three emit passes with `pal-final` or `jpn-final`, and build into a
separate directory, for example `BUILD_DIR=build-pal ./build-pc.sh pal-final`.
Release packages are still NTSC-U only until per-region packaging lands
(issue #85).
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
+65
View File
@@ -185,6 +185,13 @@ justifies. Rules:
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.
- **Commit/tree hygiene is a standing rule, not a per-session judgment** —
see AGENTS.md §Tree hygiene & release gate. In short: no local absolute
paths or usernames in tracked files (use the placeholder convention), no
contributor emails in prose (name + PR number), every vendored third-party
license declared in `NOTICE`, no bulk ROM-derived captures in the tree
(capture output dirs are gitignored — keep them on disk, untracked), and
one batched commit per session rather than one per approved fix.
## 7. The rule-2 sign-off procedure (genuine `src/game` behavior changes)
@@ -248,6 +255,62 @@ 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.
## 8. LLM session discipline (model tiers, data-before-edit, context resets)
Standing rules for LLM-agent sessions, fixed 2026-10-09 after the session
forensics on the #150 cull hunt (pi session `01a11ec6`, 2026-10-09): a
27B 4-bit-quantized default model spent an evening firing unvalidated
render fixes at the maintainer for A/B (four consecutive failures), and
the mid-session compaction summary promoted an unvalidated hypothesis to
"diagnosed" — the post-compaction turns then built three more fix
candidates on that false premise. The loss was model + discipline, not
retrieval; the rules below are the zero- and low-cost countermeasures.
- **Model tiers.** Planning, diagnosis, and anything that touches a
contested claim run on the strongest model available — the frontier
`openrouter/deepseek/deepseek-v4-pro` (no local contention) or, with
the local backend switched, `strata-coder-hard`. Only ONE local backend
can run at a time (ninfer ↔ strata), so the **standing backend is ninfer** and the
session default is its model (`qwen3.8-27b-nvfp4full`, the
`ninfer-coding.bat` 4-bit daily driver). The strong *local* tier is
the same backend under `ninfer-quality.bat` (groupwise-int
`qwen3.8-27b`, 200k ctx) — a launcher restart, not a backend
switch; the strata 125B remains the top local option at the cost of a
full switch. Mechanical labor — builds, captures,
log triage, golden sweeps, doc sync — goes to the ninfer models via
the model-pinned subagent roles in the `pi/` package (see
`pi/README.md`): 4-bit for build/capture/triage/doc-sync, and
`pairing` pinned to `inherit` so the reasoning step follows the
parent session’s tier. 4-bit is never a planning or diagnosis model.
- **Data before edit.** If a probe log for the current hypothesis
already exists on disk, the next action is *analyzing it*, not building
another fix candidate. The maintainer gates only the final candidate;
intermediate variants are judged by the golden sweep (regressions are
machine-detected), not by maintainer A/B — one A/B is the gate, not
the test harness.
- **Context resets.** After a second failed maintainer A/B, or after a
compaction whose state is contested, do not continue the session: write
a HANDOFF brief and start a fresh session from it. Compaction summaries
must carry an **evidence ledger** — one line per live hypothesis tagged
`refuted` / `unvalidated` / `validated (<artifact: log, golden frame,
maintainer A/B, commit>)`, and `"diagnosed"` is forbidden for any fix
lacking a maintainer A/B or a passing golden sweep. The
`pi/extensions/compaction-ledger/` extension encodes this prompt (and,
with `GE_COMPACT_MODEL`, summarizes on a stronger model than the
session's labor model).
- **Regression triage first.** Before any probe or theory on a bug,
ask when it first appeared. If a release window is known, list the
commits in `git log <good>..<bad>` that touch the subsystem and run the
off switch each of them shipped with, one run per switch. The #150
wall-hug see-through (D579) spent days on cull/ucode/guard-band probes; a
single `GE_NEARCLIP=0` run named the v0.5.0 feature (D543) and the fix
was one line. Corollaries: when an A/B refutes a layer, leave that layer;
when a probe shows the code behaving correctly, pivot instead of probing
deeper; a delegated decode is a cross-check, never the lead.
- **Briefs are mandatory** (§1): no investigation kickoff without a
BUDGET line and a filled brief template — including the MODEL line
below, so the tier choice is visible in the brief itself.
## Investigation-brief template
```
@@ -256,6 +319,8 @@ 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>.
PRE-FLIGHT ATTACHED: <probe output, baseline capture, PD-port pointer>.
MODEL: <tier + model id, per §8 — planning/diagnosis on the strong
model; mechanical labor delegated to the pi/ subagent roles>.
BUDGET: <N build->run cycles / ~M min>. On expiry: revert probes, write up with confidence.
CONSTRAINTS: diagnosis may trace as deep into src/game state as the evidence
needs — name the exact struct/field/logic responsible before concluding
+4 -2
View File
@@ -1,5 +1,7 @@
# AUDIO-PLAN — Phase 3 / ROADMAP B3 (libaudio → SDL)
> **Historical record — do not act on status claims in this file.** The audio track shipped (D198–D204); the "gaps" and TODOs described below are closed. Kept for the architecture rationale. Current open work and status: [`docs/ROADMAP.md`](../ROADMAP.md) (2026-09-29).
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.
@@ -36,7 +38,7 @@ More is already in place than "audio not started" suggests:
`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`,
- **The PD reference is local** at `<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`.
@@ -199,4 +201,4 @@ M-50) → v0.4.0".
| `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 |
| `<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`).
+45 -2
View File
@@ -16,6 +16,48 @@ export at `scratchpad/backlog-scrape/restore-cut-content-mod/` (gitignored).
> 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
@@ -250,8 +292,9 @@ Cross-reference:
- 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 —
likely cut before model export, or embedded in a shared body file.
**Unverified** which exact models the TCRF face shots show.
**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.
-170
View File
@@ -1,170 +0,0 @@
# 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.
+1 -1
View File
@@ -1,6 +1,6 @@
# D176(a) Sky — consolidated research notes (read-only)
Companion to `GRAPHICS-BACKLOG.md` D176(a) and findings.md M-37/M-47.
Companion to the (since retired) `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.
+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`
(`<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.
+194
View File
@@ -0,0 +1,194 @@
# D357 — Settings thresholds & values: align display units/defaults with the PD port + Turok PC
Status: **IMPLEMENTED 2026-09-30 (findings D443) for mouse sens ×, deadzone whole-%, volume step, FOV degrees, MSAA 16×, Restart game; texture filter unchanged by decision; frame-rate cap deferred (§0 presets + custom = separate project). Approved with changes; see §0.** Proposal dated 2026-09-27. Baselines: the PD port
checkout (`<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).
## 0. Sign-off (2026-09-30) — these override §3 where they differ
- **FOV:** display in **degrees** (the PC standard). Storage stays unchanged.
- **Frame rate cap:** presets **30 / 60 / 90 / 120 / 144 / 240 / Uncapped**,
**plus** an arbitrary custom value. This keeps the ini's free range and
adds a custom entry in the menu, instead of the plan's presets-only grid.
- **MSAA:** add **16×**.
- **Mouse invert:** stays a separate On/Off toggle.
- **Texture filter: do NOT rename.** The §3 premise ("3-point *is*
trilinear") is wrong. 3-point is the N64's own 3-sample bilinear
approximation, and "Linear" and "Bilinear" name the same thing. Keep
"N64 3-point" and check the current enum strings before touching them.
- **Refresh §3's "Now" column before implementing.** The stick deadzone is
already split L/R, and the draw/LOD defaults are 250, not 150.
- Related QoL, tracked separately in ROADMAP §5a: a **Restart game** action
next to Quit to desktop, for restart-flagged rows such as MSAA.
## 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.
### Implemented 2026-09-30 (D443) -- refreshed "Now" vs. result
The per-row tables below keep the 2026-09-27 audit text for history. Actual
pre-change state and what shipped (all port-only; no default or storage change,
so `kResetDefaults` is untouched and `GE_WSPROBE_RESET` stays failures=0):
| Row | Actual "Now" (pre-D443) | Shipped |
|---|---|---|
| Mouse horizontal sensitivity | 1-500 · step 5 · shown as calibrated `N/100` (50/100 at raw 100) | UI range 10-300, step 10, shown `%.1fx` (raw/100), linear bar; left the calibrated mapping |
| Stick deadzone L / R | already split `Input.PadDeadzoneL/R` 0-30000 · step 500 · calibrated `N/100` | step 300 + `dispDiv=300` `%` (whole-% grid, no key migration); left the calibrated mapping |
| Music / FX volume | step 128, `/328` | step 328 (1 display-%) |
| FOV | `Video.FovScale` 50-150 · step 5 · `%` | row "Field of view", horizontal degrees (see D443 formula); key/storage unchanged, step walks the displayed degree |
| Anti-aliasing | 1/2/4/8 · default 2 (C initialiser) | + 16x; range 1-16 (GL clamps to `GL_MAX_SAMPLES`) |
| Texture filter | Nearest / Bilinear / 3-Point | unchanged (decision §0) |
| Frame-rate cap | 30/60 toggle (sim ticks at 60 Hz) | unchanged this round |
| Restart game | -- | new action row after Quit to desktop (both UIs, shared table) |
### 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 -45
View File
@@ -3,7 +3,7 @@
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-20 (v0.3.0 release review). All are **env-gated**: unset = zero behavior
File:line cells. Last reconciled 2026-09-30 (ROADMAP §7 strip pass: D157/D157T/D157I/D157TB, D207S, D227V, D245_OLDVTX/OLDTEX, D252POOL, D288, D309, D318B suite, D320R removed and marked as tombstones; D318W demoted to detect-only). Previously 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
@@ -19,98 +19,182 @@ Two classes:
| Env var | File:line | What it does | Status |
|---|---|---|---|
| `GE_INPUTRECORD` | `port/src/input.c:1547` | Records pad input to a file (D583 Deck harness; `levelbench.sh --record`). | live tooling |
| `GE_INPUTREPLAY` | `port/src/input.c:1548` | Replays a `GE_INPUTRECORD` file (`levelbench.sh --replay`; pin `GE_RSEED`). | live tooling |
| `GE_INTERPLOG` | `port/src/libultra.c:1482` | D578 IPLOG pacing trace (`=<n>` caps the lines). | live diag |
| `GE_INTERP_ALPHA1` | `port/src/libultra.c:1489` | D578: every interpolated pass exact (identity check). | live diag |
| `GE_INTERP_FORCE` | `port/src/video.c:1058` | D578: interpolate even when the display refresh is not above the VI rate. | live diag |
| `GE_INTERPCLIP_ON / GE_INTERPCLIP_OFF` | `port/fast3d/gfx_pc.cpp:1816` | D578 in-between depth-clip widening (default off since 2026-10-10). | live switch |
| `GE_INTERPPORTAL_OFF` | `port/fast3d/gfx_pc.cpp:4156` | D578: disable the portal-scissor replay on in-between passes. | live switch |
| `GE_INTERPROOM_OFF` | `port/fast3d/gfx_pc.cpp:6059` | D578: disable the room-change exact fallback. | live switch |
| `GE_INTERPREACH_OFF` | `port/fast3d/gfx_pc.cpp:6128` | D583: disable the REACH exact fallback. | live switch |
| `GE_INTERPOBJ_OFF` | `port/fast3d/gfx_pc.cpp:2626` | D955: in-between presents draw objects at their tick pose again (no per-joint motion blend; counters still run). A group whose joints step more than 120 units in a tick is drawn exact. | live switch |
| `GE_INTERPPROPCAM_OFF` | `port/fast3d/gfx_pc.cpp:2436` | D954: in-between presents draw props at the exact camera again (no room-camera blend on the plain perspective). | live switch |
| `GE_PCDUMP_PRESENTS` | `port/src/video.c:1539` | With `GE_PCDUMP`: one capture per present instead of per game frame. | live tooling |
| `GE_NOTICKLOCK` | `port/src/libultra.c:1321` | D600: disable the display-locked retrace tick. | live switch |
| `GE_ADDRSTRICT_SELFTEST` | `port/src/port_addr.c:122` | D607 (macOS): address-window self-test at startup. | live diag |
| `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_INPUTSCRIPT_MS` | `port/src/input.c:1194` | abpad: entry times in `GE_INPUTSCRIPT` are milliseconds since SDL init instead of controller reads (`tools_pc/abpad`). C-button holds `CUPHOLD`/`CDOWNHOLD`/`CLEFTHOLD`/`CRIGHTHOLD`/`CNONE` (1.2 walk/strafe) also live in the script parser. | **live** |
| `GE_PAD_BACKGROUND` | `port/src/input.c:1472` | `=1` sets `SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS` so the port reads pads while unfocused: one real pad drives the port and 1964 side by side (`tools_pc/abpad/live_ab.ps1`). | **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_STARTMP` | `src/game/lv.c:403` (`#ifdef PORT`) | D416: `GE_STARTMP=<players>[,<mpStageIdx>]` starts a local multiplayer match with the menu's default options (as MP options -> START does); pair with `GE_MPVIRT=<players-1>` for the controller count. Unattended split-screen testing. | **live** (D416) |
| `GE_MPVIRT` | `port/src/input.c:686` | D416: report `n` extra idle controllers (1..3) so the MP menu unlocks / a 2P+ stage loads on a machine with no gamepads. Test harness only; P2..Pn get no input. | **live** (D416) |
| `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_FAKE_DECK` | `port/src/video.c:490` (`videoDetectSteamDeck`) | D283: `=1` forces the Steam Deck path on any host (Windows/Linux desktop) so the once-per-ini Deck preset logic (`Video.DeckPresetApplied`, applied only while `Video.Fullscreen=0` and `Window.Maximized=0`) can be tested; `=0` forces it off on a real Deck (skips DMI + `STEAMOS`). Unset = real detection (DMI `Valve` + `Jupiter`/`Galileo`, then `STEAMOS`). **Writes the ini** (preset + flag) — use a throwaway data dir. | **live** (test hook) |
| `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_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. | **dead** (D204 closed) — removed 2026-09-30 (ROADMAP §7 strip pass, batch 1, branch `chore/scaffolding-removal-1`); tombstone — the generator flags it GONE |
| `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_AUDIOGAINLOG=1` | `port/src/audio.c` (`audioSetNextBuffer`, cached `GE_ENVFLAG`) | D470: once per second logs `D470 gain: master=N peak=P`, the max abs sample of the final (post master-volume) buffer. Cheap; shows the gain effect headlessly. | **live** |
| `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_OBJT` | `src/game/chraction.c` (`chrSetStageFlags`/`chrUnsetStageFlags`), `src/game/propobj.c` (GAS_RELEASING destruction) | D321 stage-flag writer probe: logs every low-16-bit write to `objectiveregisters1` (tick, writing chr, actiontype/aioffset, AI-list ID) plus each toxic-gas prop destruction timestamp — used to map the Facility tanks→combat-bit trigger chain. Capped, read-only, inert unset; post-release removal candidate (M-192 item 7). | **live** (D321) |
| `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_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. | **dead** (D207 closed) — removed 2026-09-30 (ROADMAP §7 strip pass, batch 1, branch `chore/scaffolding-removal-1`); tombstone — the generator flags it GONE |
| `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_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. | **dead** (D243 FIXED) — removed 2026-10-01 (scaffolding strip batch 3, branch `chore/strip-d243-probes`); kept as a tombstone — the generator flags it GONE |
| `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. | **dead** (D243 FIXED) — removed 2026-10-01 (scaffolding strip batch 3, branch `chore/strip-d243-probes`); kept as a tombstone — the generator flags it GONE |
| `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. | **dead** (D243 FIXED) — removed 2026-10-01 (scaffolding strip batch 3, branch `chore/strip-d243-probes`); kept as a tombstone — the generator flags it GONE |
| `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. | **dead** (D243 FIXED) — removed 2026-10-01 (scaffolding strip batch 3, branch `chore/strip-d243-probes`); kept as a tombstone — the generator flags it GONE |
| `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_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. | **dead** (D243 FIXED) — removed 2026-10-01 (scaffolding strip batch 3, branch `chore/strip-d243-probes`); kept as a tombstone — the generator flags it GONE |
| `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. | **dead** (D243 FIXED) — removed 2026-10-01 (scaffolding strip batch 3, branch `chore/strip-d243-probes`); kept as a tombstone — the generator flags it GONE |
| `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`). | **live** (D219/D252, non-reproducible as of M-158) |
| `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). | **dead** (D252 fixed) — removed 2026-09-30 (ROADMAP §7 strip pass, branch `chore/strip-temp-probes`); tombstone — the generator flags it GONE |
| `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: logs any triangle whose clip-space NDC bbox covers >15% of the screen or goes non-finite, with each vertex's `w` and `any_behind_camera`. **Investigation in progress** — narrowed to two candidates (an unguarded `w`-division NaN leak in the backface-cull test just below, or the D106/D271 portal-BFS family), not yet root-caused; needs a live/headless `-level_20` run to disambiguate. | **live** (D288) |
| `GE_D236RM` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_tri1`, unconditional getenv, no state kept when unset) | M-155/pass-10 diag for D236: per-frame (`num_dls`) triangle count of the `0x0c184b50` render-mode class (`other_mode_l` exact match), heartbeat-logged every 300 frames even at zero. **REFRAMED D280's "run-to-run dependent instantiation" as ordinary room-based occlusion**, not a CPU-side gating bug — see `## D236` pass-10 writeup. | **live** (D236 OPEN, kept for future passes) |
| `GE_D236ORDER` | `port/fast3d/gfx_pc.cpp:1844` (`gfx_sp_tri1`, cached `s_d236order`) | D236 pass-12 diag: per-DL sequence log of the tree-class triangles (DL index, screen-space depth, noise min/max census) to test the draw-order hypothesis. Zero cost unset; remove once D236 concludes. | **live** (D236 OPEN) |
| `GE_D236ALPHA` | `port/fast3d/gfx_pc.cpp:1896` (`gfx_sp_tri1`, cached `s_d236alpha`) | D236 pass-14 diag: for the `0x0c184b50` tree-class render mode, logs the combine/alpha state of up to 4000 matching triangles (deduped on combine change) so the captured draw can be cross-checked against the source. Zero cost unset; remove once D236 concludes. | **live** (D236 OPEN) |
| `GE_D236ZFIX` | `port/fast3d/gfx_pc.cpp:1970` (`gfx_sp_tri1`, cached `s_d236zfix`) | D236 pass-12 test override: forces `depth_update=true` for the `0x0c184b50` tree-class render mode — a cheap mechanism test before deciding what a real fix looks like. Off by default; remove once D236 concludes. | **live, test-only** (D236 OPEN) |
| `GE_D236RAW` | `src/game/bg.c:2437` (`#ifdef PORT`) | D236 pass-16 diag: one-shot census of the raw vertex table's alpha-byte distribution (5 buckets + min/max), camera-independent, to check whether any vertex in the whole table exceeds the low ceiling. Zero cost unset; remove once D236 concludes. | **live** (D236 OPEN) |
| `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_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) — removed 2026-10-03 (strip batch 5, `chore/strip-batch5`); tombstone — the generator flags it GONE |
| `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**. | **dead** (D288 closed) — removed 2026-09-30 (ROADMAP §7 strip pass, branch `chore/strip-temp-probes`); tombstone — the generator flags it GONE |
| `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. | **dead** (D245 fixed) — removed 2026-10-02 (strip batch 4, `chore/strip-batch4`); tombstone |
| `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). | **dead** (D245 fixed) — removed 2026-10-02 with the inert sky tile-capture/shift code (strip batch 4); tombstone |
| `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. | **dead** (D252 closed) — removed 2026-09-30 (ROADMAP §7 strip pass, batch 1, branch `chore/scaffolding-removal-1`); tombstone — the generator flags it GONE |
| `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. | **dead** (D252 fixed) — removed 2026-09-30 (ROADMAP §7 strip pass, branch `chore/strip-temp-probes`); tombstone — the generator flags it GONE |
| `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. | **dead** (D245 fixed) — removed 2026-09-30 (ROADMAP §7 strip pass, branch `chore/strip-temp-probes`); tombstone — the generator flags it GONE. The pre-fix s16 sky/water vertex path (`skyPortRenderPoly` tail + `skyPortEmitTileShift`) went with it; the float path is now unconditional |
| `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). | **dead** (D341 closed) — removed 2026-09-30 (ROADMAP §7 strip pass, batch 1, branch `chore/scaffolding-removal-1`); tombstone — the generator flags it GONE |
| `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). | **dead** (D207/D241 closed) — removed 2026-09-30 (ROADMAP §7 strip pass, branch `chore/strip-temp-probes`); tombstone — the generator flags it GONE |
| `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. | dead — fully removed 2026-10-01 (ROADMAP §7 scaffolding strip, batch 2; kept here as a tombstone) |
| `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. | **dead** (D235 closed) — removed 2026-10-02 (strip batch 4); tombstone. The D235 fix (`videoResetTextureCache()` per stage load) and the `GE_D235` log stay |
| `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`. | **dead** (D235 closed) — removed 2026-10-02 (strip batch 4); tombstone |
| `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`). | **dead** (D235 closed) — removed 2026-10-02 (strip batch 4); tombstone |
| `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. | **dead** (D320 fixed via D329) — removed 2026-09-30 (ROADMAP §7 strip pass, branch `chore/strip-temp-probes`); tombstone — the generator flags it GONE; `port/src/d320repro.c` + its `lv.c` hook deleted |
| `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). 2026-10-01: also prints `PERFSTAT spikes: max=..ms at frame N (dl= run=) >20ms= >33ms=` per 300-frame window (hitches hide in the averages). | **live** |
| `GE_NOSHADERWARM=1` | `port/fast3d/gfx_pc.cpp` (`gfx_shader_list_open`) | D480 falsifier: disables the shader pre-warm (no read or write of `$S/ge007.shaders`), so first-seen combiners compile mid-frame as before. | **live** (D480) |
| `GE_RENDERINLINE=1` | `port/src/libultra.c` (`portRenderWorkerOn`) | D481 falsifier: run gfx tasks inline on the scheduler thread (the pre-D481 path) instead of on the render worker. `GE_DETERM` forces this too. | **live** (D481) |
| `GE_FAKE_LOWEND=1/0` | `port/src/video.c` (`videoApplyLowEndDefaults`) | D482: force the low-end GPU classification on (1) or off (0) for the one-time first-launch defaults check (`Video.LowEndConsidered`). Testing only, like `GE_FAKE_DECK`. | **live** (D482) |
| `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. **D473 (2026-10-01): the cache was broken (re-read on every G_VTX when unset); now a real one-shot cache.** | **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_PADMAPTEST` | `port/src/input.c` (`inputInit` -> `padMapSelfTest`) | D469: boot-time self-test of the gamepad action table. Proves preset 0 == the legacy hard-wired mapping (20,971,520 synthetic cases), logs the per-source mask table, a Custom remap, per-seat independence, the Start guard and the binding API; restores config afterwards (nothing persisted); the game then continues. Cached (`GE_ENVFLAG`). | **live** (harness) |
| `GE_D466LOG=<n>` | `port/src/drawdistgameplay.c` (`portD466FrameLog`, called from `bgDetermineVisibleRooms`) | D466 dev probe: every `<n>` calls (per player view) logs `D466 f=.. active= ext= auth= subset= hext= hauth= chrOn= chrGp= seen= hgame=` to stderr: extended vs authored room-set counts + hashes, ONSCREEN chrs vs gameplay-on-screen chrs, `HAS_BEEN_ON_SCREEN` count, and a chr-state hash. Cached. | **live** (D466 dev probe) |
| `GE_D468_SCALE=<0.05..1>` | `port/src/drawdistgameplay.c` (`portD468UpdateClamp`) | D468 dev falsifier: multiplies the AI-view-clamp fractions (and so turns the clamp on even at 16:9 / FOV 100), so a headless `GE_D466LOG` run can show the authored room set and gameplay on-screen chrs shrinking. Cached; out-of-range values ignored. | **live** (D468 dev probe) |
| `GE_SHADEPERSP=1` | `port/fast3d/gfx_opengl.cpp` (`gfx_opengl_init`, shader build) | D540 A/B switch: perspective-correct shade-colour interpolation instead of the default RDP-style screen-affine (`noperspective`) one (the sky horizon fade). Read once at GL init. | **live** (D540 A/B) |
| `GE_FOGPIXEL=1` | `port/fast3d/gfx_opengl.cpp` (`gfx_opengl_init`), `port/fast3d/gfx_pc.cpp` (`gfx_sp_vertex`) | D543 A/B switch: the D540 exact per-pixel fog (fog * w, divided by w in the shader) instead of the default per-vertex RSP fog (honest negative-w divide, CPU near clip lerps it). Read once at GL init. | **live** (D543 A/B) |
| `GE_FOGVERTEX` | (removed) | **removed** -- D540's per-vertex-fog opt-in; per-vertex fog is the D543 default, the opt-out is `GE_FOGPIXEL=1`; tombstone kept. | **removed** |
| `GE_NEARCLIP=0` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_tri1`) | D543 A/B switch: disable the CPU RSP-style near-plane clip (default on) and leave near-plane clipping to the GPU. Cached on first triangle. | **live** (D543 A/B) |
| `GE_NEARCLIPFOG=recompute` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_tri1`) | D543 A/B switch: recompute the per-vertex RSP fog value of near-clip-created vertices from their new z/w (as `gfx_sp_vertex`) instead of lerping it (default). Cached on first triangle. | **live** (D543 A/B) |
| `GE_FOGSNAP=1` | `port/fast3d/gfx_opengl.cpp` (fragment shader build) | D540 A/B switch: re-enables the D503 full-fog snap (fog >= 247/255 -> full), off by default since D540 (its hard edge swept across the ground in chunks). Cached on first shader build. | **live** (D540 A/B) |
| `GE_PADFAMILY=xbox\|ps\|nintendo` | `port/src/input.c` (`inputPadFamily`) | D471 dev override: force the controller family used for button names in the PC menus (headless screenshots without the hardware). Cached. | **live** (D471 dev knob) |
| `GE_D466_FORCE=1` | `port/src/drawdistgameplay.c` (`portD466Active`) | D466: run the authored-distance split even at DrawDistance 100% (identity test: authored set must equal extended). Cached. | **live** (D466 test hook) |
| `GE_D466_OFF=1` | `port/src/drawdistgameplay.c` (`portD466Active`) | D466 kill switch: disable the gameplay/render split (every gameplay reader falls back to the extended-range state, i.e. the pre-fix #125 behaviour). For A/B only. Cached. | **live** (D466 kill switch) |
| `GE_INPUTSCRIPT_P1` | `port/src/input.c:1216` | Split-screen test harness: scripted input for player slot 1, same entry syntax as `GE_INPUTSCRIPT` (slot 0); mouse tokens and `CHOLD`/`CREL` ignored with one warning. Used with `GE_STARTMP` + `GE_MPVIRT`. | **live** (test harness) |
| `GE_INPUTSCRIPT_P2` | `port/src/input.c:1217` | As `GE_INPUTSCRIPT_P1`, for player slot 2. | **live** (test harness) |
| `GE_INPUTSCRIPT_P3` | `port/src/input.c:1217` | As `GE_INPUTSCRIPT_P1`, for player slot 3. | **live** (test harness) |
| `GE_MEMPREDZONE=1` | `src/memp.c:340`, `src/memp.c:369` (+ periodic check in `port/src/video.c`) | D464 red-zone checker: pads every stage-bank allocation with a 64-byte pattern trailer and logs `MEMPREDZONE: HIT ...` at level unload, every 60 frames and at quit. Unset = one cached branch per allocation. | **live** (D464 dev tool) |
| `GE_CRASHTEST` | `port/src/crash.c` (`crashTestMaybe`, called at the end of `crashInit`) | Deliberate-crash hook for verifying the crash handler: `GE_CRASHTEST=<N>` recurses N frames deep (capped at 64) right after the handler is installed and faults with a null write. `ge007.crash.log` should then show `#00`..`#N-1` in `crashTestRecurse`, then `SDL_main`, CRT and system frames; each frame line carries `va=0x…` for `addr2line -f -p -e build-pc/ge007.x86_64.exe <va>`. Unset/0 = no effect. | **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_OPTIONSOVERLAY_DROPDOWN` | `port/src/optionsoverlay.c:1195` | Screenshot-only: with the overlay auto-opened on a page, opens the dropdown popup of the named row key (e.g. `Video.TextureFilter`, `__Resolution`) and focuses it. | **live** (diagnostic) |
| `GE_OPTIONSOVERLAY_ATFRAME` | `port/src/optionsoverlay.c:3362` | Re-applies the GE_OPTIONSOVERLAY / _SECTION / _SELECT request at emit frame n so it survives a level load (in-level screenshots, D558). Read once. | **live** (diagnostic) |
| `GE_OPTIONSOVERLAY_SELECT` | `port/src/optionsoverlay.c:1221` | Screenshot-only: selects the named row key on the auto-opened page (e.g. `Game.NoHitFlash`) so its tip shows (D558). | **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_PRESETPROBE` | `port/src/optionsoverlay.c:957` (`presetProbe`) | D440: one-shot at overlay init — drives the "Original N64" and "Port defaults" preset rows through the real arm -> confirm activation and checks the "Active" state; logs `GE_PRESETPROBE: PASS/FAIL`. **Writes the live config** (persisted on exit) — use a throwaway data dir. | **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` | (removed) | **removed** -- the F10 footer was replaced by the hint line under the box (menu PD parity); tombstone kept. | **removed** |
| `GE_OVNAV` | `port/src/frontoptions.c:106` | D519 follow-up harness: scripted overlay navigation on file select (O toggle, D/U move, A open, B back, P log the place), one token per 20 file-select frames. | **live** (harness) |
| `GE_FILESELECT_OVERLAY` | `port/src/frontoptions.c:96` | One-PC-options-UI (D519) test hook: opens the F10 overlay over file select without a click; value `n` > 1 also closes it `n` label-frames later. Replaces the retired front-screen probes (the FRONTOPTIONS_SECTION, FRONTNAVLOG, FRONTOPTS_WIDTHS and FRONTOPTIONS_AUTO probes). | **live** (harness) |
| `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) |
| `GE_D526` (+ `GE_D526BOX`, `GE_D526MAX`) | `port/fast3d/gfx_pc.cpp:1895,2024,2032,2034` | D526 per-triangle draw-state census probe (text-only): `GE_D526="lo-hi"` = sim-frame range (same dash syntax as `GE_D75D`); `GE_D526BOX="x0 y0 x1 y1"` = NDC region filter (same syntax as `GE_ZFBOX`, default on-screen); `GE_D526MAX` caps per-triangle lines (default 400). Per emitted in-region tri: frame, NDC bbox, z/w, geometry_mode, other_mode_l/h, combine word, sampled tile tmem/fmt/siz, first_tile_index, prim alpha, vertex alpha min/max, use_alpha/modulate/invisible — plus one per-frame line aggregating in-region tris by (comb, oml, omh, alpha) signature. Retained as a generic transparency/texture triage tool (D195/D266/D268/D273 class); the D526 grate case itself was confirmed faithful N64 behaviour, so no fix shipped. Zero cost unless `GE_D75D`/`GE_ZF`/`GE_D303`/`GE_D526` is set (folded into the D474 `tri_dbg` gate). | **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_D589POOL` | `src/game/image.c:339,1202` | D589 (macOS): texture-pool decode trace. | open diag |
| `GE_D590HDR` | `src/game/bg.c:899` | D590: `load_bg_file` header trace. | closed |
| `GE_TEXDUMPROW` | `port/fast3d/gfx_pc.cpp:1480` | D591/D592 (macOS): per-row texture dump. | closed |
| `GE_TEXIMP2` | `port/fast3d/gfx_pc.cpp:1588` | D592 (macOS): import-order trace. | closed |
| `GE_TEXSRC` | `src/game/image.c:1042,1177` | D591 (macOS): texture source-address trace. | closed |
| `GE_ZLIBDUMP` | `src/game/image.c:317` | D611 (macOS): inflate input/output dump. | closed |
| `GE_D51` | `port/src/libultra.c:496`, `src/game/model.c:125,244,544` | msgQ 32-slot overflow watch / ModelSlot layout | dead (D51 closed) — removed 2026-10-03 (strip batch 5, `chore/strip-batch5`); tombstone — the generator flags it GONE |
| `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_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) — removed 2026-10-03 (strip batch 5, `chore/strip-batch5`); tombstone — the generator flags it GONE |
| `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. | **dead** (D227 fixed) — the probe lived in the pre-D245 s16 sky path, unreachable since D245 M-201; removed 2026-09-30 (ROADMAP §7 strip pass, branch `chore/strip-temp-probes`); tombstone — the generator flags it GONE |
| `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) |
| `GE_D62` | `port/src/libultra.c:477` | osRecvMesg trace | dead (D62 closed) |
| `GE_D63` | remaining: `src/game/bg.c:2525,2908,2953`, `front.c:1410,1416,7814,7889,8083,8091,8099,8366`, `image.c:2453`, `language.c:431`, `model.c:4340`, `title.c:607` | gun-barrel sub-DL clobber hunt (D63/D64/D65/D66 — the labels drift within the blocks). Stripped M-30 (`d0789358`, `gfx_pc.cpp` trail). Stripped M-32 (`49ce620a`): `memp.c`, `bg.c:2880` (incl. the bare non-PORT `d63bgprimarycount` static + entry log), `blood_animation.c`, `dyn.c`, `rsp.c`. **Remaining blocks are all `#ifdef PORT` + getenv-guarded and inert — strip candidates for a later pass.** | dead (D63 closed) — partially removed |
| `GE_D69` | `src/game/ob.c:165,227` | stage-load (D69) object trace | dead (D69 closed) |
| `GE_D69BB` | `src/game/bg.c:2460,2479,3020` | D69 bg-binary layout trace | dead |
| `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_D178` | `src/game/front.c:6586,6594` (`#ifdef PORT`) | briefing-data u16 decode trace (blank-objectives, D143/D178) | dead (D178 closed) — removed 2026-10-03 (strip batch 5, `chore/strip-batch5`); tombstone — the generator flags it GONE |
| `GE_D56` | `src/game/model.c:227,830` | watch `Model` raw-offset reads | dead (D56 closed) — removed 2026-10-03 (strip batch 5, `chore/strip-batch5`); tombstone — the generator flags it GONE |
| `GE_D60` | `port/src/libultra.c:521,735` | gfx frame msgQ delivery trace | dead (D60 closed) — removed 2026-10-03 (strip batch 5, `chore/strip-batch5`); tombstone — the generator flags it GONE |
| `GE_D61` | `port/src/libultra.c:795` (`s_d61opened`) | opens a one-shot log file | dead (D61 closed) — removed 2026-10-03 (strip batch 5, `chore/strip-batch5`); tombstone — the generator flags it GONE |
| `GE_D62` | `port/src/libultra.c:477` | osRecvMesg trace | dead (D62 closed) — removed 2026-10-03 (strip batch 5, `chore/strip-batch5`); tombstone — the generator flags it GONE |
| `GE_D63` | remaining: `src/game/bg.c:2525,2908,2953`, `front.c:1410,1416,7814,7889,8083,8091,8099,8366`, `image.c:2453`, `language.c:431`, `model.c:4340`, `title.c:607` | gun-barrel sub-DL clobber hunt (D63/D64/D65/D66 — the labels drift within the blocks). Stripped M-30 (`d0789358`, `gfx_pc.cpp` trail). Stripped M-32 (`49ce620a`): `memp.c`, `bg.c:2880` (incl. the bare non-PORT `d63bgprimarycount` static + entry log), `blood_animation.c`, `dyn.c`, `rsp.c`. **Remaining blocks are all `#ifdef PORT` + getenv-guarded and inert — strip candidates for a later pass.** | dead (D63 closed) — partially removed — removed 2026-10-03 (strip batch 5, `chore/strip-batch5`); tombstone — the generator flags it GONE |
| `GE_D69` | `src/game/ob.c:165,227` | stage-load (D69) object trace | dead (D69 closed) — removed 2026-10-03 (strip batch 5, `chore/strip-batch5`); tombstone — the generator flags it GONE |
| `GE_D69BB` | `src/game/bg.c:2460,2479,3020` | D69 bg-binary layout trace | dead — removed 2026-10-03 (strip batch 5, `chore/strip-batch5`); tombstone — the generator flags it GONE |
| `GE_D69STAN` | `src/game/stan.c:266,276` | D69 stan-tile trace | dead — removed 2026-10-03 (strip batch 5, `chore/strip-batch5`); tombstone — the generator flags it GONE |
| `GE_D85DUMP` | `src/game/bg.c:2510` | D85 `bgWidenRoomGdl` 8→16B dump | dead (D85 closed) — removed 2026-10-03 (strip batch 5, `chore/strip-batch5`); tombstone — the generator flags it GONE |
| `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) |
| `GE_D90` | remaining: `src/game/bondview2.c:2146`, `src/game/prop.c:1391` (bondview_r.c blocks stripped M-32, `49ce620a`) | D90 bondview / prop NULL trace | dead (D90 closed) — partially removed |
| `GE_D96` | `src/game/chrprop.c:436,507` (cached `probe`) | D96 chrprop trace | dead (D96 closed) |
| `GE_D86` | `src/game/model.c:6295`, `src/game/objecthandler_2.c:143` | D86 model rwdata trace | dead (D86 closed) — removed 2026-10-03 (strip batch 5, `chore/strip-batch5`); tombstone — the generator flags it GONE |
| `GE_D87` | `src/game/ramromreplay.c:296,363,381` | D87 `ramromfilestructure` endianness | dead (D87 closed) — removed 2026-10-03 (strip batch 5, `chore/strip-batch5`); tombstone — the generator flags it GONE |
| `GE_D88` | `src/game/prop.c:1367`, `src/game/stan.c:3090` | D88 `Usetup*Z` propDef stream | dead (D88 family closed; D88.4 resolved) — removed 2026-10-03 (strip batch 5, `chore/strip-batch5`); tombstone — the generator flags it GONE |
| `GE_D90` | remaining: `src/game/bondview2.c:2146`, `src/game/prop.c:1391` (bondview_r.c blocks stripped M-32, `49ce620a`) | D90 bondview / prop NULL trace | dead (D90 closed) — partially removed — removed 2026-10-03 (strip batch 5, `chore/strip-batch5`); tombstone — the generator flags it GONE |
| `GE_D96` | `src/game/chrprop.c:436,507` (cached `probe`) | D96 chrprop trace | dead (D96 closed) — removed 2026-10-03 (strip batch 5, `chore/strip-batch5`); tombstone — the generator flags it GONE |
| `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_D71LOG` | `port/fast3d/gfx_pc.cpp:645` | D71 fast3d trace | dead (D71 closed) — removed 2026-10-03 (strip batch 5, `chore/strip-batch5`); tombstone — the generator flags it GONE |
| `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_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. | **dead** (D309/D310 fixed) — removed 2026-09-30 (ROADMAP §7 strip pass, branch `chore/strip-temp-probes`); tombstone — the generator flags it GONE |
| `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_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_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. | **dead** (D318 closed) — removed 2026-09-30 (ROADMAP §7 strip pass, batch 1, branch `chore/scaffolding-removal-1`); tombstone — the generator flags it GONE |
| `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. | dead — fully removed 2026-10-01 (ROADMAP §7 scaffolding strip, batch 2; kept here as a tombstone) |
| `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` (`d318WatchdogTick`, `#ifdef PORT`) | **The D318/D320 pin detector, ON by default, DETECT-AND-LOG ONLY since 2026-09-30.** Watches the Facility ai_22 / ai_19 and Control ai_9 hold-pose pin signatures (findings D318/D320) and logs `D318W:` lines (first detection, confirmation after 600 ticks, periodic reminders while held). It used to re-seed the attack via `sub_GAME_7F025560`; that recovery was removed because D329 fixed the cause, so a hit now means a D329 regression. `GE_D318W=0` silences it. | **live** (D329 regression detector) |
| `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.** | **dead** (D318/D320 fixed via D329) — removed 2026-09-30 (ROADMAP §7 strip pass, branch `chore/strip-temp-probes`); tombstone — the generator flags it GONE; `port/src/d318bprobe.c` + `port/include/d318bprobe.h` deleted. NB `GE_D318B_CLK` (sim-ticks-per-frame lockstep, used for the D329 pin repro) went with the suite |
| `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
+41
View File
@@ -0,0 +1,41 @@
# GE_ diagnostic-probe catalog (keep vs strip before a `main` merge)
The `macos` track added a set of env-gated, diagnostic-only `GE_` probes to
triage the D589/D591/D592 wide-pixel + GL-layer work. All of them are **off
by default** (a `getenv` returns NULL → no code runs) and **change no game
behavior** when unset. The decision owed before any merge toward `main`
(§4, post-1.0) is, per probe: **keep** (a useful shipping debug tool) or
**strip** (diagnostic-only, no shipping value, or a `src/game` edit that is
not a port-layer fix). This file is that catalog — the tracker row is ROADMAP
§7 "GE_ probe catalog."
## The macOS-track texture / ABI probes
| Probe | Where | Gate | What it does | Disposition |
|---|---|---|---|---|
| `GE_TEXDUMP` | `port/fast3d/gfx_pc.cpp` (import) + `port/fast3d/gfx_opengl.cpp` (GL upload) | master, cached `getenv` | Per-import `GE_TEXI`/`GE_TEXN` log lines; `texdump/rNNN_*` normalized bins; GL-level `tNNN_*.ppm` + `.rgba32` (the exact `glTexImage2D` input, D592 Q1). Zero hot-loop cost when unset (the `getenv` is cached once). | **KEEP** — the standing cross-platform texture-triage tool (predates the macOS track; B2/D161). |
| `GE_TEXDUMPROW` | `port/fast3d/gfx_pc.cpp` | sub-gate (needs `GE_TEXDUMP`) | Dumps the FINAL post-transform source bytes keyed by the stable RAW source address (`texdump/row_<rawaddr>_f<fmt>_s<siz>.bin`) — the D592 cross-platform byte-compare key. | **KEEP** — cheap, off by default, and the raw-addr key is the right way to compare a specific texture across platforms (the positional rNNN index is not a stable key). |
| `GE_TEXRAW` | `port/fast3d/gfx_pc.cpp` | sub-gate (needs `GE_TEXDUMP`) | Writes the RAW source bytes handed to the importer (`texdump/rNNN_*.bin`). | **KEEP** — part of the same triage family. |
| `GE_TEXP` | **`src/game/image.c`** | independent `getenv` | D589 L2: logs pool-cursor state after each pool load. | **STRIPPED 2026-10-08** — `image.c` is back to the N64 build in this region; the pool-cursor dump served the D589 L2 cross-check only. |
| `GE_SIZEOF` | `port/src/main.c` | `#ifdef PORT` + `getenv` | Startup ABI-width canary (`sizeof` of key structs/pointers) for a quick 32/64-bit sanity check on a fresh platform. | **STRIPPED 2026-10-08** — the ongoing ABI-width check is the `tools_pc/abi_ratchet.py` CI ratchet; `main.c` also drops the `game/image.h` include it needed. |
| `GE_NPOT_NOGENMIP` (+ companion single-level sampler fix) | `port/fast3d/gfx_opengl.cpp` | `#ifdef PORT` + `getenv`, default off | The D592 NPOT `glGenerateMipmap` guard (skip mips on NPOT textures) + a companion that forced a single-level min filter under the guard. **REVERTED 2026-10-08 (`b80dcd08`)** when **D592 closed as a non-issue**: the "rock striping" was a VHF-metric false-positive (natural rock grain), and the `GE_TEXSTATE` probe showed the sampler state + NPOT mip level counts are byte-identical Win/Mac, so the guard had nothing to fix. | **REMOVED** — no longer in the tree (was `9bed5904` + companion `82dbea9c`). |
| `GE_TEXSTATE` | `port/fast3d/gfx_opengl.cpp` (upload + set_sampler) | `#ifdef PORT` + `getenv`, default off, needs `--debug-gl` (logs at `LOG_NOTE`) | D592 Q2: logged the **effective sampler state** for the bound texture + per-NPOT-shape mip level counts. Confirmed the sampler state **and** the NPOT mip levels are **byte-identical Win/Mac** (the Metal backend builds the full NPOT mip chain; the aniso extension is present on both), which is the core evidence that closed D592. | **STRIPPED 2026-10-08 (`b80dcd08`)** — one-off D592 GL-state diagnostic, removed with the guard when D592 closed as a non-issue. |
| `GE_KEYLOG` | `port/src/video.c` (`videoPumpEvents`) + `port/src/input.c` (`inputComputePadSlot`) | `getenv`, default off (writes `./ge_keylog.txt`) | D594: logs SDL key/mouse-button **events** on the main (host) thread (`E main KEYDOWN/KEYUP/MOUSEBT-…`) and the scheduler-thread **per-frame poll** (`P sched A/W/S/D/LMB/RMB`, change-only) to `./ge_keylog.txt`. The two streams together answer "do the key events reach the game, and does the *different* scheduler thread ever see them?" (the cross-thread poll is the D594 prime suspect). Validated on Windows (known-good input: poll logs the all-zero baseline; event lines fire on real key/mouse press). | **STRIPPED 2026-10-08** — both helpers and their two call sites removed (`video.c`, `input.c`). The D597 headless fire check is now "no SIGSEGV while firing" only (it no longer logs `LMB=1`). |
## Notes
- **Stripping is a mechanical, behavior-neutral edit**: delete the `getenv`
+ the env-gated block; nothing else references the probe (each is self-
contained). `GE_TEXP`, `GE_SIZEOF`, and `GE_KEYLOG` are the ones still in the
tree with a real "should not ship" character; the `GE_TEXDUMP` family is the
kind of triage tool that earns its place. `GE_NPOT_NOGENMIP` (+ its
companion) and `GE_TEXSTATE` are already gone (`b80dcd08`) with the D592
close-out.
- The broader `GE_` set (the ~70 vars in `port/`: `GE_QUITFRAME`, `GE_PCDUMP`,
`GE_RSEED`, `GE_DETERM`, the `GE_OPTIONSOVERLAY_*` UI probes, the `GE_ZF*`
z-fog family, etc.) is the standing debug-tool set and is **out of scope**
for this decision — it already ships. Only the **macOS-track additions**
above need a keep/strip verdict.
- **Ordering:** strip (or consciously keep) `GE_TEXP` + `GE_SIZEOF` +
`GE_KEYLOG` as part of the §4 post-1.0 macOS merge, *before* the branch goes
near `main`; leave the `GE_TEXDUMP` family in. (`GE_NPOT_NOGENMIP` +
`GE_TEXSTATE` are already stripped with the D592 close-out, `b80dcd08`.)
-46
View File
@@ -1,46 +0,0 @@
# GRAPHICS-BACKLOG — known cosmetic / rendering issues
Deferred below level-progression and crash work (user priority, M-11).
Minor graphical glitches are acceptable in the current state. Fix these
once the game is playable start-to-finish without crashes.
## Open
| ID | Symptom | Status / notes |
|----|---------|----------------|
| ~~**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". |
| **D76** | 2D disclaimer screen only partially drawn. | §F D76. |
| ~~**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 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. **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. | **OPEN — second pass active (D266–D269); first-pass decode fix was reverted after a negative user re-test (D265)** (M-106, user QA report). The earlier IA16 framing was a misidentification: the cards are flat quads by design (no billboard opcode; byte-matched ROM emission) and bind an opaque IA8 64×16 strip plus an alpha-blended **IA4 128×16** strip (texdump r117) that is a row of ~13 faint conifer silhouettes. The wall came from `import_texture_ia4` decoding the RDP's I2:A2 nibble as I3:A1 — A2=1 canopy pixels went fully opaque, A2=2 dithering vanished (bug inherited verbatim from the PD port). **Second pass (D266–D269) re-framed the bug: the visible wall band is CI8 32×32 gradient/blob cards, not IA at all** — two draw classes per room stream (OPA + XLU), BILERP/TEXTLOD/2-cycle/**FOG-on**; texel-alpha blending proven correct by force-alpha A/B (D268). Current ranked suspects: fog range/application (D218 family) > TEXTLOD/mip sampling > draw density/culling > palette. The I2:A2 decode + AC-aware edge fixes are re-applied in-tree (uncommitted) and still owe a game-wide IA4 regression sweep before commit. |
| **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). |
| **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. |
| **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)
- D70–D74: intro-logo pixels (Rareware logo renders correctly).
- D103: viewport / native-resolution half-frame.
- D105: room geometry Z-buffer never cleared (`G_CLEAR_DEPTH_EXT`).
- D106: sky-void through doorways (portal near-plane min>max).
- D107: blurry ceilings/wall-panels (LOD mip tile fabrication).
- D112: skeletal models were "3D lines" (`d43_emit.py put_f32` byte order).
+232 -12
View File
@@ -1,5 +1,11 @@
# HANDOFF ARCHIVE — session-by-session narrative
> **Local machine paths are genericised** in this archive: `<repo>` = this repo's
> working directory, `<repos>` = the local source-repos root, `<games>` / `<videos>`
> = the local games / video roots, `<python>` = the local CPython install, `<temp>`
> = a writable native temp dir, `<msys>` = the local MSYS2 install root.
> Session facts are unchanged.
> **Reference only.** Current state lives in the [README](../../README.md)
> "Status" section and `docs/dev/LEVEL-STATUS.md`; per-finding detail is in
> [`findings.md`](findings.md) §F/§H — look up via
@@ -985,7 +991,7 @@ In KEEP-A test (sky quad only), the band shows (98,97,128) = fog color. This mea
### Environment & build
- Build: `export PATH="/c/msys64/mingw64/bin:$PATH" && cmake --build build-pc -j 8`
- Python: `"C:\Users\james\AppData\Local\Programs\Python\Python313\python.exe"` (has numpy+PIL)
- Python: `"<python>\python.exe"` (has numpy+PIL)
- Run: from repo root, ROM in `./data/`. Level: `-level_36`
- Frame capture: F12 → `ppm/frame_NNN.ppm`; or `GE_PCDUMP=lo-hi:step`
- Input script: `GE_INPUTSCRIPT="120:START;..."` (M-99: CANNOT reach mouselook)
@@ -1081,7 +1087,7 @@ Facility = `-level_34` (stageId 34 = `LEVELID_FACILITY`; `boss.c` decodes `-leve
4. Diff behaviour vs a level whose opening beat DOES work (e.g. Dam `-level_33`) to isolate "Facility data" vs "general beat machinery".
5. Any fix: port-only unless it's the documented ABI/layout class; new finding = D266+; append to `docs/porting-notes.md` if a new bug class emerges.
**Environment (unchanged from prior sessions):** build `export PATH="/c/msys64/mingw64/bin:$PATH" && cmake --build build-pc -j 8` (or `./build-pc.sh ntsc-final`); kill any running `ge007*` before linking. Exe `build-pc/ge007.x86_64.exe`, run from repo root (ROM in `./data/`). Headless: `GE_INPUTSCRIPT` (tokens A/B/Z/START/UP/DOWN/LEFT/RIGHT/CUP/CDOWN/CLEFT/CRIGHT/SDOWN/SNONE, `tick:TOKEN;…`, pulse = 6 reads), `GE_PCDUMP=lo-hi:step`, `GE_INPUTLOG=1`, F12 → `ppm/shot_NNN.ppm`. Python (PIL/numpy) at `/c/Users/james/AppData/Local/Programs/Python/Python313/python.exe`; **cast uint8 arrays to int32 before bit shifts**.
**Environment (unchanged from prior sessions):** build `export PATH="/c/msys64/mingw64/bin:$PATH" && cmake --build build-pc -j 8` (or `./build-pc.sh ntsc-final`); kill any running `ge007*` before linking. Exe `build-pc/ge007.x86_64.exe`, run from repo root (ROM in `./data/`). Headless: `GE_INPUTSCRIPT` (tokens A/B/Z/START/UP/DOWN/LEFT/RIGHT/CUP/CDOWN/CLEFT/CRIGHT/SDOWN/SNONE, `tick:TOKEN;…`, pulse = 6 reads), `GE_PCDUMP=lo-hi:step`, `GE_INPUTLOG=1`, F12 → `ppm/shot_NNN.ppm`. Python (PIL/numpy) at `<python>`; **cast uint8 arrays to int32 before bit shifts**.
---
@@ -1345,7 +1351,7 @@ will do a full manual playtest in lieu of the `verify.sh` sweep.
3. **Steam Deck test session (~1 hr, needs the hardware — who has the Deck?):** install tarball via USB, Facility walk+fire (D203 repro profile), gamepad check, `Video.DisplayFPS` on Dam + Streets, grab `ge007.crash.log` if anything faults.
4. Tag `v0.2.0-pre`; publish pre-release with known issues: D243 (Dam end race), D230 (music quality), D245 (water seam), D246 (edge strips), D197 (face wrap on Silo), D75 (front-end models), the D251 4K ghost row, and "not yet verified on real Deck hardware". Flip to final after the user's playtest passes.
**Environment:** build as before (`/c/msys64/usr/bin/bash.exe -lc 'export PATH=/c/msys64/mingw64/bin:$PATH; cd /c/Users/james/Source/Repos/gh-fullhist && ./build-pc.sh ntsc-final'`). CI (`.github/workflows/ci.yml`) builds Linux per push — the `linux-build` job's artifact is the bundle source for the tarball if local Linux testing isn't available.
**Environment:** build as before (`/c/msys64/usr/bin/bash.exe -lc 'export PATH=/c/msys64/mingw64/bin:$PATH; cd <repo> && ./build-pc.sh ntsc-final'`). CI (`.github/workflows/ci.yml`) builds Linux per push — the `linux-build` job's artifact is the bundle source for the tarball if local Linux testing isn't available.
## F10 overlay QoL session (2026-09-13, shipped in v0.2.0-pre) — D251; known bug: intermittent 4K-fullscreen "repeat of top entry" at panel bottom (MSAA×fullscreen driver suspect, unverified)
@@ -1373,7 +1379,7 @@ will do a full manual playtest in lieu of the `verify.sh` sweep.
**Environment / state notes:**
- User's live `data/ge007.ini` already has `Fullscreen = 1`, `MSAA = 4` (backup at `/tmp/ge007.ini.bak` — I did not modify their ini). Monitor: single display 3072×1728, window fullscreen at 0,0.
- `ppm/user4k.png` exists from a PowerShell `CopyFromScreen` capture but was taken with the overlay **closed** — low value; user asked to stop analyzing captures.
- Build: `/c/msys64/usr/bin/bash.exe -lc 'export PATH=/c/msys64/mingw64/bin:$PATH; cd /c/Users/james/Source/Repos/gh-fullhist && ./build-pc.sh ntsc-final'`. Launch from repo root: `./build-pc/ge007.x86_64.exe &` (needs `./data/` ROM).
- Build: `/c/msys64/usr/bin/bash.exe -lc 'export PATH=/c/msys64/mingw64/bin:$PATH; cd <repo> && ./build-pc.sh ntsc-final'`. Launch from repo root: `./build-pc/ge007.x86_64.exe &` (needs `./data/` ROM).
- Design doc for the overlay: `docs/dev/OPTIONS-MENU-PLAN.md` (approach C: port-layer overlay, zero src/ menu edits).
## M-123 (2026-09-14) — D194 ① log mined + wrap-spike fixed; spazz NOT in our writes (they're rock-stable); next: live capture with the new residue/autoaim logging, then user A/B playtest
@@ -1398,7 +1404,7 @@ will do a full manual playtest in lieu of the `verify.sh` sweep.
## M-122 (2026-09-13) — D194 GEPD-mirror aim IMPLEMENTED + user-confirmed "mechanically works"; three items left: ① crosshair/arm spazz glitch, ② aim-mode FPS drop (perf.ps1), ③ sensitivity tuning. Next session starts at ①.
**The aim rewrite landed.** After four failed stick-synthesis models (absolute → dial → relative-angle → velocity-mapped stick; see M-121 + commits `8a8d066b`/`650b7f63`/`7f65ed15`), the session found and read **GEPD's actual source** (`C:\Users\james\Games\Emulators\Nintendo 64\1964_GEPD_Edition\1964\source.tar.xz` → `MouseInjectorPlugin/games/goldeneye.c`, extracted copy at `/c/Users/james/AppData/Local/Temp/gepd-src/`) and mirrored its model exactly. **User verdict: "mechanically it works... the aim mode works how we'd want."** Committed as `ef49d9e6` on `fix/d194-mouse-dt-decouple` (branch is up to date with main; PR #75/#76 already merged in).
**The aim rewrite landed.** After four failed stick-synthesis models (absolute → dial → relative-angle → velocity-mapped stick; see M-121 + commits `8a8d066b`/`650b7f63`/`7f65ed15`), the session found and read **GEPD's actual source** (`<games>\Emulators\Nintendo 64\1964_GEPD_Edition\1964\source.tar.xz` → `MouseInjectorPlugin/games/goldeneye.c`, extracted copy at `<temp>/gepd-src/`) and mirrored its model exactly. **User verdict: "mechanically it works... the aim mode works how we'd want."** Committed as `ef49d9e6` on `fix/d194-mouse-dt-decouple` (branch is up to date with main; PR #75/#76 already merged in).
**The model (all in `port/src/input.c`, `aimGepdCompute()` ~line 1377):** mouse delta moves a crosshair POSITION accumulator (`s_gepdCrossX/Y`, clamped ±`GEPD_CROSSHAIR_LIMIT`=5.16); each tick it writes `crosshair_x/y_pos` + `gun_azimuth_angle/turning` (GEPD formulas, failsafe weapon offsets 0.15/0, RATIOFACTOR=1 for our 4:3, `GEPD_BASE_FOV`=90) and edge-scrolls the camera only past 72% toward the screen edge (`(ratio-0.72)*475*dt*(fov/90)`). **No look stick is emitted while aiming.** On aim release we stop writing and the game's own damping eases crosshair+arm back to centre; every non-aim tick adopts `crosshair_x/y_pos` so re-entry starts where the game left it (GEPD does the same).
@@ -1437,7 +1443,7 @@ will do a full manual playtest in lieu of the `verify.sh` sweep.
**RMB aim-mode (D194 continuation): two more attempts this session, both did not land, session ended mid-plan on the weekly usage limit.**
- **Attempt 1 (small, safe): raised `AIM_MOVE_THRESH` 0.3->2.5px and lowered `Input.MouseAimSpeed` default 16->14 (matching the user's live ini, which had drifted to 50).** User's verdict: **"didn't actually fix anything"** — confirms the core problem isn't a magnitude/gain issue, it's the shape of the response.
- **Real root cause found via code reading (still valid, not invalidated by what follows):** `bondviewProcessInput(s8 stick_x, s8 stick_y, ...)` in `src/game/bondview2.c` takes a **signed 8-bit stick** — the smallest representable nonzero aim-turn value is a hard jump to 10% speed (stick=61; `(stick-60)/10` is exactly 0 at stick<=60). No threshold/curve tuning on the port's synthesis side can produce a smooth ramp through an integer that can only be 60 or >=61. Confirmed via `pd_port` (`C:\Users\james\Source\Repos\pd_port`) reference: PD's own mouse-look (`src/game/player.c:3669`, `src/game/bondeyespy.c:945`) never synthesizes an N64 stick at all -- it's `theta += mdx * scale`, a continuous float angle accumulator, no floor, no int8 quantization anywhere.
- **Real root cause found via code reading (still valid, not invalidated by what follows):** `bondviewProcessInput(s8 stick_x, s8 stick_y, ...)` in `src/game/bondview2.c` takes a **signed 8-bit stick** — the smallest representable nonzero aim-turn value is a hard jump to 10% speed (stick=61; `(stick-60)/10` is exactly 0 at stick<=60). No threshold/curve tuning on the port's synthesis side can produce a smooth ramp through an integer that can only be 60 or >=61. Confirmed via `pd_port` (`<repos>\pd_port`) reference: PD's own mouse-look (`src/game/player.c:3669`, `src/game/bondeyespy.c:945`) never synthesizes an N64 stick at all -- it's `theta += mdx * scale`, a continuous float angle accumulator, no floor, no int8 quantization anywhere.
- **User also reported a second, more specific symptom mid-investigation: aim resets to center the instant mouse movement pauses (even while still holding RMB), breaking precision tasks (shoot-the-lock, watch-laser-cuts-metal-in-train puzzles).** Traced `bondviewCurrentPlayerUpdateSpeedTheta`/`speedVertaUp`/`Down` (bondview2.c) in detail: confirmed this is a smoothed rate-based decel model (`g_CurrentPlayer->speedtheta` eases toward 0 over several ticks when input stops, does NOT hard-reset) -- so a literal snap-to-center was not found in that specific mechanism. Root cause of the reset symptom specifically was **not conclusively identified** before moving to the rewrite.
- **Attempt 2 (bigger, PD-style, approved by user before starting): bypassed the s8-stick synthesis entirely for aim-mode.** `port/src/input.c` computed a continuous -1..1 turn/vert speed (`inputGetContinuousAimSpeed()`, new function + header decl) instead of synthesizing `sx`/`sy`; `src/game/bondview2.c`'s aim-mode block (`#ifdef PORT`) read it directly, bypassing the `(stick-60)/10` conversion but leaving all downstream `speedtheta`/`speedverta` easing untouched. Removed the now-dead `Input.AimBand`/`Input.MouseAimCurve` knobs. **Compiled clean (verified both PORT and N64 `#ifdef` brace balance carefully), crash-free in a headless sanity check -- but broke live: "somethings broken... can't aim at all... it locks my input completely with the mouse."** Root cause of THAT breakage was not identified (no repro available headlessly; ran out of session before a careful live-debug pass). **Fully reverted** (`git checkout -- src/game/bondview2.c port/src/input.c port/include/input.h` back to the `db6eba2b` merge commit) — repo confirmed clean, PR #62's mouse work is back to its pre-session (working, if imperfectly-tuned) state. Live ini's `MouseAimSpeed` also manually restored 14->16 to match the reverted code default.
@@ -1893,7 +1899,7 @@ current testing, folded in:
player's already-fixed 1P viewmodel isn't implicated. Narrows the
search surface.
3. **D194 mouse feel — external corroboration, not a new mechanism.**
Investigated `C:\Users\james\Games\Emulators\Nintendo 64\1964_GEPD_Edition`
Investigated `<games>\Emulators\Nintendo 64\1964_GEPD_Edition`
(the user's reference point for "feels better") — the 1964 emulator's
bundled `Mouse_Injector.dll` (closed-source, no source available; the
emulator's own `source.tar.xz` is just the stock 1964 core). Its exports
@@ -2839,7 +2845,7 @@ except `bg.c`'s `GE_D63` lines which are not `#ifdef PORT`-guarded).
## Playtest bug reports (M-49 cont., 2026-09-04) — logged for future triage, NOT v0.1.0 blockers
User playtested the **v0.1.0 win64 bundle** (`C:\Users\james\Games\goldeneye-pc-port-0.1.0-win64`,
User playtested the **v0.1.0 win64 bundle** (`<games>\goldeneye-pc-port-0.1.0-win64`,
fresh save). Bugs filed in findings §F (D191–D197), all alpha-acceptable.
NB: Cradle does **not** crash — it's the D193 scripted-sequence stall (an
earlier note here wrongly said "Cradle also crashes").
@@ -3303,7 +3309,7 @@ collision geometry. Full ranked next-steps in findings.md D205.
6. Build + smoke after probe removal: `./build-pc.sh ntsc-final`, then
`-level_09` 60 s crash-free with `GE_AUDIOTRACE=1`.
### Environment (unchanged except noted):
- Build: `/c/msys64/usr/bin/bash.exe -lc 'cd /c/Users/james/Source/Repos/gh-fullhist && ./build-pc.sh ntsc-final'` (login shell for MINGW64 PATH; SDL2.dll at `/c/msys64/mingw64/bin`).
- Build: `/c/msys64/usr/bin/bash.exe -lc 'cd <repo> && ./build-pc.sh ntsc-final'` (login shell for MINGW64 PATH; SDL2.dll at `/c/msys64/mingw64/bin`).
- Capture run: `GE_AUDIOTRACE=1 GE_INPUTSCRIPT="$(cat ../scratchpad/exact_run3_script.txt)" ./ge007.x86_64.exe -level_09` from `build-pc/`; kill with `taskkill //F //PID <pid>` (msys `kill` does not work).
- `mupen64/` (N64 emulator, user's A/B reference) at repo root — added to .gitignore this session.
- `scratchpad/` is gitignored: banktest/, exact_match.py, fire scripts, match outputs are all local-only. To re-verify the bank exoneration: `scratchpad/banktest/harness.exe` (rebuild from harness.c against port/src/romdata.c if stale) then `python scratchpad/banktest/diff_bank.py`.
@@ -3391,7 +3397,7 @@ archived user capture: `scratchpad/d202-m66/` (`check_dump.py`,
The `[EXPIRE]` line and the guard/expire code STAY (permanent, documented).
Build note for this shell: toolchain at `/c/msys64/mingw64/bin`, and
`cmake --build -j` hit "Cannot create temporary file in C:\\Windows\\" —
`ninja -j1` with `TMP/TEMP=C:/Users/james/AppData/Local/Temp` works.
`ninja -j1` with a writable native `TMP/TEMP` works.
## Done this session (M-66) — D202 ROOT CAUSE ESTABLISHED: the audible stuck door loop is sound 203 behaving exactly as ROM + ground-truth code specify — faithful N64 behaviour (original quirk), not a port bug. Disposition A/B/C pending user decision.
@@ -4765,7 +4771,7 @@ does not.
Build locally: `cmake -S . -B build-linux -G Ninja -DROMID=ntsc-final
-DCMAKE_C_COMPILER=gcc-14 -DCMAKE_CXX_COMPILER=g++-14` then `ninja -C
build-linux` (~20s on drvfs). WSL `Ubuntu` distro provisioned (toolchain +
SDL2/zlib/GL dev); repo at `/mnt/c/Users/james/Source/Repos/gh-fullhist`
SDL2/zlib/GL dev); repo at `<repo>`
(fixed the malformed `/etc/wsl.conf` `[automount]` + `wsl --shutdown`).
`data/` has the ROM + sidecars already.
@@ -5074,7 +5080,7 @@ playtest handoff: `docs/dev/M-35-PLAYTEST-CHECKLIST.md`; WI-3 design:
(`d43_emit.py` / `d69_emit.py` / `d88_emit.py --regen`, all pass clean) → `./build-pc.sh
ntsc-final` links 242/242, `-level_09` runs crash-free past frame 1200. **No `assets/*.bin`
extraction needed** — the committed `assets/` is self-sufficient; the ROM is runtime-only.
The old repo at `C:/Users/james/Source/Repos/007` has the ROM + a prior build.
The old repo at `<repos>/007` has the ROM + a prior build.
- **D168 — VERIFIED.** Rebuilt with the flip fix, captured `-level_09` frame 320:
the exit arrow points up, ammo "7|93" reads normally bottom-right — matches
@@ -6163,3 +6169,217 @@ Run from the **repo root**, not `build-pc/`.
rating, don't hack it in.
3. Prefer an **offline sidecar converter** (`tools_pc/d*_emit.py`,
D43/D69/D88 pattern) over a runtime fixup for a whole ROM-asset format.
---
# Appended 2026-10-05 (session 07j) — moved verbatim from `docs/HANDOFF.md` (briefs 2026-10-05 (session 07d) → 2026-10-05 (session 07g))
# HANDOFF — 2026-10-05 (session 07d)
## Branch
`release/v0.4.1` @ `e9cf2ddf` (D530 addendum), tree clean.
Local-only commits on top of `442eb2ff`: `aa19b354` (site SEO/redundancy),
`2c378e7e`+`24b084d0` (Known issues), `ada7bbb1` (D530 goldens), `e9cf2ddf`
(D530 addendum). **Nothing pushed.**
## This session (2026-10-05 (session 07d), user stepped away ~10:45, returns ~14:00 EDT)
1. **HANDOFF 07c item 1 (critical path) — DONE, committed (`ada7bbb1`):**
- The X220 P7 re-round had completed on the box 2026-10-05 (tree `442eb2ff`,
worktree `~/gh-fullhist-wt-p7`, `~/p7-reround.log` hit `P7 re-round DONE`
12:53 EDT, **21/21 levels rc=0** under the canonical `ge007.eep`).
- Pulled the 63 PNGs (15 MB tarball → `scratch/p7-pull/`, log →
`scratch/p7-reround-pull.log`) and committed them into
`tools_pc/golden/<level>/linux/`, replacing the D525 cleared-eep set:
**51/63 differ; 12/63 byte-identical** (save-independent scenes,
including frigate's first stem 001000 — a D117 stability signal).
- **D117 flaky-four first-stem check (D529 never-do 6, first cross-machine
test):** dam@1500 worst_cell 1.70 (phash 0), frigate@1000 byte-identical,
streets@1000 1.78 (phash 0), jungle@900 5.12 (phash 3) — all in
stable-level range, **none red → NO first-stem exclusion/widening**.
- Structural tier all `ok` (cradle@40 documented exception; Cuba 26.4%
within the documented 23.7–36.3% structural-only range). The `fail` rows
in the box report are the expected cross-eep diffs vs the old set.
- **Closes D529 never-do 3:** the index/README v0.5.0 "re-based on both
platforms (21 levels, 63 frames each)" line now stands on a
canonical-eep linux set → linux numbers unblocked (B7 satisfied).
- Wrote **D530** (findings.md detail + §F table: D523 now SUPERSEDED-by-
D529, D529 row was missing from the table — the 07c CSV had been
hand-patched, which is why `gen_findings_index.py --check` drifted;
regenerated) + `tools_pc/golden/README.md` linux-set text + History.
2. **DONE: fresh `verify.sh sweep` on the box against the NEW goldens**
(shipped the 51 PNGs to `~/gh-fullhist-wt-p7`, box diff = 51 = the
tree delta; fired 10:5x EDT, finished ~11:10 EDT): **21/21 PASS, 0
FAIL** — but the run was **crash-detect only** (GE_PCDUMP reads black
on the box's llvmpipe/WSLg, the D244 note; no pixel gate). So it is a
stability re-check; the D117 cross-machine *pixel* evidence remains the
re-round capture itself (the D530 first-stem check). Recorded as the
D530 addendum (`e9cf2ddf`) with the residual: a pixel-gated box verify
needs a working WSLg GPU session (the 10-05 re-round's pixel comparisons
ran fine, so one exists).
- Log pulled: `scratch/p7-verify-new-pull.log` (tail: `21` PASS, `0`
FAIL, `SWEEP DONE`).
3. **DONE (partial, at push time still owed): PII scrub addendum for the 51
new golden PNGs** — EXIF/text metadata sweep over all 63 linux PNGs:
**0 metadata tags** (PIL); new doc text (D530, golden README) uses
"X220" + `~/` paths only — no absolute paths/usernames/private IPs in
tracked files (the box LAN address lives only in the gitignored
HANDOFF + scratch logs). Addendum appended to
`docs/dev/notes/PII-SCRUB-REPORT.md` (gitignored). The full
LEGAL-review pass (game-content rule for screenshots) is still owed at
push time.
## User to review at ~14:00 EDT
- Local batch of 5 doc/golden commits (`aa19b354` SEO/redundancy;
`2c378e7e`+`24b084d0` Known issues; `ada7bbb1` D530 goldens; `e9cf2ddf`
D530 addendum — box sweep 21/21 crash-detect PASS).
- Then the standing HANDOFF 07c items: 2. maintainer review of the 12-file
batch; 3. P12 U2/U5/U6/U7 (deferred light-unit mechanical review);
4. re-cut squash → B7/B8/B9 (README linux numbers now unblocked);
5. push `release/v0.5.0` + tag.
## Environment
- Preview: `:8777` (PID may be stale after a reboot — `node
scratch/build_preview.mjs site && node scratch/serve_preview.mjs 8777`).
- Box ssh: `ssh` to the X220 box (Windows→box works; box has no sshd —
everything rides the same session's stdin/stdout; pushes via
`cat f | ssh ... 'cat > dest'` or tar pipes).
- 3 parked worktrees (ci/d441/modpatch) untouched.
---
# HANDOFF — 2026-10-05 (session 07e) (D531 + P12 U5)
## Branch
`release/v0.4.1` @ `164920c6`, tree clean. Three more local-only commits on
top of `e9cf2ddf`: `4829fd94` (D531 blocker fix + canonical re-capture),
`d2d75e11` (ROADMAP rows), `164920c6` (D531 addendum). **Nothing pushed.**
## This session (2026-10-05 (session 07e))
**D531 — `capture_p7.sh` canonical-re-pin blocker (P12 U5 review finding),
fixed + re-captured. AMENDS D530.**
- U5 reviewed `tools_pc/**` (the release-diff unit that owed the P7 merge):
small diffs all mechanical/clean; `p7_report.py`, `eep_convert.py`,
`abi_ratchet.py`, `docs_budget.py`, `abpad/` clean (list-arg subprocess,
raw_decode JSON, no shell, no destructive ops); deletions clean (probe
tools + the 433k-line sweep-capture PPM set). **But** `capture_p7.sh`'s
D529 adaptation was incomplete: the D524-era per-level re-pin
(`cp "$EEP_ORIG" "$EEP"`, restoring the pre-capture LOCAL save) silently
overwrote D529's pre-loop canonical install from level 1 — so **the
10-05 box re-round (D530) ran under the box-local save, not the
canonical eep**. D530's "canonical eep" provenance claim and first-stem
table are void as canonical-save evidence (D530 entry + README amended in
place; the table's numbers do check out against the 10-05 report).
**The win set is unaffected** — `verify.sh`'s `pin_eep` was correct.
- Fix: the loop now re-pins `cp "$CANON_EEP" "$EEP"` per level (EEP_ORIG
kept for the exit-restore only); `cd "$ROOT"` added (the loop's
dump/golden paths are root-relative, contradicting the header's
"invoked from anywhere" claim).
- **Re-captured:** shipped the fixed script to the box (sha matched), fresh
21-level round with `DISPLAY=:0` → 21/21 rc=0, 63 frames. **47/63 differ
from the D530 box-local-save set** (save content demonstrably moves
frames), 16 byte-identical. Committed into the linux goldens.
- **Renderer finding (corrects the 07d item-2 note):** even with
`DISPLAY=:0` the box today reports no usable glxinfo renderer (software
fallback → black read-backs) — the WSLg GPU display is
**session-state-dependent** (the 10-05 re-round's p7_report pixel table
proves it was GPU-backed then). Today's `verify.sh sweep` against the new
goldens: **21/21 PASS, 0 FAIL, crash-detect only** (log
`scratch/p7-verify-pixel-pull.log`). The pixel-gated verdict (incl. the
four D117 flaky first stems) is owed for a GPU-display window.
- Docs: D531 entry + §F row (regenerated CSV, 394 lines, `--check` OK),
D530 amended in place, golden README intro + History, ROADMAP Golden
baselines row (was still on D523 framing) + new P12 U5 row. PII addendum
#2 (63 PNGs, 0 metadata tags).
**U5 should-fixes (rows on the ROADMAP, not yet applied):**
1. `verify.sh` `pin_ini_640x480 2>/dev/null || true` fails OPEN (pin_eep
fails closed) — a failed ini write runs the gate under the user's
personal ini → spurious verdicts. Make it abort.
2. `GOLDEN_SEED="${GE_RSEED:-…}"` lets a stray env var re-seed the gate —
warn or hard-pin (capture inherits the same override via its eval out
of verify.sh, so gate/capture stay consistent either way).
3. `audiodebug.ps1` `-AB`/`-Old` are documented dead no-ops (nit).
## User to review at ~14:00 EDT (supersedes the 07d list)
- 8 local commits: `aa19b354` (site SEO/redundancy), `2c378e7e` +
`24b084d0` (Known issues), `ada7bbb1` (D530 goldens — now AMENDED BY
D531), `e9cf2ddf` (D530 addendum), `4829fd94` (D531), `d2d75e11`
(ROADMAP), `164920c6` (D531 addendum).
- Preview: `node scratch/build_preview.mjs site` → `scratch/_site/` on :8777.
## Still owed (unchanged, plus)
- Pixel-gated box verify in a WSLg GPU-display window (never-do 6 applies
if the flaky four go red: exclude/widen, do NOT recapture).
- The three U5 should-fixes above; P12 U2/U6/U7; HANDOFF 07c item 2
(maintainer review of the 12-file docs batch); squash → B7/B8/B9; push
`release/v0.5.0` + tag.
- LEGAL review pass at push time (screenshots rule — 47 new linux PNGs are
the same class: box-captured, no metadata).
---
# HANDOFF — 2026-10-05 (session 07f) (D532 + squash re-cut + PUSH)
## Pushed (2026-10-05 (session 07e)/f, maintainer approved)
- **GitHub** `origin`: new branch `release/v0.5.0` (the re-cut squashed
chain, 33 commits, tip `f077435a`) + annotated tag `v0.5.0`.
**`origin/main` untouched** (`0e8c2ce2`) — main merge + public release
wait for the maintainer's playtest.
- **Forgejo mirror**: `release/v0.5.0-squashed` + tag `v0.5.0`.
- Squash re-cut: `scratch/p11b-squash.py` (p11 + new tip `4dd1d56d`,
assert 488, new group 33 covering D529-D532 + the site/amber pass).
Tip tree byte-identical (B7), LAN scrub of intermediate trees
re-applied, `pre-squash-v050` -> `4dd1d56d` (B6), tag message in
`scratch/tag-msg-v050.txt` (B8), PII/legal addenda on the final tree
(B9: clean; 4 pre-existing toolchain-path sites only).
- **D532** (`4dd1d56d`) landed before the cut: verify.sh fail-closed
ini pin + hard-pinned seed (warn on stray `GE_RSEED`), audiodebug
dead switches removed. Docs: D532 entry/row, ROADMAP U5 row applied.
## Now owed (in order)
1. **Maintainer playtest** (C6 list: D511-D514/D519 items) on the
`release/v0.5.0` build → then remove the "not yet playtested live"
block and cut the `gh release` (C1-C9; bundles built FROM the pushed
tree).
2. **Pixel-gated box verify** in a WSLg GPU-display window (D531; the
box worktree `~/gh-fullhist-wt-p7` is still at `442eb2ff` — needs a
bundle transfer of the new tip; no LAN route, options per the P7
handback §8).
3. **P12 U2/U6/U7** quick passes (U1/U3/U4/U5 PASS; U6 = build+CI
diff, U7 = docs diff — read-only, ROADMAP rows for should-fixes).
4. CI on the new GitHub branch: first real run of the SLSA
build-provenance attestation (ROADMAP "Release build-provenance
attestation" row: confirm, then delete).
5. Box-side parallel: self-gate on the release branch (crash-detect
until the GPU window).
## Local state
`release/v0.4.1` @ `4dd1d56d` (== the pushed squashed tip tree), tree
clean. `release/v0.5.0` / `release/v0.5.0-squashed` @ `f077435a`.
`pre-squash-v050` @ `4dd1d56d`. Nothing else pending locally.
---
# HANDOFF — 2026-10-05 (session 07g) (push reverted; held local)
- **GitHub push REVERTED** (branch `release/v0.5.0` + tag `v0.5.0` deleted,
verified via `git ls-remote`); backup lives on the private Forgejo mirror
(`release/v0.5.0-squashed` + tag `v0.5.0` @ `f077435a`). Local refs:
`release/v0.5.0` / `release/v0.5.0-squashed` @ `f077435a`,
`pre-squash-v050` @ `4dd1d56d`.
- **Standing rule (maintainer):** backups → Forgejo, never GitHub; GitHub
pushes only after the maintainer approves everything.
- **Docs finalization owed:** the batch review (`scratch/docs-batch-review.txt`,
commit `ba29c558` — 16 text files) and the **PR/SEO language pass**
(brief written: `scratch/pr-seo-revise-prompt.md` — tiers T1/T2/T3, voice,
SEO parity checklist, hard rules; example fix = the v0.5.0 News tagline
leading with "plain-English wording" + the UK/US spelling item demoted to
"small stuff").
- Then: playtest (C6) → `gh release` (C1–C9) → the ONE approved GitHub push
(release branch + tag + release) → main.
---
+13 -4
View File
@@ -1,5 +1,7 @@
# LEVEL-PLAYTEST — WS6 human completion-validation checklist
> **Historical record — do not act on status claims in this file.** This checklist predates the v0.4.0 input model (D332–D337) and the campaign sign-off; use it as a template only. Current open work and status: [`docs/ROADMAP.md`](../ROADMAP.md) (2026-09-29).
Run **after** `docs/dev/LEVEL-STATUS.md` is all-21 load+render PASS. This is
the part that needs real input — a human plays each level start to finish
and confirms it matches retail. Findings feed a WS5-style triage round
@@ -47,7 +49,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 +73,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.)
+10 -2
View File
@@ -1,4 +1,12 @@
# LEVEL-STATUS — 21 solo-level load+render sweep (WS4)
# LEVEL-STATUS — level load+render sweep (WS4) — 20 solo missions + ending sequence
> **Historical record — do not act on status claims in this file.** The per-level sweep below predates the 2026-09-28 full-campaign sign-off; its open items (re-playthrough gate, M-49 completability, Cuba crash) are resolved. Current open work and status: [`docs/ROADMAP.md`](../ROADMAP.md) (2026-09-29).
**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
@@ -67,7 +75,7 @@ cause. Only remaining crash:
900. This is the **known D129 residual**: the bare `-level_54` boot
reaches the cast/credits scroll referencing a text bank the real
front-end flow would have loaded. Not a real-flow blocker; Cuba loads +
renders 300+ frames fine. Parked with D76 (`docs/dev/GRAPHICS-BACKLOG.md`).
renders 300+ frames fine. Parked with D76 (since fixed as D164).
| Level | # | Status | Crash site | Class |
|---|---|---|---|---|
-91
View File
@@ -1,91 +0,0 @@
# Linux port — status & gap analysis
Session M-38 (2026-09-02). Goal-2 on the roadmap (after the Windows alpha /
release artifact). This is a scoping + first-patch pass; **nothing here has run
on a Linux machine yet.**
## Method
Read `build-pc.sh`, `CMakeLists.txt`, `docs/building.md`, and grepped
`port/src`, `port/fast3d`, `port/include` for Windows-only headers / Win32 APIs
/ unguarded `_WIN32` branches. A local Qwen `triage` pass seeded the hunt; every
claim below was re-checked against the real files (the 27B was wrong on two of
three headline items — see "Ruled out").
## What is already Linux-ready (verified by reading)
- **`port/include/platform.h`** — sets `PLATFORM_LINUX` / `PLATFORM_MACOS` /
`PLATFORM_WINDOWS` cleanly.
- **`CMakeLists.txt`** — `find_package(SDL2 REQUIRED)`, `find_package(ZLIB
REQUIRED)`, and `else()` branches already present for the non-Windows case:
`GL_LIBRARY = GL` (l.291), `EXTRA_LIBRARIES = stdc++ m dl pthread` (l.304),
`elseif(UNIX)` platform block (l.86). `WIN32_EXECUTABLE` and the MinGW zlib
fallback are correctly `if(WIN32)` / `if(MINGW)` guarded.
- **`build-pc.sh`** — already documents the Linux apt deps
(`cmake libsdl2-dev zlib1g-dev libgl1-mesa-dev`) and is a plain bash script,
no MSYS-isms in the executable body.
- **`port/src/system.c`** — `sysSleep()` has a `nanosleep()` `#else` branch
(l.76-82); `<windows.h>` is `PLATFORM_WINDOWS`-guarded.
- **`port/src/libultra.c`** — the OS-thread kernel has a real pthreads branch
next to the Win32 `_beginthreadex` one.
- **`port/src/crash.c`** — `MessageBoxW` / `SetThreadPriority` / `__debugbreak`
all `_WIN32`-guarded, with `PLATFORM_LINUX` counterparts.
- **`port/src/video.c`** — `GE_MKDIR` has both `_mkdir` and `mkdir(p,0777)`.
- No `__declspec`, `#pragma comment`, or hardcoded `\`-path / drive-letter
usage anywhere in `port/`.
## Ruled out (Qwen triage false positives)
- ~~`system.c:75` `Sleep()` has no POSIX branch~~ — it does (`nanosleep`).
- ~~CMake needs `else()` branches for zlib/pthread on Linux~~ — already there.
## The one real code gap — FIXED this session (verification owed)
**`port/src/romdata.c`** — the fixed-address cart map (`0x10000000`, so absolute
ROM-asset symbols are live host pointers) had a **Windows-only** implementation
(`VirtualAlloc`); the `#else` branch was a stub that logged "not implemented on
this platform" and fell straight to the heap-copy fallback. In heap-copy mode
anything that dereferences a cart address *directly* (not via
`romdataGetRom()` / the PI shims) reads wrong memory — so Linux would run, if at
all, in the same degraded mode Windows only hits when `VirtualAlloc` loses the
address race.
**Change (M-38):**
- Factored the post-map work (memcpy + `pcmodelsLoadSidecars` +
`pccgLoadSidecars` + D55 RLE-header fixup) into `romdataFinishCartMap()`,
shared by both paths so they cannot drift.
- POSIX `#else` now does an anonymous `mmap((void*)CART_BASE, len,
PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED_NOREPLACE, -1, 0)`.
`MAP_FIXED_NOREPLACE` (Linux 4.17+) fails rather than clobbering an existing
mapping; where the macro is absent the plain hint is advisory and the
`at == CART_BASE` check catches a relocated result. On any failure it
`munmap`s and falls through to the identical heap-copy path as before.
- `romdataDestroy()` `munmap`s on POSIX (tracked length in `mappedLen`).
**Owed verification (needs a Linux box):**
1. Compiles under GCC on Linux (`<sys/mman.h>` resolves; no decomp-include-path
shadowing surprise).
2. `mmap` actually lands at `0x10000000` on a stock ASLR Linux — `0x10000000`
(256 MB) is low but usually free; if the kernel routinely refuses it,
consider linking the binary no-PIE or reserving the range via a linker
script so the loader keeps it clear.
3. `-level_09` boots crash-free with the mapped path taken (check the log says
"mapped at 0x10000000 (cart base)", not "using heap copy").
4. Whether the heap-copy fallback is actually survivable on Linux if step 2
fails — audit for direct `0x1xxxxxxx` derefs outside the PI shims.
## Remaining Linux work after the romdata patch lands
| # | Item | Size | Note |
|---|------|------|------|
| 1 | Run the Ubuntu **`validate`** CI job → upgrade it to a real **compile** job | S | `.github/workflows/ci.yml` already configures on Ubuntu; flipping it to `cmake --build` surfaces the real GCC-strictness gaps cheaply |
| 2 | First actual Linux runtime bring-up (`-level_09`) | M | expect a short tail of unguarded calls / struct-layout asserts GCC-on-Linux vs MinGW |
| 3 | `docs/building.md` — promote Linux from "untested" to "supported" once 1+2 pass | S | |
| 4 | SDL2 / GL context creation on X11 + Wayland | M | fast3d is GL 3.3 core; PD port runs on Linux so the path is known-good |
| 5 | Package: an AppImage or tarball equivalent of `bundle-win.sh` | M | reuse `prepare-assets.py` verbatim — it is already OS-agnostic |
## Recommended next step
Land the romdata patch behind CI (item 1) — turn the Ubuntu `validate` job into
a compile job in the same PR so the "does it build on Linux" question is
answered by the bots, then do the runtime bring-up on a real machine.
-89
View File
@@ -1,89 +0,0 @@
# M-35 QoL playtest checklist
Everything on this branch is config-gated to prior behaviour, so the
**first** thing to confirm is that nothing changed with a default
`ge007.ini`. Then flip the new keys and check the feel.
Edit `data/ge007.ini` while the game is closed.
---
## 0. Regression (default config) — do first
- [ ] Delete `data/ge007.ini`, launch, quit. It rewrites with new keys:
`[Input] MouseCaptureMode = 0`, `[Game] ScreenShakeIntensity = 1`.
Existing keys keep their values.
- [ ] Play a level normally (mouse grabbed on focus, as before). Mouse look,
aim (RMB), menus — all exactly as the last build. **No behaviour change
expected at defaults.**
- [ ] Alt-tab away and back — cursor frees / re-grabs (unchanged).
## 1. Click-to-lock capture — `Input.MouseCaptureMode = 1`
- [ ] Launch. At the main menu the **OS cursor is visible and free**; moving
the mouse moves the game crosshair 1:1 (see item 3).
- [ ] Click in the window during a level → cursor hides, mouse-look engages.
- [ ] Press **ESC** during a level → cursor reappears, mouse-look stops,
you can move the pointer off the window. (ESC does *not* quit.)
- [ ] Click back in → re-locks. First click only re-locks — it must **not**
fire the weapon.
- [ ] Open the pause/watch (Start), then a front-end menu → cursor frees
automatically. Return to the stage → re-locks without a click.
- [ ] Alt-tab out mid-level → cursor frees; alt-tab back → stays free until
you click (Quake behaviour).
- **Correct feel:** exactly like Quake/Nightdive remasters — click to play,
ESC to get your cursor back.
- **Tune:** none — it's a mode toggle.
## 2. Aim sensitivity — B3
- [ ] With `Input.MouseCaptureMode` either value, hold RMB (aim mode) and
make small aiming corrections. Default is now `MouseAimSpeed = 16`
(was 25).
- **Correct feel:** you can settle the sight on a distant guard's head
without overshooting; a fast flick still crosses the screen.
- **Tune:** `Input.MouseAimSpeed` (1–100+). Up = faster. If hipfire turning
now feels faster than aiming, raise `MouseAimSpeed` or lower
`Input.MouseTurnSpeed` (default 100). `Input.AimBand` (5–40, default 20)
widens the usable stick range past the aim gate — raise it if aim feels
capped/notchy at the extremes.
- [ ] Check the D166 hipfire pitch (mouse up/down without RMB): still
proportional taps, consistent with the yaw. Tune
`Input.HipfirePitchSpeed` (default 100).
## 3. Menu pointer 1:1 — needs `Input.MouseCaptureMode = 1`
- [ ] On the mission-select grid, move the OS cursor to each of the four
**corner** level tiles — the game crosshair must reach all of them
(this is the D169 case; absolute mapping should make it exact now).
- [ ] File-select, mode-select, difficulty-select, watch menu — crosshair
tracks the OS cursor with no drift, no lag, no acceleration.
- [ ] Keyboard nav (arrows / stick) still works on every screen.
- **Correct feel:** the crosshair sits exactly under the OS pointer, like a
normal PC menu.
- **Tune:** `Input.MenuPointerSpeed` (default 100) still scales the legacy
relative path; with capture mode the absolute path ignores it. If tracking
feels off, check the window isn't a weird aspect ratio (the map assumes the
render fills the window).
- **Note:** in legacy mode (`MouseCaptureMode = 0`) the menu still uses the
D165/D169 relative estimator — 1:1 menu tracking requires capture mode.
## 4. Screen shake — `Game.ScreenShakeIntensity`
- [ ] Default `1` — explosions shake the screen exactly as before.
- [ ] Set `0` — no shake on explosions / big hits.
- [ ] Set `2`–`3` — exaggerated.
- **Correct:** `1` is indistinguishable from the current build.
---
## Single most important thing to test first
**Item 0** — confirm a default `ge007.ini` produces a build that plays
identically to `def0e0ac`. If that holds, everything else is opt-in polish.
## Known not-done (parked, see OPTIONS-MENU-PLAN.md)
- No in-game options **menu** yet — only the config file + the expanded
`config.c` API and the `Game.ScreenShakeIntensity` hook. The menu is a
port-layer overlay design, documented, not built.
-132
View File
@@ -1,132 +0,0 @@
# M-35 — Native-PC input + options-menu QoL review
Branch: `qol/native-pc-input-menu` (cut from `def0e0ac`). Port-layer only
except one documented route-(b) `#ifdef PORT` line in `src/fr.c` (D181).
Every new behaviour is config-gated with the default reproducing prior
behaviour; `tools_pc/golden/` headless captures are unaffected at defaults
(no mouse in a headless run; `Game.ScreenShakeIntensity` default 1.0 is a
no-op multiply).
Findings: **D180** (input pass), **D181** (screen-shake hook). See
`docs/dev/findings.md` §F.
---
## WI-1 — In-game mouse: Quake-style click-to-lock
**Status: landed, feel-check owed.**
### What changed
| File:area | Change |
|---|---|
| `port/src/input.c` — new statics `mouseCaptureMode` / `captureArmed` / `windowFocused` | Capture-mode state. `mouseCaptureMode` registered as `Input.MouseCaptureMode` (0/1, **default 0**). |
| `port/src/input.c` `inputInit` | In capture mode, start **un**grabbed (`SDL_SetRelativeMouseMode(FALSE)`), wait for a click. Legacy mode unchanged (grab on start). |
| `port/src/input.c` `applyGrab()` / `reconcileGrab(menuMode)` | `applyGrab` is the old `inputSetMouseGrab` body (SDL relative-mode toggle + delta drain). `reconcileGrab` computes the wanted grab state: legacy → `windowFocused`; capture → `captureArmed && windowFocused && !menuMode`. Called once per controller-0 poll and from every notify hook. |
| `port/src/input.c` `inputSetMouseGrab(on)` | Now just records `windowFocused` and calls `reconcileGrab` — so focus loss/gain works in both modes without special-casing. |
| `port/src/input.c` `inputNotifyClick()` | Capture mode + focused → `captureArmed = 1` + regrab. No-op otherwise (game sees the click). |
| `port/src/input.c` `inputReleaseCapture()` | Capture mode + grabbed → `captureArmed = 0`, release, return 1 (caller swallows the key). Returns 0 otherwise (ESC falls through to the N64 B button, D145). |
| `port/src/input.c` `inputComputePad` idx 0 | Calls `reconcileGrab(menuMode)`; when capture-mode + free + in a stage, zeroes the local `mb` so mouse buttons never reach the game (no phantom fire from the re-lock click). |
| `port/src/video.c` event pump | `SDL_MOUSEBUTTONDOWN` → `inputNotifyClick()`. `SDLK_ESCAPE` keydown → `inputReleaseCapture()` (before the existing fall-through). |
| `port/include/input.h` | Prototypes for the three new hooks. |
### Config keys
| Key | Range | Default | Effect |
|---|---|---|---|
| `Input.MouseCaptureMode` | 0–1 | **0** | 0 = legacy (cursor grabbed whenever the window is focused). 1 = click-to-lock: free cursor until you click the window; ESC / focus-loss / opening a menu frees it; entering a stage while "armed" re-locks. |
| `Input.MouseAimSpeed` | 1–500 | **16** (was 25) | Aim-mode (RMB / LShift) sensitivity %. B3: still overshot at 25. |
Controller input is completely independent of all of the above — the pad
branch in `inputComputePad` is untouched, and `reconcileGrab` only ever
toggles `SDL_SetRelativeMouseMode`.
### D166 / hipfire-pitch interaction
The D166 hipfire pitch pulse is unchanged. It only runs when the cursor is
grabbed (mouse deltas are zero while free), so click-to-lock does not
perturb it. The M-24 aim-band (`61 + gain` into the `60 + aimBand` ceiling)
is unchanged; only the `mouseAimSpeed` scalar feeding `gx`/`gy` moved.
### Owed feel-checks (playtest checklist items 1–4)
Headless runs cannot move the mouse, so the capture ergonomics, the new
`MouseAimSpeed = 16` default, and the D166 pulse consistency under the new
model are all inferred, not observed.
---
## WI-2 — Menu pointer: absolute cursor tracking
**Status: landed for capture mode, feel-check owed. Route (a), port-only.**
### What changed
`port/src/input.c`, the `menuMode` pointer branch (the D165/D169
P-controller). Added an **absolute-position** target source: when
`mouseCaptureMode && !mouseGrabbed` (i.e. a real free OS cursor over a
front-end screen), read `SDL_GetMouseState` + `SDL_GetWindowSize` of the
mouse-focus window, normalise to `[0,1]²`, and map straight onto the live
virtual front-end rect (`loH..hiH × loV..hiV`, already derived from
`getPlayer_c_screen*` for D169). That becomes `menuTgt{H,V}` directly
(no accumulation, no drift). The P-controller/estimator below is unchanged
and simply chases the target, so the game's `cursor_h_pos/v_pos` lands
exactly where the OS pointer is.
The relative-delta path (legacy `MenuPointerMode`, or capture mode while
grabbed) is **unchanged** — `haveAbs` gates only the new assignment.
### Why this is enough for 1:1
The D169 fix already made the clamp rect track the real 440×330 field. The
only thing missing for true 1:1 was a non-drifting target: a free OS cursor
*is* an absolute device, so mapping its window fraction onto the rect is
exact. No route-(b) hook needed.
### Keyboard nav parity
Untouched — the menu branch only ever adds to `sx/sy`; D-pad / stick keys
still feed the same axes. The D118d `front.c` latent sibling was **not**
touched (no evidence the absolute path exposes it; the stick values emitted
are small P-controller corrections, never a slammed ±80).
### Caveat
Absolute tracking is only wired for `MouseCaptureMode = 1`. In legacy mode
(default) the menu still uses the D165/D169 relative estimator. If
click-to-lock becomes the default later, WI-2 is 1:1 for free; until then
menu 1:1 requires `Input.MouseCaptureMode = 1`.
---
## WI-3 — In-game video/options menu
**Status: config-system foundation landed; menu surface = design doc only.**
See `docs/dev/OPTIONS-MENU-PLAN.md`.
### Landed
- `configRegisterFloat` / `configRegisterUInt` + clamps (`port/src/config.c`,
`port/include/config.h`). `configSave` rewritten to emit all four option
kinds section-grouped in one pass.
- `Game.ScreenShakeIntensity` (0.0–10.0, default 1.0) — the first
route-(b) hook, D181, proving the pattern the menu will lean on.
### Not landed (budget) — precise resume point in the design doc
- The menu tables / handlers themselves. GE's menu system (`front.c` /
`initmenus.c` / `mainmenu.c`) differs enough from PD's `optionsmenu.c`
that it is an adaptation, not a copy. Design doc covers the entry point,
the handler shape, and the minimum option set.
---
## Verification evidence
| Check | Result |
|---|---|
| `./build-pc.sh ntsc-final` | green, 241/241 |
| `-level_09` framediff (200/320/440) | 3/3 within threshold; nonclear Δ 0.00–0.01pp (91.6%); phash 0/1/5 = documented D117 intro-pan noise |
| `GE_STARTMENU=7` boot | crash-free, no `ge007.crash.log` |
| golden dumps at defaults | unaffected (no headless mouse; shake scale 1.0 = no-op) |
| linkcheck | see PR body |
-94
View File
@@ -1,94 +0,0 @@
# M-36 — graphics / QA bug triage sweep
Branch `qol/native-pc-input-menu` (local). Goal: sort the open
`GRAPHICS-BACKLOG.md` + QA-report bugs into **root-cause families** so a
fix hits several at once, and correct one mis-attribution.
Method: headless `GE_PCDUMP` captures (`-level_36` Surface, `GE_STARTMENU=10`
briefing) + static read of the implicated port code. Frames in
`scratchpad/` this session; not committed (auto window size = 1957×1468,
can't framediff — visual only).
---
## Headline findings
### 1. The "comb interlacing" is NOT the D159 family
`GRAPHICS-BACKLOG.md` attributes D176(b) and D182(2) to `texSwapAltRowBytes`
"recurring on re-import". **That function is a full `#ifdef PORT` no-op** —
`src/game/image.c:2199` `return;` before any work. It cannot be the cause of
anything. The mis-attribution is now corrected in the backlog.
The real defect: `port/fast3d/gfx_pc.cpp` `import_texture_*` assume
`full_image_line_size_bytes == line_size_bytes` (explicit `SUPPORT_CHECK`
on rgba32/ia4/ia8/ia16/…; the rgba16 one is commented out with
*"this trips in some places with a garbage size in
full_image_line_size_bytes"*, `gfx_pc.cpp:658`). When a tile's `line`
(SETTILE pitch) differs from the source image row width — non-power-of-two
textures, or a sub-rect tile load — the importer does a flat linear read and
every row walks off by a few bytes → **diagonal shear that reads as grey
static**. Matches the Surface cliff/tree walls (`-level_36` frame 300–900:
structured rock texture tiling but each tile sheared to noise) and the
file-select spiral after re-entry.
Open question: why does D182 only scramble **after** navigating away and
back? Either the first import happens to have `line == width` and the
re-import doesn't, or the tile descriptor is stale on the second visit.
### 2. Briefing objectives are BLANK, not wrong
`GE_STARTMENU=10` briefing (Dam): header renders perfectly ("OHMSS / Agent:
James Bond / Mission 1: Arkangelsk / Part i: Dam / PRIMARY OBJECTIVES:").
Under it: just the bullet letter **"a."** with **no objective text**, and no
b/c/d lines. So D178 is the **D143/D151 blank-text family** (`langGet()`
returns NULL for the objective string slot → text silently blank), *not* a
propDef walk-desync or difficulty-decode problem. Fix = find which lang bank
the front-end briefing flow fails to load/map.
Caveat: this repro used the synthetic `GE_STARTMENU` boot (skips real
save/unlock state). The user's D178 report is from a live playthrough, and
D143 independently documents blank briefing text, so it is a real defect —
but re-confirm the exact "a." + blank shape in a real briefing.
---
## Family table
| Family | Bugs | Root cause (hypothesis) | Files | ROI |
|---|---|---|---|---|
| **A — fast3d texture line/pitch shear** | D176(b) Surface walls; D182(2) file-select bg | **HYPOTHESIS DISPROVEN (D183, `4b71435d`).** 0/166 texture loads on `-level_36` are strided; the wall texture is a 32×32 IA8 whose ROM bytes are genuinely a noise field, decoded correctly. A golden-safe de-stride + `GE_TEXRAW`/`GE_DTEX` diagnostics landed anyway (latent hardening). **D176(b)/D182(2) still OPEN** — next: ROM ground-truth decode of the wall texnum (`TEXTURE-GLITCH-ANALYSIS.md` §7) → likely a tiling-density / UV-scale / `shifts`/`shiftt`-LOD issue (RC3/D167 family) or a wrong-texture bind, not a decode bug. | `port/fast3d/gfx_pc.cpp` | was HIGH, **now needs re-scoping** |
| **B — front-end string bank not loaded** | D178 briefing objectives blank; D143 briefing text blank | **FIXED (D178, `cd1ed574`).** Not a bank-load problem — the `Ubrief*Z` segment is a raw 48-byte BE ROM image loaded with no converter; all 24 `u16` (string ids + difficulty gates) read byte-swapped on LE → `langGet()` NULL → blank, and `>=` difficulty gate always-false → objectives vanish. `romdataFixupBriefing()` swaps in place. Verified: all 4 Dam objectives render at 00 Agent with correct gating. **D143 was a symptom of this** — annotate it superseded. D151 (watch objective text) still owed a re-verify. | `port/src/romdata.c`, `src/game/front.c` (`#ifdef PORT`) | **DONE** |
| **C — D75 front-end / cutscene 3D models** | D75 Nintendo logo; D148 Dam rappel cutscene; D149 MISSION COMPLETE / mode-select models; D182(1) file-select Bond image "renders once"; D173 3rd-person Bond floats; 1P weapon viewmodel absent | mixed: compiled sub-DL corruption (D144/D146), model transform/geometry, `render_pos` arena lifetime, constructor-runs-once | `port/fast3d/gfx_pc.cpp`, `title.c`, `bondview*.c`, model path | **LOW** per fix, deep. **DEFER** (agreed COA). |
| **D — blood / particle** | D172 spray magenta/cyan; D174 no blood decals | D172: CC-mode / texture-format decode in the fast3d sprite path. D174: likely D120 `PointUsage[]` — **only takes effect after a full sidecar regen** (`d43 && d69 && d88 --regen`); verify that first. | `blood_animation.c`, fast3d sprite path; `tools_pc/d43_emit.py` | **MEDIUM**. D174 may already be fixed — regen + BUNKER1 firefight to confirm before touching code. |
| **E — transient hang / anim speed / clip** | D175 door-open stutter (Runway/Surface); D170 NPC flee runs not walks; D171 Silo grey-triangle sweep | D175: matches D155/D156/D134/D147 known classes (benign texture-import spike most likely). D170: D155/D156 wall-clock `deltaFrames` anim speed-up, or just scripted. D171: near-plane clip fan (D106 family). | frametiming clamp, `chrTick`/AI, portal near-plane | **LOW–MED** — needs interactive repro + N64 reference footage. **DEFER**. |
| **F — misc cosmetic** | D74 dead wrap-block (latent, harmless); D76 disclaimer partial (D164 fix owed an eyeball); D77 audio (Phase 3); **D176(a) black sky** | D176(a): sky DL not emitting / wrong transform — **separate from D176(b)**, own investigation (could be C-family or a sky-pass issue). | various | **LOW**, opportunistic. |
---
## Results (2 subagents, integrated + verified on branch HEAD)
- **Family B → FIXED** (`cd1ed574`). Byte-swapped raw briefing segment. Also
explains D143. High confidence.
- **Family A → hypothesis disproven** (`4b71435d`, D183). D176(b) is not a
texture decode bug. Golden-safe de-stride + diagnostics shipped for the
latent case. **D176(b) / D182(2) remain open and need re-scoping** — start
from the ROM ground-truth decode of the wall texnum; suspect
tiling-density / UV / per-LOD `shifts`/`shiftt` (RC3/D167 family) or a
wrong-texture bind.
Harness note: both agent worktrees were created from `def0e0ac` (the M-35
branch point, ~5 commits stale — the M-31 stale-worktree bug again). No harm
this round: the integrator re-applied both patches onto branch HEAD and
verified there. Check `git worktree list` HEADs on the next burst.
## Next
1. **D176(b) re-scope** — offline decode the Surface wall texnum from ROM
(`TEXTURE-GLITCH-ANALYSIS.md` §7). Structured rock in ROM → wrong bind or
UV/tiling; noise in ROM → not our bug (or a shift-LOD density issue).
2. **D176(a)** black sky — untouched, separate investigation.
3. **D182** — re-test the file-select round-trip now that `GE_TEXPITCH` /
`GE_TEXRAW` exist; the "renders once" half (D182-1) is Family C.
4. Reassess C/D/E with the user. D174 (blood) is the cheapest — just needs a
sidecar regen + BUNKER1 firefight to confirm the D120 fix.
+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
+1 -1
View File
@@ -33,7 +33,7 @@ ScreenShakeIntensity. Debug: FrameDump, InputLog.
| **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 → D224 (M-87). DEPRIORITIZED (M-87, user call) — gamepad-only, this release targets mouse/keyboard as primary.** GE's port fully no-ops the N64 Rumble Pak (`port/src/libultra.c:1220-1223` `osMotorInit/Start/Stop` all stubbed "no accessories on the PC") — GE genuinely has a rumble subsystem (`src/motor.c`, `src/joy.c`) that never reaches any output. **PD's PC port wires this straight through to real gamepad rumble**: `inputRumbleSupported()`/`inputRumble()` call `SDL_GameControllerRumble()` when the connected pad reports haptics, `Input.PadN.RumbleScale` config + an options-menu slider, `osMemSize`-style "pretend the accessory is there" pattern applied to a real one instead of faked. | P | M | Concrete PD-port precedent, straightforward port-layer wiring, ready whenever gamepad support is prioritized (no `src/` game-logic touch — GE's `motor.c` calls already exist, just need `osMotorStart/Stop` in `libultra.c` to route to a real `SDL_GameControllerRumble` call the way PD's do). |
| **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. |
+21
View File
@@ -0,0 +1,21 @@
# Release gate driver
`tools_pc/release_gate.sh` runs the automatable pre-release checks and prints one
PASS / FAIL / SKIPPED table. It wraps `verify.sh` (pixel gate) and `levelbench.sh`
(scripted run, clean-quit and crash-log detection); every run ends through
`GE_QUITFRAME`, nothing is hard-killed.
```sh
tools_pc/release_gate.sh --dry-run all # print what would run
tools_pc/release_gate.sh win # sweep vs win goldens + fire smoke + exit path
tools_pc/release_gate.sh deck <host> # 21 levels via deckrunner (shortcut must be up)
GE_MAC_DIR=<remote game dir> tools_pc/release_gate.sh mac <host>
```
Options: `--secs N` (per-level run length, default 30), `--out DIR`. Hosts and
paths come only from arguments or env (`GE_WIN_DIR`, `GE_DECK_HOST`,
`GE_MAC_HOST`, `GE_MAC_DIR`, `GE_MAC_EXE`, `GE_MAC_BUILD_CMD`,
`GE_GATE_SSH_OPTS`). The Deck pixel compare is reported SKIPPED (640x480
goldens vs 1280x800 dumps); Mac has no goldens, so it is crash-free only. The
fire smoke is `GE_INPUTSCRIPT` `MLB1`/`MLB0` (D597, the D595 regression).
Remote job, result and script files are deleted after the summaries are pulled.
+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.**
+2
View File
@@ -1,5 +1,7 @@
# Texture Glitch Analysis — PC Port
> **Historical record — do not act on status claims in this file.** Only the status table is current; the diagnosis from §1 onward is from 2026-08-29 and includes retracted fixes (RC4) — never apply them. Current open work and status: [`docs/ROADMAP.md`](../ROADMAP.md) (2026-09-29).
**Original diagnosis:** 2026-08-29 (read-only) · **Status table refreshed:** 2026-08-31 (M-31)
Companion artifacts: `<local-path>` (outside repo).
+112 -28
View File
@@ -83,40 +83,124 @@ Two useful existing properties:
### Phase 0 — Baseline & ground truth (port-only)
Goal: know exactly what the default preset does before touching anything.
**RESOLVED (2026-09-12, commit 8f2f9e1d, D248).** The ~30 fps default was
confirmed a **port artifact**, not original N64/GE behavior: `sched.c`'s
`__scHandleRetrace` read a per-client "every retrace vs. every other
retrace" flag via a hardcoded `*((s32*)client + 2)` byte offset (`sched.c`
~line 350) that only lands on the right field when `OSScClient` is 8 bytes
(32-bit pointers, true on N64). 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), silently collapsing the gfx client onto the audio client's 30Hz
path even though the VI-retrace pacemaker itself always ticked at a correct
60Hz. Fix (`#ifdef PORT`, `sched.c` ~line 348): index the struct
(`client[1].next`) instead of the hand-rolled offset, so it scales with
pointer width. Verified live via the `Video.DisplayFPS` overlay: rock-stable
30 FPS pre-fix, 60 FPS post-fix, same build/scene. Same bug class as
D122/D126/D132/D209 (pointer-width struct growth), just in the scheduler.
- [ ] 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.
**Decision-gate answer:** the default preset is now the console's real
60Hz(NTSC)/50Hz(PAL) rate — decision #1 above ("default preset = the
original pace") is satisfied by this fix, not by preserving the halved
30fps. All prior playtest/determinism baselines predate this fix and should
be treated as pre-D248.
- [x] Find why the default was ~30 rather than the console's 60 Hz — root
cause above; §F **D248**.
- [x] Record a Dxxx finding with the measured rates and root cause — D248.
- [ ] Measure actual sim rate and present rate with a dedicated periodic log
line (`sim=..Hz render=..Hz`) rather than the one-off `DisplayFPS`
overlay reading used to verify D248. Not yet built — still open if a
standing measurement (vs. a spot-check) is wanted before Phase 1/2
work lands.
- [x] **Decision gate** — resolved above.
### 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.
**Audit (2026-09-12, post-D248):** more of this than the plan assumed
already exists. Verified by reading the code directly (not just the
delegate's first pass, which mis-read one of these — see below):
- [x] **Accumulator + remainder carry, tick thread.** `portTickThread`
(`port/src/libultra.c` ~294-313) already schedules off `g_nextTickUs +=
g_tickIntervalUs` — the *scheduled* time, not `now` — so it doesn't
drift under load. It resyncs to `now + interval` only when a tick is
already overdue (`g_nextTickUs <= now`), which is a bounded-catch-up
behavior, not a naive reset. This already matches the PD
`frametimeCalculate` accumulator pattern.
- [x] **Accumulator + remainder carry, present-side limiter.**
`sync_framerate_with_timer` (`port/fast3d/gfx_sdl2.cpp` ~403-428) uses
the same shape: `next = previous_time + interval` (schedule-based, not
wall-clock-based), a sleep-then-busy-wait hybrid to hit the deadline
precisely, and only snaps `previous_time` to the real wake time `t`
when the overshoot exceeds ~1ms (`t - next < 10000` in its 100ns units)
— i.e. it already carries the remainder in the normal case and only
gives it up after a real hitch. (An earlier audit pass mis-read this as
"no remainder carry, resets every frame" — it does not; re-verified by
reading `gfx_sdl2.cpp:403-428` directly.)
- [ ] **Triple-buffered swap chain.** Not present — presentation uses a
standard SDL/GL double-buffer swap (`SDL_GL_SwapWindow`,
`gfx_sdl_swap_buffers_begin`, `gfx_sdl2.cpp` ~436-444). Not started.
- [ ] **PD safety rule (vsync off + no cap → force a max cap).** Not present.
The only existing floor/ceiling logic is the D186 guard in
`gfx_sdl_set_target_fps` / `video.c` (~289-292, ~464-467), which goes
the *other* direction — it refuses caps below 30 (because pacing still
runs inline on the sim thread, see Phase 2 note below) — there is no
"vsync off AND cap==0" case that forces a ceiling. Not started.
- [x] Config: `Video.VSync` / `Video.FpsCap` already registered
(`video.c:106-107`) and live-reapplied (`video.c` ~340-349).
**Net:** the hard part of Phase 1 (drift-free accumulator scheduling) is
already done, on both the sim-tick and present-side clocks. What's left is
narrow: a safety-cap rule and, if wanted, triple buffering — plus whatever
Phase 2 needs on top.
**Phase 2 first slice, scoped (2026-09-12).** The real gap for a 120fps
*presentation* preset isn't pacing math — it's that `gfx_run()`
(`gfx_pc.cpp:3179-3232`) composites straight into the default framebuffer
(fb 0) once per sim tick and then calls `gfx_wapi->swap_buffers_begin()`
once; there's no mechanism to show that same completed frame again on an
extra vsync between sim ticks. The existing `GfxRenderingAPI` already has
everything needed to do this **without any new GL primitives or a second
thread/context**:
- `copy_framebuffer(fb_dst, fb_src, left, top, flip_y, use_back)`
(`gfx_opengl.cpp:1270`) already blits the real window backbuffer
(`fb_src == 0` + `use_back = true` reads `GL_BACK`, confirmed
`gfx_opengl.cpp:1309-1311`) into an arbitrary allocated FBO/texture, and
can blit back the other way (`fb_dst == 0` writes straight into the real
default framebuffer, `gfx_opengl.cpp:1214`'s `fb_id == 0 -> bind 0`
convention).
- `gfx_pc.cpp` already keeps a couple of always-allocated internal
framebuffers this same way (`game_framebuffer`,
`game_framebuffer_msaa_resolved`, created at `gfx_pc.cpp:3069-3070`) — a
new "last presented frame" capture FBO is the same pattern, one more
entry.
- The tick clock (`portTickThread`) runs on its own pthread, independent of
the render/sim thread that calls `gfx_run`/`swap_buffers_begin` — so
spending extra wall-clock time *inside* `swap_buffers_begin` presenting a
duplicate frame does not touch the tick schedule at all; it only changes
how much idle time the render thread burns before the next tick message
is waiting for it. Sim rate stays exactly invariant by construction, per
decision #3.
**Smallest concrete slice:** capture the just-finished frame into a new
internal FBO right before the real `swap_buffers_begin()` call
(`gfx_pc.cpp:3230-3231`, via `gfx_rapi->copy_framebuffer(captureFb, 0, -1,
-1, false, true)`); when an experimental `Video.PresentRate` config knob
requests double-rate (2x — e.g. 120 from a 60Hz NTSC sim, 100 from 50Hz
PAL), have `gfx_sdl_swap_buffers_begin` (`gfx_sdl2.cpp`) do the normal swap,
then — using the same schedule-accumulator shape as
`sync_framerate_with_timer` — sleep to the halfway point of the tick
interval, blit the capture FBO back into fb 0
(`gfx_rapi->copy_framebuffer(0, captureFb, -1, -1, false, false)`), and
`SDL_GL_SwapWindow` again. Default (`Video.PresentRate = 0`) must take none
of these code paths, so the default preset stays byte-identical to today.
Restrict the first slice to exactly {0 = off, 2 = double}; don't generalize
to arbitrary/uncapped multipliers yet — that needs the vsync-off safety cap
from the Phase 1 list above first. No options-menu UI in this slice
(config-file-only); that's a fast follow once the mechanism is playtest-
verified not to tear/stutter.
### Phase 2 — Experimental presentation rates (port-only)
+1 -1
View File
@@ -8,7 +8,7 @@
> `#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
Companion to the (since retired) `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.
+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.
+115 -92
View File
@@ -11,6 +11,10 @@ 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 "How it was made" section carries the final project numbers.*
## Contents
- [Why this project exists](#why-this-project-exists)
@@ -46,7 +50,7 @@ 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. |
| Local agent | `unsloth/Qwen3.8-27B-GGUF:UD-Q4_K_XL` on a single **NVIDIA RTX 5090**, driven mainly through the **[pi](https://github.com/earendil-works/pi)** 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 |
| 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) |
@@ -55,60 +59,35 @@ and forth** through shared written artifacts, directed by one human.
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
"escalate when stuck" move applied at every level.
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 the
rest; 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 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
Boot to window (ROM map + SDL2) :m3, 2026-08-20, 1d
OS shims, threads, fast3d integration :2026-08-21, 2d
First rendered frames :m4, 2026-08-22, 1d
Offline asset-conversion pipeline :2026-08-22, 2d
Entire intro renders (logos to cast) :m5, 2026-08-24, 1d
section Both agents - handoff workflow
Claude joins :milestone, 2026-08-27, 0d
Stage load; Bunker 1 renders + firefight :2026-08-27, 2d
21 solo levels load + render + no-crash :m6, 2026-08-29, 1d
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
v0.3.0 released (full campaign playtested) :milestone, 2026-09-18, 0d
```text
# phase timeline (from the port's own commit dates)
Aug 16 day 0 fork + PC-port scaffolding
Aug 20 day 4 full compile + link (~230 TUs); boot to window
Aug 22 day 6 first rendered frames; offline asset-conversion pipeline
Aug 24 day 8 entire intro renders (logos -> gun-barrel -> cast)
Aug 27 day 11 Claude joins (handoff workflow begins)
Aug 29 day 13 21 solo levels load + render + unattended no-crash; SDL input layer
Aug 30 day 14 front-end flow (menu -> briefing -> start)
Aug 31 day 14 file-backed EEPROM saves
Sep 02 audio mixer (music + SFX)
Sep 14 Steam Deck hardening + F10 overlay QoL
Sep 18 final fix sweep (D309-D321) + release review
Sep 20 day 35 v0.3.0 released: full campaign at Agent difficulty, audio
complete, bundles for Windows, Linux and Steam Deck
```
- **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,
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 33** (18 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
Roughly **two weeks**, one person part-time, took the decompilation from
"builds an N64 ROM" to "boots on desktop, renders every solo level, playable
through the front end into the early game". The rest of month one (29 Aug –
18 Sep) took it from there to a public release: the 21-level sweep to
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.
@@ -117,10 +96,10 @@ issues documented rather than outstanding.
| | |
|---|---|
| Calendar time | 34 days (16 Aug – 18 Sep 2026), one person part-time |
| Commits on the port | ~690 |
| Root-caused bugs logged | `D1`–`D311` in [`findings.md`](https://github.com/jkdansereau/goldeneye-pc-port/blob/main/docs/dev/findings.md), 190 tracked entries in the index (some labels later merged or withdrawn) |
| Handoff sessions | 190+ (`M-2` … `M-191`, and counting) |
| 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/`) |
@@ -129,25 +108,30 @@ issues documented rather than outstanding.
### 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", "9/2", "9/3", "9/4", "9/5", "9/6", "9/7", "9/8", "9/9", "9/10", "9/11", "9/12", "9/13", "9/14", "9/15", "9/16", "9/17", "9/18"]
y-axis "commits" 0 --> 90
bar [1, 2, 2, 6, 9, 2, 4, 7, 57, 39, 39, 46, 11, 52, 39, 8, 8, 2, 8, 34, 17, 30, 20, 30, 57, 4, 84, 38, 22, 13]
```text
# commits per week (week start)
8/16 ## 20
8/23 ########### 109
8/30 #################### 203
9/6 ############## 141
9/13 ###################### 220
9/20 # 6 (release day only)
(each # ~ 10 commits)
```
Days with no commits are omitted (18–19 and 25–26 Aug). The step up from
8/28 onward is the collaborative phase in full swing, including the parallel
multi-agent "bursts"; the 9/15 spike is the Steam Deck playtest-feedback
batch landing at once.
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, snapshot through early September)"
"Claude" : 161
"Local model (Qwen 3.8 / pi)" : 62
```text
# commits by agent (raw count, snapshot through early September)
Claude ################ 161 (72%)
Local model (Qwen 3.8/pi) ###### 62 (28%)
(each # ~ 10 commits)
```
(Phase A, the first 26 commits, to 24 Aug, was entirely the local model,
@@ -178,10 +162,11 @@ chain in relatively few, large commits, and the local model kept contributing
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"
"Claude" : 60
"Local model (Qwen 3.8 / pi)" : 40
```text
# agent effort, milestone-weighted
Claude ############ 60%
Local model (Qwen 3.8/pi) ######## 40%
(each # ~ 5%)
```
The local model's ~40% is front-loaded and foundational (the build, the boot
@@ -195,21 +180,19 @@ result worth taking away.
This is the part worth paying attention to.
```mermaid
flowchart LR
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"]
D[(Shared artifacts:<br/>HANDOFF.md · findings.md · porting-notes.md)]
```text
# the handoff workflow
H -->|scopes task, budget, files| C
H -->|scopes task, budget, files| Q
C <-->|reads / appends| D
Q <-->|reads / appends| D
C -.->|usage limit reached| Q
Q -.->|hard structural bug| C
C -->|patch + write-up| H
Q -->|patch + write-up| H
HUMAN
direction, integration, build + playtest verification
| scopes task, budget, files | scopes task, budget, files
v v
CLAUDE (hosted) QWEN 3.8 / pi (local, RTX 5090)
| patch + write-up | patch + write-up
+--------------+ +--------------+
v v
SHARED ARTIFACTS (both agents read + append)
HANDOFF.md . findings.md . porting-notes.md
```
Both agents worked against the **same three written artifacts**, which is what
@@ -221,8 +204,8 @@ let them substitute for each other:
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. 311 numbered `D`
entries by v0.3.0 (190 tracked in the index; some labels merged or
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.
@@ -249,8 +232,7 @@ Honest notes, for anyone weighing whether this transfers.
- 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.
rewritten later.
- **The written-artifact discipline made agent output compound.** An agent
starting cold late in the project was more effective than one early on,
because the accumulated notes were good. This is also what made the two
@@ -277,8 +259,49 @@ Honest notes, for anyone weighing whether this transfers.
to Opus 5. Each tier earned its place on the problems the tier below it
couldn't close.
**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).
**Still outstanding:** see
[`docs/ROADMAP.md`](https://github.com/jkdansereau/goldeneye-pc-port/blob/main/docs/ROADMAP.md),
the single tracker for open work.
## Since the two-agent phase
The two-agent setup above is the **initial phase**, snapshotted at v0.3.0
(the project has since shipped v0.4.x and is heading to v0.5.0; the model
and harness record below is current through that point).
Since then the emphasis has shifted from "the two models that built it" to
**testing a lot of new models and harnesses**, to find what's fastest and
cheapest per task. The long-time local workhorse stayed **Unsloth's Qwen 3.8 27B**;
in the final week that broadened into a menu, and the **current standard is
NInfer + Strata**. On the hosted side, the Claude orchestrator stepped up from
**Sonnet 5 / Opus 5** to the **5.5 tier** (Sonnet 5.5 / Opus 5.5, out in the
last few weeks), which has driven the most recent work. The local menu:
- **Qwen 3.8 27B** ([Unsloth](https://huggingface.co/unsloth/Qwen3.8-27B-GGUF),
`UD-Q4_K_XL`) — the primary local workhorse for most of the port's life, doing
nearly all of the local groundwork on its own. A larger-context
[`ISTA-DASLab/Qwen3.8-27B-GSQ-RCO-GGUF`](https://huggingface.co/ISTA-DASLab/Qwen3.8-27B-GSQ-RCO-GGUF)
(`IQ3_S`) was also briefly trialed for bigger-context workloads, then shelved
(little use since v0.2.0).
- **NInfer (Windows)**
([natpate/ninfer-windows](https://github.com/natpate/ninfer-windows), v0.9.0,
CUDA 13.1 — a standalone Windows port of the NInfer engine, with MTP /
DFlash2 speculative decoding) — ran **Qwen 3.8 27B** in base, NVFP4 and
NVFP4-full artifacts; trialed in the final week, now half of the standard
and the fast day-to-day local driver.
- **Qwen 3.8 Flash-Next** via
[Strata](https://github.com/Niko1221/Strata) (Niko1221's MIT-licensed
C++/CUDA/HIP engine) — a 125B mixture-of-experts model that normally needs a
server, run **locally on a single gaming GPU** (IQ1_M, 1-bit) for the
biggest-context coding passes. The other half of the standard.
- **[claude-code-delegate-local](https://github.com/fegone/claude-code-delegate-local)**
— an MCP server that let the Claude Code orchestrator and the local models
talk, delegating Claude's subagents to the local engines. A nice tool, but
**mostly deprecated now**: hand-offs between agents are done by hand for now,
pending a better parallel-agent handoff system that drops the MCP overhead.
- **GPT-6 (Sol)** via [OpenRouter](https://openrouter.ai) — a frontier hosted
model, **trialed** to see how it held up (not the hardest-work tool).
The practical effect: the project stopped depending on any one model or its
usage limits. The local GPU is now the default path for high-volume work, with
hosted frontier models reserved for the problems the local models can't close.
+373 -78
View File
@@ -7,46 +7,47 @@ D43-D50,RESOLVED,"model-file loading → offline sidecar ""Plan B"" (d43_emit.py
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,OPEN,front-end 3D model transforms (D77 audio split out below —,"OPEN (parked). M-32 triage (see §F ""D75 ADDENDUM""):"
D75,FIXED,front-end 3D model transforms (D77 audio split out below —,FIXED 2026-10-02 (branch fix/fidelity-gaps; by-eye check owed):
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,PARTIAL,Native-PC input pass (QoL run):,"LANDED, config-gated, feel-checks owed"
D186,PARTIAL,"Video.FpsCap throttles the sim, not just presentation (M-39 diag, M-42 clamp).","CLAMP LANDED (port/fast3d/gfx_sdl2.cpp, port/src/video.c); real fix owed"
D180 · D181,FIXED,Native-PC input pass (QoL run):,FIXED (2026-09-26 bookkeeping:
D186,CLOSED,"Video.FpsCap throttles the sim, not just presentation (M-39 diag, M-42 clamp).",CLOSED (2026-09-28 bookkeeping:
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,PARTIAL,Mouse input needs another tuning pass —,PARTIAL — M-120 human playtest…
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,PARTIAL,Phase-3 software audio mixer:,"LANDED (port/src/mixer.c, port/include/mixer.h, include/PR/abi.h) —"
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,?,"QoL gap (M-87, from a PD-PC-port survey):","DEPRIORITIZED (M-87, user call)."
D224,CLOSED,"QoL gap (M-87, from a PD-PC-port survey):",CLOSED (2026-09-28 bookkeeping:
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,OPEN,"QoL ask: player-adjustable HUD scale for the ammo counter, bottom-left pickup/item status text, and top-of-screen dialogue/subtitle text.","OPEN — root cause / call sites identified, not designed or implemented."
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…"
D244,OPEN,Self-hosted CI runner 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,?,"Silenced PPK ""slap"" + broader real-time mixer corruption (M-52→M-67).",ROOT CAUSE ESTABLISHED (M-66); Disposition C implemented + measured (M-66b) —
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,?,"Alarm klaxon starves combat SFX and doesn't recover after it stops (surfaced by D206, M-77).",MONITORING (M-77c: user reports the alarm is acceptable in play after D206; no fix shipped) —
D207,CLOSED,"Alarm klaxon starves combat SFX and doesn't recover after it stops (surfaced by D206, M-77).",CLOSED (2026-09-28 bookkeeping:
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).…"
@@ -55,55 +56,55 @@ D152+,FIXED,D152 addendum (M-31):,"FIXED (src/snd.c, port/src/libultra.c, #ifdef
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,OPEN,In-game stutter / brief hang during normal play —,"OPEN — observed, not investigated"
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
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.",FIXED (2026-09-29 bookkeeping:
D177,FIXED,Ladders non-functional —,"FIXED (M-36) — high confidence; interactive climb test owed Owed climb test considered cleared by the 2026-09-28 v0.4.0 campaign sign-off (ROADMAP §3 bookkeeping, 2026-10-01); reopen if seen."
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,OPEN,"""No blood effect"" (user QA report).",OPEN — observed; first step = BUNKER1 firefight with regenerated sidecars
D183,OPEN,"M-36 Family A (""texture line/pitch shear"") is DISPROVEN for the Surface repro (-level_36).",Hypothesis DISPROVEN + diagnostics shipped; D176(b)/D182(2) still OPEN
D173,FIXED,Third-person Bond model spawns too high —,"MOSTLY FIXED as a side effect of D243 (2026-09-18, user-confirmed)."
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,PARTIAL,Third-person Bond model spawns too high —,PARTIAL (2026-09-29 bookkeeping:
D179,FIXED,Packaged build crashes after the logos —,"Diagnosed, not fixed —"
D180,PARTIAL,"Native-PC input pass (M-XX QoL run, qol/native-pc-input-menu, port-only).","LANDED, port-only, no #ifdef PORT.…"
D181,?,Game.ScreenShakeIntensity —,"Menu row pulled (v0.2.1, user testing)."
D184,PARTIAL,F10 port-layer options overlay —,"LANDED, port-only, no #ifdef PORT in src/."
D211,PARTIAL,"PC FOV slider — Video.FovScale (M-83 QoL wave, port-only, WIDESCREEN-FOV-PLAN.md Phase 4).",PARTIAL — M-84 refix…
D212,PARTIAL,Anisotropic-filtering config wiring —,"LANDED, port-only. Build clean."
D213,PARTIAL,On-screen FPS readout —,"LANDED, port-only. Build clean; bunker1 no-op confirmed."
D214,PARTIAL,"Keyboard rebinding — [Input.Bind] config section (M-83, port-only, PD parity, Phase 4 debt).",PARTIAL — M-84: code PASS (alt/ctrl); one doc-only fix.
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,PARTIAL,"Game.SkipIntro — boot straight to the file-select menu (M-83, port + one #ifdef PORT game-code line, PD parity, Phase 4 quick win).",LANDED + VERIFIED this session.
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,PROPOSED,Wide FOV exposes the per-level draw-distance / fog boundary —,"Logged (M-84), fix proposed + parked."
D218,FIXED,Wide FOV exposes the per-level draw-distance / fog boundary —,FIXED (2026-09-26 reclassified; 2026-09-29 default corrected:
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,PARTIAL,"1P muzzle flash position shifts upward at higher Video.FovScale (M-87, user QA, Jungle/M16).","ANALYZED (M-91) — the suspected unscaled-viewmodel root does NOT hold; this is correct perspective behavior, likely cosmetic/QoL."
D85,OPEN,Room primary/secondary DL → texLoadFromGdl garbage.,"OPEN (safety-netted; widen/pool fixes landed, geometry now renders)"
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) —
D132,CLOSED,D88 propDefs layout audit (M-20):,CLOSED (2026-09-28 bookkeeping:
D157,FIXED,"Campaign never unlocks the next level (M-30, user bug —","FIXED (src/bondtypes.h, #ifdef PORT —…"
D160,PARTIAL,Dam level-end cutscene (Bond off the dam / bungee) never plays —,"DIAGNOSTIC SHIPPED, not root-caused (c95713f5, #ifdef PORT + GE_D160=1)…"
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)"
D117,CLOSED,"Frame nondeterminism root-caused; GE_DETERM attempted, not achieved.",CLOSED (2026-09-28 bookkeeping:
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,PARTIAL,"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…",partial — see rows
D266-D269,OPEN,"D236/D265 wall, second pass:",OPEN (the wall itself) —
D270,?,Backfilled (M-132) — this label was never given its own row; only code comments and the HANDOFF archive documented it.,dead-ended into D271/D272; infrastructure kept in tree
D271,FIXED,"D236 wall, third pass —",FIXED-but-insufficient (src/game/bg.c #ifdef PORT) —
D276,OPEN,"D236 wall, eighth pass —","REFRAMED — oml=0xc81049d8/fmt=3 is the primary suspect now, not D269's CI8 pair; 0x0c184b50 unattributed; wall still open, see D236"
D279,?,Backfilled — this label was consumed by an undocumented partial session (2026-09-14 ~23:00) that left artifacts in scratch/ (d279_run.log… —,undocumented — artifacts only
D281,OPEN,"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… —","OPEN — user-reported regression, not root-caused; mitigation is user-facing guidance only (release notes:"
D150,FIXED,watch OBJECTIVES / BRIEF page crash:,"FIXED (src/str.c #ifdef PORT); interactive re-verify pending Owed re-verify considered cleared by the 2026-09-28 v0.4.0 campaign sign-off (ROADMAP §3 bookkeeping, 2026-10-01); reopen if seen."
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,CLOSED,"D236 wall, third pass —",CLOSED (2026-09-29 bookkeeping:
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,?,"D236 wall, ninth pass —","REFRAMED again — no fix; next steps in detail section, see D236"
D275,?,"D236 wall, eighth pass —","done — probes live, uncommitted; see D276 for what they found"
D274,OPEN,"D236 wall, seventh pass —","d267_slot_addr fix live (always-on); content/decode question CLOSED; mip-presence CLOSED; wall still open, see D236/D276"
D273,OPEN,"D236 wall, fifth pass —","RULED OUT (port/fast3d/gfx_pc.cpp palette_to_rgba32, pre-existing/uncommitted GE_D273) —"
D272,OPEN,"D236 wall, fourth pass —","OPEN — probe GE_D272 added to port/fast3d/gfx_pc.cpp (env-gated, uncommitted); wall still open, see D236"
D283,FIXED,"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… —","FIXED (2026-09-30, port-only) —"
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).
@@ -121,9 +122,9 @@ D136,RESOLVED,"Stage-unload crash — objFree obj->prop reads float garbage (M-2
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,?,"Mission-failed / objective-failed → permanent black screen (M-28, user bug report —","MITIGATED (port/src/libultra.c, M-28, #ifdef PORT/port-only).…"
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,PARTIAL,"Facility outro-cutscene ""hang"" —","GUARDED (src/game/model.c, #ifdef PORT)…"
D152,FIXED,"Mission-failed / objective-failed → permanent black screen (M-28, user bug report —","FIXED (user, 2026-09-26:"
D154,CLOSED,"bg.c room-geometry ray/hit-test GBI parser port (M-28, WRITTEN / UNVERIFIED / UNCOMMITTED).",CLOSED (2026-09-28 bookkeeping:
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 —…"
@@ -131,23 +132,23 @@ D130,RESOLVED,"crash class C2 (Facility -level_34, Runway -level_35):",resolved
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,OPEN,Wrong textures load after dying/restarting a level —,OPEN — needs: does it repro on a *fresh* level boot (rules out reload-state) vs only after death/restart (confirms it); which surfaces; a screenshot.…
D236,OPEN,"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).","OPEN, sixteenth pass (2026-09-18) —"
D237,OPEN,"QoL ask (M-106, user QA):",OPEN — not designed; port-only
D238,OPEN,"QoL ask (M-106, user QA):",OPEN — not designed; port-only (port/src/input.c + config.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,OPEN,"Player gunshot SFX timing/cadence reportedly does not match the N64 original's rate (M-106, user QA report).","OPEN — not root-caused; static call-site tracing only, no runtime verification possible in this environment."
D241,OPEN,Player gunshot SFX goes silent under some conditions —,"OPEN — not newly root-caused; high confidence this is D207 reproduced, not an independent bug."
D242,?,Player-death sequence UX regression vs N64 original:,NOT A PORT-LOGIC BUG as far as static analysis can tell.
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,OPEN,IsWater water: two competing patterns with a visible seam that moves as the player looks around (mouse/aim) —,"OPEN — REOPENED, M-142 headless probe found the tc-shift k measurably flips during ordinary view rotation (new root-cause evidence); an opt-in GE_D245_FIXEDSHIFT candidate is build-verified but needs…"
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); narrowed to the portal/room-visibility BFS, needs a live screenshot+location, not more c…"
D249,CLOSED,"LOD/culling breaks down on large open levels (Streets, Egyptian named specifically) at *default* FOV —",CLOSED (2026-09-28 bookkeeping:
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,PARTIAL,"Particle effects cycle through a rainbow palette (M-124, user playtest of the v0.2.0-pre candidate).","PARTIALLY FIXED — a real, distinct manifestation was found and fixed, but the bug family is not fully closed, and is intermittent on BOTH platforms, not Deck-specific (corrected 2026-09-18)."
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…"
@@ -159,42 +160,336 @@ D261,FIXED,v0.2.1 (user request:,FIXED — feel-check received; W/S sensitivity
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,?,"D236 investigation (port-layer, port/fast3d/gfx_pc.cpp):",PARKED — reverted to baseline; evidence + suspect list above.
D258,OPEN,PAL/JP asset conversion is broken at the source-data level —,"OPEN — repair path verified, not applied (out of v0.2.0 scope)."
D265,CLOSED,"D236 investigation (port-layer, port/fast3d/gfx_pc.cpp):",CLOSED (2026-09-26 bookkeeping:
D258,DEFERRED,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,PARTIAL,Steam Deck SIGSEGV during D255 live-testing = SEPARATE bug:,"PARTIALLY FIXED, RECURRED PAST THE FIX (see D305, 2026-09-18)."
D286,OPEN,"User-confirmed real ~8s frame freeze on Caverns, Steam Deck —","OPEN — one confirmed occurrence, no repro trigger identified, not root-caused; distinct from D255/D285."
D287,OPEN,"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… —","OPEN — one confirmed occurrence, real (not self-recovering), not root-caused; gfxFrameMsgQ full is a lead, not a confirmed cause."
D288,OPEN,"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 —… —","OPEN — visually confirmed and precisely bounded (frames ~562-741 of a bare -level_20 boot), root cause not yet identified; promoted from the long-parked GRAP… |"
D289,OPEN,"Dam: a crash in the computer room, not reproducible on a second attempt (M-141, Steam Deck user playtest).","OPEN — needs a human re-test specifically on a build with the D255 fix (this session's release/v0.3.0, not any earlier build) to see if it still happens.…"
D290,OPEN,"Watch's in-game ""controller"" section shows no control-pad graphic (M-141, Steam Deck user playtest).","OPEN — observed, not investigated."
D291,OPEN,"Dam: texture glitches when the truck drives past (M-141, Steam Deck user playtest).","OPEN — observed, not investigated."
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,OPEN,"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… —",OPEN — not investigated.…
D294,PARTIAL,"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… —",PARTIAL -> fix landed pending by-eye check (2026-10-03:
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,OPEN,"Control Center, 00 Agent:","OPEN — single live report, un-triaged; observed only under a gdb-attached debug-harness launch (tools_pc/debug.ps1), not the packaged release artifact —"
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,?,Full HUD (not just the aim-mode crosshair) missing in a v0.3.0 dev build —,"s — regression reported 2026-09-16 during v0.3.0 work (build ce8427cf); user says ""don't fix, just for context""; not yet investigated —"
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,OPEN,"Some muzzle flashes (M16 named specifically, ""and some others"") draw wrong —","OPEN — reported only, not yet investigated."
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,OPEN,"Dam intro/truck wheels render with odd transparent-looking areas, possibly z-fighting (user QA, 2026-09-18).","OPEN — reported only, not investigated."
D307,OPEN,"In-level security camera entities can be flipped facing the wrong direction (backwards) on some levels, e.g.","OPEN — reported only, not investigated."
D308,OPEN,"Occasional z-fighting confirmed on some levels' geometry, general/broader than the D306 truck-wheel instance (user QA, 2026-09-18).","OPEN — reported/confirmed by the user, not investigated."
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,PARTIAL,Complete freeze in modelSetAnimFrame2WithChrStuff root-motion loop —,"GUARDED (#ifdef PORT, D156-guard extension in the same file) —"
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,OPEN,"F10 menu items flicker randomly/intermittently (reported on the main-menu context, user report, v0.3.0 playtest build, 2026-09-18).",OPEN — confirmed still present post-D316 fix (2026-09-18); user deprioritized chasing it further for now.
D315,OPEN,Menu scaling differs between main menu and in-game —,"OPEN — disclosed as a known issue, deferred to v0.3.1 (bigger design question, not a mechanical 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,RESOLVED,Facility (regular play path):,RESOLVED (2026-09-19):
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,OPEN,"D318-class sweep: aim-hold → _update AI lists in 8 levels carry the same as-authored softlock race (static analysis, 2026-09-20).","OPEN — static sweep complete (Facility ai_19, Control ai_9, Depot ai_12 flagged high/med-high; Bond-combat loops likely safe); no live confirmation beyond D318 itself; probe-verify path + generalized…"
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,CLOSED,"File-select ""PC Options"" label:",CLOSED (2026-09-28 bookkeeping:
D349,CLOSED,GE watch settings ARE saved to the Bond file when the watch closes:,"SUPERSEDED (2026-10-04, P2b correction):"
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,CLOSED,F10 full-width control boxes misaligned with their contents (2026-09-27).,CLOSED (2026-09-28 bookkeeping:
D368,CLOSED,Simplify F10 click targets:,CLOSED (2026-09-28 bookkeeping:
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 (2026-09-28 bookkeeping:
D378,FIXED,GEPD use/reload separation (2026-09-27).,FIXED (2026-09-28 bookkeeping:
D379,FIXED,Crosshair colour: authored texture is red (2026-09-27).,FIXED (2026-09-28 bookkeeping:
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,CLOSED,PD-style keyboard rebinding in both options UIs (2026-09-27).,CLOSED (2026-09-28 bookkeeping:
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,FIXED,M7 AllUnlocked's read-time fake unlocks leak into saved EEPROM (2026-09-27).,"FIXED 2026-09-30 (port-only per-slot raw/seen merge on EEPROM write, libultra.c geD387MergeSlotWrite; isolated ON-vs-OFF byte-identity matrix passed)."
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,CLOSED,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,CLOSED,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).,CLOSED (2026-09-28 bookkeeping:
D400,CLOSED,"File-select ""PC Options"" label:",CLOSED (2026-09-28 bookkeeping:
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):"
D541,PARTIAL,One A press advanced two menu screens on the Steam Deck (Desktop Mode) —,"PARTIAL 2026-10-05 (mitigation landed, port-only:"
D542,OPEN,"Stray polygons left of Bond's gun, frequent on foggy levels (Streets, Surface 2) —",OPEN 2026-10-05 (triage:
D543,FIXED,Surface 2: lit area following Bond brighter than 1964 —,"FIXED 2026-10-06 (CPU RSP-style near clip in gfx_sp_tri1 + per-vertex fog default; build-verified, by-eye vs 1964 at next playtest)"
D544,FIXED,OS cursor flickers with F10 open on the main menu —,"FIXED 2026-10-05 (port-only, input.c; build-verified, maintainer mouse check owed)"
D545,FIXED,Player rockets can pass through the ground when fired near Bond's feet —,"FIXED 2026-10-06 (AVOID_UB array, gun.c; maintainer Archives preset A/B:"
D546,FIXED,PC settings standardization:,FIXED 2026-10-05 (port-only; build + maintainer check owed; golden re-base batched with D543)
D547,FIXED,Game.AllUnlocked applies live (no restart) —,"FIXED 2026-10-05 (port-only, main.c/optionsoverlay.c; build-verified, maintainer mission-select check owed)"
D548,FIXED,Walls near rocket fire/explosions banded black/white/yellow/blue (Aztec dark corridor; also seen on Deck) —,FIXED 2026-10-06 (gfx_opengl.cpp input clamp; root fix = D543 CPU near clip; maintainer Aztec re-check owed)
D549,FIXED,Thrown grenade/knife/object:,"FIXED 2026-10-06 (AVOID_UB coord3d, gun.c x3; build-verified)"
D550,FIXED,"Static title bar (no more per-second ""NN fps"" refresh) + project mark as favicon / window / taskbar icon —","FIXED 2026-10-17 (port + tooling only, video.c/gfx_sdl2.cpp/make_icon.py; ninja-command-verified + full build 249/249; maintainer live window/taskbar check owed)"
D551,?,"Opt-in update check (Game.CheckUpdates, default off):",ADDED 2026-10-06 (port layer only; live-verified Windows/Linux/Deck + F10 UI capture; security review fixes applied)
D552,FIXED,Dam ending cutscene: the first camera shot no longer swivels up onto Bond —,FIXED 2026-10-06 (port-only addition removed; code path back to the decomp; maintainer-verified live)
D553,FIXED,Surface 2: the haze around Bond brightened/darkened as he moved —,FIXED 2026-10-06 (fast3d:
D554,OPEN,F10 settings menu: wording/casing pass + two-line tips —,DONE 2026-10-06 (port/src/optionsoverlay.c; maintainer by-eye check owed)
D555,OPEN,Crosshair mouse pointer over the F10 / PC Options overlay —,"DONE 2026-10-06 (port/src/input.c, optionsoverlay.c, tools_pc/make_icon.py; maintainer by-eye check owed)"
D556,OPEN,"F10 feedback pass (tips trimmed, conditional-row order, wording fixes) + mouse scrollbar —","DONE 2026-10-06 (port/src/optionsoverlay.c; build only, by-eye check owed)"
D557,FIXED,"New/blank profiles start with Look ahead OFF on PC (Rule-2 sign-off, maintainer 2026-10-06) —",FIXED 2026-10-06 (src/game/file2.h DEFAULT_OPTIONS under #ifdef PORT; existing saves untouched; win gate 21/21)
D558,FIXED,F10 overlay tips cut off in a level (not on the front end) —,FIXED 2026-10-06 (optionsoverlay.c textRenderWide; verified by in-level capture)
D559,FIXED,"Look ahead off for untouched existing folders (maintainer-approved, port layer only) —",FIXED 2026-10-06 (watchsettings.c migrateUntouchedLookAhead)
D560,FIXED,F10 copy pass 3 (maintainer line review) —,FIXED 2026-10-06 (optionsoverlay.c tips/labels/value names; display text only)
D561,FIXED,"F10: ""Menu pointer"" row removed (front-end-only legacy option; confused players) —","FIXED 2026-10-06 (row/tip/names removed; Input.MenuPointerMode ini key, Direct default and the legacy code path kept for one release)"
D562,FIXED,F10 crosshair pointer hidden while a pad/keyboard drives the menu —,FIXED 2026-10-06 (drawCrosshairPointer draws only while s_mouseActive)
D563,FIXED,F10 root hub: Left/Right no longer open pages; held nav inputs latched on open/page change —,FIXED 2026-10-06
D564,FIXED,"F10: game's own ""Look up/down"" row removed (dead for mouse and centred pad, stacked with Invert look); value pinned to factory; tip polish —","FIXED 2026-10-06 (port only; Mouse / Controller ""Invert look"" are the inversion controls)"
D565,FIXED,AI awareness followed two player render settings:,FIXED 2026-10-06 (maintainer decision:
D566,FIXED,Steam Deck Game Mode showed the on-screen keyboard at every launch (SDL text input left on) —,FIXED 2026-10-06 (port only:
D567,FIXED,Front-end PC Options with a pad pulled the game's crosshair to the top-left (overlay synced the cursor to the untouched OS mouse) —,FIXED 2026-10-06 (port only:
D568,FIXED,F10 dropdown popup drawn empty and misplaced for a long list on a mid-page row (negative top) —,FIXED 2026-10-06 (port only:
D569,?,Crosshair color follows the PD port:,CHANGED 2026-10-06 (maintainer request; old presets migrate to Custom RGB)
D570,FIXED,Scaled crosshair (Crosshair size != 100%) showed a faint copy of the top bar's tip under the bottom bar (WRAP tile flags) —,FIXED 2026-10-06 (port only:
D571,?,"Project icon restyled to the in-game sight red as pixel art (exe, window, favicon) —",CHANGED 2026-10-06 (maintainer request; make_icon.py; social preview unchanged)
D572,?,Linux tarball glibc floor lowered from 2.38 to 2.31:,CHANGED 2026-10-07 (maintainer request; packaging only; X220 glibc 2.36 fresh install PASS)
D573,FIXED,"sndPlaySfx / langGet pointer guards used an absolute [64 KiB, 16 GiB) literal; replaced by portAddrIsMapped() (DRAM arena views + mapped cart image) —","FIXED 2026-10-07 (from #108, italoarruda; Windows build + golden:"
D574,FIXED,Legacy front-end menu-pointer Velocity mode deleted (Input.MenuPointerMode / Input.MenuPointerSpeed); retired ini keys dropped silently —,FIXED 2026-10-07 (D561 follow-up; build + ini runtime check)
D575,FIXED,Update check (D551) gave no result on Linux/macOS when /releases/latest exceeded 64 KB (curl SIGPIPE); parse helpers moved to a header with an offline test —,FIXED 2026-10-07 (harness 19/19; live Linux/Deck matrix owed)
D576,FIXED,"ge007.log written next to the ini (previous run kept as ge007.prev.log, 8 MiB cap) —",FIXED 2026-10-07 (Windows runtime check)
D577,FIXED,"F10 slider labels ended in "".."" at HUD 150:",FIXED 2026-10-07 (captures HUD 100 unchanged / HUD 150 whole)
D578,OPEN,Frame interpolation above 60 fps (Video.FpsCap 90/120/144):,BUILT 2026-10-07 (Windows:
D579,FIXED,Issue #150: hugging a wall makes geometry vanish (N64 native widescreen does not).,"FIXED 2026-10-09 (maintainer-verified, Dam gate 16:9; golden sweep owed)"
D583,FIXED,120 fps + VSync on G-Sync/VRR:,"FIXED on PC 2026-10-09 (G-Sync OLED, maintainer by eye); Deck 90 good on most levels (per-level pass 2026-10-10); open:"
D584,FIXED,Cradle catwalk shadow flicker at every frame rate (regression from D579).,FIXED 2026-10-10 (maintainer Deck:
D614,OPEN,Deck Bunker fight texture-cache thrash:,"OPEN: measured, fix not shipped (2026-10-10)"
D605,FIXED,"Issue #145 (v0.6.0): F10 ""Invert look"" never reached the look stick in the HONEY role (game styles 1.1/1.3 -- left stick looks, right stick is the digital move…",FIXED 2026-10-09 (v0.6.0 line; port-only:
D606,FIXED,Issue #144 (v0.6.0 triage):,"FIXED 2026-10-09 (v0.6.0 line; port-only in port/src/optionsoverlay.c, src/game zero-diff; off by default so goldens stay byte-identical; smoke-verified:"
D407,CLOSED,Tanks cannot be boarded/exited on PC —,CLOSED (2026-09-28 bookkeeping:
D408,FIXED,Game.SkipIntro skips the post-mission failure dossier:,FIXED 2026-09-30 (SkipIntro hook gated to first boot; live dossier check owed).
D415,FIXED,"Shooting a light fixture does not ""kill"" its flickering light (Bunker, Caverns):",FIXED (2026-09-29 bookkeeping:
D416,RESOLVED,"2-player split-screen crashes (issue #99, v0.3.0 4827bc8, fault addr 0xa43a0ed4, PC 0x14009e758):",RESOLVED in-harness (converter + D417 + D418 fix the 2P crash and split-screen flicker; 18/18 2P/3P/4P x 6-stage load runs clean; live pad play still owed).
D417,RESOLVED,enum PROP unsigned on GCC:,"RESOLVED (src/bondconstants.h PROP__PORT_SIGNED = -1, D3/D142 precedent; Rule-2 review done 2026-10-06:"
D418,RESOLVED,"Split-screen: fast3d safe-area crop stretched the last-set (half-height) viewport over the whole window, so each player's view filled the screen and the last-d…","RESOLVED (gfx_set_split_screen, port-only)."
D419,RESOLVED,Split-screen input shared between players / P1 lost solo controls:,"RESOLVED (per-slot slotPlayer(), port-only; no current-player swaps from the poll thread; live-tested OK)."
D420,FIXED,MP score / stats screen shows an odd big number (issue #99) —,FIXED 2026-10-01 (mp/virtual-seats); 2P/3P/4P watch-menu runs show 0s.
D421,CLOSED,Gadget cycling (pad B / gadget key) disabled in 2+ player matches —,"CLOSED 2026-10-01: moot, no MP inventory holds a gadget (mp_weapon.c); hook reviewed + reverted."
D422,FIXED,P2+ have no centred-aim mode in split-screen (single-slot aim state) —,FIXED in tree (commit ac92e0ab:
D423,CLOSED,Unidentified black arrowhead in P2's split-screen view (stage 38) —,"CLOSED 2026-10-01: faithful (level shadow at a doorway top), maintainer-confirmed live."
D424,FIXED,WeaponStats.RecoilSpeed/b44[] byte order:,"FIXED in tree (WS_B44, port-guarded); user-verified vs GEPD/1964 2026-09-29."
D425,FIXED,Tank run-over death sound loops:,FIXED (2026-09-29 bookkeeping:
D426,RESOLVED,Tank cannot be steered with the mouse out of aim mode (issue #116).,RESOLVED (user 2026-09-29:
D427,FIXED,"Automatic fire cadence is frame-counted, ~2-3x faster than N64 at 60 fps (issue #114).","FIXED in tree (modulus scaled by ticks/frame; measured 6 ticks/shot at 30 and 60 cap); user-verified vs GEPD/1964 2026-09-29 (AK47 mag empties in ~3 s, Klobb normal)."
D428,FIXED,No gun turn-sway with direct mouse look (issue #117).,FIXED in tree (2nd pass:
D429,CLOSED,Tank full pass: aim-mode (RMB) mouse behaviour and tank sticking forward/back (follow-up to D426).,CLOSED faithful (user 2026-09-29:
D430,FIXED,Shot light fixtures do not go dark:,FIXED in tree (D154-style words.w0/.w1 decode); user-verified vs GEPD/1964 on Caverns 2026-09-29.
D431,FIXED,Light fixtures never reached lightFixtureBreak on PC:,FIXED in tree (fixture table records the image id; bg.c hit code looks it up; free-slot guard); user-verified vs GEPD/1964 2026-09-29.
D432,FIXED,"Gunshot sounds ""off"" after D427:",FIXED in tree (gate offset rounded to odd at 1 tick/frame); measured shot/sound cadence identical to N64 rules; residual audible difference vs 1964 tracked as D433.
D433,OPEN,"Slight audible difference in automatic-gun shot sound vs GEPD/1964 after cadence was matched (user by-ear, Caverns).",OPEN (not a timing issue; audio layer suspected).
D434,FIXED,Raw ((u8*)&g_Textures[n])[0] & 0xf hit-type reads are wrong on PC; latent OOB once D431 returned real texture ids (found by sub-agent review).,"FIXED (.hitTexture at 3 sites, port-guarded); live check owed."
D440,FIXED,"""Original N64"" / ""Port defaults"" one-press presets in the F10 overlay and front-end PC Options (+ -n64preset/-portpreset).",FIXED (port-only; headless-verified; user live accept owed).
D441,PARTIAL,64-bit pointer-width sweep (part 1):,"PARTIAL (census + GE_HIGHARENA_BASE tier-1 build + 35 sites fixed + CI ratchet, 2026-10-03; tier-1 all-level sweep 21/21 PASS, 0 crashes, 2026-10-05; the above-4-GiB tier still owed)."
D442,FIXED,Game.AllUnlocked reworked into a pure RAM/query-time override:,"FIXED; verified 2026-10-03 headless (mission select all 20 / only real progress when OFF, full cheat menu, eep byte-identical)."
D443,FIXED,"D357 settings-display cleanup (mouse ×, whole-% deadzone and volume steps, FOV in horizontal degrees, MSAA 16×) plus a ""Restart game"" action (orderly D344 quit…",FIXED (port-only; reset probes pass; both-UI visual check and one Restart click owed; Linux relaunch path not compiled).
D454,FIXED,Knife slash crashed on Windows:,FIXED (verified by maintainer live:
D461,FIXED,Item-slot allocation overran into the next stage-bank block:,FIXED (maintainer-verified:
D462,FIXED,vtxstore.c sized its two record tables with the N64 stride 0x14 for a 24-byte struct unk_09B7A0_struct_parent on PC (Vertex* unk00 widened); both are indexed a…,FIXED (build + solo -level_09 and 4P GE_STARTMP=4 smoke runs exit clean; no crash; by-eye owed in solo).
D444,FIXED,Front-end PC Options:,FIXED (build-verified; human mouse test owed)
D455,FIXED,Binary-first census of exe-static addresses truncated to 32 bits (D454 class) (2026-09-30).,FIXED/CLEAN (no further REAL site beyond D454)
D447,FIXED,Pillarbox output mode (Video.AspectMode = Original) for the Original N64 preset; crop overscan no longer cancels Wide/Cinema.,"FIXED (build + 3 captures verified; Wide/Cinema, 16:9 Ratio, mouse and 1964 comparison owed to a human)"
D445,FIXED,Crouched-rifle firing animation (crouched_rifle_firing_animation_groupA) never resolved on PC -> SIGSEGV in modelConstrainOrWrapAnimFrame when an MP player cro…,"FIXED — layout only (§D5 linker adjacency, D164 class):"
D446,FIXED,Couch 2P watch-menu text (the MP pause page) differs by +-1-2 per channel between builds whose only difference is data layout (2026-09-29).,FIXED — port-only (fast3d):
D448,PARTIAL,Split-screen pad seats:,"PARTIAL (committed 918ac5f9 on feat/mp-splitscreen-99, build + 2P smoke OK; plug/unplug behaviour not exercised at runtime)."
D456,?,"Font-pointer / text-string-pointer slots retyped from s32 to real pointers (ABI/layout exception; front.c, options.c, language.c, bondview*, mpmenu.c, propobj.…",
D457,?,64-bit ABI retypes part 2:,
D453,?,"memp allocator API made 64-bit clean (D441 step 0; ABI/layout exception, §A1).",2026-09-30
D451,FIXED,Automatic-fire gate at unsteady frame rates (D427 follow-up) and D173 residual.,"FIXED (RULE-2-SIGNOFF, maintainer 2026-09-30:"
D463,FIXED,fast3d divided by zero (0xc0000094) importing a texture for a zero-area TextureRectangle on a tile with line = 0:,FIXED (port-layer guard in import_texture; 8 repeats of the crashing config + the 18-run sweep + solo -level_09 + menu boot all rc=0 with no crash; the guard fired in 3 of 8 repeats).
D464,PARTIAL,"GE_MEMPREDZONE=1: 64-byte red-zone checker for the memp bump allocator (all banks), side table + checks at level unload / every 60 frames / orderly quit; re-in…","TOOL LANDED (local, feat/memp-redzone); MP sweep owed."
D465,FIXED,#127 Dam/Caverns/Complex water slid side to side:,"FIXED 2026-10-01 (fix/d465-water-primlod, local); rate measured = N64; maintainer by-eye on Dam:"
D466,FIXED,#125 Xenia's boss fight on Jungle triggers early:,"FIXED 2026-10-01 (fix/d466-drawdist-gameplay, local); RULE-2-SIGNOFF (port-feature leak fix; design-review approval); verified headless (room sets, on-screen counts), player-facing shot/autoaim/use/p…"
D467,CLOSED,#130 Golden Gun empty click with each shot:,CLOSED: faithful by code + log (audited 2026-10-01; by-ear check of held-trigger behaviour vs 1964 is maintainer-owned)
D468,FIXED,AI view clamp: under ultrawide (> 16:9) or Video.FovScale > 100 the AI's on-screen tests used the wider rendered frustum; they now keep the cartridge-widest vi…,"FIXED 2026-10-01 (feat/d468-ai-view-clamp, local); RULE-2-SIGNOFF (policy approved by the maintainer 2026-10-01); verified headless (clamp on/off switching + falsifier)."
D469,FIXED,Gamepad presets + per-seat controller rebinding (ROADMAP 5a; supersedes outside PR #109):,"FIXED 2026-10-01 (merged into release/v0.4.1, local); preset 0 == old mapping proven (20,971,520 cases); live pad test owed (maintainer); Jinx 1.2/1.3 undefined (gap)"
D470,FIXED,Audio master volume + output-device selection (ROADMAP 5a):,FIXED (live listening test owed)
D471,FIXED,Controller-family button names (no glyphs) in the PC menus + player-index LEDs:,FIXED 2026-10-01 (local); headless captures for Xbox/PS/Nintendo; live PS/Nintendo pad check owed
D472,FIXED,"F10 overlay polish: multi-pad ownership (audit G1-G3), widescreen pillarbox (D335b), red EXPERIMENTAL rows, unknown-ini-key warning.","FIXED 2026-10-01 (merged; port-only, no src/game)."
D473,FIXED,"Uncached getenv(""GE_D75V"") on every G_VTX:",FIXED 2026-10-01 (local); Statue display-list CPU about -55% in steady windows
D474,FIXED,fast3d micro-optimisations:,FIXED 2026-10-01 (local); output-identical by construction; measured gain on the dev machine within noise
D475,OPEN,Statue/Cradle hitches diagnosed:,OPEN (diagnosed 2026-10-01):
D476,FIXED,CI4 textures keyed on their own 16-entry palette bank instead of the whole 256-entry table (D217 hash):,FIXED 2026-10-02 (local); Statue sweep CI4 cache misses 189 -> 36
D477,?,CI8 palette re-imports measured and ruled out as a CPU-side hitch cause:,MEASURED 2026-10-02 (local); premise of the D475/D476 follow-up refuted on the dev box; GPU/driver-side cost unmeasured
D478,FIXED,"D441 read-side census re-run (319 warnings, 129 outside the chr.c D34 initialisers) triaged against the current tree:","FIXED 2026-10-02 (local; snprintf bound); census triage recorded, Stage B items listed"
D479,FIXED,Split-screen GE_STARTMP harness hid the ammo counter:,"FIXED 2026-10-02 (local); harness-only, 2P frame shows the counter in both halves"
D480,FIXED,"Shader pre-warm: every created shader pair (shader_id0, shader_id1) is remembered in $S/ge007.shaders and compiled at the first frame of the next session, so a…","FIXED 2026-10-02 (local, perf/shader-prewarm); output-identical (menu frame exact), 0 in-play compiles on the warm run"
D481,FIXED,Low-end ~45/30 fps (#92):,"FIXED 2026-10-02 (local, perf/render-worker); X220 (HD 3000) 44-46 -> 60.0 fps, GE_RENDERINLINE=1 falsifier restores 45.9"
D482,FIXED,Lighter first-launch defaults on Atom/Celeron-class GPUs (#92 follow-up):,"FIXED 2026-10-02 (local, perf/lowend-defaults); X220 forced-path:"
D483,FIXED,#136: the PC crouch key stood Bond up when he switched to the sniper rifle; the N64 keeps the stance (the weapon's DISABLE_CROUCH flag blocks both crouch-down…,"FIXED 2026-10-02 (local, fix/d483-sniper); scripted crouch -> switch -> release keeps SQUAT"
D484,FIXED,"#136: portScaleFovY's 20-degree floor raised the game's own zoom FOVs, capping sniper/camera magnification at 20 degrees instead of the N64's 7 (about a third…","FIXED 2026-10-02 (local, fix/d483-sniper, maintainer-approved 2026-10-02); full zoom renders at 7 degrees, normal views pixel-identical"
D485,FIXED,"#136: sniper/camera zoom stepped once per game frame with no tick scaling, so at the port's 1 tick/frame it zoomed 2x faster in real time than the N64 at 2 tic…","FIXED 2026-10-02 (local, fix/d483-sniper); 15 -> 7 takes 16 frames at 60 fps (was 8) = the N64 at 30 fps"
D486,FIXED,#126: explosions dropped in heavy firefights (turrets on Cradle/Egypt/Aztec):,"FIXED (a)(c)(d); drops + (b) faithful, closed."
D487,FIXED,"PAL: the in-level ammo counter was not drawn (only a small dot where the ""7 / 93"" counter should be) —",FIXED; maintainer PAL play check 2026-10-02:
D488,FIXED,PAL build reported the Brazilian MPAL TV type instead of PAL (osTvType) —,FIXED
D489,FIXED,FPS dropped sharply while adjusting F10 watch sliders (music/FX) with a pad or arrow keys:,"FIXED 2026-10-02 (port-only UI layer, fix/d489-pad-slider-commit, merged into release/v0.4.1 2026-10-03); maintainer pad check passed 2026-10-03"
D490,FIXED,Dam first-tower guard walked a small circle:,"FIXED 2026-10-02 (fix/d490-glass-hull, merged into release/v0.4.1 2026-10-03); maintainer by-eye Dam check passed 2026-10-02"
D491,FIXED,"Struct reads across separately-declared adjacent globals (the D5/D215 class), found by the D24 overflow audit:","FIXED 2026-10-03 (fix/d491-adjacent-global-reads, local); builds clean, 900-frame -level_09 smoke clean; no by-eye effect expected beyond room-membership-driven behaviour"
D492,CLOSED,Emulator .eep saves are not drop-in compatible:,CLOSED (converter shipped 2026-10-04:
D493,FIXED,Pad rumble kept going after quitting the game:,"FIXED 2026-10-03, maintainer-verified in play (DualSense; Original-mode edges incl."
D494,FIXED,"Sniper rifle: the PC crouch key now crouches/stands freely with a disable-crouch weapon (GEPD parity), never injecting C-down for it; supersedes D483's stance…","FIXED 2026-10-03, maintainer-verified in play"
D495,FIXED,"Tank: with the default pad preset X changed the tank weapon and Y left it, because in-tank faces fell back to the fixed menu faces; PadMapCtx.tank routes Next/…","FIXED 2026-10-03, maintainer-verified in play"
D496,FIXED,3P/4P split-screen: weapon-pickup messages garbled and oversized —,"FIXED 2026-10-03 (condensed 3+P text is faithful, maintainer-confirmed)"
D497,FIXED,PC Options paging now matches the game's dossiers:,"FIXED 2026-10-03, maintainer-verified in play"
D498,FIXED,Xbox 1.2 Christmas / 1.3 Frost / 1.4 Elektra gamepad presets:,"FIXED 2026-10-03, maintainer-verified with a pad"
D499,FIXED,Trilinear texture filter option:,"FIXED 2026-10-03, maintainer-checked (no visible difference; kept)"
D500,FIXED,"Aztec shuttle area: polygons ""fell apart""; the per-frame vtx pool (dyn.c, no bounds check, Aztec -mvtx40) could overrun into the half the render worker draws;…","FIXED 2026-10-03, maintainer-verified at the Aztec spot"
D501,CLOSED,Ultrawide 16:9 pillarbox for cutscenes and the watch:,CLOSED (reverted 31ade91f)
D502,FIXED,PC Options pad navigation:,"FIXED 2026-10-03, maintainer-verified with a pad"
D503,PARTIAL,"Surface: near-full-fog silhouettes on 32-bit output (fog snap applied); the remaining Surface dish is the widescreen frustum, 4:3 matches N64 —",FIX LANDED (by-eye pass owed)
D504,FIXED,F10 / PC Options menu:,FIXED (port-only; by-eye pass vs the PD port owed)
D505,FIXED,Menu wording pass: 27 maintainer-approved text edits across both PC options UIs —,FIXED (port-only; by-eye pass owed)
D506,FIXED,Menu slider values standardized:,FIXED (port-only; by-eye pass owed)
D507,FIXED,Menu: one-line description of the focused option in both PC options UIs —,FIXED (by-eye pass owed)
D508,FIXED,Forced aspect ratios:,FIXED (by-eye pass owed)
D509,FIXED,Held frames after an aspect-ratio change:,FIXED (by-eye pass owed)
D510,FIXED,F10 overlay + FPS counter the same on-screen size on the front end and in a level —,FIXED (live check owed)
D511,OPEN,Four PD-parity settings:,DONE headless (live checks owed)
D512,OPEN,Game.HudScale also scales the F10 overlay and FPS counter —,DONE headless (live check owed)
D513,OPEN,"Input.ControlScheme: Modern / OG controls (#139, T7) —",DONE headless (pad pass owed)
D514,OPEN,Emulator saves load directly:,DONE headless (by-eye mission select owed)
D515,FIXED,Empty arch tag from a fresh build dir:,FIXED (headless-verified)
D516,FIXED,"Control style Original shows the game's own N64 styles (1.1-2.4, per seat, saved to the Bond file) instead of the D498 presets —",FIXED (live pass 2026-10-04 incl.
D517,FIXED,Holding left/right on a Bond settings row dropped the fps:,FIXED (live pass 2026-10-04)
D518,FIXED,Pad layout preset was one global value:,FIXED (live pass 2026-10-04 incl.
D519,FIXED,"One PC options UI: the file-select ""PC Options"" entry opens the F10 overlay; the MENU_PC_OPTIONS screen is deleted —",FIXED (headless-verified 2026-10-04; by-eye pass owed)
D520,FIXED,"F10 overlay slider wedge, slider marker and dropdown arrow antialiased (they stepped in whole canvas pixels that grow with the window) —",FIXED (headless before/after capture; by-eye owed)
D521,PARTIAL,A fixed-frame golden window that overlaps the intro flyby is platform-nondeterministic (cross-platform goldens); verify.sh's linux pixel-gate skip is driver-co…,PARTIAL (rule recorded 2026-10-04; the two residual bands CLOSED 2026-10-05 by D525's box re-round —
D522,FIXED,The golden window's frame-900 stem is not settled gameplay on several levels —,FIXED 2026-10-05 (recipe-side re-base:
D523,CLOSED,The golden recipe does not pin save state:,SUPERSEDED by D529 (2026-10-05) —
D524,FIXED,The golden tooling was destructive and drift-prone:,FIXED 2026-10-05 (both pins idempotent —
D525,FIXED,"The whole <level>/linux/ golden set was stale, not just on the six re-based levels —","FIXED 2026-10-05 (21-level box re-round, 63 frames, save pinned present; structural parity 21/21, Archives/Streets bands attributed to save state, not a renderer)"
D526,CLOSED,P13 Dam-ending grate / edge-railing transparency is faithful N64 behaviour (port == 1964/GEPD); the per-triangle census probe is retained as a generic draw-sta…,"CLOSED — faithful, confirmed on 1964/GEPD (2026-10-05):"
D527,OPEN,GE_PCDUMP written first:last (a colon where the dash belongs) silently dumps EVERY frame —,OPEN (harness documentation; a log-or-fail-closed fix is owed in the port layer)
D528,OPEN,"GE_STARTMENU skips the EEPROM read, so front-end-only sessions never import the emulator save —",OPEN (harness documentation; the D514 imported-save test must run from a normal boot; a log-or-import fix is owed in the port layer)
D529,FIXED,The golden gate depends on the save file's CONTENT —,FIXED 2026-10-05 (canonical save in-tree at tools_pc/golden/ge007.eep; verify.sh installs it like the ini pin; all 21 win sets re-captured under it —
D530,?,The box (X220) linux golden re-round landed —,AMENDED BY D531 (2026-10-05) —
D531,FIXED,capture_p7.sh's D529 adaptation was incomplete —,"FIXED 2026-10-05 (loop re-pins the canonical save per level + cd ""$ROOT""; fresh 21/21 box re-capture committed —"
D532,FIXED,The three P12 U5 should-fixes applied —,FIXED 2026-10-05 (tooling-only:
D533,FIXED,"The probe-doc drift that would have failed CI, plus three tracked-file accuracy/hygiene fixes (release-sweep spot-check) —",FIXED 2026-10-05 (GE-ENV-PROBES.md missing the GE_D526 family → gen_env_probes.py --check rc 1 = a CI-failing step; build-pc.sh local MSYS2 root in two messages; docs/fidelity.md og line; release-not…
D534,FIXED,Pre-release review of the 2026-10-05 local batch —,FIXED 2026-10-05 (verify.sh/capture_p7.sh INT/TERM trap; release-notes linux-gate wording; dead security-and-fidelity-status.md links; future-dated 2026-10-07 labels normalised; D526 probe dangling-e…
D540,FIXED,Fog rework (Deck playtest):,FIXED 2026-10-05 (port:
D450,FIXED,"Unplugging and replugging a controller was not picked up (user, GameSir-G7 Pro; 2026-09-29).","FIXED — port-only: the render thread's gfx_sdl_handle_events drains the same SDL queue as video.c's host pump but had no case for SDL_CONTROLLERDEVICEADDED/REMOVED, so it usually swallowed them and i…"
D436,FIXED,"Persistent (hipfire) crosshair, opt-in:",FIXED (shipped 2026-09-29; rule-2 sign-off:
D617,OPEN,"The HD pack's ge007.tdb is the emulator's full per-draw key list, not the pack's PNG list —",OPEN — plan written (docs/dev/TEXPACK-RICE-DRAW-PLAN.md); whole-texture recipe confirmed correct by 9 tdb hits with exact ;WxH agreement; PNG-filename and brute-force param searches are dead ends
D618,?,"The community HD pack's non-CI keys are per-draw rects partly unreachable from N64 tile state (440x1 strips), and a rect key cannot land in the whole-texture p…",STOPPED 2026-10-06 — tdb ;WxH are per-draw rects (440x1 ×301 over tmem's 128-byte pitch = recompile-side); ~100 byte-domain variants score 0 hits; content-join does not discriminate; rect-keyed repla…
D619,?,"The community HD pack's remaining 1790 PNGs are blocked by four independent causes (recompile-side rect keys, wrong hash domain, rect-vs-whole-texture format,…",WRITTEN UP 2026-10-06 —
D620,OPEN,The HD pack's non-CI keys hash the pack's own re-encoded content —,MEASURED 2026-10-07 —
D621,PARTIAL,The PD dabs-mod texture-pack notes mine:,PLAN LANDED 2026-10-07 —
D622,?,B0 sprite-bank confirmation —,"PASS 2026-10-07 — two human-driven GE_TEXNUMDUMP+GE_TEXDUMP runs (front end 377 unique texnums / 521 dumps, -level_09 combat 321 / 316; evidence scratch/b0/):"
D623,OPEN,Phase B1–B3 measured:,MEASURED 2026-10-07 —
D624,OPEN,B4 content scorer built + tuned:,"BUILT 2026-10-07 — 5-stage pipeline (geometric prefilter → content rank → GT-tuned auto-accept → vision judge → human queue) per the 2026-10-07 decision (commits 512e6d57, 95d38637)."
D625,?,"texpack A5: downscale-to-GL-max instead of the D615 vanilla fallback, plus first-use decode-stall metering —","BUILT 2026-10-10, not yet A/B-verified —"
D626,FIXED,Windows DPI awareness was never declared:,FIXED 2026-10-10 (gfx_sdl2.cpp:
D627,FIXED,DexDrive / GameFAQs .n64 saves import directly:,"FIXED 2026-10-10 (eepimport.c + eep_convert.py dexdrive-to-pc); byte-identical to the reference extraction, malformed inputs rejected untouched; by-eye mission select owed"
D628,RESOLVED,"The exact-CRC CI half of the GoldenEye-007-HD join is sound (256 texnums, independently re-verified), but its structural half was scored against GE_TEXDUMP cap…","CI HALF ACCEPTED 2026-10-10 (256/256 (crc,fmt,siz) agreement with the rice census, 256 distinct texnums, palette-variant collision resolved by ROM palcrc, builder resolves all 256 sources); structura…"
D632,OPEN,Exit BSOD 0x119 recurred (2026-10-10) despite D344; the first minidump shows it fires in the kernel's process-exit GPU cleanup (DXGPROCESS::Destroy -> DXGCONTE…,FIX BUILT 2026-10-10 (fix/d632-exit-teardown):
D636,FIXED,Non-CI pack textures ARE fingerprint-keyed:,FIXED 2026-10-10 (feat/texpack-fp):
D637,?,Fingerprint fallback in the loader:,BUILT 2026-10-10 (feat/texpack-fp); C CRC == Python on 80 ROM glyphs; not run-verified (game runs on hold)
D638,FIXED,HD-pack ammo icon cut off:,FIXED 2026-10-10 (chore/v070-agentic-batch):
D639,OPEN,Exclusive fullscreen picked the wrong resolution:,FIX BUILT 2026-10-10 (chore/v070-agentic-batch):
D643,FIXED,Issues #145 (b)(c) / #149:,FIXED 2026-10-12 (port-only; GE_PADMAPTEST PASS; maintainer pad pass vs the #145/#149 reports; 1:1 N64-pad stick check owed when an adapter is available)
D644,FIXED,File-select royal crest drew on an opaque dark square (#146):,"FIXED 2026-10-12 (import_texture_ia4; 1964-confirmed symptom, maintainer-verified; Win sweep unchanged)"
D645,FIXED,"Original 1.1: F10 ""Invert look"" did not invert vertical look (#145 a):",FIXED 2026-10-12 (F10 row and watch row synced in Original; maintainer-verified)
D646,FIXED,"F10 dropdown popup drew over the hint/tooltip panel (popup bounded by the screen edge, not the settings box) —",FIXED 2026-10-12 (ddGeom bounded to the box; maintainer-verified)
D951,OPEN,"v0.6.0 release gate: Windows golden sweep flags are timing noise in the test, not rendering changes —",OPEN (test-side; 2026-10-13)
D952,FIXED,"macOS: black window, no frame, when the game starts while the display is asleep (SDL3 Cocoa VSync waits forever on a CVDisplayLink that never started) —","FIXED 2026-10-13 (port-only, gfx_sdl2.cpp; Mac A/B:"
D953,FIXED,Deck: wide Field of view feels choppier while turning —,FIXED 2026-10-13 (v0.6.0; LOD + prop fog fade/visibility follow the game FOV; maintainer Deck-confirmed)
D954,FIXED,"Doors and guards flicker at 90 fps, not at 60:","FIXED 2026-10-13 (port-only, gfx_pc.cpp + interpportal.c; maintainer Deck-confirmed)"
D955,FIXED,"Objects (props, doors, guards) moved at the 60 Hz tick inside a 90 Hz picture:","FIXED 2026-10-13 (port-only, gfx_pc.cpp + interpportal.c; sky pass; maintainer Deck-confirmed)"
D607,OPEN,Address model unified into one header:,DONE 2026-10-07 (macos branch; Windows identity gate:
D608,FIXED,D34 ANIM_DATA_x re-basing guard in assets/animationtable_data.h was __x86_64__-only:,"FIXED 2026-10-07 (macos branch; mechanical guard widening, arm64 link clean)"
D609,FIXED,macOS arm64 SIGKILL at exec:,"FIXED 2026-10-07 (macos branch; probe-confirmed; arm64 keeps the 4 GiB pagezero, floor 11.0)"
D610,FIXED,GE_MEMPOOL_CEILING (port/src/n64stubs.c) hard-coded the D441 base-0 arena top; now #if PORT_ADDR_BASE != 0-keyed so arm64 gets the arena-relative ceiling and b…,FIXED 2026-10-07 (macos branch; base-0 identity by construction)
D611,FIXED,"Mempool arena start is truncated to 32 bits at three carriers (osVirtualToPhysical, PHYS_TO_K0, src/boss.c:168 u32 start) so arm64 macOS banks live at arena-re…","DIAGNOSED 2026-10-07 (macos branch; arena-start leg fixed as D612, rest owed with the #95 re-application)"
D612,PARTIAL,Arena-start leg fixed (src/boss.c:,PARTIAL 2026-10-07 (macos branch:
D613,FIXED,PR #95 (macos branch) re-applied onto release/v0.5.1 —,FIXED 2026-10-07 (macos branch)
D586,FIXED,"Apple gen_macho_syms.py --base parse was dead code (undefined args), the shifted literals emitted bare (assembler ""expected newline""), and the generated .s dir…",FIXED 2026-10-07 (macos branch)
D587,FIXED,macos sweep: 9 of the first 13 levels SIGSEGV at one site —,FIXED 2026-10-07 (macos branch)
D588,FIXED,Windows x86_64 SIGSEGV (deterministic 3/3) in texReadBits under lvlStageLoad:,FIXED 2026-10-07 (macos branch)
D589,RESOLVED,macOS arm64: wide-pixel (32-bit RGBA / 16-bit) textures render as colourful static / confetti (the Surface 1/2 billboard trees) while x86_64 renders them corre…,RESOLVED 2026-10-08 (macos branch; byte order fixed+verified = D591; the SECOND CAUSE D592 is CLOSED as a non-issue —
D590,FIXED,"Windows x86_64 deterministic SIGSEGV in load_bg_file (bg-load worker thread, 4/4 incl.",FIXED 2026-10-08 (macos branch)
D591,FIXED,arm64 macOS fast3d mis-classifies every DRAM/cart texture source as a “C-compiled array” and byte-swaps it —,FIXED + Mac A/B VERIFIED 2026-10-08 (macos branch; 9c59cbe7) —
D592,CLOSED,"arm64 macOS — the ""rock striping"" (the remaining cross-platform frame diff after the D591 byte-order fix) is a VHF-metric FALSE-POSITIVE, not a GL-layer defect.",CLOSED (non-issue) 2026-10-08 (macos branch; VHF-metric false-positive —
D593,OPEN,"Windows x86_64 — an occasional render-thread stall fired the kernel's ""no frame rendered for N ms"" watchdog (observed once, 8001 ms, on an NVIDIA RTX 5090 duri…",OPEN 2026-10-08 (Windows; single observation; repro + root-cause owed)
D594,FIXED,arm64 macOS — partial input:,FIXED 2026-10-08 (macos branch; actHeld menu gate + macOS reconcileGrab focus gate → PD MLOCK_ON; LMB fire verified on the Mac; unblocked D595)
D595,FIXED,arm64 macOS — deterministic SIGSEGV in modelSetAnimFrame when an AI plays a hit/stagger/death animation (fire → triggered_on_shot_hit → modelSetAnimation2); th…,FIXED 2026-10-08 (macos branch; read the base at full pointer width —
D596,FIXED,"the win/ golden set predates D565/D569/D570, so verify.sh on the macos branch (and the main tip) reported a REGRESSION worst_cell=3.106875 that is a stale refe…","FIXED 2026-10-08 (macos branch; win goldens regenerated from e0a2d4b9, 21/21 PASS; recipe-side only, no game-code change; the linux/deck sets need their own re-capture round before any new cross-plat…"
D597,FIXED,"GE_INPUTSCRIPT gains MLB<n>/MRB<n> (sustained mouse-button tokens, slot 0):",FIXED 2026-10-08 (macos branch; Windows menu + forced-level keylog checks; Mac sweep-fire no-crash run + no-env no-op run; the live E1 LMB fire is the struck-animation ground truth; cmd-wrapper gotch…
D598,RESOLVED,"arm64 macOS gameplay is ""a bit choppy"" on heavy levels (Cradle-class) —",RESOLVED 2026-10-08 by D600 (display-locked retrace tick; live playtest confirmation owed)
D599,FIXED,arm64 macOS: SIGBUS (signal 10) the moment a game controller is connected —,FIXED 2026-10-08 (macos branch; Mac M3 re-run with a connected Xbox pad:
D600,FIXED,VSync-on frame pacing:,FIXED + LIVE-CONFIRMED 2026-10-08 (macos branch; Mac M3:
D601,FIXED,"Long-session hang at stage load (macOS playtest, Archives):","FIXED + LIVE-CONFIRMED 2026-10-08 (macos branch; #ifdef PORT guard, D152/D285 class; evidence = live lldb read of the hung process; maintainer full-campaign completion incl."
D602,OPEN,macOS end-credits SIGBUS:,"MITIGATED 2026-10-08, OPEN (macos branch; langGet returns NULL for offset > 1 MiB and logs the bank state once as D602:; needs one natural-flow re-trigger to diagnose)"
D603,FIXED,"arm64 macOS multiplayer (2P, stage 38) SIGSEGV in modelSetAnimFrame via playerTick:","FIXED 2026-10-08, live confirmation owed (macos branch; identity at PORT_ADDR_BASE==0; headless 2/3/4P + solo smoke clean but the death/firing-animation path is not reachable headlessly)"
D604,FIXED,arm64 macOS multiplayer respawn SIGSEGV:,FIXED + LIVE-CONFIRMED 2026-10-08 (macos branch; maintainer 2P respawn OK; identity at PORT_ADDR_BASE==0)
1 # GENERATED by tools_pc/gen_findings_index.py -- do not hand-edit.
7 D51-D58
8 D59-D68
9 D70-D74
10 D75
11 D76 · D164
12 D159
13 D161
14 D165 · D166
15 D180 · D181
16 D186
17 D187
18 D188
19 D189
20 D190
21 D191
22 D193
23 D194
24 D195
25 D197
26 D198
27 D199
28 D200
29 D196
30 D192
31 D221
32 D222
33 D223
34 D224
35 D225
36 D226
37 D227
38 D229
39 D230
40 D231
41 D233
42 D234
43 D244
44 D228
45 D204
46 D203
47 D201
48 D202
49 D206
50 D207
51 D208
52 D209
53 D210
56 D168
57 D169
58 D178
59 D175
60 D176
61 D177
62 D172
63 D174
64 D183
65 D173
66 D179
67 D180
68 D181
69 D184
70 D211
71 D212
72 D213
73 D214
74 D215
75 D216
76 D217
77 D218
78 D219
79 D220
80 D85
81 D86 · D87
82 D88.4
83 D132
84 D157
85 D160
86 D89-D92
87 D93-D102
88 D103-D107
89 D108-D112
90 D113-D116
91 D117
92 D118
93 D150
94 D260-D265
95 D266-D269
96 D270
97 D271
98 D276
99 D279
100 D281
101 D282
102 D283
103 D280
104 D275
105 D274
106 D273
107 D272
108 D137
109 D119 · D120
110 D120
122 D139
123 D140
124 D141
125 D152
126 D154
127 D156
128 D155
129 D153
130 D151
132 D164
133 D165
134 D166
135 D235
136 D236
137 D237
138 D238
139 D239
140 D240
141 D241
142 D242
143 D243
144 D245
145 D246
146 D247
147 D248
148 D249
149 D250
150 D251
151 D252
152 D253
153 D254
154 D255
160 D262
161 D264
162 D263
163 D265
164 D258
165 D284
166 D285
167 D286
168 D287
169 D288
170 D289
171 D290
172 D291
173 D292
174 D293
175 D294
176 D295
177 D296
178 D297
179 D298
180 D299
181 D300
182 D301
183 D302
184 D303
185 D304
186 D305
187 D306
188 D307
189 D308
190 D309
191 D310
192 D311
193 D312
194 D313
195 D314
196 D315
197 D316
198 D317
199 D318
200 D319
201 D320
202 D321
203 D322
204 D323
205 D324
206 D325
207 D326
208 D327
209 D328
210 D329
211 D330
212 D331
213 D332
214 D333
215 D334
216 D335
217 D336
218 D337
219 D338
220 D339
221 D341
222 D342
223 D343
224 D344
225 D345
226 D346
227 D347
228 D348
229 D349
230 D350
231 D351
232 D352
233 D353
234 D354
235 D355
236 D356
237 D358
238 D359
239 D360
240 D361
241 D362
242 D363
243 D364
244 D365
245 D366
246 D367
247 D368
248 D369
249 D370
250 D371
251 D372
252 D373
253 D374
254 D375
255 D376
256 D377
257 D378
258 D379
259 D380
260 D381
261 D382
262 D383
263 D384
264 D385
265 D386
266 D387
267 D388
268 D389
269 D390
270 D391
271 D392
272 D393
273 D394
274 D395
275 D396
276 D397
277 D398
278 D399
279 D400
280 D401
281 D402
282 D403
283 D404
284 D405
285 D406
286 D541
287 D542
288 D543
289 D544
290 D545
291 D546
292 D547
293 D548
294 D549
295 D550
296 D551
297 D552
298 D553
299 D554
300 D555
301 D556
302 D557
303 D558
304 D559
305 D560
306 D561
307 D562
308 D563
309 D564
310 D565
311 D566
312 D567
313 D568
314 D569
315 D570
316 D571
317 D572
318 D573
319 D574
320 D575
321 D576
322 D577
323 D578
324 D579
325 D583
326 D584
327 D614
328 D605
329 D606
330 D407
331 D408
332 D415
333 D416
334 D417
335 D418
336 D419
337 D420
338 D421
339 D422
340 D423
341 D424
342 D425
343 D426
344 D427
345 D428
346 D429
347 D430
348 D431
349 D432
350 D433
351 D434
352 D440
353 D441
354 D442
355 D443
356 D454
357 D461
358 D462
359 D444
360 D455
361 D447
362 D445
363 D446
364 D448
365 D456
366 D457
367 D453
368 D451
369 D463
370 D464
371 D465
372 D466
373 D467
374 D468
375 D469
376 D470
377 D471
378 D472
379 D473
380 D474
381 D475
382 D476
383 D477
384 D478
385 D479
386 D480
387 D481
388 D482
389 D483
390 D484
391 D485
392 D486
393 D487
394 D488
395 D489
396 D490
397 D491
398 D492
399 D493
400 D494
401 D495
402 D496
403 D497
404 D498
405 D499
406 D500
407 D501
408 D502
409 D503
410 D504
411 D505
412 D506
413 D507
414 D508
415 D509
416 D510
417 D511
418 D512
419 D513
420 D514
421 D515
422 D516
423 D517
424 D518
425 D519
426 D520
427 D521
428 D522
429 D523
430 D524
431 D525
432 D526
433 D527
434 D528
435 D529
436 D530
437 D531
438 D532
439 D533
440 D534
441 D540
442 D450
443 D436
444 D617
445 D618
446 D619
447 D620
448 D621
449 D622
450 D623
451 D624
452 D625
453 D626
454 D627
455 D628
456 D632
457 D636
458 D637
459 D638
460 D639
461 D643
462 D644
463 D645
464 D646
465 D951
466 D952
467 D953
468 D954
469 D955
470 D607
471 D608
472 D609
473 D610
474 D611
475 D612
476 D613
477 D586
478 D587
479 D588
480 D589
481 D590
482 D591
483 D592
484 D593
485 D594
486 D595
487 D596
488 D597
489 D598
490 D599
491 D600
492 D601
493 D602
494 D603
495 D604
+9986 -111
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -31,7 +31,7 @@ passes together fit in ~55 s.
## Source material
`C:\Users\james\Videos\NVIDIA\Build-pc\new\` — ShadowPlay recordings
`<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.
+31
View File
@@ -1,5 +1,7 @@
# Release plan — "drop in your ROM and play"
> **Historical record — do not act on status claims in this file.** Part A shipped as the NTSC-U drop-in ROM flow (PR #84, issue #6). The remaining Part B pieces are tracked in the roadmap. Current open work and status: [`docs/ROADMAP.md`](../ROADMAP.md) (2026-09-29).
Status: proposal / not yet scheduled. Owner: TBD.
## Goal
@@ -303,3 +305,32 @@ offer to create it).
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.
+33
View File
@@ -0,0 +1,33 @@
---
title: Documentation
description: The technical documentation for the GoldenEye 007 PC port; the architecture, the N64-to-PC bug catalogue, and how the project is developed.
date: '2026-10-05'
modified_time: '2026-10-05'
---
## Documentation
The technical documentation for the port: the **architecture** and how the
project is **developed**. (Building and running from source live on the
separate [Building](building.md) page.)
## Architecture
- **[Internals](internals.md)** — the architecture: the software RSP, the
port layer, the asset pipeline, and the phased plan. The single reference
for what the port is and how it's put together.
- **[Porting notes](porting-notes.md)** — the catalogue of N64→PC bugs: the
recurring classes of defect and the ground-truth pattern each fix follows.
## Development
- **[Development process](dev-process.md)** — the investigation workflow:
task budgets, file partitioning, the finding-log discipline, and the rules
for what counts as a diagnosis versus a change.
- **[Agentic development](dev/agentic-development.md)** — the case study:
how the two-AI-agent setup ran, its timeline and numbers, and an honest
read on what did and didn't work.
- **[Security status](security.md)** — what the release packages install
(and don't): no networking unless you opt in to the update check, no telemetry, no ROM, no system writes.
- **[Fidelity status](fidelity.md)** — how faithfully the port reproduces
the N64 game's logic, and what was audited to back that claim.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

+72
View File
@@ -0,0 +1,72 @@
---
title: Fidelity status
description: How faithfully the port reproduces the N64 game: unmodified decompiled logic, Makefile-mirrored region macros, one game-logic-adjacent change, and PRNG notes.
date: '2026-10-05'
modified_time: '2026-10-05'
---
## Fidelity status
This page answers one question plainly: *how faithfully does the port
actually reproduce the original N64 game's logic*?
The audit was run end-to-end on 2026-09-15 and re-verified for the v0.3.0
release bundles on 2026-09-18.
**v0.5.0 update pass, 2026-10-05:** two of the checks were re-run
directly against the current tree — the region build macros were
compared line-by-line against the original N64 Makefile's macro sets
(all three regions match exactly), and the library file classification
(which original files compile versus get PC-shimmed) was re-checked
against the project's own ground-truth manifest (the CMake build
compiles the libultra and libultrare audio sets, the gu matrix helpers,
and `io/vitbl.c` only — no silent gaps, no silent duplicates). The
full game-logic audit was not re-run in this pass; recent `src/game`
edits on the release branch are finding-gated (for example the D519
front-end options work, and a model-c scripted-camera clamp removal
that returned those files to pure decompilation). The full engineering
record behind each item is in the project's finding log
([`docs/dev/findings.md`](https://github.com/jkdansereau/goldeneye-pc-port/blob/main/docs/dev/findings.md)).
## The rule and what was checked
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 (re-run 2026-10-05).
- **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 (re-run 2026-10-05).
- **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.*
+56
View File
@@ -0,0 +1,56 @@
---
title: Framerate and pacing
description: How the GoldenEye 007 PC port holds a steady 60 fps, draws frames above 60 on fast displays, and keeps per-frame game timing correct on a fast CPU.
date: '2026-10-05'
modified_time: '2026-10-10'
---
## Framerate and pacing
The port targets a **steady 60 fps** with VSync and a frame cap, including on
low-end GPUs. Since v0.6.0 the cap can also go above 60 on a faster display
(see [Above 60 fps](#above-60-fps)).
The N64's game code assumed a **60 Hz vertical-interrupt tick** — it ran a
few ticks per displayed frame. On a PC the CPU is far faster, so left alone
the game logic would race ahead. The port handles this in two places:
- **Frame pacing** happens in `port/src/video.c` on the render thread (the
software RSP thread), which paces the frame and does the swap and FPS
accounting.
- **Per-frame counters** in the game are gated with `portN64FrameStep()` so
they advance at the *display* rate, not the CPU rate. Getting this wrong is
a recurring class of subtle timing bug (a counter that ticks too fast, a
stall that isn't real) — the [porting notes](porting-notes.md) track these
(for example D13, D22 and D486).
On very weak integrated GPUs (Atom/Celeron class) the first launch writes
lighter values for draw distance, LOD distance and anti-aliasing into
`ge007.ini` so the game still holds 60 fps.
### Above 60 fps
`Video.FpsCap` (F10, *Frame rate cap*) accepts Auto, 30, 60, 90, 120, 144, 165 and 240.
**Auto** is the default on fresh installs (an existing `ge007.ini` keeps its
value) and matches the display's refresh rate, up to 240 (the renderer draws at most four
frames per game tick); on a 60 Hz display it
is plain 60.
- **The game still simulates at its original 60 Hz (NTSC) tick.** The extra
frames are drawn between two game ticks by blending the previous and current
frame's matrices (camera and objects), so no game state changes.
- **Cost:** one game tick of extra latency.
- **Inert on 60 Hz displays.** The present rate is the lower of the cap and the
display's refresh rate. The *Original N64* display mode caps at 60.
- **Fallbacks:** a fast camera turn or a large change in the visible room set
draws a full (non-blended) frame instead.
- The design comes from f1zz1ec0ke's PR #137; the port reworked it onto the
render worker thread and from a fixed 2x to any present rate, so a 90 Hz
Steam Deck OLED works (D578, D583, D584).
Checked by the maintainer: 120 Hz with VSync on a G-Sync display on PC, and 90
fps on the Steam Deck. Known issue: on the Deck at 90, centre firefights in
Bunker 1/2 can drop several fps, and Jungle can feel slightly less smooth.
*More: [porting notes](porting-notes.md) — the timing-related findings (D13,
D22, D486).*
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

After

Width:  |  Height:  |  Size: 330 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 523 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 939 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 456 KiB

After

Width:  |  Height:  |  Size: 396 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 893 KiB

After

Width:  |  Height:  |  Size: 817 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

+12 -26
View File
@@ -1,27 +1,13 @@
new_name,original,video_session,snapshot_time
shot-01.jpg,Build-pc 2026.09.13 - 16.05.44.10.mp4_snapshot_02.11.149.jpg,2026.09.13 - 16.05.44.10,02:11:149
shot-02.jpg,Build-pc 2026.09.13 - 16.05.44.10.mp4_snapshot_06.00.446.jpg,2026.09.13 - 16.05.44.10,06:00:446
shot-03.jpg,Build-pc 2026.09.13 - 16.05.44.10.mp4_snapshot_07.56.664.jpg,2026.09.13 - 16.05.44.10,07:56:664
shot-04.jpg,Build-pc 2026.09.13 - 16.05.44.10.mp4_snapshot_08.33.264.jpg,2026.09.13 - 16.05.44.10,08:33:264
shot-05.jpg,Build-pc 2026.09.13 - 16.05.44.10.mp4_snapshot_10.16.041.jpg,2026.09.13 - 16.05.44.10,10:16:041
shot-06.jpg,Build-pc 2026.09.13 - 16.05.44.10.mp4_snapshot_11.03.430.jpg,2026.09.13 - 16.05.44.10,11:03:430
shot-07.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_02.25.190.jpg,2026.09.13 - 16.54.07.12,02:25:190
shot-08.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_03.05.000.jpg,2026.09.13 - 16.54.07.12,03:05:000
shot-09.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_05.39.999.jpg,2026.09.13 - 16.54.07.12,05:39:999
shot-10.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_05.55.651.jpg,2026.09.13 - 16.54.07.12,05:55:651
shot-11.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_06.25.650.jpg,2026.09.13 - 16.54.07.12,06:25:650
shot-12.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_09.33.997.jpg,2026.09.13 - 16.54.07.12,09:33:997
shot-13.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_12.09.456.jpg,2026.09.13 - 16.54.07.12,12:09:456
shot-14.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_12.59.497.jpg,2026.09.13 - 16.54.07.12,12:59:497
shot-15.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_13.09.496.jpg,2026.09.13 - 16.54.07.12,13:09:496
shot-16.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_15.22.656.jpg,2026.09.13 - 16.54.07.12,15:22:656
shot-17.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_15.39.495.jpg,2026.09.13 - 16.54.07.12,15:39:495
shot-18.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_16.54.496.jpg,2026.09.13 - 16.54.07.12,16:54:496
shot-19.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_17.29.495.jpg,2026.09.13 - 16.54.07.12,17:29:495
shot-20.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_17.39.495.jpg,2026.09.13 - 16.54.07.12,17:39:495
shot-21.jpg,Build-pc 2026.09.13 - 17.49.35.18.mp4_snapshot_00.21.484.jpg,2026.09.13 - 17.49.35.18,00:21:484
shot-22.jpg,Build-pc 2026.09.13 - 17.49.35.18.mp4_snapshot_00.31.328.jpg,2026.09.13 - 17.49.35.18,00:31:328
shot-23.jpg,Build-pc 2026.09.13 - 17.49.35.18.mp4_snapshot_02.03.469.jpg,2026.09.13 - 17.49.35.18,02:03:469
shot-24.jpg,Build-pc 2026.09.13 - 17.54.08.19.mp4_snapshot_00.11.972.jpg,2026.09.13 - 17.54.08.19,00:11:972
shot-25.jpg,Build-pc 2026.09.13 - 17.54.08.19.mp4_snapshot_02.36.532.jpg,2026.09.13 - 17.54.08.19,02:36:532
shot-26.jpg,settings menu.jpg,settings-menu,
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 Build-pc 2026.09.13 - 16.05.44.10.mp4_snapshot_02.11.149.jpg v040/dam0.png 2026.09.13 - 16.05.44.10 v0.4.0 attract pass 2026.09.28 02:11:149
3 shot-02.jpg shot-03.jpg Build-pc 2026.09.13 - 16.05.44.10.mp4_snapshot_06.00.446.jpg v040/runway.png 2026.09.13 - 16.05.44.10 v0.4.0 attract pass 2026.09.28 06:00:446
4 shot-03.jpg shot-04.jpg Build-pc 2026.09.13 - 16.05.44.10.mp4_snapshot_07.56.664.jpg v040/surface 1.png 2026.09.13 - 16.05.44.10 v0.4.0 attract pass 2026.09.28 07:56:664
5 shot-04.jpg shot-09.jpg Build-pc 2026.09.13 - 16.05.44.10.mp4_snapshot_08.33.264.jpg v040/frigate 0.png 2026.09.13 - 16.05.44.10 v0.4.0 attract pass 2026.09.28 08:33:264
6 shot-05.jpg shot-11.jpg Build-pc 2026.09.13 - 16.05.44.10.mp4_snapshot_10.16.041.jpg v040/surface 2.png 2026.09.13 - 16.05.44.10 v0.4.0 attract pass 2026.09.28 10:16:041
7 shot-06.jpg shot-16.jpg Build-pc 2026.09.13 - 16.05.44.10.mp4_snapshot_11.03.430.jpg v040/statue 0.png 2026.09.13 - 16.05.44.10 v0.4.0 attract pass 2026.09.28 11:03:430
8 shot-07.jpg shot-20.jpg Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_02.25.190.jpg v040/archives 0.png 2026.09.13 - 16.54.07.12 v0.4.0 attract pass 2026.09.28 02:25:190
9 shot-08.jpg shot-21.jpg Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_03.05.000.jpg v040/cradle 0.png 2026.09.13 - 16.54.07.12 v0.4.0 attract pass 2026.09.28 03:05:000
10 shot-09.jpg shot-24.jpg Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_05.39.999.jpg v040/aztec 0.png 2026.09.13 - 16.54.07.12 v0.4.0 attract pass 2026.09.28 05:39:999
11 shot-10.jpg shot-27.jpg Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_05.55.651.jpg v040/control 0.png 2026.09.13 - 16.54.07.12 v0.4.0 attract pass 2026.09.28 05:55:651
12 shot-11.jpg shot-28.jpg Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_06.25.650.jpg v040/streets 0.png 2026.09.13 - 16.54.07.12 v0.4.0 attract pass 2026.09.28 06:25:650
13 shot-12.jpg shot-29.jpg Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_09.33.997.jpg v040/jungle.png 2026.09.13 - 16.54.07.12 v0.4.0 attract pass 2026.09.28 09:33:997
shot-13.jpg Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_12.09.456.jpg 2026.09.13 - 16.54.07.12 12:09:456
shot-14.jpg Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_12.59.497.jpg 2026.09.13 - 16.54.07.12 12:59:497
shot-15.jpg Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_13.09.496.jpg 2026.09.13 - 16.54.07.12 13:09:496
shot-16.jpg Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_15.22.656.jpg 2026.09.13 - 16.54.07.12 15:22:656
shot-17.jpg Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_15.39.495.jpg 2026.09.13 - 16.54.07.12 15:39:495
shot-18.jpg Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_16.54.496.jpg 2026.09.13 - 16.54.07.12 16:54:496
shot-19.jpg Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_17.29.495.jpg 2026.09.13 - 16.54.07.12 17:29:495
shot-20.jpg Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_17.39.495.jpg 2026.09.13 - 16.54.07.12 17:39:495
shot-21.jpg Build-pc 2026.09.13 - 17.49.35.18.mp4_snapshot_00.21.484.jpg 2026.09.13 - 17.49.35.18 00:21:484
shot-22.jpg Build-pc 2026.09.13 - 17.49.35.18.mp4_snapshot_00.31.328.jpg 2026.09.13 - 17.49.35.18 00:31:328
shot-23.jpg Build-pc 2026.09.13 - 17.49.35.18.mp4_snapshot_02.03.469.jpg 2026.09.13 - 17.49.35.18 02:03:469
shot-24.jpg Build-pc 2026.09.13 - 17.54.08.19.mp4_snapshot_00.11.972.jpg 2026.09.13 - 17.54.08.19 00:11:972
shot-25.jpg Build-pc 2026.09.13 - 17.54.08.19.mp4_snapshot_02.36.532.jpg 2026.09.13 - 17.54.08.19 02:36:532
shot-26.jpg settings menu.jpg settings-menu
Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 218 KiB

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

After

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

+237 -180
View File
@@ -1,49 +1,135 @@
---
title: GoldenEye 007 PC Port
# v0.3.0: status bumped from v0.2.1; Honest-status bullets synced with the
# README's v0.3.0 known issues (D255/D282/F10-dup/overscan items removed as
# fixed). Description kept at 133 chars (Bing's 160 limit).
# v0.6.0: status bumped from v0.5.0. Description kept short (Bing's 160 limit).
description: >-
A native PC port of the 1997 N64 classic, built from decompiled source with
a software RSP. v0.3.0 for Windows, Linux and Steam Deck.
A native PC port of GoldenEye 007 (Nintendo 64, 1997), built from
decompiled source with a software RSP. v0.6.0 for Windows, Linux and
Steam Deck.
# Freshness signals for jekyll-seo-tag (article:published_time / modified_time
# and the WebPage JSON-LD dates). Keep these equal to the last real content
# change of THIS page, not to the last release.
date: '2026-10-05'
modified_time: '2026-10-13'
locale: en_US
# The masthead is this page's title, so its first content h2 must NOT get the
# amber page-title look (double header). See the .flat-title rule in
# _includes/head-custom.html.
flat_title: true
---
<!-- v0.2.1 (Bing SEO): the "# GoldenEye 007 PC Port" h1 was removed here --
the Cayman masthead already renders the site title as an <h1>, so the page
was emitting two h1 tags. -->
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 native PC port of the original 1997 Nintendo 64 _GoldenEye 007_, compiled
from the game's [decompiled source](https://github.com/n64decomp/007) with the
N64's graphics coprocessor (RSP) running in software, the same architecture
as the [Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark), the
same Rare "Indy" engine family, one hardware generation apart.
[Status](#status) · [Download](#download) · [See it running](#see-it-running) · [Known issues](#known-issues) · [Documentation](#documentation)
**Status: v0.3.0.** The full single-player campaign runs at a steady 60 fps
and is completable end to end (all 21 missions playtested); all 21 missions
load and run clean on Windows, Linux and Steam Deck, and audio (music + SFX)
plays throughout. The known rough edges — mostly cosmetic rendering defects,
stretched-not-native widescreen, and missing features — are listed plainly
under [Honest status](#honest-status).
## Status
**This is a pre-1.0 release, not a finished product** — v1.0 is the target
for a polished, feature-complete build; expect rough edges and missing
features until then. See the
[README's Roadmap section](https://github.com/jkdansereau/goldeneye-pc-port#roadmap)
for direction (PAL/JP support, real widescreen, LAN multiplayer under
consideration, and more).
<div class="cblock" markdown="1">
<p align="center">
<img src="media/goldeneye-gh-preview.gif" width="70%"
alt="~12 s gameplay montage from live play sessions">
<br><em>~12 s gameplay montage from live v0.3.0 play sessions, running in the port.</em>
</p>
### v0.6.0 (2026-10-13) — frame rates above 60, experimental macOS, fixes.
- **Frame rates above 60:** the frame cap offers 90, 120, 144, 165 and 240, and **Auto**
(the default on fresh installs) matches your display. Frames between game
ticks are blended; the game still simulates at its original rate. It costs
one game tick of latency and does nothing on a 60 Hz display. A rework of
f1zz1ec0ke's #137. See [framerate and pacing](framerate-and-pacing.md).
- **macOS on Apple Silicon, experimental:** a `macos-arm64` download (not
notarized; run `xattr -dr com.apple.quarantine <folder>` once before the
first launch) or build from source. Based on danturn's #95.
- **Fixes:** geometry vanishing against a wall (#150), the first launch opening
on the wrong monitor (#151), a Linux end-credits crash (#152), a hat-spawn
crash (#153), the Cradle catwalk shadow flicker, and stale pixels beside the
native-widescreen picture.
- **Known issues:** on the Steam Deck at 90 fps, Bunker 1/2 centre firefights
can drop several fps and Jungle can feel slightly less smooth; Cradle's
turret explosions may draw wrongly (not yet investigated).
</div>
<details>
<summary>v0.5.0 (2026-10-07) — split-screen multiplayer, one options menu, closer to the N64</summary>
<div class="cblock" markdown="1">
- **Split-screen multiplayer for up to 4 players**, each player with their
own pad, controls and aim settings.
- **One options menu** for every setting, laid out like the Perfect Dark
port's, with the game's own crosshair as the mouse pointer. Pick the game's
own N64 control styles (1.1–2.4) per player, or the Xbox release's presets
with per-player pad rebinding; PlayStation and Nintendo pads show their own
button names.
- **Closer to the N64:** fog and haze (Surface 2's ground fog, the sky at the
horizon), automatic-weapon fire rate, sniper and camera zoom, Dam and
Caverns water, the Jungle boss fight's timing, guard visibility in
widescreen, and the Dam ending camera.
- **Fewer crashes and glitches:** a multiplayer crash with the crouched rifle,
rockets passing through the ground, and broken geometry around Aztec's
shuttle.
- **A steady 60 fps on low-end GPUs** (tested on an Intel HD 3000 laptop),
with lighter first-launch defaults on Atom/Celeron-class graphics.
- **Emulator saves load directly:** copy a 1964 or Project64 save in as
`data/ge007.eep` and it is converted on first launch.
- Also: more audio, video and control settings (master volume, audio device,
trilinear filtering, draw distance up to 800%, PC-friendly crouch and
look-ahead defaults), and controllers are picked up again when replugged.
</div>
</details>
What remains is a short list, under [Known issues](#known-issues).
**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. The [README's Roadmap
section](https://github.com/jkdansereau/goldeneye-pc-port#roadmap) is the
single tracker for what comes next.
<details id="known-issues">
<summary>Known issues</summary>
<p>Full table in the <a href="https://github.com/jkdansereau/goldeneye-pc-port/blob/main/docs/ROADMAP.md#known-issues">roadmap's known-issues section</a>; root causes in the <a href="https://github.com/jkdansereau/goldeneye-pc-port/tree/main/docs/dev">finding log</a>.</p>
<ul>
<li><strong>NTSC-U ROMs only in the release packages.</strong> PAL and JP ROMs convert, build and run from source, but release packages ship the US (NTSC-U) ROM. On the roadmap.</li>
<li><strong>Changing the aspect ratio in a level can briefly glitch the gun/hand model</strong> (cosmetic, one-off). Change the ratio from the front-end PC Options, or accept it (D509).</li>
<li><strong>No ARM Linux build; macOS is experimental.</strong> The release ships Windows, Linux and Steam Deck (keyboard, mouse and controller-button rebinding are all supported, defaults in the <a href="https://github.com/jkdansereau/goldeneye-pc-port#controls">README's Controls section</a>). macOS on Apple Silicon is an experimental, not-notarized download (run <code>xattr -dr com.apple.quarantine &lt;folder&gt;</code> before the first launch) or a source build; ARM Linux is on the roadmap.</li>
<li><strong>Saves from v0.4.0 and earlier can hold fake unlocks from <code>All unlocked</code></strong> — <code>All unlocked</code> has not written your save since v0.5.0, but saves made while it was on in v0.4.0 and earlier can hold fake unlocks and are not repaired; back up <code>data/ge007.eep</code> if you used it on an older build (D442, D387).</li>
<li><strong>The first frame of a level takes a little longer</strong> while its textures upload — a brief FPS-counter dip; nothing to fix (D475, D480).</li>
<li><strong>In widescreen, far objects almost fully in fog can still show a faint distant building edge</strong> (4:3 matches the N64). Cosmetic (D503).</li>
</ul>
</details>
<details>
<summary>Release history</summary>
<p class="warn"><strong>Warning:</strong> always use the latest release — earlier builds are kept in the release history for reference only; they lack the features and fixes of newer versions, so don't install an older release.</p>
<ul>
<li><strong>2026-10-07 — v0.5.0</strong>: split-screen multiplayer, one options menu, closer-to-N64 fog and fidelity fixes, emulator save import. <a href="https://github.com/jkdansereau/goldeneye-pc-port/releases/tag/v0.5.0">Release notes</a>.</li>
<li><strong>2026-09-28 — v0.4.0</strong>: 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. <a href="https://github.com/jkdansereau/goldeneye-pc-port/releases/tag/v0.4.0">Release notes</a>.</li>
<li><strong>2026-09-20 — v0.3.0</strong>: the first release with the complete campaign playable end to end at 60 fps on Windows, Linux, and Steam Deck. <a href="https://github.com/jkdansereau/goldeneye-pc-port/releases/tag/v0.3.0">Release notes</a>.</li>
<li><strong>2026-09-04 → 2026-09-16 — v0.1.0 – v0.2.2</strong>: the alpha and beta cycle — build chain, software RSP, first rendered frames, front end, and per-level stabilization across the campaign.</li>
</ul>
</details>
## Download
<div class="cblock" markdown="1">
| 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. |
| **macOS** (Apple Silicon, experimental) | macos-arm64 tarball | SDL2 and the C++ runtime are bundled. Not notarized: run `xattr -dr com.apple.quarantine <folder>` once before the first launch. Tested on one Mac; reports welcome. |
| ARM Linux | — | No download; on the roadmap. |
**Bring your own ROM.** Both bundles contain no ROM and no game assets: you
supply your own GoldenEye 007 N64 ROM (the
@@ -53,105 +139,37 @@ 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.
the packages carry no ROM or game assets, and run only with a ROM you
already own. The binaries are not code-signed; each
release artifact carries a GitHub build-provenance attestation you can
verify with `gh attestation verify <file> --repo
jkdansereau/goldeneye-pc-port`.
</div>
What the release actually installs is covered on [Security status](security.md);
how faithfully it tracks the N64 game, on [Fidelity status](fidelity.md).
## See it running
<p align="center">
<img src="img/shots/shot-06.jpg" width="45%" alt="Bunker 1, rendered by the port (v0.2.0 playtest)">
<img src="img/shots/shot-01.jpg" width="45%" alt="Dam, rendered by the port (v0.2.0 playtest)">
</p>
<details>
<summary><strong>Full playtest gallery</strong>: 19 in-engine captures from the v0.2.0 Windows playtest, across the campaign</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.2.0)">
<div><small>Dam</small></div>
<div class="viewer" id="viewer">
<div class="vbar">
<span class="dots"><i></i><i></i><i></i></span>
<span class="lbl">viewer &mdash; in-engine captures, 3840&times;2160</span>
<span class="idx" id="vIdx">1 / 14</span>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-02.jpg" width="100%" alt="In-engine: Facility (v0.2.0)">
<div><small>Facility</small></div>
<div class="vstage">
<img id="vImg" src="img/shots/feature-splitscreen.jpg" alt="GoldenEye 007 PC port — 2-player split-screen multiplayer, in-engine capture">
<button class="vbtn prev" id="vPrev" aria-label="previous capture">&#8249;</button>
<button class="vbtn next" id="vNext" aria-label="next capture">&#8250;</button>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-03.jpg" width="100%" alt="In-engine: Runway (v0.2.0)">
<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.2.0)">
<div><small>Surface</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-05.jpg" width="100%" alt="In-engine: Surface (v0.2.0)">
<div><small>Surface</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-06.jpg" width="100%" alt="In-engine: Bunker 1 (v0.2.0)">
<div><small>Bunker 1</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-07.jpg" width="100%" alt="In-engine: Silo (v0.2.0)">
<div><small>Silo</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.2.0)">
<div><small>Frigate</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-10.jpg" width="100%" alt="In-engine: Frigate (v0.2.0)">
<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.2.0)">
<div><small>Surface 2</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-13.jpg" width="100%" alt="In-engine: Bunker 2 (v0.2.0)">
<div><small>Bunker 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.2.0)">
<div><small>Statue</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-19.jpg" width="100%" alt="In-engine: Statue (v0.2.0)">
<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.2.0)">
<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.2.0)">
<div><small>Cradle</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-23.jpg" width="100%" alt="In-engine: Cradle (v0.2.0)">
<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.2.0)">
<div><small>Aztec</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-25.jpg" width="100%" alt="In-engine: Aztec (v0.2.0)">
<div><small>Aztec</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-26.jpg" width="100%" alt="In-engine: Frigate (v0.2.0)">
<div><small>Frigate</small></div>
</div>
</p>
</details>
More captures may land here as playtesting continues.
<div class="vcap"><span id="vCap">2-player split-screen &mdash; v0.5.0</span></div>
</div>
## 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
campaign. It's a pre-1.0 build 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;
@@ -164,72 +182,111 @@ More captures may land here as playtesting continues.
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.
- **AI disclosure**: development here used AI coding agents — Claude Code
plus a local open-weight model on a single RTX 5090, directed by one person
in their spare time. The project is as much a study of that process as it
is a port; the write-up records what happened and takes no position for or
against using LLMs on a project like this. See [the full
write-up](dev/agentic-development.md) — the setup, timeline, handoff
workflow, and an honest account 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
The R4300 game code's logic and control flow are the decompilation's, and
remain the reference we preserve. The port's changes inside `src/` are few in
kind and each is gated behind `#ifdef PORT`: mechanical 32→64-bit
pointer-width fixes, a couple of approved timing fixes, and the opt-in
*All unlocked* and *Skip intro* hooks. The N64's Reality Signal Processor
(RSP), the graphics coprocessor that builds and executes each frame's
display list, is emulated in software: the port interprets the graphics
command stream (GBI) 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
- **Particle colours** can still drift through a rainbow palette
intermittently: one genuine cause is fixed this release, a second hasn't
been found yet (D252).
- **Some muzzle flashes draw an extra erroneous long flash** straight up from
the gun, on top of the correct one. Cosmetic only (D303).
- Water levels show a moving seam between two water patterns (D245);
occasional z-fighting on some geometry (D308); in-level security-camera
props can occasionally face backwards (D307). Minor and cosmetic.
- **The front-end Nintendo logo renders as two white blobs** and the Rareware
logo's texture filtering looks off (D75).
- Surface 1's 2D billboard trees render as a solid wall of tree texture
instead of discrete sprites; under active investigation (D236).
- **Widescreen is stretched, not native**: 16:9 stretches the 4:3 frame to
fill the display; automatic FOV scaling keeps the framing comfortable and
gameplay is unaffected, but a distortion-free native widescreen render is
still on the roadmap. Distant geometry can also drop out on the biggest
open levels (D249).
- **Gunshot SFX can sound off during sustained/rapid fire** (D240/D241).
- **`All unlocked` is highly experimental** — don't enable it until you have
at least one save written, or it can break audio and mouse aim
(D257/D259/D281).
- Bond's cutscene positioning is fixed and right the large majority of the
time now; on the rare occasion he's off it's a small drift — no more
floating or spin-glitching (D173/D292/D243).
- No macOS/ARM support; no controller rebinding UI yet.
The full list, with root causes and fix status: the
[README's Status section](https://github.com/jkdansereau/goldeneye-pc-port#status)
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.
- [Building](building.md): full build and asset-extraction guide.
The technical docs live on the [Documentation](documentation.md) page — the
architecture, the N64→PC bug catalogue, and how the project is developed.
[Building](building.md) covers building and running from source.
---
<script>
// console-viewer carousel: one media slot, cycle the captures. Vanilla JS.
(function () {
var slides = [
{ src:"img/shots/feature-splitscreen.jpg", cap:"2-player split-screen — v0.5.0" },
{ src:"img/shots/feature-options.jpg", cap:"PC options menu, custom crosshair colour — v0.5.0" },
{ src:"img/shots/shot-01.jpg", cap:"Dam — v0.5.0 intro attract, 4K" },
{ src:"img/shots/shot-03.jpg", cap:"Runway — v0.5.0 intro attract, 4K" },
{ src:"img/shots/shot-04.jpg", cap:"Surface — v0.5.0 intro attract, 4K" },
{ src:"img/shots/shot-09.jpg", cap:"Frigate — v0.5.0 intro attract, 4K" },
{ src:"img/shots/shot-11.jpg", cap:"Surface 2 — v0.5.0 intro attract, 4K" },
{ src:"img/shots/shot-16.jpg", cap:"Statue — v0.5.0 intro attract, 4K" },
{ src:"img/shots/shot-20.jpg", cap:"Archives — v0.5.0 intro attract, 4K" },
{ src:"img/shots/shot-21.jpg", cap:"Cradle — v0.5.0 intro attract, 4K" },
{ src:"img/shots/shot-24.jpg", cap:"Aztec — v0.5.0 intro attract, 4K" },
{ src:"img/shots/shot-27.jpg", cap:"Control — v0.5.0 intro attract, 4K" },
{ src:"img/shots/shot-28.jpg", cap:"Streets — v0.5.0 intro attract, 4K" },
{ src:"img/shots/shot-29.jpg", cap:"Jungle — v0.5.0 intro attract, 4K" }
];
var img = document.getElementById("vImg"),
cap = document.getElementById("vCap"),
idx = document.getElementById("vIdx"),
i = 0;
function show(n) {
i = (n + slides.length) % slides.length;
img.src = slides[i].src;
img.alt = "GoldenEye 007 PC port — " + slides[i].cap +
", in-engine capture, 3840×2160";
cap.textContent = slides[i].cap;
idx.textContent = (i + 1) + " / " + slides.length;
}
document.getElementById("vPrev").addEventListener("click", function () { show(i - 1); });
document.getElementById("vNext").addEventListener("click", function () { show(i + 1); });
document.addEventListener("keydown", function (e) {
if (e.key === "ArrowLeft") show(i - 1);
else if (e.key === "ArrowRight") show(i + 1);
});
show(0);
})();
</script>
<small>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.</small>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Software",
"name": "GoldenEye 007 PC Port",
"genericName": "Native PC port of a Nintendo 64 game",
"softwareName": "GoldenEye 007 PC Port",
"category": "GameCategory",
"description": "A native PC build of GoldenEye 007 (Rare, 1997, Nintendo 64), compiled from the n64decomp/007 decompilation with the N64's RSP emulated in software. Ships no ROM and no game assets; you supply your own.",
"softwareVersion": "0.5.0",
"datePublished": "2026-10-05",
"dateModified": "2026-10-07",
"operatingSystem": ["Windows", "Linux", "Steam Deck"],
"downloadUrl": "https://github.com/jkdansereau/goldeneye-pc-port/releases",
"distributionType": "Software-Offline",
"officialDistribution": {
"@type": "SoftwareDistribution",
"downloadUrl": "https://github.com/jkdansereau/goldeneye-pc-port/releases",
"distributionLicense": "MIT"
},
"license": "https://opensource.org/licenses/MIT",
"isFreeApplication": true,
"url": "https://jkdansereau.github.io/goldeneye-pc-port/",
"sameAs": "https://github.com/jkdansereau/goldeneye-pc-port",
"publisher": {
"@type": "Person",
"name": "jkdansereau",
"url": "https://github.com/jkdansereau"
},
"gallery": [
"https://jkdansereau.github.io/goldeneye-pc-port/img/shots/shot-28.jpg",
"https://jkdansereau.github.io/goldeneye-pc-port/img/shots/shot-01.jpg",
"https://jkdansereau.github.io/goldeneye-pc-port/img/shots/shot-24.jpg"
],
"alternateName": "GoldenEye 007 (Nintendo 64, 1997) on the PC"
}
</script>
+32
View File
@@ -0,0 +1,32 @@
---
title: Input and aim
description: How the GoldenEye 007 PC port maps keyboard, mouse and modern controllers onto the N64's controller, including mouse aim.
date: '2026-10-05'
modified_time: '2026-10-05'
---
## Input and aim
`port/src/input.c` maps **SDL2 keyboard, mouse and gamepad** events into the
N64's own controller structs, so the game's input code runs unmodified.
**Mouse aim** is modelled on the *GEPD* mouse-injector behaviour that the 1964
"GoldenEye/Perfect Dark Edition" bundle used — an **independent
reimplementation** of that behaviour (not derived code), credited to its
author in the README. It comes in two styles, the N64's own aim feel or a
centered FPS-style, with per-device sensitivity, smoothing and Y-inversion.
**Controllers:** modern pads get a dual-stick layout, deadzones,
sensitivity, southpaw, trigger thresholds, Rumble-Pak vibration, and
PlayStation/Nintendo button names. **Everything is rebindable** — keyboard,
mouse and each controller — in the F10 options overlay. Split-screen needs one
controller per extra player.
**Control style** offers **Ext** (this port's scheme) or **Original**, the
game's own N64 styles 1.1–2.4, chosen per seat. New profiles start with
**Look ahead** off (it fights mouse look) and **Crouch** on Toggle; the
game's own Look up/down row was removed in favour of **Invert look** on the
Mouse and Controller pages.
*More: [internals](internals.md) §7 (input & saves); the aim model's
provenance is the D194 lineage in `port/src/input.c`.*
+48 -3
View File
@@ -3,14 +3,14 @@ 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
## 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](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);
> [`ROADMAP.md`](https://github.com/jkdansereau/goldeneye-pc-port/blob/main/docs/ROADMAP.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).
@@ -382,6 +382,51 @@ sync/flush) is shared.
rumble/mempak.
This is simpler than a full Memory Pak/PFS emulation.
* **F10 settings overlay: mouse and scrolling** (`port/src/optionsoverlay.c`,
`port/src/input.c`; D544, D314, D304):
* *Cursor.* `inputSuspendForOverlay()` (`input.c`) drops relative-mouse
grab and calls `mouseRequestCursor(1)`. With `Input.CrosshairCursor` on
(default, D555) `inputApplyMouseRequests()` (host thread) then hides the OS
cursor while the overlay is open and `drawCrosshairPointer()` in
`optionsoverlay.c` draws the game's own `crosshairimage` sprite at the mouse,
last in the overlay DL, at the game's on-screen size; the front end's own
crosshair keeps tracking the mouse (D519) and the pointer is drawn exactly
over it (`inputFrontEndCursorUiFrac()`), so only one shows; in a level it
follows the OS mouse. With the option off the OS arrow shows over the box. One owner of the cursor state (no show/hide race).
* *Hover vs selection.* `overlayHandleInputLocked()` maps the pointer to a
visible row with `overlayRowAtY()` (follows the eased scroll position, so
a click lands on the row it visually covers). Hover sets `s_hover` only
while `s_mouseActive` (mouse moved or clicked); it is a focus cue, never
the selection. The tip line (`helpVis` in the emit path) follows the
hovered row while the mouse is in use, else the selected row. The
wheel clears `s_mouseActive`.
* *Clicks.* Left-click on a row selects it; headers, links and Back act on
the whole row, otherwise only a click in the control span to the right of
the name changes the setting (toggle/cycle, open a dropdown, start a
binding capture, or set + drag a slider). With a dropdown open
(`s_ddRow >= 0`) it owns the pointer: hover picks, click confirms, click
outside or right-click cancels. Right-click elsewhere = Esc/back (closes a
dropdown first, then one page, then the overlay). `s_rmbBlock` holds that
off until release after a binding capture, so a right-click just bound
does not also back out.
* *Scrolling.* Long pages scroll as a window
over the visible-row list (`s_visIdx`/`s_visN`, first displayed entry
`s_scroll`, section header pinned). `overlayUpdateScroll()` moves the
window only when the selection leaves it (D304). The wheel arrives on the
host thread: `optionsOverlayScroll()` only queues notches in
`s_wheelPending` (SDL atomic, D314) and `overlayApplyWheel()` applies them
on the scheduler thread. The drawn position `s_scrollF` eases toward
`s_scroll` each emit (snaps on page change via `s_scrollSnap`).
* *Scrollbar (D556).* A page that overflows (same test as the "^ v" title
marker) draws a thin track + thumb in the card's right margin
(`overlayScrollbar()`: x = right-7..right-4, clear of the value text that
ends at right-10); thumb size = visible/total rows, position from
`s_scrollF`. Mouse: a press on the thumb drags it, a press on the track
pages up/down; both call `overlayScrollTo()`, which sets `s_scroll` and
moves `s_sel` into the window (like the wheel; otherwise
`overlayUpdateScroll()` would snap back). A scrollbar press never reaches
the row-click path.
---
## 8. Recommended plan
@@ -411,7 +456,7 @@ sync/flush) is shared.
* 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).
* Port `pdsched.c` (GE variant) to drive the software RSP.
* Port `pdsched.c` (GE variant) to drive the software RSP. *(Done differently: GE compiles its real `src/sched.c` and shims its hardware leaf calls in `port/src/libultra.c`, which runs the software RSP inline — see the CMakeLists.txt `SRC_ENGINE` comment.)*
* Goal: render the title screen / first level.
### Phase 3: Audio + input
Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 MiB

After

Width:  |  Height:  |  Size: 16 MiB

+531 -4
View File
@@ -19,15 +19,17 @@ good that you are looking at one of these.
## Contents
- [A. Pointer-width struct growth (32→64); the dominant class](#a-pointer-width-struct-growth-3264--the-dominant-class)
- [A. Pointer-width struct growth (32→64); the dominant class](#a-pointer-width-struct-growth-3264-the-dominant-class)
- [B. 16-byte PC `Gfx` / `Vtx` vs 8-byte N64](#b-16-byte-pc-gfx--vtx-vs-8-byte-n64)
- [C. Big-endian rodata / ROM data read on little-endian PC](#c-big-endian-rodata--rom-data-read-on-little-endian-pc)
- [C2. Port-layer / SDL shims](#c2-port-layer--sdl-shims)
- [D. N64 hardware idioms fast3d does not emulate](#d-n64-hardware-idioms-fast3d-does-not-emulate)
- [D2. The HUD/model "X-mirror"; RESOLVED](#d2-the-hudmodel-x-mirror-d114d116--resolved-it-was-an-upside-down-capture)
- [D2. The HUD/model "X-mirror"; RESOLVED](#d2-the-hudmodel-x-mirror-d114d116-resolved-it-was-an-upside-down-capture)
- [D3. GCC/mingw makes an all-non-negative `enum` UNSIGNED](#d3-gccmingw-makes-an-all-non-negative-enum-unsigned)
- [D4. N64 "interrupts off" must be a real lock on PC](#d4-n64-interrupts-off-is-not-free-on-pc--it-must-be-a-real-lock)
- [D4. N64 "interrupts off" must be a real lock on PC](#d4-n64-interrupts-off-is-not-free-on-pc-it-must-be-a-real-lock)
- [D5. Loop bounds that assume linker adjacency of two globals](#d5-loop-bounds-that-assume-linker-adjacency-of-two-file-scope-globals)
- [D12. Collapsing an LOD binding to tile 0 breaks a DETAIL binding](#d12-collapse-an-lod-binding-to-tile-0-is-wrong-for-a-two-texture-detail-binding-d236)
- [D13. Per-render accumulation is not associative (1 vs 2–3 ticks/frame)](#d13-per-render-accumulation-is-not-associative-the-ports-1-tickframe-can-reach-states-the-n64s-23-ticksframe-never-did-d329)
- [E. Process / method notes](#e-process--method-notes)
## A. Pointer-width struct growth (32→64): the dominant class
@@ -105,6 +107,15 @@ for hardcoded indices into any `padding[]`/`u8[]` union arm.
through the correctly-typed sibling field (`ObjectRecord.model`)
instead, `#ifdef PORT`, N64 line/cast kept verbatim under `#else`.
§F **D255** (M-140).
- **D392** (the firing-guard pose variant): `chrlvUpdateAimendbackShoulders`
reads `((f32*)arg1)[12/13/16/17]` into a
`weapon_firing_animation_table` whose *first* member is a union of a
`s32` and a `ModelAnimation *`. N64's 4-byte union places those slots at
`max_up/max_down/free_arm_frac_up/free_arm_frac_down`; on PC the 8-byte
union shifts the floats, so the raw indices read `aim_end_frame/max_up/
max_right/free_arm_frac_up`. The wrong aim clamp bends the torso ~49° and
pushes the hands through it. Under `#ifdef PORT`, use the four named
fields; retain the raw indices for N64. §F D392.
**Lesson.** When a value reads as a clean constant (0, 1, NULL) rather
than garbage, a pointer byte is a prime suspect; the high bytes of a
@@ -249,6 +260,50 @@ framing — it never looked like game logic to begin with.
decompilation of what the write side already does, `#ifdef PORT` with the
N64 hand-rolled offset kept verbatim under `#else`. §F **D248**.
- **One layout bug often has sibling call sites.** D264 fixed a
cross-global `ModelRenderData` template read in the watch item preview;
the identical read in `watchRenderController` (`D_80035D04 + 0x3c`) stayed
broken until D290. When fixing a raw-offset global read, grep for every
other read of the same globals (`D_80035D00`/`D_80035D04` here) before closing.
- **When a split N64 struct is made inline on PC, its placeholder fields
become dead, and any code still reading them reads uninitialised memory.**
D100 turned `player.model` into a real inline `struct Model`, but the
decomp's `field_59C..field_650` placeholders (raw N64 offsets into that
model) stayed as separate fields. `bheadAdjustAnimation` kept reading
`field_5C0` (= N64 `model.animframe1`) and `animFlipFlag` (= `model.gunhand`),
which nothing ever wrote (D336: garbage anim frames, Frigate scene wrecked,
sensitive to memory layout). After inlining a split struct, grep every
placeholder in the old range and remap each read to the real field.
Diagnostic tell: a hardware watchpoint on the "poisoned" field sees *no
writes at all*.
### A-dup. Renaming an IDO duplicate member re-points every read of it (D307)
IDO accepts a struct that redeclares an inherited member name (`inherits ObjectRecord;` then its own `pad`); GCC rejects it, so the port renames one. **The rename changes which field existing code reads**: on N64 the later declaration shadows the inherited one, so `rec->pad` meant the derived record's own field. After renaming it (`lookpad`), unchanged game code silently binds to the inherited field instead. D307: `setupCctv` read the mount pad as the look-at pad and every security camera faced its wall. When a port rename resolves a duplicate, grep every `->name` on that struct type and decide per site which field N64 meant (PD's named-field sources are the tie-breaker).
### A-cnt. A record's "count" field may not count what the converter copies (D303)
The offline model converter sizes each record's trailing arrays from its count field. Check what the *runtime consumer* does with that count before trusting its name: GE's op-22 `DisplayListPrimaryRecord.numVertices` is really the number of star-flash ARMS, and `dorottex()` reads `count*4` vertices (PD names it `stargunfire.unk00` and reads `unk00*4`). Sizing by the field name dropped 3/4 of every muzzle star; the missing vertices came from neighbouring sidecar bytes and drew as spikes. For every record type, derive the copied extent from the reading loop, and cross-check PD's typed struct.
### A-buf. Growing one region of a shared buffer must move every hard-coded offset after it (D342)
When a PC-larger asset gets a bigger reservation inside a shared scratch buffer (D45: the wallet model 0xA000 -> 0x17000 in `ptr_logo_and_walletbond_DL`), grep for every other user of that buffer that computes a fixed offset (`base + 4096*10`). Those still point into the old boundary and silently overwrite the enlarged asset; the corruption only shows when the asset is not reloaded.
**A1 cross-tag (D441, 2026-09-30):** the 32→64 widening of `(u32)`/`(s32)` *pointer casts* (rebase arithmetic on RAM copies, segment/anim-table bases) is the same class. Pattern: `#ifdef PORT` `(uintptr_t)` arm + untouched `#else`. Exception: `(s32)&ANIM_DATA_x` is an intentional offset (D34: 4 GiB-aligned base, low 32 bits = segment offset) — widen only the *base* it is added to, never the offset. Census of the remaining ~185 sites in the touched files is still owed (D441).
**A1 cross-tag (D457, 2026-09-30): a pure pointer-width retype can change behaviour by shifting the allocation layout.** Widening `struct player` (+32 B) / `struct Model` (+16 B, inline x4 in player) flipped the default-boot file-select menu to an all-black frame (no crash, identical log) while `-level_09` was fine; allocating the player block at its old size restored it. The menu depends on the MEMPOOL_STAGE layout (player block precedes `g_GfxBuffers[0]`, D98) through something address-sensitive. After any struct widening, run the default-boot menu with a `GE_PCDUMP` frame (not only a level) and check the frame extrema, not just rc. Also: a hardcoded record stride (`vtxstore.c` `n * 0x14` for a record that is 0x18 bytes on x86-64) is a silent overrun that widening makes worse; grep literals near allocs of retyped structs.
**A1 cross-tag (D441, 2026-10-03): the read side, `s32` back to a pointer, sign-extends.** An arena pointer stored in an `s32` (a local, a global, a field, a function parameter named like an id: `pathid`, `opcode`, `resolution`, `helddst`) truncates harmlessly below 4 GiB, but `(T *)s32` and the implicit `s32` -> `T *` conversion (a `-Wint-conversion` "makes pointer from integer", no cast to grep for) sign-extend bit 31. Invisible while the arena sits below 0x80000000; the tier-1 test build (`GE_HIGHARENA_BASE=0x90000000`) turns each one into a 0xffffffff9xxxxxxx fault, in crash order. Fix: `(T *)(uintptr_t)(u32)(EXPR)` under `#ifdef PORT`. Also grep the port layer for address-range **upper bounds**, not only base literals: fast3d's DRAM-vs-C-array texture test ended at a `0x90000000` literal, so with the arena moved there every DRAM texture was bswapped (mirrored texel pairs, no crash). Ranges must come from `portaddr.h` (base + `PORT_DRAM_SIZE`). And pin `GE_RSEED` + `GE_INPUTSCRIPT` before diffing level frames between builds: unseeded runs differ every frame.
**A1 cross-tag (D588, 2026-10-07): re-basing a LIVE host pointer through `portN64ToHost` mis-maps a native (out-of-window) pointer that *collides* with the D131 image-rel range.** `texSetBitstring` took the compressed-source cursor as an `s32` and re-based it (`portN64ToHost((u32)pos)`) — correct for an in-window (macOS) pointer whose low 32 bits are a genuine N64 offset, but wrong for a **native** pointer (e.g. the Windows libwinpthread worker-thread stack at `0x618d…`–`0x620d…`, whose low 32 bits fall in `[0x40M,0x70M)`): `portN64ToHost` maps it onto the PE image (`imagebase + (low32 − 0x40M)`), landing past the image end → deterministic SIGSEGV in `texReadBits`. Rule: a *stored N64 u32 field / DRAM address* is the legitimate `portN64ToHost` re-base input; a *live host pointer* must be carried at full pointer width (take the parameter as `u8 *` / `void *` under `#ifdef PORT`) so no truncate-then-rebase happens at all. Grep for `portN64ToHost((u32)<live-ptr-param>` — the feeder `rzipGetSomething()` (truncating `s_rz_nextin` to `s32`) was the second site. §F D588.
**A1 cross-tag (D590, 2026-10-08): the same collision, one site up the call chain, and the general diagnostic tell.** `load_bg_file` reads its stack `header` array through `PORT_N64PTR(void, ptr_bg_data)` where `ptr_bg_data` is a full-width host `bg_addr_t` (D457) — the truncate-then-re-base maps the bg-load worker stack onto the PE image and faults at `header[1]`. **Diagnostic tell for the whole class: if the fault address equals `imagebase + (low32(faulting pointer) − 0x40000000)` — i.e. the top 32 bits were dropped and the remainder re-based into the D131 image-rel window — the bug is a live host pointer fed through `portN64ToHost` / `PORT_N64PTR`, not a game-logic fault.** The check is a one-line arithmetic compare against the crash log's FAULT ADDR (Windows: `ge007.crash.log` prints both; `R8`/`RSP` in the same record give the raw pointer) — far cheaper than rebuild-and-run hypothesis testing. `PORT_ADDR_STRICT` does NOT catch this variant: strict only rejects u32s that map to no known N64 region, and a colliding in-window low32 passes it (a clean strict run is path-coverage evidence only). The legitimate `PORT_N64PTR` operand is a *stored N64 u32 field or DRAM address* (ROM-serialized records); a stack array, a `mempAllocBytesInBank` result, or any D457-retyped host pointer must not go through it. §F D590.
**A1 cross-tag (D591, 2026-10-08): host-space vs N64-raw-space mixup in a range *classification* test (not a rebase) — bswaps every in-window source on the `PORT_ADDR_BASE != 0` platform.** `gfx_tex_source_is_c_array` (`port/fast3d/gfx_pc.cpp`) stripped `PORT_ADDR_BASE` first (putting the pointer in N64 raw space, `0x70xxxxxx`), then compared against **host-space** bounds — `PORT_DRAM_V1_BASE`/`PORT_DRAM_K0_BASE` are `PORT_ADDR_BASE + 0x70000000`/`+ 0x80000000` (`port/include/portaddr.h`), i.e. `0x100070000000` on arm64 macOS. At `PORT_ADDR_BASE == 0` (x86_64) the two spaces coincide and the test is accidentally correct; at 16 TiB the raw pool offset is *above* the raw cart window and *below* the host-space DRAM bases, so every DRAM and cart texture source fails all three range checks and falls through to the C-array default — `gfx_tex_normalize_source` then hands fast3d a `PD_BE32`-swapped copy of the whole texture pool (the D589 confetti / D219-M-114 pink-HUD symptom class; a clean rev32 in the import-time dumps while the decode-time pool stays byte-identical). **Diagnostic tell: a texture-format-dependent byte-reversal that appears on *one* platform, with the pool-write side proven clean (decode-time dumps byte-identical) — check the platform-gated *classification* of the source address, not the writer; and on any `PORT_ADDR_BASE != 0` build, grep for range tests that strip the base before comparing to `PORT_DRAM_*` bounds (or, conversely, compare a raw offset to host bases). The test must use one space end to end: the host pointer against host-space bounds, or the raw offset against raw bases.** §F D591.
**A1 cross-tag (D595, 2026-10-08): the simplest member — a plain `(s32)` *read* of a live host pointer as the rebase *base* (no `portN64ToHost`, no classification), silently identity on x86_64 and wrong at `PORT_ADDR_BASE != 0`.** `initResolveAnimTable` (`src/game/initactorpropstuff.c`) resolves the `StruckAnim` reaction tables (`death_stagger`, `g_HitReactionTable[*].{death,flinch}Anims`) as `struck_anim = base + offset`, where `base` is the live host pointer `ptr_animation_table` (`mempAllocBytesInBank(…, MEMPOOL_PERMANENT)`) and `offset` is a small N64 `PTR_ANIM_*` (e.g. `0x1C`). The PORT path read the base as `(*((s32 *)entries))`: on x86_64 the PERMANENT bank is s32-low (`0x70xxxxxx`) so the 4-byte read is identity; on arm64 macOS (`PORT_ADDR_BASE` = 16 TiB, `0x10007xxxxxx`) it drops the high 32 bits, so every `struck_anim` landed in bogus low memory and the first AI hit/stagger/death reaction (`triggered_on_shot_hit → modelSetAnimation2 → modelSetAnimFrame` deref of `anim->unk07`) SIGSEGV'd. **Diagnostic tell: an arm64-only NULL/garbage-pointer fault in an *animation / table-resolution* path where the pointer is `base + small-rom-offset` and `base` is a `mempAllocBytesInBank` result — grep the `(s32)`/`(u32)` reads of such bases (the sibling sites `bondhead.c` / `bondview2.c` already use a full-width `(uintptr_t)&ptr_animation_table->data`; `initResolveAnimTable` was the one still on the `(s32)` form).** Fix: read the base at full width (`(*entries).struck_anim`), add the offset as `u32`; identity on every `PORT_ADDR_BASE == 0` platform. §F D595.
## B. 16-byte PC `Gfx` / `Vtx` vs 8-byte N64
Any buffer reservation, `memcpy` size, slot stride, or pool budget
@@ -269,6 +324,9 @@ expressed in N64 `Gfx`/`Vtx` units is **half-size** on PC.
`(word >> (8*(3-i))) & 0xff`). `gdl++` (advances by `sizeof(Gfx)`) is already
correct. Watch `(s32)ptr` truncation in vtx-base math and `x | 0x80000000`
KSEG0 folds (identity on PC; just drop the OR, and guard segmented w1).
Also raw-index **writes** into static DLs: D465 (`unk_092E50.c` water
controller patched `((u32*)dl)[8]`, meaning N64 `Gfx[4].w0`, which hit
`Gfx[2]` on PC; use `dl[n].words.w0` instead).
Instances: `bgTestHitOnObj` (`propobj.c`, FIXED); `bgTestRayIntersectionInRoom`
+ `bgTestBulletHitBackground` tail (`bg.c`, D154; ported M-28, re-audited +
bug-fixed M-30, playtest-gated).
@@ -539,6 +597,30 @@ through a converter or a runtime bswap fixup reads scrambled.
## C2. Port-layer / SDL shims
- **Darwin assembly uses Mach-O symbol spelling.** GAS absolute symbols that
are `.global foo` / `.set foo, address` on ELF/PE must become `.globl _foo`
/ `.set _foo, address`; Darwin also rejects the ELF `.section .data`
spelling used by these symbol-only files. Generate transformed build-tree
copies rather than changing the cross-platform source files (D294).
- **Darwin GCC cannot lower ELF-style `#pragma weak alias = target`.** It can
ICE in `assemble_alias`; compile generated copies with those pragmas removed
and provide Mach-O indirect symbols (`.set _alias, _target`) separately
(D294).
- **Mach-O's default `__PAGEZERO` reserves the entire low 4 GiB.** A build
can compile and link successfully yet fail every fixed N64 mapping at
runtime (`0x10000000`, `0x70000000`, `0x80000000`). Link with
`-Wl,-pagezero_size,0x10000` so null-page pointers remain protected while
the cartridge and DRAM ranges are available (D294).
- **Darwin has no `MAP_FIXED_NOREPLACE`; a fixed-address hint can relocate.**
Once `__PAGEZERO` is deliberately bounded below the cart range, use
`MAP_FIXED` for the `0x10000000` cartridge mapping. Retaining the advisory
hint silently falls back to a heap ROM and later crashes on direct absolute
ROM-symbol reads (D294).
- **AppKit event polling is main-thread-only.** This port has both a host-main
`videoPumpEvents()` loop and fast3d's render-thread event pump. The latter
must be disabled on macOS or `SDL_PollEvent` raises
`NSInternalInconsistencyException`; the main-thread loop already handles
the complete event set (D294).
- `#include <PR/os.h>` in a port `.c`/`.h` that also sees `<errno.h>`
breaks: `OSContStatus`/`OSContPad` have a `u8 errno;` field vs errno.h's
macro. libultra.c wraps the include in `#pragma push_macro("errno")` /
@@ -556,12 +638,45 @@ through a converter or a runtime bswap fixup reads scrambled.
(exact for hold-to-aim; a toggle scheme needs `g_CurrentPlayer->
insightaimmode`). GE's native pitch is **inverted** (C-up → look down);
hide it so mouse-down looks down; `MouseInvertY` flips (D118, M-24).
- **N64 A/B bit names are not modern pad A/B semantics (D393).** GE's
gameplay `GE_CONT_A` cycles weapons and `GE_CONT_B` contextually uses or
reloads. Assigning Xbox A/X both to native A and B/Y both to independent
crouch leaves *no* use button. For modern controls, map A to a dedicated
use edge, X to a dedicated reload edge, B to crouch, Y to a weapon-cycle
edge; preserve native accept/cancel bits in front menus/watch/tank. Track
held buttons across UI transitions so closing F10 does not trigger an
unexpected use/reload/cycle.
- `osContGetReadData(pad)` must fill **one OSContPad per channel**
(`MAXCONTROLLERS`-long array), not just controller 0; joy.c passes the
whole `samples[i].pads` array (D118).
- Controller state has one source: `port/src/input.c`. `libultra.c`'s SI
section marshals `inputComputePad()` into `g_contPad[]`; it is driven by
`osContStartReadData` (per logic tick), no separate `video.c` frame hook.
- **The in-level watch/pause is not a "menu" to `input.c`** (`current_menu`
stays `RUN_STAGE`), but it reads the stick as **discrete** input: ±0x2E
on X picks an option value or flips a page, and Y scrolls rows. Any path
that turns a continuous device (the mouse) into stick deflection must be
gated off there (`!outside_watch_menu || pause_state`), or small hand
motion becomes phantom menu choices. D330/#103: mouse → legacy stick
fallback silently reset Auto-Aim/Look-Ahead to OFF. The direct-write
look paths already gate on this; the fallbacks must too.
- **A port-side shadow of game data must be re-synced every time the game
re-derives the original, not just the first time.** D252: `texReset()`
re-resolves the global image DLs' texture pointers on *every* stage load;
the compiled shadow copies (`gimgSyncCompiledGlobalDLs`, D68) only accepted
a write while a slot still held its unresolved marker, so every stage after
the title kept pointers into freed memory. It looked fine until the new
stage overwrote that region (rainbow explosions mid-level). Key a sync on
*which slots* are synced (learned once), never on "still unresolved".
Headless `-level_XX` boots hide this class (the first stage is the level
itself): repro through a real boot → title → level sequence.
- **A texture re-declared in another format must be normalised with the geometry it was IMPORTED with (D245).** GE's sky water loads a CI8 image and draws it through an RGBA16 tile; fast3d imports it as CI8 (D229) but its triangle path computed the UV divisor from the RGBA16 tile (half the width) and the whole mip-chain height (43 rows, not the 32-row mask period). Anything that changes the importer's format/extent must change the tri-path `tex_width/tex_height` with it, and wrap must equal the N64 mask period.
- **CPU-built RDP triangles need float vertices, not `Vtx` (D245).** Where GE builds RDP edge/texture coefficients on the CPU (sky/water), 32-bit S/T and positions are normal; squeezing them through s16 `tc`/`ob` costs visible precision on horizon-scale geometry. Use `G_FLOATVTX_EXT` (`port/include/floatvtx.h`).
- **A first-run-only preset is skipped forever by any earlier launch that created the ini, and window keys are not an "untouched" signal (D283).** Seeding values *before* `configLoad()` only works when no `ge007.ini` exists; one launch in a different environment (SteamOS Desktop Mode has no `STEAMOS`) writes a plain ini and the preset never runs. Apply presets *after* the load, record a one-shot flag key, and gate on keys the user must change deliberately (`Video.Fullscreen`, `Window.Maximized`) — `Window.Width/Height` are rewritten from the live window on every clean exit (`videoSaveWindowState`), so "still at default" never holds for them.
**SDL3 on macOS: a GL swap with VSync can wait forever (D952).** SDL3's Cocoa backend implements the GL swap interval itself: `SDL_GL_SwapWindow` waits, with no timeout, for a tick from a `CVDisplayLink` that SDL creates and starts once, with the context. If that link never runs (seen when the game starts while the display is asleep), the first swap never returns: black window, zero frames, every other thread waiting behind the render thread. Tells: `sample` shows `Cocoa_GL_SwapWindow -> SDL_WaitCondition*` on the render thread and the process has no `CVDisplayLink` thread. Fix pattern: leave SDL's interval at 0 and set `kCGLCPSwapInterval` on the context with `CGLSetParameter`, which waits in the driver (SDL2's behaviour). Reproduce with `pmset displaysleepnow` before launching.
## D. N64 hardware idioms fast3d does not emulate
@@ -865,6 +980,52 @@ through a converter or a runtime bswap fixup reads scrambled.
(ginit.s is missing from this repo; the expansion lives there), not UV,
shade, or the combiner.
## D14. LOD tile selection is a 2-cycle-only RDP feature; stale `G_TL_LOD` must not affect 1-cycle texture rectangles (D341)
`G_TL_LOD` is sticky other-mode state. Game code that draws a 1-cycle texture rectangle after an LOD-textured draw never clears it, because on the RDP it is irrelevant outside 2-cycle mode. fast3d's LOD/detail tile folding (D107/D236) must therefore not run for 1-cycle texrects, or they sample a neighbouring tile's stale declaration. Related: textures the CPU rewrites in place at recurring addresses (dyn pool) need a content-hashed cache key, and remember game code reaches them through the 0x80000000 KSEG0 mirror.
## D12. "Collapse an LOD binding to tile 0" is wrong for a two-texture DETAIL binding (D236)
**M-198 (finding D236 pass 26).** GE's `texLoadFromGdl` resolves a room DL's
`G_NOOP` texture reference through one of three handlers, chosen by
`in->words.w0 & 7` (`src/game/tex.c`):
| type | handler | TMEM layout |
|---|---|---|
| 0 `TEXTURETYPE_LOD` | `texHandleType0` | ONE load block at TMEM 0; tiles 0..n are LOD levels **of that one image** |
| 1 `TEXTURETYPE_DETAIL` | `texHandleType1` | TWO load blocks: the **detail** texture at TMEM 0, the **base** image at TMEM `texGetSizeInBytes(detail,0)` on tiles 1.. |
| 2 `TEXTURETYPE_MIPMAP` | `texHandleType2` | as type 0 |
fast3d's `gfx_lod_tile_offset` (`port/fast3d/gfx_pc.cpp`) folded **every**
`G_TL_LOD` draw to tile 0, on the reasoning (D107) that "GE loads the whole mip
chain at TMEM 0". That is true for types 0 and 2 and **false for type 1**,
where TMEM 0 holds the detail texture and the image you actually want is on
tile 1. The port therefore drew the detail texture in place of the base image —
and never even *imported* the base image, so every probe looking for it came
back empty.
On Surface 1 this was the whole of D236: all four treeline cards
(`tex1198`–`tex1201`, RGBA5551 64×17 cut-outs) are type-1 bindings sharing one
detail texture, `tex2465` — an opaque 32×32 IA8 noise tile. Result: an opaque
tiled noise wall where the treeline belongs, for 20+ investigation passes.
**Tells.** (a) A texture the game demonstrably decodes into the pool is never
imported by the renderer — that is a *binding* bug, not a decode, alpha, mip or
geometry bug, and no amount of work on the texture's contents will move it.
(b) The wrong thing drawn is itself a real, correctly-decoded texture from the
same draw — look for a second texture bound by the same command.
**Discriminating the two layouts at draw time.** Prefer an invariant that a mip
chain can never satisfy. Tiles 1.. of a type-0/2 binding are LOD levels of tile
0's image, so they necessarily carry the **same `fmt`/`siz`**; a type-1 pair is
two unrelated textures and generally does not. Deliberately choose the
asymmetric test — a mip chain can never trip it, and a detail pair that happens
to share a format merely keeps the old behaviour. Two weaker discriminators
were tried live and both misfire: "tile 1 sits at a different TMEM address"
matches ordinary mip chains (their levels are at different TMEM offsets too),
and "tile 1's TMEM slot has a registered load" matches them as well, because
`rdp.loaded_texture[]` is persistent state that is never cleared between binds.
## D2. The HUD/model "X-mirror" (D114/D116): RESOLVED, it was an upside-down capture
**M-33 (finding D168).** There was no mirror. `gfx_opengl_dump_bound_fbo`
@@ -896,7 +1057,10 @@ for (s = SP_LEVEL_EGYPT; s >= SP_LEVEL_DAM /* == 0 */; s--) // never ends
- Symptom: a silent hang (kernel-heartbeat stall, no crash log) inside a
loop over an enum range; the counter holds a huge value in gdb.
- Instances: **D142**; `LEVEL_SOLO_SEQUENCE` in
- Instances: **D417** (`enum PROP`: `getPropForHeldItem()` returns -1 for the
fist and callers test `prop >= 0` -> unsigned enum lets -1 through ->
`modelLoad(-1)` crash in any 2P+ stage; sentinel `PROP__PORT_SIGNED`);
**D142**; `LEVEL_SOLO_SEQUENCE` in
`fileGetHighestStageDifficultyCompletedForFolder` froze the SELECT FILE
screen. `DIFFICULTY` was already safe (`DIFFICULTY_MULTI = -1`).
- Fix: add a never-used negative sentinel enumerator under `#ifdef PORT`
@@ -981,6 +1145,13 @@ past it (walk off the end).
links 0x60 bytes *before* `legalpage_text_array` → only line 1 renders
(== the D76 "disclaimer half-drawn" bug; it was never an image-table
issue). Fix: `#ifdef PORT` uses `array + ARRAY_COUNT(array)`.
- Instance: **D491**; the same assumption in struct reads, not loop bounds:
`*(coord3d *)&D_80044904` (bg.c portal bbox seeds) and
`*(coord3d *)&D_80035EA4` (gunfire.c casing rot) read three separately
declared scalars as one struct. mingw emits such runs in **reverse** order,
so the read picks up a different neighbour. `-Warray-bounds` flags these
("partly outside array bounds of 's32[1]'"); confirm with `nm -n` on the
exe. Fix: `#ifdef PORT` reads each named global.
- Grep for `= &` / `(TYPE *)&` on the RHS of a loop-terminator compare, and
any `for`/`while`/`do` whose end pointer is the address of a *different*
symbol than the one being iterated.
@@ -1019,6 +1190,12 @@ silently disappears.
upstream. This class isn't limited to display-list cursors; audit any
`-O2`-only "quietly broken but not crashing" symptom for the same shape.
Fix: `return` under `#ifdef AVOID_UB` (`#else` keeps the N64 body).
- Instances (latent, 2026-09-25 Q7 warning audit): `constructor_menu07_missionsel()` and
`constructor_menu0D_missioncomplete()` (`src/game/front.c`), both `DL = f(DL)` at the
caller. Currently harmless: old/new objects disassemble identically at `-O2` (GCC
keeps `frontDrawCursor()`'s result in `rax`), but a clang/arm64 build is not bound
by that. `return DL;` under `#ifdef AVOID_UB`. A `-Wreturn-type` sweep lists the
remaining candidates; the other 7 current hits have callers that ignore the value.
- The one flagged in-source with an explicit comment is
`grep -rn "missing a \"return\"" src/` (`gunfire.c`). Others (like D77's)
exist without the banner; suspect this class whenever an `-O2` build
@@ -1078,6 +1255,11 @@ and turns every such overrun into a fatal `*** stack smashing detected ***`
the identical overflow is cosmetic (the JPN-cache pointer is valid memory in
any language); the crash severity is a PC artifact of that global being NULL
outside Japanese mode.
- Instance (D420): `mpmenu.c` `mp_watch_menu_display` `char rankbuffer[4]`
receives "Rank: 1st" / "P<n> KILLS"; GCC puts `scores[4]` next to it, so
the MP Scores page printed the rank text's bytes as numbers (1932599354 =
": 1s"). Tell: a displayed number whose hex is printable ASCII from a
neighbouring string. Widened to `[64]` under `#ifdef PORT`.
- Fix policy: **`-fno-stack-protector` globally** (`CMakeLists.txt`) so Linux
matches the N64 and MinGW builds; the code is byte-matched to the ROM and
cannot be "fixed" per site without diverging. Where a case is cleanly
@@ -1127,6 +1309,14 @@ non-flag ones (data loads, one-time validation, subsystem init) explicitly.
Grepping the skipped `init_*`/`update_*` functions for calls with no
matching state flag is the concrete check.
Second instance (D479): the `GE_STARTMP` split-screen harness skipped the
file-select screen, whose init sets `selected_folder_num = -1` for an MP
launch. The harness left the boot default `FOLDER1`, so every stage load ran
`fileLoadSettingsForFolder(0)` against an unvalidated (all-zero) `saves[]`
record: options 0, i.e. ammo counter, sight and auto-aim off in harness
matches only. A real menu launch never applies a Bond file in MP. Symptom
looked like a HUD bug; the tell was that it never reproduced from the menu.
## D11. A chr's own per-tick animation-position logic can silently overwrite an AI script's scripted position in the same frame (D243)
`chrTick` (`src/game/chr.c:2442`) always runs one of three `chr->actiontype`-
@@ -1158,8 +1348,181 @@ produce the same visual symptom but need opposite fixes.
**Update 2 (same investigation, immediately after): freezing a *value* and freezing a *pointer target* are different operations — treating them the same crashed twice.** Extending the same freeze technique to `render_pos` (a `Model*` field, unlike `field_488.pos` a plain value) by skipping the call that reassigns it (`subcalcmatrices`) seemed equivalent to M-167's "skip the write" but wasn't: `render_pos` is a pointer into a per-tick dynamically-reallocated arena, and skipping its reassignment only works if the pointer it's left holding is still valid — which depends on model lifecycle, not just on this tick's logic. Two fixes in a row tried to detect "is it safe to skip" by inspecting either the pointer's *value* (a sentinel check, `!= NULL && != -1`) or a *cached identity* (comparing the pointer against a remembered "already validated" pointer) — both crashed, because **the allocator reuses model-pointer addresses across genuinely different logical model instances**: a freshly-`CREATE`d model can land at the exact address a previously-removed model occupied, so both the sentinel check and the identity check wrongly conclude "this is already valid" for an instance whose `render_pos` is still an uninitialized tombstone. This is the same "don't guess validity from a value that can coincidentally look right" trap as the paragraph above, encountered a second time from a different angle (a *pointer's* target identity, not a *field's* content). **The robust fix:** never skip the call that establishes the pointer's validity — let it always run — and apply the freeze as a *contents* copy afterward, on top of a buffer that's now unconditionally guaranteed valid. If you must "hold something steady" and that something is reached through a pointer that some other code reassigns, freeze the *pointee's bytes*, not the *act of assignment* — the assignment is usually also the thing keeping the pointer sane.
## D12. Depth precision: the port matches N64 exactly — z-fighting seen on PC is probably faithful (D306/D308)
When a user reports z-fighting, do NOT assume the software renderer has less depth precision than the RDP. Verified for GE: the RSP's vertex transform (`rsp/graphics/gmain.s`) writes post-transform x/y/z/w back to the vertex buffer as **full 32-bit 16.16 fixed-point** (`sdv` stores), not s16 8.8 — the RDP interpolates from full-precision NDC and quantizes once, into its 24-bit z-image. The port keeps floats through `gfx_sp_vertex` and writes a `GL_DEPTH24_STENCIL8` buffer: same or better precision at every stage, same near/far. So coincident geometry authored into the level fights on both platforms; a PC-only explanation needs positive evidence (an N64 reference of the same spot), not just the existence of the symptom. A mitigation like switching to 32-bit float depth would *reduce* fighting but diverge from N64 behavior — that's a rule-2-style decision, not a silent fix.
**Probe technique that worked (vs. three variants that didn't):** detecting z-fighting from emitted triangles requires filtering out (a) tessellation gradients — adjacent tris of one surface have near-equal depths by construction — and (b) camera-motion coincidences — as the camera moves, unrelated world points pass through the same screen cell and their depths happen to sit close. What survives both: a coarse screen-cell grid keeping a short per-cell depth history, flagging only **non-monotonic** (a-b-a) oscillation within a small tolerance that **recurs many times at the same cell**. Monotonic drift = gradient; one-off close pairs = motion artifact; sustained recurrence = a real fight. `GE_ZF` in `port/fast3d/gfx_pc.cpp` implements this (`GE_ZFTOL` tolerance, `GE_ZFBOX` region filter). **Caveat (M-201):** `GE_ZF` actually quantises NDC z to ±255, not 24 bits, so at its default tolerance it flags unrelated nearby surfaces; treat a hit as a lead, not proof. Before calling anything "z-fighting", check the separation against the near plane: at close range 24-bit depth resolves far finer than any non-coplanar model parts, so "whole surfaces in the wrong order" there is depth *state* or geometry, not precision. Use `GE_D306C` (per-triangle depth-state census) for that; D306 turned out to be authored geometry seen from an unusual camera.
## D13. Per-render accumulation is not associative: the port's 1 tick/frame can reach states the N64's 2–3 ticks/frame never did (D329)
GE on N64 renders gameplay at ~20–30 fps, so `g_ClockTimer` (= `speedgraphframes`, 60 Hz ticks per rendered frame) is normally 2–3. The port at 60 fps gives 1. Code that folds `numticks` ticks into **one** downstream call is not equivalent at different granularities. `modelTickAnim` sums `playspeed*speed` over the ticks, then makes one `modelSetAnimFrame2WithChrStuff` call, which only moves `framea`/`frameb` when `floor(frame)` crosses an integer. At speed < 1 and 1 tick/frame a clamped (`framea == frameb`) non-looping anim never crosses, so it stays pinned even after its endframe is raised. At 2+ ticks it crosses on the next frame. Result: permanent guard freezes on PC (D318/D320) that the console could not sustain.
- **Tell:** a state that holds forever at 60 fps, is frame-exact (integer frame, `fa == fb`), and has a per-tick step < 1.0 unit.
- **Repro correctly:** force the granularity with `GE_D318B_CLK=N` (TEMP probe, lockstep N ticks/frame) and A/B N=1 vs N=2. Wall-clock runs on a slow host (WSLg renders ~5 fps → mixed clk 1–6) and `GE_DETERM=1` (advances per VI-retrace request, also mixed clk) both give uncalibrated results.
- **Where else to look:** any `for/while (numticks)` accumulator followed by a single threshold/floor consumer; D193 (AI locomotion rate) and D243 (cutscene `numticks` bursts) touched the same axis from the other side.
- **Frame-counter gates (D427/D451):** a gate that reads the current `g_ClockTimer` to rescale a frame counter (`88C % (rate*(clk>=2?1:2))`) is only exact at steady pacing. Prefer the tick accumulator already kept beside the counter (`field_890`) and test for a threshold *crossing* (`t/P != (t-clk)/P`): stateless and exact on average at any pacing.
## D15. A texture-rectangle idiom that is sub-1-native-pixel tall relies on the RDP's floor-based fixed-point scanline stepping — now replicated in `gfx_draw_rectangle` (D397, RESOLVED 2026-09-28)
GE's front-end vertical-gradient trick (`titleRenderFolderMenuBackgroundLines`,
`src/game/title2.c`) draws a tall image as 299 separate 1-scanline-tall
`gSPTextureRectangle`s, one per source row, each tinted by a per-row
interpolated `gDPSetPrimColor`. The lower-right Y coordinate uses the
universal N64 idiom `((y+1)<<2)-1`, which in U10.2 units is **3, not 4** — a
0.75-native-pixel-tall rect, not 1.0. This is correct *on real hardware*
specifically because the RDP's scan converter steps by truncating
(flooring) the fixed-point Y to an integer scanline before testing coverage,
so `y*4+3` still falls in scanline `y` and nothing else — the "-1" is a
hardware-truncation idiom, not a literal float height.
`gfx_pc.cpp`'s `gfx_draw_rectangle` does not replicate that truncation: it
converts the U10.2 corners straight to continuous NDC floats
(`ulyf/lryf = ±(y / (4·HALF_SCREEN_HEIGHT)) + 1`) and hands them to the GPU's
own continuous rasterizer, which uses pixel-center/coverage rules, not
floor-stepped scanlines. A rect that is 0.75 (native) pixels tall, stacked
299 times with each row offset by exactly 1.0 native pixel from the last,
does not reliably rasterize as "exactly the one scanline the N64 idiom
intended" under a continuous rasterizer — whether a given row rounds to 0 or
1 device pixel depends on where its fractional edges land relative to the
device pixel grid, and that phase drifts row-to-row (worse under any
non-integer native→window viewport scale, `gfx_adjust_viewport_or_scissor`'s
`ratioX`/`ratioY`). Stacked over ~300 rows filling the screen, sporadic
per-row rounding reads as a periodic horizontal comb/interlace — this is the
leading mechanism for D397's "interlaced" file-select background and intro
gun-barrel, and is a **first-render, not re-entry-triggered** artifact
(unlike D182), because it is a rasterization property of the geometry itself,
not stale state.
**Tell:** any GE front-end/menu draw that fakes a large image or gradient by
stacking many independent 1-scanline (or otherwise sub-1-native-pixel-tall)
`gSPTextureRectangle`s rather than one multi-row texture — the `(<<2)-1`
lower-right idiom is the signature to grep for. A single such rect is
invisible (sub-pixel rounding on one edge); many stacked ones make the
rounding pattern visible as banding.
**Fix (landed 2026-09-28, RESOLVED):** `gfx_draw_rectangle` (`gfx_pc.cpp`) now replicates the RDP truncation. After the sub-pixel offset is applied, it floors the U10.2 top edge to its scanline and the bottom edge to the *last covered* scanline — the RDP scanline range `[top_scan, bot_scan+1]` — before converting to NDC: `top_scan = uly>>2`, `bot_scan = (lry-1)>>2`, then `uly = top_scan<<2`, `lry = (bot_scan+1)<<2`. This is a **no-op for whole-pixel rects** (their edges are already `4k`/`4k−1`, and the `(<<2)−1` bottom idiom already yields `bot_scan` = the intended last row), so only sub-pixel-tall strips change. **Confirmed by a falsifiable 3-scale PCDUMP test:** the comb period is fixed at ≈3 *native* rows across 640/1024/1280 (window-period = 3×scale) — the beat of 0.75-px strips at a 1.0-px pitch — exactly the fixed-point-flooring signature, ruling out the scale-invariant LERP/CC factor. Verified by playtest: gun-barrel + file-select gradient render clean, comb gone.
**Not the cause (checked and ruled out for D397):** the CC/LERP scaling of
`ENV_ALPHA` as `value/255.0f` (`gfx_pc.cpp` around the `G_CCMUX_ENV_ALPHA`
case) is byte-identical to the Perfect Dark PC port's (`gfx_pc.cpp` in the
local `pd_port` checkout) — same formula, no special-cased "5-bit RDP
coefficient" scaling in either port. The real RDP color combiner's A/B/C/D
inputs are 8-bit values (PRIM/ENV alpha come from an 8-bit `gDPSetEnvColor`
field); there is no coarser-precision quirk to correct here. A uniform
darkening from a nonzero `ENV_ALPHA` LERP factor is very likely faithful
(possibly authored/original-dev-intentional tinting), not a bug — do not
"fix" it without an N64/1964-GEPD reference screenshot proving otherwise.
## D16. Split-screen: port-side "one full-screen viewport" assumptions break 2P+ (D418)
Two traps from the #99 split-screen bring-up:
- **Safe-area crop (`gfx_adjust_viewport_or_scissor`) caches the LAST SP
viewport's Y range and stretches it over the whole window.** Right for the
solo 220-line viewport, wrong for 2P+ (each player's viewport is a sub-rect):
every player's view filled the window and the last-drawn (order is shuffled
per frame, `get_nth_player_from_shuffled`) covered the rest -> alternating
full-screen flicker. Fix = `gfx_set_split_screen()`. Any new port feature that
assumes "the viewport is the screen" needs the same 2P+ gate.
- **Per-frame port hooks must sit on the path frames really take.** The
scheduler calls `gfx_run` directly (`port/src/libultra.c`), NOT through
`videoSubmitCommands`; a hook placed only in the latter silently never ran.
Log once on the first state change when adding such a hook.
Debug recipe that found it: capture consecutive frames after the stage starts
(GE_PCDUMP range, or the harness `GE_STARTMP=<players>[,<stageIdx>]` +
`GE_MPVIRT=<players-1>` to start a match with no one at the menu) and look at
them — the symptom was obvious in one montage.
## D17. Un-stubbing a `-1` sentinel re-activates every dormant consumer of the value (D431/D434)
A port stub that forces a value to `-1` (D154 `texnum`, D135) can hide latent layout bugs in its other readers. When restoring a real value, grep ALL readers of the field first and check each for raw-layout reads and table-bounds assumptions. Instance: D431 made light-fixture hits return real image ids, which reached `chrprop.c`/`propobj.c` reading `((u8*)&g_Textures[n])[0] & 0xf` as a hit type -- on PC byte 0 is the low byte of `dataoffset` (LE bitfield layout, D67), so it indexed `g_HitTypeSounds[13]` with 0..15. Use the `.hitTexture`/`.hitSound` bitfields (as `chr.c` does).
## D18. A port feature that widens a shared range (draw distance, FOV) leaks into gameplay through every reader of the state it widened (D466)
`Video.DrawDistance` scaled the one far-clip value that both the renderer and gameplay read, so `room_rendered`, `PROPFLAG_ONSCREEN`, `CHRFLAG_HAS_BEEN_ON_SCREEN` and the fog intensities all grew with it, and AI scripts ("on screen", "seen", target-in-sight, spawn out of view) fired earlier than on N64. Rule: when adding a "see farther / wider" option, grep every reader of the state it feeds and split render from gameplay; the render consumer keeps the extended value, the gameplay readers get a stored copy computed at the authored value.
- **Tell:** `PROPFLAG_ONSCREEN`-style flags are dual-use (render gate AND AI input), so never gate the flag itself; store a separate verdict bit.
- **Techniques that worked:** run the original traversal twice with a port flag that pins the single far read (a static read, never `viSetZRange`) and snapshot the result; keep the "gameplay verdict" in an unused bit of a u8 flag byte (`prop->flags & 0x80`) written by every setter of the dual-use flag; store the authored fog values at load time instead of recomputing from the live multiplier.
- **Triage trap:** check whether a `PROPFLAG_*` hit is on `prop->flags` (runtime u8) or on `obj->flags` / pdef flags (u32 definition bits) before treating it as a reader (`propobj.c:8417`, `prop.c` 233-467 are the latter).
- **Remaining class member:** FOV/widescreen widening of `camIsPosInScreen` and `c_lodscalez` (D222).
- **Fixed-size per-frame budgets are readers too (D500):** the dyn.c vtx pool (`-mvtx`, as low as 40 KB) is bump-allocated with no bounds check and sized for what the N64 could see; widened views overran it (Aztec triangles). Look for unchecked per-frame pools sized by per-level tokens when a widening feature shows corruption only in busy scenes.
- **A pixel ratio is not a fog factor (D503):** "~97% fogged" was a pixel-colour ratio; derive any fog snap cutoff from measured vertex fog, not from it. And before blaming fog for a far object, do the cheap 4:3 + N64-FOV test: if it vanishes, the cause is the widened frustum, not fog.
- **Reveal, not regression (D503):** the N64's 16-bit framebuffer hid 2-5 level differences (near-full fog against haze); 32-bit output shows them, plus the room-scissor edges of nearly fogged objects. Measure pixel values against the fog colour before blaming draw distance.
## D19. A "click after the shot" report needs the trigger-hold duration, not just the state machine (D433/D467)
State-machine audits of a gun's fire/dry-fire path (GE: `gunTickHandState`, CLICKY weapons) are tick-driven and match the console at any tick/frame ratio; an extra click/sound that appears "with the shot" is usually the trigger still being down when the recoil state ends (about 14 ticks for the Golden Gun). Measure with `GE_STARTWEAPON` + `GE_INPUTSCRIPT` holds of 4/9/16 frames and a `state/mag/88C/890/clk` log at the sound-play sites before suspecting the audio layer; and remember `GE_QUITFRAME` counts frames from boot, the level's `g_GlobalTimer` starts ~270 later.
## D20. Running an RCP task inline on the scheduler thread delays retrace delivery (D481)
The N64 scheduler (`src/sched.c` `__scMain`) both starts RSP/RDP tasks and forwards each VI retrace to the game. On hardware the task runs on separate processors, so the scheduler is never busy. If the port executes the task synchronously inside `osSpTaskStartGo`, any retrace that arrives during it reaches the game late, and `boss.c`'s half-frame tick gate then skips the following retrace (33 ms frames). Tell: frame rate stuck below 60 at every graphics setting, moderate render and GPU times, and the lost frames failing the tick check (`skip_tick`), not the pending-gfx check. It only shows when the task takes a large fraction of a frame (slow hardware), so a fast dev box never sees it. Run hardware-side work off the scheduler thread and signal completion with the same events the hardware would.
## D21. `gDPLoadBlock` with dxt = 0 depends on the RDP's odd-row TMEM read swap; static assets need it emulated, the tex.c pipeline must not get it (D75)
The RDP swaps the two 32-bit halves of every 8-byte group on odd TMEM rows when *reading*, and compensates on *load* only when LoadBlock's dxt counter advances. A `gsDPLoadBlock(..., dxt = 0)` of a multi-row image therefore samples as "memory with odd rows pair-swapped". Rare's compiled-in assets that load this way (the Rareware logo's RAREWARE text, `DL_RAREWARETEXT`) are pre-swapped in the data and look scrambled ("interleaved/combed", identical at every texture-filter setting) if fast3d uploads them linearly. `tex.c` uses dxt = 0 for the whole runtime texture pipeline too, but there `texSwapAltRowBytes` was the matching pre-swap and D159 no-ops it, so those images are already linear. fast3d (`import_texture`) therefore swaps only dxt = 0 loads whose source is a compiled-in C array (`gfx_tex_source_is_c_array`). Generalisable tells: a texture that looks identically garbled at filter 0/1/2, interlaced along ROWS with a 2-texel horizontal shift, drawn from `assets/*.c`; decode the C array offline both ways before suspecting filtering/clamp. Counting how often a new texture-path branch fires on a level boot (a temporary env-gated log) caught a ~300-per-level over-reach before it shipped.
## D22. Per-frame counters: gate them with `portN64FrameStep()` (D486)
Decomp code that advances a counter once per *call* (per rendered frame) instead of by `g_ClockTimer` runs 2-3x faster in real time at the port's 1 tick/frame (and every frame when uncapped, `clk == 0`). D486 found three on the explosion/turret path: autogun shot counter (`unkAC++`), explosion part spawning, explosion/smoke shake decay. `port/include/porttick.h` `portN64FrameStep()` is the shared Rule-2 gate: true on every call at >= 2 ticks/frame (N64-identical), every second tick below, so the counter keeps the N64's 2-ticks-per-frame pace (the D427/D451 reference). If the gated counter also scales a per-event quantity by `g_GlobalTimerDelta` (autogun damage per shot), charge the full 2-tick frame on gated calls or the rate halves. Verify by A/B at `Video.FpsCap` 60 vs 30: the two must match. Any such gate is game logic and needs Rule-2 sign-off.
## D23. Region bring-up: tooling and port code silently assume NTSC-U (D258)
PAL/JP had never been built. What broke was all port-side: (1) asset emitters hard-coded `scripts/filelist.u.csv` (so on a PAL/JP ROM they would use US offsets with no error); (2) `romdata.c` treated "not PAL" as NTSC-U (rejecting a JP ROM's country byte); (3) port code referenced globals that exist only under a region-specific define (`LEFTOVERDEBUG` is US/JP-only), which surfaces as a PAL-only link error; (4) the decomp's PAL/JP filelists were incomplete and mislabelled. With a real ROM, prove every filelist row by bytes (same-name US file comparison, constant-shift search, header-walk for variable files) instead of trusting labels. Whenever port code touches a symbol inside a `#if` region block, check all three `REGION_DEFS` sets.
## D24. Decomp out-of-bounds writes that the N64 stack layout absorbed (D490)
Some decomp functions write past a local array in edge cases. On the N64, IDO's stack layout put a padding local (often a `pad[]` the decomp keeps for matching) right after the array, so the overflow was harmless. GCC lays the stack out differently, so the same write corrupts a live neighbour. D490: `sub_GAME_7F03ECC0` writes up to 6 entries into `s32 rem[4]` when an object's bbox is flat, overwriting `pts[0]` and giving every glass pane a phantom collision vertex (guards circled pads they could not reach). Tells: a bug that only appears for degenerate inputs (flat or zero-size boxes, coincident points); a value that is exactly 0 or a tiny denormal where a real number belongs; a `pad[]` local next to a fixed-size array in the decomp. Fix pattern: enlarge the array under `#ifdef AVOID_UB`, keep the original under `#else`, change no reads, so the result matches the N64's uncorrupted path.
Also the read side: D496 (`generate_language_specific_text_for_weapon` strcat'd onto an uninitialised caller stack buffer for 3+ players; init under `AVOID_UB`).
## D25. Cross-platform goldens: a fixed frame stem is only comparable at settled gameplay (D521)
`GE_PCDUMP`/`GE_QUITFRAME` step the port's *sim* frame counter (`port/src/video.c:1406` `++frames`, one per `portRenderGfxTask()` at `port/src/libultra.c:1256`), and retrace messages are dropped at the posting site when the game's queue is nearly full (`port/src/libultra.c:753`), so a slow box stretches wall time without advancing `frames` -- frame stems are comparable, wall time is not. What is NOT comparable is any stem inside the intro flyby, whose settle point is wall-clock paced (D117): on a ~54 fps Linux box the flyby was still running at frame 900 (settles ~830-1130 run to run) while the Windows golden at 900 was settled, which reads as a 92.9% "different scene" delta if reported bare. Tells: over-tol2 % >30% at one stem and 1-7% at the next; phash Hamming in the 80s; a dense dump whose consecutive diffs sit at 80-95% and drop to 1-5% once settled. Never compare across platforms with `GE_DETERM=1` (call-sequenced retraces: stable run to run, different frame -> sim mapping). Before calling a residual band a renderer bug, classify it: a constant ~20 delta confined to a screen band (Archives' top 3 grid rows, all three stems) is systematic and needs a same-sim-state capture pair; a uniform d 3-6 over the whole frame with cell means <= 11 (Cuba) is the dither/quantization noise floor. **RESOLVED 2026-10-05 (D525):** both residual bands were *save state*, not renderer — the pair that closed them was a re-round of the whole linux set with `data/ge007.eep` pinned present (Archives 19.956-24.554% -> 3.590-3.859%, worst cell 50.8 -> 2.31; Streets 1200's 16.0-148.4 cell-mean band -> 3.616-3.983%, worst cell 4.43). The diagnostic tell that separates the two: re-capture one side with the save present and the band collapses to the noise floor, while a renderer defect survives identical sim state.
## D26. Never bound a host address with an absolute number (D573)
A plausibility guard written as `p < 0x10000 || p >= 0x400000000ULL` encodes today's layout (DRAM at `0x7000_0000`, cart at `0x1000_0000`). It silently rejects every real pointer once the arena moves (macOS `PORT_ADDR_BASE`, the D441 above-4-GiB tier), and it accepts any unmapped address below the bound. Test the actual windows with `portAddrIsMapped()` (`portaddr.h`: DRAM arena views + mapped cart image). Tells: a feature that goes completely silent or blank (all SFX, all text) on one platform or arena flavour while the rest of the game runs; a guard comment that names the windows next to a literal that does not.
## D27. Rendering one display list twice: per-frame renderer state must not advance per pass (D578)
Frame interpolation (D578) draws each game DL several times. Everything a fresh frame starts from has to be the same for every pass: emulated RSP/RDP state, segment table, viewport caches, and the GL backend's noise seed (`frame_count` in `gfx_opengl.cpp`, which advances in `start_frame` and seeds dither noise; letting it advance per pass changed thousands of pixels on early-level frames). Do NOT restore caches that mirror live GL state (`rendering_state`). Tells: a pass-0 vs pass-N diff (`GE_INTERP_SELFCHECK`) that is large on a few frames (noise, dithered alpha) or 1-4 px on rare frames (a DL relying on the texture left bound by the previous frame). Also: never re-read a game DL after posting SP/DP done; draw every pass before it.
## D29. A "default depends on whether the user chose" needs the load-time `seen` flag, not the value (D578)
`configLoad` writes the registered defaults to a missing ini *before* subsystems init, so by `videoInit` the file already contains every key and its value cannot tell "fresh" from "chosen". Use `configIntKeyWasLoaded(key)` (the per-option `seen` flag from the last load) for fresh-install-only defaults (gamescope VSync off), and re-`configSave()` if the override must persist. Likewise a new sentinel for an existing numeric key (FpsCap Auto = -1) must not reuse 0 when old inis already give 0 a meaning, and the registered min must be widened or the load clamps it away.
## E. Process / method notes
- **Never `exit()` while another thread may be inside the GL driver (D344).**
The GL context lives on the render (scheduler) thread; quit events arrive on
other threads. Request the quit (`videoRequestQuit`), let the render thread
`glFinish` + unbind at a frame boundary and park, then exit from the host.
An unclean GL teardown bugchecked the maintainer's PC (0x119, HAGS on).
Harness runs should end with `GE_QUITFRAME=<n>`, not a `timeout` kill.
- **Headless front-end scripts: turn the mouse off (D343).** In menus the
pointer follows the real OS mouse (WI-2 absolute tracking), so the desktop
mouse moves the in-game cursor mid-`GE_INPUTSCRIPT`. Put
`[Input] MouseEnabled = 0` in the pinned test ini. Script "frames" were
about one per rendered frame on the file select screen; calibrate with a
log, not by assumption.
- **Per-slot headless scripts: `GE_INPUTSCRIPT_P1..P3` drive seats 2-4.**
Same entry syntax as `GE_INPUTSCRIPT` (slot 0); "frame" counts that
controller's reads. Mouse tokens and CHOLD/CREL are ignored on slots 1-3
(one warning per script). Recipe for a scripted 4P split-screen run:
`GE_STARTMP=4,<stage> GE_MPVIRT=3 GE_INPUTSCRIPT_P1=... GE_INPUTSCRIPT_P2=... GE_INPUTSCRIPT_P3=... GE_QUITFRAME=...`.
- **`textMeasure` height is 0 without a trailing newline (D343).** It only
counts completed lines. Measure `"text\n"` when you need a height (front.c's
folder text does this); the width is unaffected.
- **Periodic audio glitches: measure the host audio device before touching
the pipeline (D322).** A clock-regular overflow/garble pattern (~5 min period,
~45 s bursts, independent of level and load) looked like a port timing beat
and cost several game runs to chase. `tools_pc/sdl_drain_monitor.c` opens SDL
exactly like `port/src/audio.c` with no game and no window; on the
maintainer's Focusrite USB interface it showed the same ~312 s drain dips
(to 88 %) outright, and running it on several SDL backends at once showed the
culprit was SDL2's WASAPI path, not the device (DirectSound was clean; the
port now prefers it on Windows). Run it first on any "audio gets garbled"
report, and compare backends with `SDL_AUDIODRIVER` before blaming the hardware.
- Investigation loop is: reproduce → env-gated capped probe → root-cause
→ narrow `#ifdef PORT` fix → visual verify (`GE_PCDUMP` +
`tools_pc/pixcount.py` vs `docs/reference/n64-footage-*`).
@@ -1169,6 +1532,43 @@ produce the same visual symptom but need opposite fixes.
writing anything new; same Rare engine family.
- Don't re-investigate a closed §F finding or re-derive a format spec
that already has a converter.
- **When re-implementing a ROM bitstream/codec offline, PORT the derivation
from the decomp; never reconstruct it from the comment.** A half-right
bitstream parser in this codebase does not fail loudly — it returns
confident, plausible, wrong output. Three hits in four D236 passes:
(1) a texture-header parse that omitted the leading control byte
(`image.c:2568`) still produced sensible-looking formats and dimensions,
and was only caught by counting implausible entries (1020/2699 vs 1/2699
when correct) and noticing the format nibble only ever took values
0/4/8/12 — the tell that a field was straddling a boundary;
(2) omitting the per-image byte-align at the end of each LOD
(`image.c:1096`: discard the partial byte, and skip a whole byte when
already aligned) silently desynchronised every LOD after the first;
(3) `texInflateRle`'s run-length `fudge` was *guessed* from the doc comment
instead of ported from the loop at `image.c:1605`, and still decoded
images — just with the wrong alpha coverage (66.7% vs the correct 43.1%).
Practical rules: copy the arithmetic statement-for-statement even when it
looks like it could be closed-formed; build a cheap global plausibility
counter into the tool and print it every run; and cross-check one decoded
artifact against an independent expectation (here, that a LOD chain's
dimensions halve exactly and its alpha coverage stays roughly constant)
before trusting any number the tool emits.
- **A negative result from a diagnostic is worthless until the diagnostic has
been shown capable of producing the positive.** Two independent hits: D219's
"verify diagnostic caps exempt the real target signature before trusting
'clean'", and D236 pass 21, where a static setup scanner located GE's `PROP`
enum with `startswith("typedef enum PROP")` — which matches
`typedef enum PROPFLAG` hundreds of lines earlier, yielding an enum with **no
`PROP_*` members at all**. Every prop id then resolved to `?` and every
"foliage props placed" count came back 0, which is exactly what a clean,
meaningful negative looks like. Build the positive control **into** the probe:
have it print what it is capable of matching (`foliage props recognised (9):
…`) and fail loudly when that set is empty, so the failure mode cannot be
read as a finding. Applies equally to grep-based audits over generated C,
where near-miss identifier prefixes (`PROP` / `PROPFLAG` / `PROPDEF_TYPE`) are
the norm.
- **`docs/dev/findings-index.csv` is generator-derived** (`tools_pc/gen_findings_index.py`): the status cell is normalised from the `## Dxx` entry's §F summary-table row in `findings.md`, so hand-editing the CSV is silently reverted on the next regen (hit live in the D219/D252 M-158 session). Update the table row, then run the generator.
- **Every game launch rewrites `data/ge007.eep`** (the eeprom shim stores on
every write; validation resets, cheat patches and normal saves all write).
@@ -1493,3 +1893,130 @@ produce the same visual symptom but need opposite fixes.
`propobj.c:~2393`) — those 4 rely on div-by-zero yielding NaN/inf without
trapping on both MIPS and x86-64 SSE, matching retail behavior; do not
"fix" them.
- **x64 Windows backtraces: unwind with the PE's .pdata, never walk RBP.**
`-fno-omit-frame-pointer` on MinGW-w64 GCC does not give an RBP chain:
most prologues set `RBP = RSP + N` (Win64 allows an offset frame
pointer), so `[RBP]` is not the saved RBP and a manual walk dies at
frame 1. `RtlLookupFunctionEntry` + `RtlVirtualUnwind` from the
exception CONTEXT is reliable and needs no dbghelp/symbols (crash.c,
2026-09-30). Print `va = preferred ImageBase + RVA` per frame so
`addr2line -e ge007.x86_64.exe` resolves it even if the image was
relocated. Test with `GE_CRASHTEST=<N>`.
- **Crossing msys -> native: pass files, not quoted strings; get paths from
bash's own `pwd -W`.** The msys argv conversion re-escapes embedded `"`
as `\"`, which `cmd.exe` does not understand, so an inline
`cmd //c "... \"C:\path\" ..."` silently never runs (build-pc.sh's TMP
probe was dead this way). And with Git Bash plus MSYS2 `usr/bin` on one
PATH, `/tmp`, `mktemp` and `cygpath` can belong to different runtimes
(different Windows dirs). Never trust `command -v cmake` for the MSYS2
toolchain either: a pip-installed cmake can shadow it (2026-09-30).
### Poll-thread input must never re-point `g_CurrentPlayer` (D419)
`inputComputePad` runs on the scheduler thread (`joyPoll`), a real pthread, not the game thread. Any save/swap/restore of `g_CurrentPlayer`/`player_num`/`g_playerPerm` from there races the game tick's own `set_cur_player`: the restore can leave the game thread on the wrong player and it then spins in list walks that compare against `g_CurrentPlayer->...` (seen: `bondinvCycleForward` infinite loop after a split-screen respawn). Resolve the owning player explicitly (`g_playerPointers[slot]`) and touch its fields directly; for game functions that take no player argument, present a native button bit instead of calling them.
**A1 cross-tag (D492, 2026-10-03): serialized multi-byte fields are host-endian in this port.** Structs copied raw to a file (the EEPROM save via `geEepromRW`) store s32/u16 fields little-endian, while N64 images are big-endian. A raw-byte CRC over the region gives the same value on both, which hides the difference until the stored checksum words themselves are compared. Tell: data that is intact but fails validation, with `calc_crc` equal to the big-endian reading of the stored word. Any field inside the CRC range must be swapped *before* recomputing the checksum.
**A1 cross-tag (D455, 2026-09-30):** an exe-static address (array/global/function in `.data`/`.rodata`/`.text`) held in a `u32`/`s32` truncates on Windows (image base `0x140000000`; Linux 0x20000000 hides it). Binary tell: `mov r64,[rip+.refptr.X]` ... `mov r32,r32` before the pointer is used or passed. The address load may precede a `jmp` join, so an adjacent-instruction scan misses it; follow jumps. A census of the whole exe found only the D454 site; `_*Segment*` linker symbols are ROM constants (`.set` in `romassets_u.s`), not exe addresses, and are benign in `(u32)` casts.
**`GE_MEMPREDZONE=1` (D464): catch allocator overruns deterministically.** Set it before any run to pad every `mempAlloc*` block with a 64-byte pattern trailer and check at level unload, every 60 frames and on orderly quit. A hit prints `MEMPREDZONE: HIT ... caller=0x<addr>`; resolve with `addr2line -f -C -i -e build-pc/ge007.x86_64.exe 0x<addr>` to the allocation site, then compare its size expression with `sizeof` of how the buffer is indexed (an N64 literal stride vs a widened struct is the D461/D462 class). Zero hits is only "no overrun reached the trailer"; whole-bank allocations are unpadded until shrunk. Unset = one cached branch per allocation.
**Data-driven input mapping: factor the pure core, keep the old code as a test oracle (D469).** To refactor a hard-wired input block into a table without changing default behaviour, move the decision into a pure function over (raw source bits, bindings, previous-state, incoming mask) and keep a verbatim transcription of the old block next to it, used only by an env-gated boot self-test (`GE_PADMAPTEST`) that compares them exhaustively over the (small) input space. 2^15 source combinations x prev x ctx x incoming masks is ~21M cheap calls, ~1 s, and gives a byte-identity proof no live pad is needed for. Watch the side inputs the old code mutated implicitly (here the incoming `button` mask: the southpaw swap acted on keyboard fire too, so the pure function takes it as a parameter). Table edits arrive from other threads: the poll thread must rebuild into a spare buffer keyed by a cheap config signature and publish by atomic pointer, never parse (no `strtok`) or re-point shared state in place.
**Audio device/gain changes belong on the producer thread (D470).** The SDL audio path is a queue (`SDL_QueueAudio`), fed and sized only from `audioSetNextBuffer`/`audioGetAiLengthBytes`. To change the device live, have UI/event threads only set atomics and do the close/reopen at the top of `audioSetNextBuffer`; never close `dev` from the UI thread. Apply master gain to a scratch copy (never the game's buffer) and pass the original pointer through at 100 so the default stays byte-identical. `SDL_AUDIODEVICEREMOVED.which` is the opened device id for output devices.
**Overlay/menu input must name its owner pad; never hard-code seat 0 (D472).** When several pads can open a shared UI, keep a per-pad edge array (one shared `prev` flag masks edges and double-toggles), record the opener, and resolve the effective owner (opener -> lowest attached -> 0) at read time so a detach cannot strand the UI. Port-layer 2D that must pillarbox under native widescreen: wrap it in `PORT_HUD_ASPECT(CENTER)` and map the mouse through `vis = (4/3)/portNativeAspect()`.
### The options row table has two UIs; new row kinds need a front-end answer (menu PD alignment)
`port/src/optionsoverlay.c`'s `rows[]` is consumed by the F10 overlay and by `frontoptions.c` through the `optionsRow*` accessors. A row kind that only one UI understands (separator, Back, hub-only actions) must be hidden from the other in `optionsRowIsShown()`, and should be a `ROW_ACTION` with a key prefix so the section-reset and reset-probe loops (which skip `ROW_ACTION`) stay correct. Moving a row between sections silently changes what that section's "Reset to defaults" resets: re-run `GE_WSPROBE_RESET` after any regroup.
### Never put a blocking vsync swap ahead of the draw the game is waiting on (D578)
A render worker that must post "task done" after drawing must not swap (a blocking VSync present) before the draw: under a compositor (gamescope) each swap blocks a full refresh and the game misses retraces while CPU/GPU idle. Draw, post done, then present; derive interpolation alphas from predicted vblanks (swap-return times), not a free-running timer.
## D28. A "fall back to exact" gate on a per-tick rate needs a graded response, not a cliff (D578)
A fallback that turns interpolation off for the whole frame above a low threshold (1.5 deg/tick view turn) fired on ordinary stick turns (9-64% of frames on a 90 Hz Deck). Prefer pulling the blend alphas toward 1 in proportion to threshold/measured (bounded offset, still smooth) and reserve full fallback for genuine cuts. Also: the display list references live game memory, so deferring a draw past SP/DP done is unsafe without copying every referenced buffer.
A second, unscaled cliff hid behind the first: the modelview 6 deg/tick gate set "exact" directly, so softening the projection check changed nothing. When softening a fallback, grep every writer of the fallback flag. Also: a per-pass cost estimate must divide by passes actually drawn (discarded restarts included) and clamp single outliers, or one exact restart collapses the pass budget (D578).
## D30. Detect "the culled set changed" from a stable per-item id in the DL, not from matrix pairing (D578)
When a game builds its visible set and per-item clip rects for the current camera only, an interpolated pass cannot repair a changed set. The cheapest exact detector is a per-item marker already in the DL (GE: `gSPSegment(14, room vertices)` before each room's `gSPDisplayList`; the base is unique per room). Record the sorted unique ids per game frame and compare prev -> cur; do not infer it from matrix-pairing counts (rooms share matrices). Clip rects derived from a recursive portal descent are path-dependent intersections, so they cannot be recomputed per portal in isolation.
## D31. A render worker must not read live game state; snapshot it on the game thread at task submit (D578)
With the D481 render worker the game thread is already building frame N+1 (running `bgDetermineVisibleRooms`, resetting per-frame arrays, replacing matrices) while frame N is being drawn. Any port-side code that re-derives something from game globals at draw time (here: replaying the portal traversal) sees a half-updated mix: counts and arrays disagree mid-compare, matrices point at the next frame's allocation. Capture what the replay needs on the game thread at the one synchronous point per frame (`osSendMesg(sched_cmdQ, gfx task)` in `rspGfxTaskStart`; recognise it by `msg == g_gfxTaskSettingsList`), key it by the display-list pointer, and read only the snapshot (plus truly static level data) on the render side. A probe that compares live game output with a replay will report "extra/missing" noise that is really this race.
## D32. A replay of game code at an interpolated camera needs the camera in the game's own matrix convention (D578)
GE folds the camera lookat into the room projection (`field_10E0 = lookat * persp`) while `field_10CC` is the world-to-view matrix bg.c culls with, in a different (unscaled, un-offset) position space. To get the second at a blended camera from the first: `L = P * persp^-1` (row vectors; `matrix_4x4_multiply(a,b,r)` means `r = b*a`), take rotation and camera position from `L`, convert the position delta back with the world scale (`D_800364CC`), rebuild `field_10CC`. Verify the derivation on exact frames first (derived rotation == `field_10CC` rotation, `(cam - model_pos) * scale == derived position`): both held to 1e-5 / 3e-3, which also proves the multiply-order and layout assumptions before any blended frame is trusted. Accept a projection pair as "the room projection" only when that rotation check passes, and cache the replay per pass: pure-perspective and HUD loads between room draws must not flip a room's scissor between two code paths.
## D33. "Enable depth clamp" is not a fix until you find every far-plane reject, and read the matrix layout before editing a column (D578)
A GL-side `GL_DEPTH_CLAMP` is moot when the software RSP already drops whole triangles on the CPU (`z > w` CLIP_FAR trivial reject): audit CPU rejects before blaming or relying on the GL clip stage, and check whether the clamp is already globally on. Separately, when patching a perspective matrix that GE pre-multiplies by a lookat (`lookat * persp`, row vectors), the z column is `A*L[i][2]` for the basis rows and `A*T.z + B` for the translation row (T is nonzero: the lookat is built from camera-minus-model-origin). Reading row 3 as `k*L[2]` is a column-vector habit; the result is a heading-dependent tilt, not a clean plane shift.
## D34. Diagnose "geometry vanishes" by counting outcomes and capturing the frame, not by switching features off one at a time (D578)
Eight A/B switches on a visual dropout (far clip, depth, cull, scissor, projection lerp ...) all failed to move it; two cheap instruments did what they could not. Per-room triangle fate counters (seen / rejected by plane / culled / eye-crossing / emitted), compared between the suspect pass and the reference pass of the SAME frame, showed the CPU side was equal and eliminated a whole class at once. Two traps: (1) log both directions (or an aggregate), never only the losses, or selection bias manufactures a "systematic loss"; (2) an automatic capture trigger must be insensitive to legitimate change. An interpolated image can only contain what the previous and current exact frames contain, so "differs from BOTH neighbours" isolates real glitches, whereas "differs from the exact frame" fires on every camera turn and burns the capture cap.
## D35. A CPU-side clip must clip where the GPU would, and the GPU runs with depth clamp (D579)
`GL_DEPTH_CLAMP` is on globally (`gfx_opengl.cpp`), so GL never clips at the near plane: geometry between the eye and the near plane is drawn, as on the N64. The D543 CPU clip, added to stop attribute extrapolation across the camera plane, cut at `z = -w` and so deleted exactly that geometry, and a wall the player hugs vanished (#150, a v0.5.0 regression). The cut belongs at the camera plane (`w = eps`), with fog for clip-created vertices evaluated at `max(z/w, -1)` so the near-plane haze is unchanged. Before adding any CPU reject or clip, check which GL clip stages are actually live; see also D33.
## D36. Under VRR the swap is not a clock: pace presents from one continuous grid (D583)
With G-Sync/FreeSync a blocking swap returns when *we* present, so swap-return gaps measure our own cadence; any scheduler that derives its period or phase from them (or rebuilds its slot grid per game tick from the last swap) feeds back and wobbles, and a VRR OLED shows every frame-time change as a brightness change. Present from one continuous grid at the display period, snapped to exactly tick/N when within ~1% (no beat between the game clock and the display clock), re-anchored only on a real stall; never drop a queued present when a new game frame arrives (ring), base the ready time on the measured whole-frame draw (capped, so load frames are not learned), and avoid letting a queued present fall due during the next frame's draw. Measure cadence with a gap histogram, not presents/s alone.
## D37. Interpolation gates: never normalise a tiny measurement by (1 - alpha) near 1, and measure before any widening (D583)
A per-tick rate estimated as offset / (1 - alpha) amplifies anything that is not proportional to (1 - alpha): float rounding in an acos (~0.05 deg) and a fixed offset added by a later step (the far-clip widening read as 0.16 deg) became 1.5-3 deg/tick on passes with alpha just under 1, and every such frame was redrawn from scratch for a change nobody could see. Measure on the value before any post-processing, and only where the denominator is large or the measurement well above its noise. Count what each gate costs (redraws per 10 s), not just how often it fires.
## D38. Pair cameras by nearest, not one-to-one; and an exact fallback should draw once (D583)
GE loads the same camera projection once per room/object (~60 per pass). Pairing each load one-to-one with the previous frame's loads runs out of partners whenever the count changes (a set of rooms entering or leaving view) and pairs the leftover with another kind of projection: one object is drawn at the wrong camera or depth on an in-between present and appears to flicker in step with the room change. Pair a camera with the nearest previous camera even if already claimed, and give identical source matrices an identical result within a pass (memo). Separately: when a frame falls back to exact, draw the exact image once and copy it to the remaining present slots; redrawing n identical passes is pure cost.
## D39. Behind-camera triangles still need a facing test: use det[x y w], not "skip" (D584)
Dropping the cull for triangles with a vertex at w < 0 (because x/w is undefined there) draws both faces of two-faced translucent walls (Cradle catwalks) and reads as flicker at every frame rate. The homogeneous determinant det[x y w] is the correct facing test for any w: for all w > 0 its sign equals the NDC cross, and for mixed w it equals the classic XOR behind-eye flip, with no division. Also: when bisecting on a test box, use builds made from a known commit; a backup's file name is not its provenance (D584 lost one round to a mislabelled binary).
## D40. Deck draw cost: count before optimising, and never redraw past the budget (D583)
On the Deck (Game Mode: stderr is dropped, so instruments must go to ge007.log via sysLogPrintf), a heavy pass is ~6 us per batch, and batches track texture changes about 1:1. A counter at the top of `import_texture` counts lookups, not uploads; split hits, misses and time before concluding anything. "Skip the rebind when the same texture is re-issued" looked like a win on paper and changed nothing: consecutive batches really use different textures. For interpolation, any decision that redraws a whole pass (softened restart) must check the measured pass time against the refresh budget first: one slightly-off in-between image is invisible, a missed present is not.
## D41. A long frame time on one render thread is not draw cost until you line presents up against draws (D583)
Before optimising a "slow" interpolated frame, check what its time contains: with one worker that both draws and presents, a frame's measured draw includes nothing but work, yet the presents queued behind it go out late by up to the whole draw, and the symptom (uneven 8/14 ms gap pairs at full present rate) looks exactly like an over-budget pass. Profile on-CPU time (`perf` works on SteamOS, `perf_event_paranoid` 2 is enough for your own process) and rule out the GPU clock (QAM Manual GPU Clock) before touching batching; then log each draw's start/end next to each present's due/late time (`GE_INTERPLOG=<n>`). On the Deck in Game Mode, gamescope only shows windows Steam launched: a harness started over ssh runs hidden and throttled, so launch through a Steam shortcut (`tools_pc/deckrunner.sh`).
## D42.. Combiner alpha only reaches the framebuffer through the blender: no FORCE_BL, no blend (D414)
fast3d's `use_alpha` (blender inputs CLR_MEM/1MA) is a heuristic for "this is an alpha-blended draw". On the RDP the blender only acts on interior pixels when `FORCE_BL` is set (or via coverage/`CVG_X_ALPHA`); a render mode with XLU-style inputs but no `FORCE_BL` is opaque regardless of combiner alpha. Vanilla GE never noticed because its combiner alpha there is `SHADE` alpha (255). A mod's DL with cycle-2 alpha `(0,0,0,0)` drew fully transparent = black. GL blend is now gated on `FORCE_BL | CVG_X_ALPHA`.
**Tell:** rasterized with correct depth, immune to vertex-shade boosting, correct raw texels, black output. **Bisect fast:** output raw `texVal0/texVal1` in the fragment shader (bypasses combiner + blend); if that is right, the fault is combiner or blend state, not textures.
## D43. A capture taken through the feature under test is not ground truth (D628)
`GE_TEXDUMP` dumps what the GL upload receives, which is the *texture pack's*
image whenever a pack supplies that texnum - so dumps captured with
`Video.TexturePack` set cannot validate a pack join: the hold-out compares the
pack PNG with a dump of itself and scores a perfect 1.000. 52 of 346 dumps in
one such set were pack-substituted, and the false "pack and ROM textures are
the same size" premise read off them then gated the matcher down to nothing
(real packs are 4x-128x upscales). Two habits avoid the whole class: capture
ground truth with the feature OFF (and record in the dump dir which switches
were set), and before trusting a precision number, check that the hold-out's
dims/format still look like the *source* data rather than the replacement.
A 100 % score on a derived metric is a circularity smell, not a result.
**"Faithful" input mode = emit native bits and add nothing (D643).** A port that routes pad buttons to PC *actions* (dedicated use/reload calls, crouch, gadget cycle) cannot also claim to be the N64 pad: the game's own handlers (e.g. B = use-else-reload) only run on the native bit, so a button bound to two actions double-fires, and a fixed menu lockout reinterprets faces. For a strict mode, give the published bind table a native-button layer, zero every port-action mask in it (so side paths that read the action bitset, like a dedicated use/reload hook, go inert without being touched), and make the pure mapper `orig`-flagged and context-free; prove it with the same boot self-test (per-source table across stage/menu/tank). Also stop role-swapping sticks to match the game's selected style: a virtual pad keeps left = analog, right = C.
## D44. RDP IA4 is I3:A1; a decode tweak justified by "hardware layout" needs a texel dump (D644)
IA4 = 3-bit intensity + 1-bit alpha (nibble = I<<1 | A). An I2:A2 decode (D266) turned the A=0 nibbles 2/4/6 into semi-opaque black and drew the file-select crest on a dark square. When a texture's transparent texels render opaque, histogram the raw nibbles/bytes (`GE_TEXRAW` + `GE_D526` for the draw's fmt/oml/comb) before touching blender or alpha-compare code: the importer's alpha expansion is the cheapest suspect.
## D45. Interpolating GE: rooms carry the camera in the projection, props carry it in the modelview (D954)
GE loads two projections per view: `field_10E0` = perspective * lookat for rooms (modelviews are scale*translation), and the plain perspective `projmatrix` for chrs, doors, props, glass, explosions and the gun (camera baked into each modelview; LOD reads `-m[3][2]` as view depth). An interpolator that blends only the projection moves the rooms and leaves every prop at the exact camera, which reads as props flickering against the world at high frame rates only. Give the plain perspective the room camera blend (`P * R^-1 * Rb`, row vectors, `R = L * P`), and stop before anything camera-attached (the gun inherits the loaded projection; its `gSPPerspNormalize` marks the boundary). Check the matrix order at runtime (`R * P^-1` affine) instead of assuming it.
## D46. GE models are matrix-skinned; every view matrix is relative to the current room's origin (D955)
Chrs and objects load per-joint matrices from `model->render_pos` through segment 3 (`gSPSegment(3, render_pos)` in `subdraw`); the vertices are the model file's shared static arrays, so per-object identity lives in the matrix block, not in vertex buffers (interpolate matrices, keyed by (Model*, joint); never by draw ordinal, which shifts with LOD, weapons, hats and decals). Those matrices have the camera baked in, and every view matrix (rooms, props, the lookat in `field_10E0`) is relative to `current_model_pos`, the current ROOM's origin, which jumps at a room change: anything that compares matrices across ticks must rebase by the camera delta and treat a room change as a discontinuity.
Loaded 100 of 512 files, more files were not shown because too many files have changed in this diff. Show more