diff --git a/.github/release-notes.md b/.github/release-notes.md index c9f087d7..14b1c015 100644 --- a/.github/release-notes.md +++ b/.github/release-notes.md @@ -1,217 +1,329 @@ -## GoldenEye 007 PC Port +# GoldenEye 007 PC Port — v0.4.0

- ~12 s gameplay montage from live v0.3.0 play sessions (no audio track) + ~24 s gameplay montage from live v0.4.0 play sessions, opening on the Runway tank (no audio track)

-> 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 20 missions (Agent difficulty). It's an early public cut: all 20 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. +Platforms: **Windows x86-64** and **Linux x86-64 (including Steam Deck)**. +Region: **NTSC-U (US) only** — see [Things to know](#things-to-know). -### What's new since v0.2.2 +v0.4.0 is a large feature and fidelity release. The full campaign runs at a +steady 60 fps with music and SFX throughout, and has been playtested end to +end (all 20 missions, Agent difficulty) on Windows and on Steam Deck with a +physical controller. This is a pre-1.0 release: the known issues below are +real, and some areas (multiplayer, PAL/JP, HD assets) are still ahead of us. -- **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. +--- -### Known issues +## New features -- 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. +### Native widescreen *(on by default)* -### Downloads +The world now renders natively at your display's aspect ratio instead of +stretched 4:3: geometry is undistorted (wider view, same vertical FOV), the +in-level HUD keeps its shape and anchors to the screen edges, and the 4:3 +front-end menus and ending-credits sequence are pillarboxed. Turn it off +with the **`Native widescreen`** toggle (F10 → Display) to restore the old +stretched frame. (D334/D335) + +### A true, stable 60 fps + +v0.3.0 could settle at 30 fps game-wide on some setups. The cause was a +pointer-width bug in the game's frame scheduler: a "present every frame" +flag was read back at a hardcoded byte offset that the 32→64-bit transition +silently moved, so the render path ran on the audio client's every-other- +frame cadence. Fixed at the root; the game now holds a rock-stable 60 fps +(measured with the in-game FPS readout) on both platforms. (D248) + +### A complete aim system for mouse *and* controller + +- **`Aim style`** — **N64** (default): the crosshair deflects and the + camera follows once it reaches the screen edge. **Centred (PC)**: the + crosshair is fixed at screen center and your mouse — or your right stick — + moves the camera directly. In v0.3.0 the centred mode only worked with a + mouse; it now works with a controller too, using the N64 original's own + stick-response curve so full stick matches the original's top turn rate. + (D332/D333/D337/D404) +- **`Aim range`** (shown while Centred is selected) — **PC** (default, + full-screen feel) or **N64** (the original 65% stick limits). (D338) +- **Reticule jitter in aim mode is gone** — mouse aim now runs on the + Perfect Dark PC port's aim model. (D332) +- Per-device sensitivity: `Mouse horizontal/vertical sensitivity`, and for + controllers `X axis / Y axis look sensitivity (controller)` plus + `Look smoothing (controller)`. + +### Rebuilt options (F10 overlay + front-end PC Options) + +- Both options screens are reorganized into functional sections — **Input, + Gameplay, HUD, Graphics, Audio, Display** — each with its own *Reset to + defaults*; per-profile settings scoping; and the old "save file" wording + is now **profile**. (D353–D356) +- The **F10 in-game overlay** was refreshed GE-style: category headers, + full controller support (value-adjust with hold-to-repeat), fixed pointer/ + keyboard navigation, and new rows: **Show FPS**, **Skip intro + (experimental)**, pad + **Invert look / Southpaw / Deadzone / Trigger threshold**. (D237/D345– + D347/D360–D370) +- The front end gained a **PC Options screen** beside the file-select bar, + and save-file **Copy/Erase** moved into the bottom bar. Long sections + (Input) page within the screen -- a "Page 1/2" marker plus a bottom hint + on every page -- so every row stays on screen and stays reachable; the + mouse wheel and W/S also page the list, and the selection clamps at page + edges. (D343/D406/D407) +- Watch-only settings — **Auto-aim**, **Look ahead** and the rest — are now + editable from the PC options and **persist correctly** (this closes the + "auto-aim won't stay on" report, issue #103). (D330/D350/D352/D354) +- **`HUD scale`** (75–150%, default 100%): scales the ammo counter, pickup + text and subtitle bars for smaller or larger displays. (D226) + +### Crosshair customization *(defaults leave the N64 crosshair untouched)* + +**Show crosshair** (on/off), **Crosshair colour** (the original red, +or green / blue / yellow / cyan / magenta / white, or custom RGB), **Crosshair size** +(default 100% of the original drawing), and **Crosshair style** (Original / +Thin cross). (D373/D379/D381/D382) + +### In-game key rebinding + new default layout + +A two-group key editor (**Movement**, **Actions**, each with its own *Reset +to defaults*) lives in both options screens. The new default **Actions** +layout is the GEPD-style preset (e.g. Action/next-weapon on the keys you'd +expect from a modern layout); existing INI bindings are honored and older +ones migrate automatically. (D371/D374/D383/D385) + +### Controller improvements + +- **Sane pad defaults**: **A = use/interact, X = reload, Y = weapon cycle** + (v0.3.0 mapped two buttons to crouch and had no in-game use button). (D393) +- Crouch uses the engine's real crouch input, with a **`Crouch mode`** + toggle (hold / latched). (D375) +- Xbox-variant controller parity, **Southpaw**, and per-stick + **deadzone** + **trigger threshold** rows. (D237/D394) +- **The N64 Rumble Pak now drives real gamepad haptics** (Steam Deck + included), scaled by a global **`Vibration`** slider (default 50%). (D401) +- The F10 overlay is fully gamepad-driven. (D347/D395/D396) + +### `All unlocked` ships as a documented EXPERIMENTAL feature + +F10 → Gameplay → **`All unlocked (EXPERIMENTAL)`** opens every mission, 007 +mode and the full cheat menu. On a fresh install it now yields the game's +own fresh-slot layout instead of corrupting state (D281) — but enabling it +can still bake synthetic cheat/completion data into your save that switching +it off does not undo: **complete a level normally first, and back up +`data/ge007.eep` before enabling it.** (D387) + +### Steam Deck / Linux + +The first clean SteamOS/glibc build, plus a first-launch preset for decks +without a config (native 1280×800, 2× MSAA, vsync, 250% draw/LOD +distances). (D402/D283) + +--- + +## Bug fixes + +### Fidelity (verified against the N64 original / era-correct references) + +- **Water** on the `IsWater` levels (Dam, Frigate, Surface 2, …) matches + the original: the moving seam between two patterns and the pattern + "resetting" as you moved are gone. (D245) +- The **particle "rainbow" effect** (intermittently recoloured sparks and + explosion residue) no longer occurs. (D252) +- **Frigate: "every polygon breaks except the gun"** after turning — and + the rare "Bond briefly out of place" quirk — were the same never-written + fields read by the head-bob animation; both are fixed. (D336/D311) +- Surface 1/2: the tree backdrop that rendered as a solid wall of texture + now renders as proper camera-facing tree cards. (D236) +- Ejected **shell casings** render again. (D331) +- The intro **gun-barrel blood drip** draws correctly. (D341) +- The extra erroneous **long muzzle flash** on some guns is gone. (D303) +- Occasional **z-fighting** fixed; **security-camera props** no longer face + backwards; the front-end **Nintendo logo and copyright page** render + correctly. (D308/D307/D403) +- The **train-intro soldier pose** is correct. (D392) +- **Tanks** (Runway, Streets) can be boarded and exited again — the new + use/reload button split had removed the B-button tap the engine's tank + handlers use; B is presented again in tank states, and use keeps its + no-reload-fallback semantics everywhere else. A short input lockout during + the sit-down animation means a double-tap can't cancel boarding, and in-tank + mouse aim has its own **`Tank aim speed`** setting (`Input.TankAimScale`, + default 100) so the turret matches your on-foot feel. (D407) +- File-select background / gun-barrel comb rendering fixed. (D397) +- Stale-texture artifacts after level transitions fixed. (D235) +- Distant-geometry dropout on the biggest open levels (Streets, Egyptian) + substantially improved at default settings. (D249) +- The **Facility execution-scene watchdog** from v0.3.0 stays enabled as a + safeguard; the animation-pinning that motivated it is root-caused (a tick + granularity divergence) and fixed, so the scene plays out as authored. + (D329/D318) + +### Front end & UI + +- Randomly **flickering F10 menu items** are gone. (D314) +- The watch's **controller-page graphic** renders again. (D290) +- File-select **folders and Bond photos** no longer vanish after backing + out of a file. (D342) +- File-select bottom row (SELECT FILE / Copy / Erase / PC Options) aligned. + (D398–D400) + +### Audio + +- **Gunshot SFX** cadence matches the N64 original's rate, and the + intermittent silence of the PP7/AK47 under sustained fire near a looping + sound is gone. (D240/D241) +- The silenced **PPK "slap"** and the broader real-time mixer corruption + behind it are fixed. (D202) +- Windows audio now prefers the **DirectSound** backend over WASAPI (lower, + more robust latency). (D322) +- In-stage music **re-synchronizes** correctly at stage transitions and + level entry. (D355) +- Long-session audio degradation (issue #87): pool hardening shipped, plus + an optional `GE_D322=1` telemetry probe if it ever recurs. + +### Stability & renderer + +- **Orderly game exit**: quitting no longer lands inside the graphics + driver (a class of Windows driver bugchecks seen during testing). (D344) +- A **non-self-recovering Windows freeze** after the window sat idle and + lost focus is fixed (mouse-mode changes now applied on the window + thread). (D287) +- **MSAA above your driver's maximum no longer black-screens** (the request + is clamped to `GL_MAX_SAMPLES`), and two framebuffer-state bugs in the + render/resolve paths were fixed — community contribution, PR #106, + security-reviewed. (D405) +- Port-wide code audit: missing returns in front-end menus, missing + prototypes, undefined-behaviour shifts. + +--- + +## Options reference + +All settings available in v0.4.0, in both options screens (front-end +**PC Options** and the in-game **F10 overlay**). Defaults in parentheses. + +| Setting | Section | Default | +|---|---|---| +| `Native widescreen` | Display | On | +| `Frame rate cap` | Display | 60 (30 / 60) | +| `Show FPS` | Display | Off | +| `MSAA` | Display | 2× (1/2/4/8, clamped to driver max) | +| `Aim style` | Input | N64 (N64 / Centred (PC)) | +| `Aim range` | Input | PC (shown while Centred) | +| `Mouse horizontal/vertical sensitivity` | Input | calibrated midpoint | +| `Invert look (mouse)` / `Invert look (controller)` / `Southpaw` | Input | Off | +| `X / Y axis look sensitivity (controller)` | Input | 100% (native) | +| `Look smoothing (controller)` | Input | 0 | +| `Tank aim speed` | Input | 100% (`Input.TankAimScale`) | +| `Deadzone (left/right stick)` | Input | 70% | +| `Trigger threshold` | Input | 23% | +| `Vibration` | Input | 50% | +| `Crouch mode` | Input | Hold | +| `Bindings…` (Movement / Actions editors) | Input | GEPD-style preset | +| `Auto-aim` / `Look ahead` | Gameplay | Off (watch-backed) | +| `Skip intro` / `No hit flash` | Gameplay | Off | +| `All unlocked (EXPERIMENTAL)` | Gameplay | Off | +| `HUD scale` | HUD | 100% (75–150%) | +| `Show crosshair` / `Crosshair colour` / `Crosshair size` / `Crosshair style` | HUD | N64-intact (size 100%) | + +--- + +## Things to know + +- **NTSC-U (US) ROMs only.** PAL and JP ROMs are not supported in this + version; asset repair for those regions is on the post-release roadmap. + (D258) +- **The default Actions key layout is new** (GEPD-style). If you preferred + the v0.3.0 defaults, rebind them in *Input → Bindings…* or reset the + group. Existing INI bindings are auto-migrated and still honored. +- **Back up `data/ge007.eep` before using `All unlocked`** (see above). +- Steam Deck: the first launch in Game Mode applies the Deck preset only if + no `ge007.ini` exists yet; F10 changes always win afterwards. + +## Known issues + +- On Facility, if gas leaks during Ourumov's monologue he can pause for up + to ~10 s before resuming the scripted shootout — a latent race that exists + in the N64 original too (where it softlocks permanently); the port detects + and auto-recovers it. (D318) +- With *Native widescreen* on, the F10 options overlay stretches with the + window instead of pillarboxing like the front-end menus (legible; + cosmetic). (D335b) +- The front-end Rareware logo shows a subtle texture-filtering artifact + (the Nintendo logo and legal page are clean). Cosmetic only. (D75) +- **`All unlocked` is experimental** (see above); the fresh-install + corruption case is fixed, but saves made while it is on are not + guaranteed recoverable by switching it off. (D387) +- **`Skip intro` is experimental — not recommended for regular use yet.** + With it on, a failed or aborted mission skips the post-mission failure + report screen and returns straight to the menus (the mission still records + correctly). An older audio quirk on some saves is also still being chased. + (D408/D216) + +## Roadmap since v0.3.0 (research, not yet features) + +- ROM-level **mod support** investigated and found viable (the game's file + table lives in the ROM); HD texture packs and the drop-in-ROM flow are + tracked post-1.0. (D325–D328) +- A measured CPU budget on low-end hardware (issue #92) reshaped the + performance investigation toward the GPU. (D339) +- Controller options wave 2 (presets, pad reassignment UI) and PAL/JP + support are planned post-v0.4.0. + +--- + +## Downloads | File | Platform | |---|---| -| `goldeneye-pc-port--win64.zip` | Windows x86-64 | -| `goldeneye-pc-port--linux-x86_64.tar.gz` | Linux x86-64 (incl. Steam Deck) | +| `goldeneye-pc-port-0.4.0-win64.zip` | Windows x86-64 | +| `goldeneye-pc-port-0.4.0-linux-x86_64.tar.gz` | Linux x86-64 (incl. Steam Deck) | Each contains the engine executable, a README, license texts, and the -`prepare-assets` tool. **No ROM, no game assets.** The Windows bundle carries -its runtime DLLs; the Linux bundle carries SDL2, so on both platforms nothing -needs to be installed first. +`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. -### Running it +## Running it 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. +legally own (NTSC-U only, as noted above). No Python, no toolchain, nothing +to install. 1. Unpack the archive. 2. Make a `data/` folder next to the executable and put the ROM in it, 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.) + (`data/pcmodels-ntsc-final/`, `data/pccg-ntsc-final/`) once, and saves + them for every future run. (The generator is + `prepare-assets/ge007-convert` inside the bundle; you can also run it + manually; it prints what it's doing.) -**Steam Deck:** sideload the unpacked folder (USB or a file manager), do steps -2–3, then add the executable to Games → *Add Game* as a non-Steam game. +**Steam Deck:** sideload the unpacked folder (SFTP, USB or a file manager), do +steps 2–3, then add the executable to Games → *Add Game* as a non-Steam +game. The first launch in Game Mode picks up the Deck preset (native +1280×800, 2× MSAA, vsync) automatically if no config exists yet. -**In-game settings on the Deck:** the options overlay is fully gamepad-driven: -it opens with **Select**, the D-pad or left stick (up/down) moves between -options, **A** steps the selected option forward, **B** steps it back, and -**Start** (or Select again) closes. No keyboard needed. +**In-game settings on the Deck:** the options overlay is fully +gamepad-driven: it opens with **Select**, the D-pad or left stick (up/down) +moves between options, **A** steps the selected option forward, **B** steps +it back, and **Start** (or Select again) closes. No keyboard needed. Full steps are in the bundled `README.md`. -### Verify the download +## Verify the download ``` -sha256sum -c goldeneye-pc-port--win64.zip.sha256 -sha256sum -c goldeneye-pc-port--linux-x86_64.tar.gz.sha256 +sha256sum -c goldeneye-pc-port-0.4.0-win64.zip.sha256 +sha256sum -c goldeneye-pc-port-0.4.0-linux-x86_64.tar.gz.sha256 ``` -### Source & docs +## Source & docs , built on the -[GoldenEye 007 decompilation](https://github.com/n64decomp/007), architecture -after the [Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark). +[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 diff --git a/.gitignore b/.gitignore index ff95f231..61e855ba 100644 --- a/.gitignore +++ b/.gitignore @@ -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,12 @@ build-pyinst/ # Local working junk (frame captures, timerun state, PPM dumps) - moved here # from repo root / scratch on 2026-09-18; never commit. /local/ +build-pc-cmd.bat + +# v0.4.0 modern-options wave: per-landing sweep capture dirs (capture.sh output) +tools_pc/sweep-captures/capture-*/ + +# Stray full CPython install that landed in repo root (2026-09-28); +# referenced by no project tooling. Never commit. +/Python/ +docs/media/*.mp4 diff --git a/AGENTS.md b/AGENTS.md index 680ee103..36ca548b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -39,6 +39,59 @@ Needs CMake + SDL2 + zlib + OpenGL, and must run from the MSYS2 MINGW64 shell (see `build-pc.sh` header and `docs/building.md`). ROM goes in `./data/` (not distributed); assets must be extracted from it first (`docs/building.md`). +**Windows build environment (three recurring failure modes — diagnose in this order):** + +1. **`Cannot create temporary file in C:\Windows\: Permission denied`** at the + link step. The PE toolchain (ninja → cmd → gcc/ld) needs a writable + TMP/TEMP; the msys→native env conversion drops it in non-login shells + (agent harnesses; the `C:/msys64` tree here was built for `D:/M/msys64`, + so its conversion is unreliable). `build-pc.sh` now **self-heals**: it + probes a native child's TMP (via a file — piped `cmd.exe` stdout is + unreliable under msys console emulation) and, if broken, re-runs + cmake+build under PowerShell with `TMP`/`TEMP` set natively (the + native→native boundary passes env through intact; only msys→native is + broken). If you run cmake/ninja *by hand* from a broken shell: run them + from cmd/PowerShell with `C:\msys64\mingw64\bin` on PATH. + **Guard revision (2026-09-28):** the re-exec writes a self-contained + `.ps1` to a temp file and runs `powershell -File` with plain path/word + args only (the msys→native argv conversion mangles `$`-bearing + `-Command` strings; a file write is conversion-free). Inside: PATH is + built explicitly (never trust the inherited value); TMP is chosen from + writable candidates (`%LocalAppData%\Temp` first — + `[System.IO.Path]::GetTempPath()` honours the inherited *broken* TMP, + observed as `C:\Windows\`); every native step self-logs to + `build-pc/ge007-native-reexec-{diag,cmake,build}.log`; a null + `$LASTEXITCODE` ("did not execute") is a distinct failure (exit 2/3) — + in PS 5.1 a piped native command errors out non-terminating and never + sets it, so an unguarded `if ($LASTEXITCODE -ne 0)` passes vacuously. + In `.ps1`, `-DROMID=$Var` is a literal (bare tokens don't expand) — it + must be `"-DROMID=$Var"`. Launch powershell plainly — `env -i` before it + breaks the nested cmake launch (verified 2026-09-28). + **OPEN (next session):** the guard still fails *inside* `build-pc.sh` + (cmake "did not execute": the `.ps1` runs, but `$LASTEXITCODE` stays + empty) while the *identical* standalone invocation succeeds (cmake runs + and the build completes — that is how the D401 builds were made). First + test: diff the heredoc-generated `.ps1` against a hand-extracted copy + (suspect: heredoc line-ending/whitespace corruption). + **Working workaround:** extract the heredoc `.ps1` and run + `powershell -NoProfile -ExecutionPolicy Bypass -File "C:\msys64\usr\bin" "C:\msys64\mingw64\bin" C:/msys64 build-pc `. + Re-confirmed 2026-09-28: in-script re-exec fails again (cmake configure + dies inside the ps1; re-exec logs left stale) while the identical + standalone invocation reconfigured and built cleanly (extract with + `awk "/<<'PS1'\$/{f=1;next} /^PS1\$/{f=0} f" build-pc.sh > /tmp/ps1` + — mind the leading whitespace, and `export PATH` to include mingw/bin + first, failure mode 3). +2. **`cannot open output file ge007.x86_64.exe: Permission denied`.** A + **running** `ge007.x86_64.exe` locks the output file (Windows rule; you + can't relink over a live PE). Check with `Get-Process | Where-Object { + $_.ProcessName -like '*ge007*' }` and close the game before rebuilding. + An agent must not kill the user's game process to make a link succeed. +3. **`cc1.exe: ... libmpfr-6.dll: cannot open shared object file`.** The + calling PATH lacks `C:\msys64\mingw64\bin` (the gcc driver finds cc1 via + its own directory, but the child needs the mingw DLL dir on PATH). In an + msys shell: `export PATH="/c/msys64/mingw64/bin:/c/msys64/usr/bin:$PATH"` + before building. + ## Verification ritual (after any build-affecting change) 1. **Undefined symbols.** Every symbol referenced by the compiled set (see `CMakeLists.txt`: `SRC_GAME`, `SRC_ENGINE`, `SRC_LIBAUDIO`, `SRC_LIBULTRARE_AUDIO`, `SRC_LIBULTRARE_DATA`, `SRC_GU`, `SRC_PORT*`) must be defined exactly once in the compiled set or in `port/`. Symbols that live in EXCLUDED files (`libultra/io/*`, `libultrare/io/*` except `vitbl.c`, `libultra/os/*`, `libultrare/os/*`, `sched.c`, `rmon.c`, `vi.c`, `src/*.s`) must be provided by `port/src/libultra.c`, `gesched.c`, `n64stubs.c`, `random.c`, or `ucode.c`. diff --git a/README.md b/README.md index 69febc4f..336f7552 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,12 @@ # GoldenEye 007 PC Port [![CI](https://github.com/jkdansereau/goldeneye-pc-port/actions/workflows/ci.yml/badge.svg)](https://github.com/jkdansereau/goldeneye-pc-port/actions/workflows/ci.yml) -![license](https://img.shields.io/badge/license-MIT-green) +![license](https://img.shields.io/badge/license-MIT-ffb454) + +

GoldenEye 007 (Nintendo 64, 1997) on the PC — +decompiled, ported, and playable at 60 fps.

+ +[Download](#download) · [News](#news) · [Status](#status) · [Roadmap](#roadmap) · [Building](#building) · [Docs](#documentation) · [Legal](#legal) A native PC port of _GoldenEye 007_ (Rare, 1997, Nintendo 64), compiled from the [GoldenEye 007 decompilation](https://github.com/n64decomp/007): the @@ -12,25 +17,24 @@ in a dedicated `port/` layer, following the architecture of the [Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark), the same Rare "Indy" engine family, one hardware generation apart. -**v0.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). +**v0.4.0** (released 2026-09-28) is out for Windows and Linux (including +Steam Deck, where the Linux bundle sideloads as-is). It is the most complete +release to date: the full campaign runs at a steady 60 fps with the known +issues now few and mostly cosmetic ([Status](#status)). Free to download, +build on and modify (you bring the ROM). **This is a pre-1.0 release, not a finished product.** v1.0 is the target -for a polished, feature-complete build; until then, expect rough edges, -missing features, and breaking changes between versions. See -[Status](#status) for what works today and [Roadmap](#roadmap) for where -this is headed. +for a polished, feature-complete build; until then, expect missing features +and the occasional breaking change between versions. See [Status](#status) +for what works today and [Roadmap](#roadmap) for where this is headed. **AI disclosure:** development here was agentic - Claude Pro plus a local open-weight model on a single RTX 5090, as of August–September 2026. This project is as much a study of *that process* as it is a port: whether current LLMs can carry a codebase like this, and what actually goes wrong -along the way. Judge the result for yourself. -I'm one person doing this in my spare time, not a team. See -[Background](#background) for the full setup, timeline, and an honest -account of what worked and what didn't. +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 @@ -39,11 +43,31 @@ account of what worked and what didn't.

~12 s gameplay montage from live play sessions -
All in-engine, running in the port — a ~12 s gameplay montage - from live v0.3.0 play sessions. + alt="~24 s gameplay montage from live play sessions"> +
All in-engine, running in the port — a ~24 s gameplay montage + from live v0.4.0 play sessions, opening on the Runway tank · + 12 level stills in the project index

+--- + +## News + +- **2026-09-28** — **v0.4.0**: native widescreen, a complete aim system for + mouse and controller, in-game key rebinding, crosshair customization, + rumble-pak haptics, a rebuilt options overlay, and a broad fidelity-fix + pass (water, particles, billboard trees, front-end logo, gunshot SFX, a + true stable 60 fps). [Release notes](https://github.com/jkdansereau/goldeneye-pc-port/releases/tag/v0.4.0) · + [downloads](#download). +- **2026-09-20** — **v0.3.0**: the first release with the complete campaign + playable end to end at 60 fps on Windows, Linux, and Steam Deck. + [Release notes](https://github.com/jkdansereau/goldeneye-pc-port/releases/tag/v0.3.0). +- **2026-09-04 → 2026-09-16** — **v0.1.0 – v0.2.2**: the alpha and beta + cycle — build chain, software RSP, first rendered frames, front end, and + per-level stabilization across the campaign. + +--- + ## Download | Platform | Bundle | Notes | @@ -53,8 +77,8 @@ account of what worked and what didn't. 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). +distribute. Earlier builds: v0.3.0, v0.2.2, v0.2.1, v0.2.0, and v0.1.0 +alpha, same page. You can also build it yourself; see [Building](#building). ### Quick start @@ -67,17 +91,22 @@ included or distributed. Then: 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. +Read the [Status](#status) section first: the small number of known issues +in this release are listed plainly there. + +--- ## Status -**v0.3.0 - playable, with known rough edges.** The full single-player -campaign is completable end to end (all 20 missions, Agent difficulty, -playtested), at a steady 60 fps; all 20 solo missions — plus the -end-of-campaign credits sequence — load, render and run crash-free, verified -on Windows, Linux and real Steam Deck hardware. Feedback -is very welcome. +**v0.4.0 - fully playable, with a small set of known caveats.** The full +single-player campaign is completable end to end (all 20 missions, Agent +difficulty, playtested), at a steady 60 fps; all 20 solo missions — plus +the end-of-campaign credits sequence — load, render and run crash-free, +verified on Windows, Linux and real Steam Deck hardware. The most common +defect classes from earlier releases — particle colour drift, water seams, +z-fighting, billboard trees, muzzle flashes, the front-end Nintendo logo, +gunshot SFX — are fixed in this version; what remains is a short list, +below. Feedback is very welcome. What a release actually installs (no networking, no telemetry, no ROM or game assets shipped) and how faithfully the port tracks the original N64 @@ -87,62 +116,44 @@ game's logic: [Security & fidelity status](docs/security-and-fidelity-status.md) start), front-end menu navigation on the left stick to match the F10 overlay (D282); all 20 solo missions load, render and are crash-free (full campaign playtested end to end at Agent difficulty, including the ending sequence); steady 60 fps -(software RSP off the presentation critical path); full audio: in-level music and SFX; keyboard + mouse (click-to-lock, 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 +(software RSP off the presentation critical path); full audio: in-level music and SFX; keyboard + mouse (click-to-lock, an aim style of your +choice — N64 or centred FPS-style — with per-device sensitivity) and a modern +dual-stick controller layout (use/reload/weapon-cycle on A/X/Y, rumble-pak +vibration on supported pads); native widescreen at any aspect ratio +(undistorted world, HUD anchored to the screen edges, 4:3 menus pillarboxed); +in-game key rebinding (keyboard/mouse) with a GEPD-style default layout; +crosshair customization (opt-in); Bond is fixed in cutscenes (no more floating or spin-glitching) and his third-person model positioning generally is right the large majority of the time now, at most a small drift when off; file-backed saves; faithful N64 progression by default (F10 → *All unlocked* opens every level, 007 mode and the full -cheat menu); F10 in-game options overlay (resolution, frame cap, MSAA, -filtering, FOV, sensitivity); Windows and Linux, including Steam Deck. +cheat menu); F10 in-game options overlay (video, input, gameplay, HUD, +graphics, audio; frame cap, MSAA, filtering, FOV, sensitivity, key rebinding, +crosshair, vibration); Windows and +Linux, including Steam Deck. **Known issues:** -- 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). +- With native widescreen on, the F10 options overlay stretches with the + window instead of pillarboxing like the front-end menus (legible; cosmetic; + the F10 *Native widescreen* toggle restores the old stretched frame + throughout). The world/HUD widescreen rendering itself is correct (D335b). +- The Rareware front-end logo shows a subtle texture-filtering artifact + (Nintendo logo and legal page are clean). Cosmetic only (D75). +- This release ships NTSC (US) assets; PAL/JP ROMs are not supported in this + version (D258). +- **`All unlocked` is experimental: back up `data/ge007.eep` before enabling + it.** Any save while ON, even a profile-settings change, can permanently + write artificial cheat unlocks and completion times into the EEPROM; + switching OFF does not undo them (D387). Do not use it on a save whose + original progression you need to preserve. - Assorted further cosmetic defects are tracked in [`docs/dev/GRAPHICS-BACKLOG.md`](docs/dev/GRAPHICS-BACKLOG.md). -- No macOS or ARM support; no controller rebinding UI. +- No macOS or ARM support. Keyboard/mouse rebinding shipped in v0.4.0; + controller-button rebinding is not supported yet. Root causes and fix status for every item: the [release notes](https://github.com/jkdansereau/goldeneye-pc-port/releases) and the finding log in [`docs/dev/findings.md`](docs/dev/findings.md). @@ -151,23 +162,23 @@ and the finding log in [`docs/dev/findings.md`](docs/dev/findings.md). 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 +extract, drop your ROM in `data/`, launch it once (the first run generates the derived assets), and add the executable as a non-Steam game. SDL2 is bundled, so no dependencies need installing. On SteamOS the first launch seeds `ge007.ini` with Deck-friendly -defaults: native 1280×800 fullscreen, VSync, 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. +defaults: native 1280×800 fullscreen, VSync, 2× MSAA, and 250% draw/LOD +distance (the N64-authored fade distances read short on the close-up 7" +panel); everything is changeable in the options overlay and persists +afterwards. **Do that first launch in Game Mode, not Desktop Mode** — an ini +created by an earlier Desktop Mode launch (e.g. while testing before adding +it as a Steam shortcut) permanently skips the Deck preset, since any +existing ini always wins over it (D283). If your resolution isn't 1280×800 +on first Game Mode boot, just set it manually: F10 → *Resolution*. The +renderer is CPU-bound (software RSP); expect original N64-era performance at +60 fps rather than more. This release was playtested on real Deck hardware; +the v0.1.0-era Facility crash (D203) did not recur: its root cause was +identified and fixed (D253), and a separate intermittent SIGSEGV in heavy +firefights/terminal destruction (D255) and an audio-thread crash (D305) are +both fixed and live-verified on real hardware as of this release. **In-game settings on the Deck.** The options overlay is fully gamepad-driven: it opens with **Select**, the D-pad or left stick (up/down) moves between @@ -176,6 +187,8 @@ options, **A** steps the selected option forward, **B** steps it back, and 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 @@ -184,10 +197,8 @@ 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. +- **Controller (pad) button rebinding UI** (keyboard/mouse rebinding shipped + in v0.4.0), and macOS/ARM builds. - **LAN multiplayer**: reviving GoldenEye's original split-screen/deathmatch netplay across multiple PCs on a local network. Genuinely under consideration, but early and not started; no ETA. @@ -198,11 +209,14 @@ Not currently planned: new game modes GE never shipped (e.g. co-op), online (non 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. + frame cap, MSAA, texture filtering, FOV/draw distance, mouse feel, key + rebinding, crosshair and vibration; launch with `-fresh` for a clean-slate + run. - **Read it**: [`docs/internals.md`](docs/internals.md) maps the architecture and the software RSP; [`docs/porting-notes.md`](docs/porting-notes.md) is the catalogue of N64→PC bug classes hit along the way. Game logic in @@ -213,6 +227,8 @@ it helps prioritize. 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 @@ -222,8 +238,8 @@ 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: -36 days (16 Aug – 20 Sep), ~835 commits, 199 findings root-caused and -logged (`D1`–`D321`). +43 days (16 Aug – 28 Sep), 997 commits, 286 findings root-caused and logged +(`D1`–`D408`). The full write-up (timeline, handoff mechanism, effort breakdown, an honest "what worked / what didn't"): [`docs/dev/agentic-development.md`](docs/dev/agentic-development.md). @@ -231,6 +247,8 @@ 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 @@ -246,13 +264,15 @@ code with this one. | **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 | +| **Status** | v0.4.0 public release; full campaign playable at 60 fps (see [Status](#status)) | Playable full game | | **Why it exists** | To run the *original* N64 game from source, and as a [case study in AI-agent collaboration](#background) on a hard low-level codebase | To get a playable PC release of the remaster | They answer a different question: how to get the *remaster* onto PC by machine translation. This project answers how to get the *original 1997 game* onto PC, running from its reconstructed source. +--- + ## Requirements You need a GoldenEye 007 (Nintendo 64) ROM that you legally own, in @@ -272,6 +292,8 @@ 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, @@ -311,6 +333,8 @@ cd goldeneye-pc-port The executable is written to `build-pc/ge007.x86_64` (on Windows, `build-pc/ge007.x86_64.exe`). +--- + ## Running 1. Create a `data/` directory in the repo root. @@ -329,21 +353,33 @@ lives in `ge007.eep`. Launch with `-fresh` to wipe both before starting |---------------------|--------------------------|---------------| | Move / strafe | `W` `A` `S` `D` / arrows | Left stick (or D-pad) | | Aim / look | Mouse | Right stick | -| Fire (Z) | Left mouse / `LCtrl` | Right trigger | -| Aim mode (R) | Right mouse / `LShift` | Left trigger | -| Use / accept (A) | `Space` / `E` / `X` | A / X | -| Reload / cancel (B) | `R` / `F` | B / Y | -| 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 | `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 overlay | `F10` | Select (A toggles/steps; B backs; D-pad/stick adjusts sliders; Start closes) | -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). +The default layout is the GEPD-style preset on the keyboard and the standard +dual-stick scheme on controllers (left stick move, right stick look, +triggers fire/aim, A use, X reload, B gadget cycle, Y weapon cycle, stick +click crouch). The watch gadget cycle selects the next owned gadget in the +N64 inventory. The controller layout matches Rare's Xbox 1.1 (Jinx) button +roles (user-tested on a physical pad; D394), and the remaining Xbox schemes +(1.2 Christmas, 1.3 Frost, 1.4 Elektra) are planned as selectable presets +and not yet available. Keyboard → Bindings edits **keyboard/mouse only**; a +controller can navigate those pages and use B to go back, but +controller-button rebinding is not supported yet. Mouse sensitivity, Y-inversion and the aim/turn split are tunable in the -`[Input]` section of `ge007.ini`. +F10 overlay (*Keyboard → Sensitivity*) or the `[Input]` section of +`ge007.ini`. + +--- ## How it works @@ -376,6 +412,8 @@ tools_pc/ PC-port helper + analysis scripts docs/ see below ``` +--- + ## Documentation Key docs are also published as a site: @@ -391,6 +429,8 @@ Key docs are also published as a site: | [`docs/dev/`](docs/dev/) | The raw engineering record: the full finding log, per-level status, graphics backlog, playtest matrices, and [`docs/dev/game-behavior-reference.md`](docs/dev/game-behavior-reference.md) (a secondary-sourced playtest reference for how the retail game is meant to behave; repo-only, code is ground truth). | | [`docs/SetupGuide.md`](docs/SetupGuide.md), [`docs/StyleGuide.md`](docs/StyleGuide.md) | Inherited from the decompilation this repo forks. | +--- + ## Credits This port is a thin layer on a large amount of other people's work. @@ -448,6 +488,8 @@ This port is a thin layer on a large amount of other people's work. - [pi](https://pi.dev/): the local coding-agent harness. - [Claude / Claude Code](https://claude.com/claude-code) (Anthropic). +--- + ## Legal This is a non-commercial fan preservation/research project, in the same @@ -480,3 +522,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. + +--- + +*Last updated 2026-09-28 — v0.4.0 · GitHub · Releases* diff --git a/build-pc.sh b/build-pc.sh index 34a6388c..6e0cc3a8 100644 --- a/build-pc.sh +++ b/build-pc.sh @@ -14,7 +14,7 @@ # Example (Linux): # sudo apt install cmake libsdl2-dev zlib1g-dev libgl1-mesa-dev # Example (macOS): -# brew install cmake sdl2 zlib +# brew install cmake libsdl2-dev # Example (Windows/MSYS2): # pacman -S mingw-w64-x86_64-toolchain mingw-w64-x86_64-SDL2 \ # mingw-w64-x86_64-zlib mingw-w64-x86_64-cmake @@ -24,6 +24,132 @@ set -euo pipefail ROMID="${1:-ntsc-final}" BUILD_DIR="${BUILD_DIR:-build-pc}" +# --------------------------------------------------------------------------- +# Windows toolchain env guard (see AGENTS.md "Build" — the recurring +# "Cannot create temporary file in C:\Windows\: Permission denied" link +# failure). +# +# The PE toolchain (ninja -> cmd -> gcc/ld) creates temp files under the +# child's TMP/TEMP. A full MSYS2 MINGW64 login shell arranges that for +# native children; other shells (agent harnesses, non-login shells) do not +# -- the msys->native env conversion silently drops/breaks TMP and the +# linker falls back to C:\Windows\. Probe what a native child actually +# sees (via a file: msys console emulation makes piped cmd.exe stdout +# unreliable), and if broken, re-run the cmake+build steps under +# PowerShell. Crucially the temp vars are set INSIDE PowerShell: the broken +# boundary is msys->native, while native->native (powershell -> cmake -> +# ninja) passes env vars through intact. +# GE_PC_BUILD_VIA_NATIVE=1 prevents a re-exec loop. +# --------------------------------------------------------------------------- +if [ -n "${MSYSTEM:-}" ] && [ -z "${GE_PC_BUILD_VIA_NATIVE:-}" ]; then + _probe="$(mktemp -d "${TMPDIR:-/tmp}/gebuildprobe.XXXXXX")" + _probe_win="$(cygpath -w "${_probe}" 2>/dev/null || echo 'C:\\msys64\\tmp')" + /c/Windows/system32/cmd.exe //c "if exist \"%TMP%\" (echo ok > \"${_probe_win}/ok.txt\") else (echo bad > \"${_probe_win}/bad.txt\")" >/dev/null 2>&1 || true + if [ ! -s "${_probe}/ok.txt" ]; then + command -v powershell >/dev/null 2>&1 || { + echo "ERROR: native TMP is unusable from this shell and powershell.exe was not found for the fallback." >&2 + echo "Manual fix: run cmake+ninja from cmd/PowerShell with C:\\msys64\\mingw64\\bin on PATH, or" >&2 + echo "set the user env vars (setx TMP \"C:\\msys64\\tmp\"; setx TEMP \"C:\\msys64\\tmp\") and re-open the shell." >&2 + rm -rf "${_probe}"; exit 1 + } + echo "==> native TMP unusable from this shell; re-running cmake+build via PowerShell" + export GE_PC_BUILD_VIA_NATIVE=1 + _here="$(pwd -W 2>/dev/null || cygpath -w "$(pwd)")" + # Resolve the Windows-side toolchain paths from what msys found (no + # hardcoded install dir: C:\msys64 on this box, D:\M\msys64 + # elsewhere). usr/bin first: mingw gcc.exe needs msys-2.0.dll from + # there. + _cmake_root="$(dirname "$(command -v cmake)")" # .../mingw64/bin + _msys_root="$(cd "${_cmake_root}/../.." && pwd -W 2>/dev/null || cygpath -w "$(cd "${_cmake_root}/../.." && pwd)")" # msys install root (.. twice: bin -> mingw64 -> root) + _bin_w="$(cygpath -w "${_cmake_root}")" + _usrbin_w="$(cygpath -w "${_msys_root}/usr/bin")" + # The PowerShell script is written to a .ps1 FILE, not passed via + # -Command: the msys->native argv conversion mangles $-bearing + # strings (a '\$LASTEXITCODE' check arrived as an empty token and a + # failed cmake would have exited 0 silently). A file write is + # conversion-free; only plain path/word arguments cross the boundary. + _ps1="$(mktemp "${TMPDIR:-/tmp}/ge007-rerun.XXXXXX.ps1")" + cat > "${_ps1}" <<'PS1' +param([string]$UsrBin, [string]$Bin, [string]$MsysRoot, [string]$Here, [string]$BuildDir, [string]$RomId) +# Self-logging: a headless shell loses this process's console output, so +# every fact that matters goes to diag/log files in the build dir. +$diag = Join-Path $BuildDir "ge007-native-reexec-diag.log" +$cmakeLog = Join-Path $BuildDir "ge007-native-reexec-cmake.log" +$buildLog = Join-Path $BuildDir "ge007-native-reexec-build.log" +# The inherited native PATH may be mangled/dropped by the broken msys->native +# env conversion, so build it explicitly: the toolchain only needs the two +# msys bin dirs plus the usual system dirs. (usr/bin first: mingw gcc/cmake +# need msys-2.0.dll from there.) +$env:PATH = "$UsrBin;$Bin;C:\Windows\System32;C:\Windows;C:\Windows\System32\Wbem" +# Writable temp for the native toolchain. [System.IO.Path]::GetTempPath() +# CANNOT be used here: it honours the inherited (broken) TMP/TEMP env vars +# and returned C:\Windows\. Pick the first writable candidate instead. +$tmpdir = $null +foreach ($cand in @((Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'Temp'), (Join-Path $MsysRoot 'tmp'))) { + try { + if (-not (Test-Path $cand)) { New-Item -ItemType Directory -Force -Path $cand -ErrorAction Stop | Out-Null } + $probe = Join-Path $cand 'ge007-tmp-write-test.tmp' + 'x' | Out-File -FilePath $probe -ErrorAction Stop + Remove-Item $probe -ErrorAction SilentlyContinue + $tmpdir = $cand + break + } catch { } +} +if ($null -eq $tmpdir) { "no writable temp dir found" | Out-File $diag -Encoding ascii; Write-Host 'no writable temp dir for the native toolchain'; exit 4 } +$env:TMP = $tmpdir +$env:TEMP = $tmpdir +"start UsrBin=[$UsrBin] Bin=[$Bin] MsysRoot=[$MsysRoot] Here=[$Here] BuildDir=[$BuildDir] RomId=[$RomId] tmp=[$tmpdir]" | Out-File $diag -Encoding ascii +$exe = "$Bin\cmake.exe" +"exe=[$exe] exists=[$(Test-Path -LiteralPath $exe)]" | Out-File $diag -Append -Encoding ascii +Set-Location $Here +"cwd=[$([System.IO.Directory]::GetCurrentDirectory())]" | Out-File $diag -Append -Encoding ascii +try { + # -DROMID must be a DOUBLE-QUOTED string: a bare `-DROMID=$RomId` token + # does NOT expand the variable in PowerShell (cmake got the literal + # "$RomId" and failed with "Unknown ROMID"). + $out = & $exe -S . -B $BuildDir "-DROMID=$RomId" 2>&1 + "cmake ran LASTEXITCODE=[$LASTEXITCODE]" | Out-File $diag -Append -Encoding ascii + $out | Out-File -FilePath $cmakeLog -Encoding ascii +} catch { + "cmake TERMINAL ERROR: $($_.Exception.Message)" | Out-File $diag -Append -Encoding ascii +} +if ($null -eq $LASTEXITCODE) { Write-Host "cmake.exe did not execute (diag: $diag)"; exit 2 } +if ($LASTEXITCODE -ne 0) { Write-Host "cmake re-configure failed (rc=$LASTEXITCODE; log: $cmakeLog)"; exit $LASTEXITCODE } +Write-Host "cmake reconfigured (TMP=$($env:TMP))" +try { + $out = & $exe --build $BuildDir -j 2>&1 + "build ran LASTEXITCODE=[$LASTEXITCODE]" | Out-File $diag -Append -Encoding ascii + $out | Out-File -FilePath $buildLog -Encoding ascii +} catch { + "build TERMINAL ERROR: $($_.Exception.Message)" | Out-File $diag -Append -Encoding ascii +} +if ($null -eq $LASTEXITCODE) { Write-Host "cmake --build did not execute (diag: $diag)"; exit 3 } +if ($LASTEXITCODE -ne 0) { Write-Host "build failed (rc=$LASTEXITCODE; log: $buildLog)"; exit $LASTEXITCODE } +"done" | Out-File $diag -Append -Encoding ascii +Write-Host "cmake + build ok (native re-exec, TMP=$($env:TMP))" +PS1 + _ps1_win="$(cygpath -w "${_ps1}")" + rc=0 + # NOTE: do NOT launch powershell via `env -i` (tried 2026-09-28): a + # native child started with a scrubbed msys env cannot launch + # cmake.exe (CreateProcess fails silently; LASTEXITCODE stays null). + powershell -NoProfile -ExecutionPolicy Bypass -File "${_ps1_win}" "${_usrbin_w}" "${_bin_w}" "${_msys_root}" "${_here}" "${BUILD_DIR}" "${ROMID}" || rc=$? + rm -f "${_ps1}" + if [ "${rc}" -ne 0 ]; then + # Surface the .ps1's own diagnostics (console output is lost in a + # headless shell) plus whatever the failing step wrote. + for _lg in "${BUILD_DIR}/ge007-native-reexec-diag.log" "${BUILD_DIR}/ge007-native-reexec-cmake.log" "${BUILD_DIR}/ge007-native-reexec-build.log"; do + if [ -s "${_lg}" ]; then + echo "---- ${_lg} (last 40 lines) ----" >&2 + tail -40 "${_lg}" >&2 + fi + done + fi + exit "${rc}" + fi + rm -rf "${_probe}" +fi + echo "==> Configuring PC port (ROMID=${ROMID})" cmake -S . -B "${BUILD_DIR}" -DROMID="${ROMID}" diff --git a/docs/_config.yml b/docs/_config.yml index c4e66b6c..14895403 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -6,8 +6,10 @@ title: GoldenEye 007 PC Port # 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 (charcoal/off-white/amber, square and +# flat.) show_downloads: false # Canonical site location - used by jekyll-seo-tag (canonical URLs, og: tags) diff --git a/docs/_includes/head-custom.html b/docs/_includes/head-custom.html index 253dcf8d..7f7e687c 100644 --- a/docs/_includes/head-custom.html +++ b/docs/_includes/head-custom.html @@ -3,267 +3,242 @@ - + diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html new file mode 100644 index 00000000..f1fd73f4 --- /dev/null +++ b/docs/_layouts/default.html @@ -0,0 +1,48 @@ + + + + + + + {%- seo -%} + {%- include head-custom.html -%} + + +
+
+ {{ site.title }} +
{{ site.description }}
+ +
+
+ +
+ {{ content }} +
+ +
+ +
+ + diff --git a/docs/dev/CONTROLLER-INPUT-PLAN.md b/docs/dev/CONTROLLER-INPUT-PLAN.md new file mode 100644 index 00000000..d6e6ff7c --- /dev/null +++ b/docs/dev/CONTROLLER-INPUT-PLAN.md @@ -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`). diff --git a/docs/dev/CUT-CONTENT-BACKLOG.md b/docs/dev/CUT-CONTENT-BACKLOG.md index 13b12787..b8e57282 100644 --- a/docs/dev/CUT-CONTENT-BACKLOG.md +++ b/docs/dev/CUT-CONTENT-BACKLOG.md @@ -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. diff --git a/docs/dev/D356-SETTINGS-REGROUP-PLAN.md b/docs/dev/D356-SETTINGS-REGROUP-PLAN.md new file mode 100644 index 00000000..a31735bb --- /dev/null +++ b/docs/dev/D356-SETTINGS-REGROUP-PLAN.md @@ -0,0 +1,369 @@ +# D356 — Settings regroup: functional sections, save-file scoping, per-section reset + +Status: **LANDED (D356, 2026-09-27, port-only)** — signed off and implemented +this session; verification in `docs/dev/findings.md` D356 (GE_WSPROBE_RESET +front + in-stage probes, regressions, build). Supersedes the "BOND FILE" +section introduced in D353. Baselines: Turok PC port options (Nightdive, list +provided by the maintainer), the PD port's `port/src/optionsmenu.c` +(`C:\Users\james\Source\Repos\pd_port`), and plan §6 of +`docs/dev/OPTIONS-MENU-PLAN.md` (the original "align with Turok" note). + +## 1. Goal + +The D353 "BOND FILE" section is the one section named after *where a value is +saved* — an implementation detail — while every other section is functional +(DISPLAY, GRAPHICS, MOUSE/AIM, CONTROLLER, GAME). Its contents are also a +mishmash (aim/look toggles + HUD toggles + audio), and it exposes N64-watch +rows that **collide with or duplicate** the PC-native rows the port added +(watch look-invert vs `Input.MouseInvertY`/`Input.PadLookInvertY`; watch aim +control vs `Input.AimMode`). The baseline ports (Turok, PD) never name a +section after the save and never re-expose legacy overrides. + +D356 rewrites the row table so that: + +1. Every section is functional, using the baseline names: + **INPUT, GAMEPLAY, GRAPHICS, AUDIO, VIDEO**. +2. Save-file scope is communicated the PD way — *in the section title* + ("GAMEPLAY (File 1)"), generalized from the D353 BOND-FILE annotation — + plus a single **Save file: File N [change]** row at the top of the + options screen (PD "Player N" pattern; D354's default resolution applies). + "Bond file" appears nowhere in the UI. +3. Redundant/colliding watch rows are removed from the **menu surface only** + (the established D181/D216/D304 pattern): keys, in-game watch behavior, + and ini hand-editing all remain untouched. No game-code change (rule 2). +4. A **Reset to defaults** action row per section (Turok's per-category + reset; plan §6 Gate E, made concrete) with a **confirmation contract**: + edge-triggered activation (no auto-repeat), two-step arm→confirm, never + repeatable (WATCH-SETTINGS-PLAN §6 gate: "separate confirmation and + persistence policy before wiring a button" — §5.4 below is that policy). +5. A **context-aware file accessor** (front-end target vs active stage + target) with the "none" chooser position retired, so "Save file: File N" + is always a true statement (§5.5). +6. Section resets iterate the section's **declared** rows (including + conditionally hidden ones, §5.6) and reset verification exercises the UI + action itself, headlessly (§5.8). + +## 2. Non-goals + +- No `src/game` changes; the N64 watch menu keeps all its options and its + N64 wording (ground truth). +- No key rebinding (Phase 4) — but the section layout leaves room for a + "KEY BINDINGS" page exactly where Turok/PD have one. +- Master volume stays M3-backlog; the AUDIO section is shaped for it + (Turok's Master/Sound/Music trio). +- N64 build untouched. + +## 3. Exposure filter (menu surface only) + +| Row (key) | Verdict | Reason | +|---|---|---| +| `Bond.Look` ("Look up/down (watch; stacks)") | **Hide** | Duplicates `Input.MouseInvertY` + `Input.PadLookInvertY`; stacking both produces the double-negation trap. | +| `Bond.AimControl` ("Aim control") | **Hide** | Collides with `Input.AimMode` (N64 vs PC aim model) — two rows fighting over the same domain. | +| `Bond.AutoAim` | Keep | Unique gameplay assist; orthogonal to aim style/range. | +| `Bond.LookAhead` | Keep | Unique; no PC counterpart, no conflict. | +| `Bond.Sight`, `Bond.Ammo` | Keep | Unique HUD-visibility options; compose with `Game.HudScale`. | +| `Bond.Music`, `Bond.FX` | Keep | Core of D350–D355. | + +Hidden rows are commented out in `rows[]` with a D356 note (D181/D216 style: +config keys + watch path stay live; ini power users can still set them). + +## 4. New layout (5 pages + save-file row) + +`MAX_PAGES=8`, `MAX_PROWS=15` — all pages below the caps (D353 overflow log +still guards). + +Top row (drawn by `frontoptions.c` above the paged content, not paged): +**`Save file: File 1 [change]`** — cycles files via `watchSettingsChooseFile` +(D354 default resolution: explicit pick > viewed folder > first valid; +auto-init fallback). Hidden in F10 (existing `optionsRowIsBondChooser` +mechanism, generalized). F10 targets the active file, as today. + +| Page | Rows (key · kind) | Title annotation | +|---|---|---| +| **INPUT** | `Input.MouseSensitivity` · `Input.MouseInvertY` · `Input.AimMode` · `Input.AimRange` · `Input.PadLookInvertY` · `Input.PadDeadzone` · `Input.PadTriggerPct` · *Reset to defaults* | | +| **GAMEPLAY** (File N) | `Bond.AutoAim` · `Bond.LookAhead` · `Bond.Sight` · `Bond.Ammo` *(save, dim "(save)" tag)* · `Game.SkipIntro` · `Game.NoHitFlash` · `Game.AllUnlocked` · `Game.HudScale` · *Reset to defaults* · `__QuitToDesktop` | (File 1) | +| **GRAPHICS** | 11 rows unchanged from D353 · *Reset to defaults* | | +| **AUDIO** (File N) | `Bond.Music` · `Bond.FX` *(save; title annotation only — homogeneous section, PD-style)* · (master volume, M3) · *Reset to defaults* | (File 1) | +| **VIDEO** | `Video.Fullscreen` · `__Resolution` · `Video.VSync` · `Video.FpsCap` · `Video.DisplayFPS` · *Reset to defaults* | | + +Moves from D353 layout: mouse/aim + controller rows merge into **INPUT** +(the two old headers `__HdrMouse`/`__HdrPad` are dropped); DISPLAY → **VIDEO** +(rename, `Video.DisplayFPS` moves from GAME); GAME section is renamed +**GAMEPLAY** and absorbs the surviving watch toggles; the `__HdrBond` header +and `__BondFile` chooser row are retired (replaced by the top save-file row); +`Bond.Look`/`Bond.AimControl` rows are commented out (§3). + +Scope annotation: the D353 "(File N)"/"(no file)" title annotation — today +special-cased to the BOND FILE page via `optionsRowIsBondChooser()` — is +generalized: **any page containing per-file rows annotates**. Sections with +per-file rows: GAMEPLAY, AUDIO. + +F10: same table (F10 already shows all rows, chooser hidden). The +(Gameplay/Audio) annotation in-stage shows the active file number (the +context-aware accessor, §5.8 — *not* the raw chooser value). "Reset to +defaults" rows stay visible in F10 (O2) under the same confirmation +contract (§5.4). + +**(save) tag (O1):** the four mixed-scope GAMEPLAY save rows carry a subtle +dim "(save)" suffix — the only place a section mixes scopes. AUDIO is +homogeneously per-file, so its title annotation is sufficient (no tags). +Implementation: one `int saveScoped` field on `struct Row` + dim-ink suffix in +the row-draw path (frontoptions.c already draws the dim annotation). + +## 5. Code changes + +All in `port/` (D350-family); files: `port/src/optionsoverlay.c`, +`port/src/frontoptions.c`, `port/include/optionsoverlay.h`, +`port/src/watchsettings.c` (only if reset needs a new enqueue helper — +expected not to, see below). + +### 5.1 Row table (`optionsoverlay.c`) + +- Rename headers: `__HdrDisplay`→VIDEO (last page), add `__HdrInput` + (INPUT), `__HdrGameplay` (GAMEPLAY); delete `__HdrBond`, `__HdrMouse`, + `__HdrPad`, and rename `__HdrGame`→GAMEPLAY. New section order in the + table: INPUT, GAMEPLAY, GRAPHICS, AUDIO, VIDEO (Turok's order: Input, + Gameplay, Graphics, Audio, Video). +- Move the 7 mouse/aim + 3 controller rows under INPUT; move the 4 surviving + `Bond.*` toggles + music/FX into GAMEPLAY/AUDIO; comment out + `Bond.Look`/`Bond.AimControl` (§3). +- New `ROW_ACTION` rows, one per section: `.key="__Reset
"`, + `.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=`** (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. diff --git a/docs/dev/D357-SETTINGS-VALUES-PLAN.md b/docs/dev/D357-SETTINGS-VALUES-PLAN.md new file mode 100644 index 00000000..2cad493c --- /dev/null +++ b/docs/dev/D357-SETTINGS-VALUES-PLAN.md @@ -0,0 +1,160 @@ +# D357 — Settings thresholds & values: align display units/defaults with the PD port + Turok PC + +Status: **PROPOSAL — awaiting sign-off** (2026-09-27). Baselines: the PD port +checkout (`C:\Users\james\Source\Repos\pd_port`, `port/src/optionsmenu.c` + +`input.c`/`video.c` config registrations), the Turok PC (Nightdive) list from +the maintainer's notes (`docs/dev/notes/OPTIONS-MENU-PLAN.md` §6 — *taken from +user notes, not checked independently*), and the N64 watch (ground truth for +the `Bond.*` rows only). Triggered by maintainer report: the current values +are "a bit confusing". + +Companion change landed with this doc's review: the terminology settled on +**"Profile"** as the end-user term for a per-player game file -- the +front top row reads **Profile**, the per-file row tag reads +**"(per profile)"** and the F10 unavailable value reads **"Select a +profile"** (§4). + +## 1. Problem + +The D356 regroup fixed the *layout*; the *values* still mix three unit +families with no consistent rule: + +- **raw internal units shown as-is**: `Input.MouseSensitivity` (1–500, 100 = + 1× — the "100" is a % of a 1.0 multiplier, not a count), + `Input.PadDeadzone` (raw 0–30000, `dispDiv=300` → "23%" but the step of 500 + raw is 1.67 display-%, so the slider lands on 23, 25, 26…), + `Bond.Music`/`Bond.FX` (0–32767, `dispDiv=328`, step 128 = 0.39 display-%); +- **N64-era vocabulary on PC rows**: texture filter "Nearest / Bilinear / + **3-Point**" (the N64 watch term for trilinear), "FOV scale %" instead of + degrees; +- **defaults that don't read as defaults**: MSAA shows "4x" fine, but + `FpsCap` 0–1000 with step 1 invites 117 FPS; `Video.LodDistance` default + 150% vs `DrawDistance` 150% with a 25-step grid (100/125/150/175…). + +The PD port is the house style to copy: per-stick sensitivity as a **±10 +multiplier (default 1.0, negatives invert)** (`input.c:1537`), mouse speed +**-30..30 (default 2.5)**, deadzone **raw 0–32767 per stick+axis**, rumble +**0–1**, MSAA as a **2x/4x/8x/16x dropdown**, frame cap as **"N FPS"**, +volumes as 0–100 %. Turok's (user's list) conventions: single mouse +sensitivity knob, per-section resets (landed, D356), master/sound/music +volume trio (master still M3-backlog). + +## 2. Principles + +1. **Display, don't rescale the world.** Every change is port-only (row + table, `dispDiv`/step/`unit` fields, value-text, `kResetDefaults`): no + `src/game` change, and config *storage units* are kept wherever a change + would migrate existing ini files (principle 3). +2. **Human units in the value column**: `%` on a 0–100 grid, `×` multipliers + with 1.0 as the default, `N FPS`, `Off/2x/4x/8x`, `Unlimited`. A slider + step must map to a **whole display unit** (the deadzone 1.67%-step bug is + the canonical example to fix). +3. **Ini stability**: renaming a config key's storage unit is a migration. + Where the plan needs it, register a *new* key (e.g. + `Input.PadDeadzonePct`) and keep the old key as a read-once legacy alias — + the D333 `Input.AimMode`→`Input.AimStyle` alias pattern (`input.c:1962`). + Each migration gets a findings note. +4. **`kResetDefaults` stays in lockstep** with the config C initializers + (D356 Gate E): every default change in this plan updates the table in + `optionsoverlay.c` in the same commit; the `GE_WSPROBE_RESET` probe + re-verifies. +5. **Defaults = the config C initializers**, never a new menu-only default. + +## 3. Row-by-row audit & proposal + +Current state from `optionsoverlay.c` `rows[]` + the `configRegister*` +ranges (2026-09-27). "Display" = what the value column shows today. + +### INPUT + +| Row | Now (default · range · step · display) | PD port | Turok (notes) | Proposal | +|---|---|---|---|---| +| Mouse sensitivity | 100 · 1–500 · 5 · raw `100` | `MouseSpeedX/Y` float −30..30, def 2.5 (sign = invert) | single knob | **Display as a × multiplier**: keep storage (100 = 1.0×), value text `%.1f×` (v/100); range 10–300 (0.1–3.0×), step 10 (0.1×). Label stays "Mouse sensitivity". | +| Invert look (mouse) | Off/On | folded into the −/× sign of MouseSpeed | — | **Keep On/Off** (PD's negative-multiplier style is an alternative; a separate invert is simpler next to the × slider). Decision: confirm. | +| Aim style / Aim range | N64/Centred (PC) · PC/N64 | — (GE-only, D337/D338) | — | Keep as-is (GE-specific; hiddenIfOn already sane). | +| Invert look (controller) | Off/On | sign of stick scale | — | Keep. | +| Stick deadzone | 7000 raw · 0–30000 · 500 · ÷300 → `23%` (1.67%-steps) | raw 0–32767 per stick+axis, `DEFAULT_DEADZONE` | MISSING | **New key `Input.PadDeadzonePct` 0–100, step 1, default 23** (7000/30000×100); `input.c` scales back to raw at apply; old key becomes a legacy alias (principle 3). Display `N%` on a whole-% grid. | +| Trigger threshold | 23 · 1–99 · 1 · `%` | — (GE extra) | — | Keep. | + +### GAMEPLAY + +| Row | Now | Baseline | Proposal | +|---|---|---|---| +| Auto-aim / Sight / Look ahead / Ammo (profile) | Off/On (N64 watch 0/1) | N64 watch = ground truth | Keep; tag now reads **(profile)** (this doc's companion change). | +| Skip intro / No hit flash / All unlocked | Off/On | Turok: screen-shake/hit-flash family | Keep. | +| HUD scale | 100 · 75–150 · 5 · `%` | — | Keep (whole-% grid already; range cap D226). | + +### GRAPHICS + +| Row | Now | Baseline | Proposal | +|---|---|---|---| +| Anti-aliasing | 1/2/4/8, def 4, shows `None/2x/4x/8x`, restart | PD dropdown 2x/4x/8x/**16x**, def 4x | **Add 16x to the sequence** (`kMsaaSeq`, range clamp 1–16) — SDL/GL supports it; PD has it. Decision: perf cost on mid GPUs (restart already flagged). | +| Texture filter | Nearest / Bilinear / **3-Point** | PC convention: Nearest/Linear/Bilinear | **Rename the enum names to `Nearest / Linear / Bilinear`** (3-point *is* trilinear; the N64 term is the last one left on a PC row). Storage 0/1/2 unchanged. | +| Anisotropic filtering | 4 · 1–16 · 1 · `x` | Turok 1–16 | Keep; **dim/disable when filter = Nearest** (anisotropy only applies to filtered sampling) — `hiddenIfOff`-style guard, UI-only. | +| FOV scale | 100 · 50–150 · 5 · `%` | Turok 3: 60–120° slider | **Display in degrees**: the 100% baseline is the N64 vertical FOV scaled to the port's widescreen math — value text `N°` (v/100 × baseline, 1 decimal); storage unchanged. Decision: degrees vs % (Turok uses degrees; % is the existing mental model — pick one, recommend degrees). | +| Native widescreen / Widescreen auto FOV / Crop overscan | Off/On | — | Keep. | +| Draw distance | 150 · 100–400 · 25 · `%` (hidden under auto-FOV) | Turok: draw-distance row | Keep the % grid (25% steps read fine); **step 25 → display already whole-%**. No change. | +| LOD distance | 150 · 25–400 · 25 · `%` | — | Keep; note default 150 vs DrawDistance 150 is intentional parity. | + +### AUDIO + +| Row | Now | Baseline | Proposal | +|---|---|---|---| +| Music volume (profile) | 32767 · 0–32767 · 128 · ÷328 → 0.39%-steps | Turok: 0–100% | **step 128 → 328** (exactly 1 display-%; 32767/328 = 99.9 ≈ "100%"). Storage unchanged (N64 `VOLUME_MAX` semantics). | +| FX volume (profile) | same | same | same. | +| *(master volume)* | — | Turok trio | M3-backlog, out of scope (shape the section for it, D356 note). | + +### VIDEO + +| Row | Now | Baseline | Proposal | +|---|---|---|---| +| Fullscreen | Off/On | Turok: window mode (borderless missing) | Keep; borderless = separate backlog item. | +| Resolution | action row (presets) | both | Keep. | +| VSync | On/Off | both | Keep. | +| Frame rate cap | 60 · 0–1000 · 1 · `Uncapped/N FPS` | Turok: "up to 120 FPS" / uncapped | **Preset grid**: 0/30/60/120/144 (step = next preset, wraps to Uncapped); value text already `N FPS`/`Uncapped`. Decision: preset grid vs free 1-step (recommend presets; 117 FPS has no purpose). | +| Show FPS | Off/On | both | Keep. | + +## 4. Terminology (end-user term = "Profile") + +Decision (2026-09-27, UX review): the end-user term for a per-player +game file is **"Profile"** — the per-player bundle of progress + +per-file settings. Rejected: **"game file"** (the instruction manual's +current term) reads, in modern PC usage, as the *program's* own files +("Verify integrity of game files"), not the user's save; **"Save"** was +the fallback (universally understood) but reads as a verb in the row tag +and as progress-only, which these files are not. "Profile" covers both +and fits the dossier styling of the screen. + +Final string set (both UIs: front options + F10 overlay): + +| String | Where | +|---|---| +| **"Profile"** + value `N` | front options top row (was "Save file: File N") | +| **"(Profile N)"** / **"(none)"** | section-title annotation on save-scoped sections (was "(File N)"/"(no file)") | +| **"(per profile)"** | per-file row tag (was "(save)", then "(profile)", then "(game file)") | +| **"Select a profile"** | F10 unavailable value (was "Select file") | + +**Manual sync TODO:** the instruction manual's "game file" should be +updated to "profile" (the manual is a port document we own). + +"Bond file" remains in code identifiers/config keys (`Bond.*` rows, +`watchSettingsChooseFile`, ini keys) only -- never user-visible. + +## 5. Gates + +1. Port-only per principle 1; the deadzone key migration (principle 3) is + the only config-surface change and follows the D333 alias pattern. +2. `kResetDefaults` updated in the same commit as every default/range + change; `GE_WSPROBE_RESET` (front + `-level_33`) re-run, all sections + `failures=0`. +3. Build via `build-pc-cmd.bat` (the pi-shell MSYS env gap, D356 note). +4. Visual check owed (maintainer): front options screen + F10 in all five + sections; the overlap fix from the same session (the level-1 save-file + row no longer paints under the first content row). + +## 6. Out of scope + +- Key rebinding (Phase 4), master volume (M3), output-device selection, + borderless window, per-axis controller sensitivity (PD-style four-knob — + GE's one-knob design is D238/D304). +- Any `src/game` change; the N64 watch keeps its own values and wording. diff --git a/docs/dev/F10-GE-STYLE-PLAN.md b/docs/dev/F10-GE-STYLE-PLAN.md new file mode 100644 index 00000000..ac14238e --- /dev/null +++ b/docs/dev/F10-GE-STYLE-PLAN.md @@ -0,0 +1,129 @@ +# F10 visual refresh — GoldenEye dossier styling (proposal) + +Status: **superseded by D363 after user playtest** — the dossier/paper +concept below was implemented as D362, then replaced with a dark +watch-green/Bank Gothic version. This is the historical proposal for a +port-layer F10 overlay proposal, not a replacement for GE's pause watch or the +front-end PC Options screen. Reference surfaces: `src/game/front.c` +`constructor_menu15_cheat`/`frontSetupMenuBackground`, the existing +`port/src/frontoptions.c` cheat-list-inspired options screen, and GE's in-stage +watch. Current overlay: `port/src/optionsoverlay.c` `optionsOverlayEmit`, +`overlayRowAtY`, `sliderBarSpan`, `maxVisibleRows`. + +## 1. Direction and boundaries + +Make F10 read as an **MI6 dossier laid over the game scene**, not a generic +SDL debug panel: dark translucent scrim, warm paper/ivory content card, +black Zurich-style menu text, red for active options/slider fills and an +occasional muted gold title/accent. The front-end PC Options already uses +GE's actual 3D folder and Zurich text; use it as the visual reference, not +as a display-list dependency. In-stage F10 must remain an inexpensive flat +2D overlay: no loading a 3D wallet model, new ROM assets, front-end init +functions or `src/game` modifications. Preserve the current two-level +category -> settings navigation and the shared settings/commit API. + +The initial port-only implementation lives in `optionsOverlayEmit()` and +uses one shared `OvLayout` for both 320x240 stage and 440x330 front canvases. +Headless captures live under `scratch/d362-test/` (not distributed). Manual +visual review on a bright stage and controller/mouse feel-check remain owed. + +## 2. Concrete visual hierarchy + +- **Backdrop/card:** keep an adjustable translucent dim over the scene; + replace the opaque blue-black rectangle with a near-opaque ivory panel + (subtle border and one offset shadow). Do not mimic the actual folder + texture unless a later, measured prototype proves the simple card + insufficient. Preserve legible contrast against both dark and bright + levels, including safe-area crop and widescreen pillarboxes. +- **Masthead:** small gold/black `MI6 / OPTIONS` or `PC OPTIONS` heading, + fine black rule (a restrained `CLASSIFIED` stamp can be added later if + measured text fits) rather than a blue title/instruction block. Keep the + category name on the **same + page above its first setting**, especially INPUT over Mouse sensitivity. +- **Root:** numbered `1. Input` ... `5. Video`, GE's dossier/menu numbering + and title case as on `frontoptions.c`; a narrow translucent dark selection + box behind the focused item. Category order and labels stay unchanged in + the shared row table; casing is presentation-only. +- **Section:** a compact `PREVIOUS`/Back tab, title and optional + `(Profile N)` annotation, then two columns: label at left, value at right. + Unselected values in black, active/toggled values in GE's dark red + (`0xA00000FF` is the cheat-screen reference). Slider tracks are thin + charcoal with a red fill and an obvious knob; show the numerical value + outside the bar. A mixed-scope `(per profile)` marker must remain legible + without colliding with the value column; measure it and, if necessary, + use a small second line rather than silently removing the distinction. +- **Actions:** Reset retains its two-press/three-second confirmation and + displays `CONFIRM` in red only while armed. Quit remains visually distinct + from resets (and retains its existing activation semantics). Back/Close + affordances should match the actual click targets, not become decoration. +- **Footer:** one quiet, short line for mouse/arrows/controller, with an + explicit `ESC: Back / F10: Close` cue. No newly invented glyphs or fonts + that may be missing from the game's text bank. FPS readout is independent. + +Suggested layout sketch (not pixel positions): + +``` + MI6 / PC OPTIONS CLOSE + ----------------------------------------------- + PREVIOUS GRAPHICS + Anti-aliasing 2x + Texture filter Bilinear + Draw distance [------|----] 50/100 + ... + Reset to defaults [ENTER] + ESC Back / F10 Close +``` + +## 3. Implementation boundaries + +1. Extract a port-only layout helper for fonts, palette, + panel rectangles and spacing; preserve `rows[]`, `rowAdjust`, persistence, + file scoping, reset logic and input bindings. Use GE's loaded Zurich Bold + font for body *only after* validating it exists on both stage and front + contexts; Bank Gothic can remain a fallback/header font. Text clipping + must use the whole viewport as `textRender` expects. +2. Replace only the fill/text passes in `optionsOverlayEmit()` first. Emit + fills before text as currently, reset GBI state explicitly and keep + `OV_BUF_CMDS`/DL size bounds. Avoid mutable texture uploads or new assets. +3. One shared geometry calculation must drive panel/card positions, + `maxVisibleRows`, row Y, slider-bar span, close/back hitboxes, and + `overlayRowAtY`. In particular, don't change visual row spacing without + updating hit-testing (D304/D316), and don't use naive OS-window pixel + mapping instead of `gfx_get_ui_screen_rect` (D316/D335). When a page + scrolls, keep its section title visible; a clipped row cannot be clicked. +4. Use measured text widths to reserve space for values, `Confirm`, + `(Profile N)` and restart markers. At 320x240, allow scrolling/short + contextual hints, not overlapping text or tiny glyphs. At 440x330 and + ultrawide, centre or constrain the card rather than stretching the + columns across the entire viewport. +5. Make any GE-like UI sound effects opt-in to a *separate* follow-up: F10 + runs in controller polling, and D361 prohibits game save/audio calls + from that thread. A cosmetic refresh must not reintroduce that lockup. + +## 4. Acceptance gates + +- Compare screenshots with the existing front Options/cheat dossier and the + in-stage watch; user reviews the live palette/layout before closing the task. +- Native 320x240, front-end 440x330, windowed 4:3, 16:9 and 21:9, + fullscreen, safe-area crop on/off: INPUT title visible; no text clipping, + unreadable values, wrong hitboxes, phantom/duplicated bottom rows or + overflow in the display list. Check graphics's longest labels and AUDIO's + per-profile markers with and without a valid file. +- Mouse click/drag/wheel, arrows and D-pad/stick, ESC back, F10/Start/X + close, focused/armed reset, and quit action all behave **exactly** as + before. F10 Audio on file select still commits via D361's game-thread + queue; test both fresh and existing EEPROM without altering the user's + save (backup/restore for probes). +- With overlay closed, append no panel DL and retain existing golden frame + hashes (unless FPS display is explicitly enabled). Run `./build-pc.sh + ntsc-final`, link/duplicate-symbol checks, front-end + stage smoke, + `GE_WSPROBE_F10FRONT` and targeted live playtest. Log the result in + `docs/dev/findings.md` after implementation, not in advance. + +## 5. Explicit non-goals + +No fullscreen 3D folder in gameplay, changes to the N64 watch, new config +keys, row wording/value changes, save-format changes, controller remapping, +new persistent assets, or `src/game` behavioral edits. The front-end PC +Options screen is a **visual reference**, not part of the first styling +patch; harmonizing its tiny decorative details is a later optional pass. diff --git a/docs/dev/GE-ENV-PROBES.md b/docs/dev/GE-ENV-PROBES.md index b420b7fe..bfb87b0e 100644 --- a/docs/dev/GE-ENV-PROBES.md +++ b/docs/dev/GE-ENV-PROBES.md @@ -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-21 (D322 probe added; v0.3.0 release review was 2026-09-20). All are **env-gated**: unset = zero behavior +File:line cells. Last reconciled 2026-10-07 (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 @@ -20,7 +20,7 @@ Two classes: | Env var | File:line | What it does | Status | |---|---|---|---| | `GE_PCDUMP` | `port/fast3d/gfx_opengl.cpp:1079` (`is_pcdump`), `gfx_opengl.cpp:700`, `port/src/video.c:188`, `port/src/config.c:46` (`configGetFrameDump`) | Per-frame framebuffer PPM dump to `$PCDUMP` dir (verification-ritual golden captures, `tools_pc/framediff.py`/`pixcount.py`). Also settable via `[Debug] FrameDump` ini (env wins). **M-33/D168: the writer now emits rows top-to-bottom — captures before that fix are vertically flipped.** | **live** | -| `GE_INPUTSCRIPT` | `port/src/input.c:306` | Headless scripted controller-0 input: `":,…;…"` (`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: `":,…;…"` (`SUP/SDOWN/SLEFT/SRIGHT/SNONE` sustain, buttons pulse; `ZHOLD`/`ZREL` sustain fire, D207 M-201). Sole input source when set. | **live** | | `GE_INPUTLOG` | `port/src/input.c:329`, `port/src/config.c:53` (`configGetInputLog`) | Logs computed pad state (buttons + stick) each poll when non-zero. Also `[Debug] InputLog` ini. Also traces D165 `menuptr est/tgt/eff/stick`. | **live** | | `GE_STARTMENU` (+ `GE_STARTMENU_PAGE`, `GE_STARTMENU_DIFF`) | `src/game/lv.c:398-402` (`#ifdef PORT`) | Boot straight into a front-end menu id (13=MISSION_COMPLETE, 10=BRIEFING, 7=MISSION_SELECT, 12=MISSION_FAILED, 6=MODE_SELECT); `_PAGE`=folder row (def 1=Dam), `_DIFF`=0..3. Crash-test a screen fast. | **live** | | `GE_UNLOCK_ALL` | `src/game/debugmenu_handler.c:1098` (`get_debug_enable_all_levels_flag`, `#ifdef PORT`) | Mission-select shows every solo level (playtest jump-to-any-level aid). Cached in a `static int c`. | **live** | @@ -33,7 +33,6 @@ Two classes: | `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_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) | @@ -42,6 +41,7 @@ Two classes: | `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 ``, 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) | @@ -58,20 +58,48 @@ Two classes: | `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). | **TEMP** (D252 fixed M-201; remove after release) | | `GE_DVM` | `src/game/gunfire.c` (`gunUpdateAndFire` gate dump + `gunRenderFirstPersonGunModels` render dump; `#ifdef PORT`) | 1P weapon viewmodel (D215). `[DVM]` line: the `field_87F` show/hide gate terms + `Gun_hand_without_item` sub-terms + `copy_of_body_obj_header` RootNode/Switches/numRecords. `[DVMr]` line: `87F` + `cmds_emitted` = gfx commands the weapon `subdraw()` produced (healthy ≈ 150; the D215 bug showed **1**). Rate-limited ~1/s per hand. **Found + verified D215** (`renderdata.flags` read 0 → all weapon DL nodes gated off). Probe kept inert. | **live** (D215 FIXED, probe kept) | | `GE_DTEX` | `port/fast3d/gfx_pc.cpp:1001` | Per-`import_texture` param log (dims / line / size / lod / gen_mipmaps). Used to root-cause RC2 and D159. Kept as an inert diagnostic. | **live** (RC2/RC3 not finished) | | `GE_TEXDUMP` | `port/fast3d/gfx_pc.cpp:1020`, `port/fast3d/gfx_opengl.cpp:700` | PPM dump of every uploaded texture + a `fmt/siz/palfmt/palidx/pal[0..3]` line. Root-caused D161 (Depot ceiling). Kept as an inert diagnostic. | **live** (RC3 not finished) | | `GE_D116` | `port/fast3d/gfx_pc.cpp:1710,2362`, `src/game/textrelated.c:265,541` (cached `ge_d116`) | HUD/text "X-mirror" (D114/D116) vbo/uv trace. **D114/D116 CLOSED — NOT A BUG (M-33/D168): the captures were upside-down, not mirrored.** Probe is now dead scaffolding — strip on the next cleanup pass. | dead (D114/D116 closed) | -| `GE_D288` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_tri1`, right after the D233 trivial-reject guard; cached `ge_d288`) | M-152 diag for the Silo intro stray triangle: 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_D288` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_tri1`, right after the D233 trivial-reject guard; cached `ge_d288`) | M-152 diag for the Silo intro stray triangle: frame-gated to `num_dls` 550–755 (M-155), now **skips `any_behind_camera==1` triangles** (M-155 follow-up — the x/w divide is meaningless for w≤0; that subset was pure false-lead noise) and logs the render-mode word (`oml`) so a live capture can ID the culprit asset against level tables. Logs any triangle whose clip-space NDC bbox covers >50% of the screen or goes non-finite, with each vertex's `w`. Bare `-level_20` re-run: 1965 sane hits in the window, still zero for the target color RGB(83,62,44) — consistent with M-155's working theory that the bare boot's scripted camera never reaches the bug state; **headless probing is at its limit, live capture is next**. | **live** (D288) | | `GE_D245V` | `src/game/sky.c:962` (`skyPortRenderPoly`-adjacent, `#ifdef PORT`) | D245 water-seam diag: per-call min/max of the recomputed S/T texture-coordinate ranges for the water fan vertices (rate-limited), to check whether the seam tracks a tc-shift boundary. Inert unset. | **live** (D245 OPEN) | | `GE_D245_FIXEDSHIFT` | `src/game/sky.c:946` (`#ifdef PORT`) | D245 test override: pins the water tc shift `k` to the given integer (0..SKY_TC_MAX_SHIFT) instead of the computed per-vertex value, to A/B the allowShift=TRUE behavior. Off by default (unset = current behavior). | **live, test-only** (D245 OPEN) | | `GE_D252` | `port/fast3d/gfx_pc.cpp:733` (`import_texture`, cached `s_d252`) | D252 rainbow-particle diag: one-shot per (address,size) import log of the tile state (tmem/fmt/siz/addr/size/line) for small textures so a suspect particle tile can be checked against its source bytes by hand. Remove once D252 is resolved. | **live** (D252 OPEN — non-reproducible) | +| `GE_D252POOL` | `src/game/image.c` (`texLoad`), `src/game/initmttex.c`, `port/src/gimgfixup.c` | **TEMP** (M-201): one line per new texture load (pool main/other, free bytes) + `POOL-FULL` events + stage pool range; `D252SYNC: N slots re-synced, M changed` per stage load (the D252 fix at work: M>0 from the 2nd stage on). Load-time only, no fps cost. | **TEMP** (D252 fixed; remove after release) | +| `GE_D245_OLDVTX=1` / `GE_D245_OLDTEX=1` | `src/game/sky.c` (`skyPortRenderPoly`) / `port/fast3d/gfx_pc.cpp` (CI8 import + tri-path dims) | **A/B** (M-201): restore the pre-fix s16 sky/water vertex path / the pre-fix 32x43, 16-wide water texture geometry. | **TEMP** (D245 fixed; remove post-release) | +| `GE_DYNTEXHASH_OFF=1` | `port/fast3d/gfx_pc.cpp` (`import_texture` cache key) | **A/B** (M-201, D341): disable the content-hashed cache key for dyn-pool textures (reverts to the stale address-only key). | **TEMP** (A/B switch) | +| `GE_D207S` | `src/game/gunfire.c` (player shot-sound block, `#ifdef PORT`) | **TEMP** (M-201): one line per player shot-sound decision: `D207S t= hand item rate -> H1|H2|SKIP` (SKIP = both per-hand sound handles still held). Read-only, uncapped, cheap; use instead of `GE_AUDIOTRACE` for live captures (that probe does file I/O on the audio path). | **TEMP** (D207/D241) | +| `GE_D306C="f0 f1 [wmax]"` | `port/fast3d/gfx_pc.cpp` (after `zf_note_emit`) | **TEMP** (M-201): depth-state census of every emitted triangle in frames f0..f1 (GL depth mode, geometry_mode, other_mode_l, tile-0 texture, NDC centroid, z/w range, min w); optional `wmax` keeps near geometry only. Cap 60000 lines. | **TEMP** (D306 closed; remove post-release) | +| `GE_INTROCAM=` | `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=` | `src/game/bondview_r.c` (`#ifdef PORT`, after the start-inventory setup) | **TEMP/test-only** (M-201, D303): adds the item + 400 rounds of its ammo and makes it the starting right-hand weapon (M16 = 13). Pair with `GE_INPUTSCRIPT` `ZHOLD`. | **TEMP** (useful repro tool) | +| `GE_D235` | `src/boss.c` (per-stage `while (!done)` block, after `memaReset`; `#ifdef PORT`) | D235 evidence probe: logs how many GL-texture-cache entries the per-stage `videoResetTextureCache()` call purges at each level load. A cold boot into a level should log 0; a death/restart (or any transition from a level that uploaded textures) logs >0 — direct evidence of the stale-entry mechanism. The clear itself is **not** gated on this var (it's the fix, see below); the var only controls the log line. | **live** (D235) | +| `GE_D235_OFF` | `src/boss.c` (same block) | **TEMP** A/B switch: skips the per-stage `videoResetTextureCache()`, i.e. reverts the D235 fix for one run. For comparing a restart with and without the fix. | **TEMP** (D235; remove when D235 closes) | +| `GE_D235_RELOAD=` | `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 `` retraces, then re-enter that level via `bossSetLoadedStage()`, the front end's own mission-start call. A headless death/restart without the pointer-menu walk. Pair with `GE_INPUTSCRIPT="800:START;1200:A;1300:RIGHT;1400:A"` (abort) on `-level_XX`. | **TEMP** (D235; remove when D235 closes) | +| `GE_D235_NEXT=` | `src/boss.c` (with `GE_D235_RELOAD`) | **TEMP**: re-enter a *different* stage after the abort (level A → title → level B), used for D336 (Runway → Frigate). `GE_D85TEX=1` now also logs each stage's texture-pool size and address range (`src/game/initmttex.c`). | **TEMP** | +| `GE_D320R="tick:action;..."` | `port/src/d320repro.c` (hooked in `src/game/lv.c` per-tick PORT block) | **TEMP** forced-repro harness keyed on `g_GlobalTimer`. Actions: `roster`, `jump=,list=,off=`, `bit=`, `trace=`, `rename=,`, `seed=,` (D320); `explode=,` (explosionCreate ahead of Bond) and `cheat=gl|rl|inv|` (game cheat activation) for D252 headless explosion/debris captures. | **TEMP** (D320/D252) | +| `GE_AIMLOG=1` (+ `GE_INPUTSCRIPT` tokens `MDX` `MDY` `RHOLD` `LHOLD` `RREL`) | `port/src/input.c` | D337 aim-model harness: scripted sustained mouse deltas (treated as a grabbed mouse) and aim holds, plus a per-poll line with aim state, active device, the game's drawn crosshair x, `crosshair_x/y_pos` and camera angles. Measures range, edge-scroll and jitter headlessly. | **live** | +| `GE_PERFSTAT=1` | `port/fast3d/gfx_pc.cpp` (`gfx_run`/`gfx_end_frame`), `port/fast3d/gfx_opengl.cpp` (start/end_frame), `src/boss.c` (main loop) | #92/D339 perf probe: per-300-frame averages of render phases (pre / display-list / post / swap+pace), tris and draw batches per frame, game-thread busy time per scheduler message, and a GL_TIME_ELAPSED GPU timer (meaningful only when GPU-bound). | **live** | +| `GE_D75V` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_vertex`, end of the copy/transform loop; cached env check) | D75 (Nintendo logo) per-batch vertex probe: for each `G_VTX` batch in frames `lo..hi`, logs frame#, source pointer, n_vertices, original dest_index, raw ob[0..2] of the first vertex, min/max of the transformed x/y/z/w across the batch, and the MP translation row. Used to prove the transform is correct (M-195) and that every batch encodes v0=0 (slot-overwrite hypothesis). Value form `"lo-hi"` (e.g. `300-900` = the logo screen); unset = off. | **live** (D75) | +| `GE_D75D` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_vertex` G_VTX case + `gfx_sp_tri1` rejection/emit gates; cached env check) | D75 (Nintendo logo) per-frame draw-path probe (M-196): for frames `lo..hi`, logs one line/frame with the V/T token stream (`seq`), max triangle vertex index, cumulative vertices loaded, and per-triangle accounting — trivial-reject / backface-cull counts, how many tris reach GL, how many of those are on-screen (NDC bbox ⊂ [−1,1]²), invisible-flag count, vertex-color min/max, first oml word, depth z/w range + NaN count, texture-sampler presence (`utex` = GL shader, `cutex` = combiner), combine mode, prim/env color. Used to REFUTE the slot-overwrite hypothesis (DL is streamed, max index 15) and narrow the bug to a texture-modulate material rendering black. M-197 added logo-mode `lg_*` fields (sampled only on tris with the logo combine word — the frame-level `utex`/`cutex` are first-tri-wins and were the source of the "combiner claims 0" false anomaly): `lg_utex`/`lg_tex` + tile-0 descriptor + texture buffer address/id/size for the logo material. Value form `"lo-hi"` (logo window ≈ `244-320` on a bare front-end boot); unset = off. Companion one-shot `D75DCC:` log in `gfx_generate_cc` fires when `combine_mode==0x009ffe4f19ffe4f1` to decode the logo's combine slots. | **live** (D75) | + +| `GE_PDMOUSEAIM` | `port/src/input.c:487` (`pdMouseAimEnabled`) | Force on/off the PD-style mouse-aim model (D194 lineage); unset = default (on in N64 aim mode unless legacy GEPD aim is selected). | **live** | +| `GE_QUITFRAME` | `port/src/video.c:966` | D344 harness clean self-quit: exits after frame `` completes and swaps (replaces `timeout` kills; the host never exits under an in-flight frame). Cached. | **live** (harness) | +| `GE_ROOMPOOL` | `port/src/video.c:371` (`portRoomPoolScale`) | Scales the port's room pool by a float factor — before/after captures, and a user opt-out if the larger pool ever causes trouble on a level. | **live** | +| `GE_BINDPROBE` | `port/src/input.c:885` | One-shot dump of the resolved PC bindings after setup (D383 bind-capture verification). | **live** (diagnostic) | +| `GE_BINDCAPTUREPROBE` (+ `GE_BINDCAPTUREACTION`) | `port/src/optionsoverlay.c:1000,1005` | D383 isolated host→menu smoke probe: when the overlay force-opens, injects a scripted bind-capture keypress (ESC/DEL/F2). Test data dir must be private — capture persists the key. | **live** (diagnostic) | +| `GE_OPTIONSOVERLAY_SECTION` | `port/src/optionsoverlay.c:989` | Isolated screenshot/probe of a nested F10 page by row key (opens that header; never a menu mode). | **live** (diagnostic) | +| `GE_OPTIONSOVERLAY_SELECTBACK` | `port/src/optionsoverlay.c:995` | Screenshot-only: pins the selection to the Back row when the overlay auto-opens, to check it against a normally selected setting on the same page. | **live** (diagnostic) | +| `GE_OPTIONTREEPROBE` | `port/src/optionsoverlay.c:888` | D388: opt-in in-memory walk of the real F10 page tree; writes no settings, binds or profile bytes; restores UI state afterward. | **live** (diagnostic) | +| `GE_OVPOINTERMAP` | `port/src/optionsoverlay.c:1726` | D391 pointer-map diagnostic: on LMB-down (and every 120 samples) logs SDL mouse coords vs GL drawable pixel coords + the hover row, to separate hit-band/scroll errors from coordinate-space mismatch. | **live** (diagnostic) | +| `GE_FOOTERPROBE` | `port/src/optionsoverlay.c:2232` | One-shot log of the overlay footer/help-line metrics (x extents). | **live** (diagnostic) | +| `GE_FRONTOPTIONS_SECTION` | `port/src/frontoptions.c:295` | Isolated headless screenshot of a nested front-end options page by key (changes navigation only; never edits profile or config values). | **live** (diagnostic) | +| `GE_FRONTNAVLOG` | `port/src/frontoptions.c:430,573` | Logs each front-options navigation select (section/row/item number). | **live** (diagnostic) | +| `GE_FRONTOPTS_WIDTHS` | `port/src/frontoptions.c:673` | D406b: one-shot label-metrics log for the options rows (layout diagnostics). | **live** (diagnostic) | +| `GE_FRONTOPTIONS_AUTO` | `port/src/frontoptions.c:818` | D406 test hook: enters the options screen without a mouse click so headless runs (`GE_INPUTSCRIPT` D-pad stepping + `GE_FRONTNAVLOG`) can exercise it. Never active without the env. | **live** (diagnostic) | +| `GE_WSPROBE` (+ `GE_WSPROBE_FRONT`, `GE_WSPROBE_RESET`, `GE_WSPROBE_F10FRONT`) | `port/src/watchsettings.c:616,568,512`, `port/src/optionsoverlay.c:1566` | D354 watch-settings probe family: `WSPROBE[=tick]` logs before/after the queued settings commit one tick apart; `_FRONT[=tick]` drives a headless front-end change scenario; `_RESET` prepares an options-reset after 120 front ticks and verifies persistence once the stage drains; `_F10FRONT` fires a one-shot F10→front poll after the file-select save becomes available (avoids the joyDisablePoll deadlock). | **live** (diagnostic) | +| `GE_ZF` (+ `GE_ZFTOL`, `GE_ZFBOX`) | `port/fast3d/gfx_pc.cpp:1653-1657` | Z-fight grid probe: per tri emit, buckets the centroid cell and flags cells that fight ≥5 times (cap 600 hits); `ZFTOL`=tolerance, `ZFBOX="x0 y0 x1 y1"`=world-box filter. | **live** (diagnostic) | ## Dxx diagnostic probes — finding closed unless noted @@ -112,6 +140,12 @@ Two classes: | `GE_D318T` | `port/src/d318watchdog.c:270` (`#ifdef PORT`) | D318 watchdog timeline probe: per-tick log of the Facility ai_22 softlock signature window (c78 list/offset/attack state, c67 health) while the watchdog watches. Inert unset. | **live** (D318 verification) | | `GE_D322` | `port/src/audio.c` (`audioSetNextBuffer`), `src/snd.c` (`sndD322PoolSummary`) | D322 pool-telemetry probe: one line per 5 s of wall clock with the three pool layers a long session can exhaust — `sfx=count/max` (8-voice SFX soft cap), `evtq=n/64` (event queue occupancy; full = silent post drops, M-71), `pv free/lame/alloc` (24 physical synth voices shared by SFX + music; `-1`s before sndInit), plus the D204 pipeline fields. Cheap (~30 reads/s), campaign-safe; read together with `GE_D204=1`. | **live** (D322 capture) | | `GE_D318W` | `port/src/d318watchdog.c:136` (`#ifdef PORT`) | **Not a probe — the D318 port-side watchdog itself, ON by default.** Detects the Facility ai_22 hold-pose softlock signature (a latent N64 race, see findings.md D318) for 600 consecutive ticks and re-seeds c78's attack via the game's own fresh-attack entry point. `GE_D318W=0` opts out; logs `D318W:` lines when it acts. **Permanent fix, not strip-candidate.** | **live** (D318 fix — permanent) | +| `GE_D294` | `src/boss.c:498` (`#ifdef PORT`) | One line per stage reset: stage number + current MA malloc value (D294 pool investigation). | **live** (D294) | +| `GE_D294BANK` | `src/game/bg.c:605` | D294: restarts the bank-window counter on stage change (pairs with the `GE_D104` windowing). | **live** (D294) | +| `GE_D303` | `port/fast3d/gfx_pc.cpp:1717` | D303: logs up to 200 tri emits with per-vertex bbox + w-sum (degenerate-triangle hunt). | **live** (D303) | +| `GE_D307` | `src/game/prop.c:709` | D307: load-time dump of every CCTV record's inputs + derived orientation, for word-for-word PC-vs-ROM comparison. | **live** (D307) | +| `GE_D318B` (+ `GE_D318B_ALL`, `GE_D318B_CLK`) | `port/src/d318bprobe.c:91,97,210` | D318/D320 boundary probe suite (M-199): per-tick ground truth of the aim-hold/derail/anim-pin state machine for env-selected chrs (`"5,9"`, max 4); `_ALL` logs every ACT_ATTACK tick; `_CLK=N` locksteps N sim ticks per rendered frame. Read-only. **TEMP — remove when D318/D320 conclude.** | **TEMP** (D318/D320) | +| `GE_D329` (+ `GE_D329LOG`) | `src/game/model.c:3516,3519` (`#ifdef PORT`) | **The D329 anim-pin fix gate — ON by default** (unlike the other probes here): re-derives a stale pinned frameb when the endframe no longer blocks it. `GE_D329=0` A/B-disables; `GE_D329LOG` logs each trigger (cap 2000). Cached. | **live** (D329 fix) | ## Notes diff --git a/docs/dev/GRAPHICS-BACKLOG.md b/docs/dev/GRAPHICS-BACKLOG.md index 6f1a2bf4..c629fae5 100644 --- a/docs/dev/GRAPHICS-BACKLOG.md +++ b/docs/dev/GRAPHICS-BACKLOG.md @@ -10,7 +10,8 @@ once the game is playable start-to-finish without crashes. |----|---------|----------------| | ~~**D114/D116**~~ | ~~HUD text + ammo digits render each glyph individually X-mirrored~~ | **CLOSED — NOT A BUG (M-33, §F D168).** The `GE_PCDUMP` PPM writer never reversed `glReadPixels` rows, so every capture was upside-down; "mirrored" was a misread of inverted asymmetric content. The game renders correctly on hardware (developer-confirmed). Writer fixed (`gfx_opengl.cpp`). "Every stage verified non-mirrored" was correct — there was no mirror. | | ~~**D74 wrap-block**~~ | ~~`gfx_pc.cpp:1546/1551` PORT block is dead code (guard `cms & G_TX_WRAP` always false; indexes per-texunit arrays with the vertex index → OOB if reached).~~ | **RESOLVED (M-30, RC3/D167).** The in-vertex-loop wrap block was reworked and hoisted to a per-texunit pre-wrap window (`gfx_pc.cpp` ~1574, now `cms`-correct and indexed by texunit not vertex), behind the `Video.WrapFix` knob (default OFF). No longer dead code or OOB. See §F "RC3 · D167". | -| **D75** | ~~Nintendo logo mispositioned; gun-barrel Bond model absent~~; cast-roll character models — status unknown. (Rareware logo OK.) | **Shrunk (M-33, upright captures after D168).** The "overlaps D114 shared mirror" note is withdrawn. **Gun-barrel Bond RENDERS correctly** (walk + fire, upright) — the "absent" reports were the flipped capture. **Nintendo logo IS still broken** — renders as two overlapping white ellipsoids shifted left, no wordmark/geometry (a real `logoinst` model transform/geometry bug, not a flip). Cast roll not re-captured — recheck with `GE_PCDUMP="1400-2200:25"`. `findings.md` §F "D75 Bug 2 — M-33 UPDATE". | +| **D392** | In the opening attract-mode Train demo, a soldier consistently looks more hunched on PC than in 1964/GEPD; his hand intersects his body at the wrong position. User comparison: `docs/img/animation-glitch-pc-port.png` and `docs/img/animation-glitch-1964gepd.png`. | **FIXED — user visually verified (D392).** Train's normal opening (`-level_25`) has a central firing guard (`chr=5`) whose torso/shoulder aim offsets reached `aimupback=-0.855`, `shoulder=-0.606,+0.868` radians by tick 100. `chrlvUpdateAimendbackShoulders` used N64 `((f32*)arg1)[12/13/16/17]` offsets into a firing table whose first member widens from a 4-byte to an 8-byte pointer union on PC; it read *other* floats and bent the torso/arms to wrong angles. `#ifdef PORT` now reads `max_up/max_down/free_arm_frac_up/free_arm_frac_down` by name; with the same short Train intro probe the torso offset stays 0, shoulders ~0.028/+0.014 at tick 100. User's original comparison: `docs/img/animation-glitch-pc-port.png` vs `docs/img/animation-glitch-1964gepd.png`. The user subsequently confirmed the soldier's pose looked fixed in-game; this session's image tool itself cannot display PNGs. No ramrom demo override needed; ordinary `-level_25` reproduces. | +| **D75** | ~~Nintendo logo mispositioned; gun-barrel Bond model absent~~; cast-roll character models — status unknown. (Rareware logo OK.) | **Nintendo logo + GoldenEye logo + legal page FIXED (M-197, user-confirmed live 2026-09-21).** Root cause: the D50 sidecar converter (`tools_pc/d43_emit.py`) reserved + remapped the model's embedded texture-blob span but never copied the pixel bytes into the emitted RZ image → TEXEL0 sampled a zeroed buffer → the texture-modulate logo rendered black (M-195/M-196 had proven emission/transform/combiner all correct). Same latent bug hit `PgoldeneyelogoZ` + `PlegalpageZ`. Fix = tooling-only (blob raw-copy + round-trip check); ntsc sidecars regenerated; logo window now renders 23k px (7.59%) vs 0–33 pre-fix. Gun-barrel Bond renders correctly (M-33). Cast roll still unverified — recheck with `GE_PCDUMP="1400-2200:25"`. `findings.md` §F D75 (M-195/M-196/M-197 updates) + M-197 at file tail. | | **D76** | 2D disclaimer screen only partially drawn. | §F D76. | | ~~**D77**~~ | ~~No audible music on PC.~~ | **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. | @@ -25,15 +26,19 @@ once the game is playable start-to-finish without crashes. | ~~**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. | +| **D236** | Surface 1: the 2D billboard trees that appear at the start of the level and around the map don't draw as discrete tree sprites — they render as a wall/sheet of tree texture instead of the proper 2D tree objects. | **FIXED (M-198, pass 26) — awaiting user play-test sign-off.** Root cause was a texture-BINDING bug, not decode/alpha/mip/geometry: GE binds the four Surface 1 treeline cards (`tex1198`-`tex1201`, RGBA5551 64x17 cut-outs) as `TEXTURETYPE_DETAIL`, where `texHandleType1` (`tex.c`) loads the *detail* texture at TMEM 0 and the real base image at TMEM `texGetSizeInBytes(detail,0)` on tiles 1+. fast3d's `gfx_lod_tile_offset` folded every `G_TL_LOD` draw to tile 0 (D107, correct for TEXTURETYPE_LOD/MIPMAP), so the port drew the shared detail texture -- `tex2465`, an opaque 32x32 IA8 noise tile, which is exactly the 'noise painter' D267/D276 measured at 1023/1024 texels opaque -- and never imported the cards at all. Fix: when a tile pair differs in both TMEM address and `fmt`/`siz` under `G_TL_LOD`, sample the base image (tile 1). `Video.DetailBaseTile`, default on. Live-verified on Surface 1 (discrete snow-covered pines) and Surface 2; Depot clean; bunker1 golden bit-identical with the fix on/off; `type=1` bindings exist only in sevx/depo/sevb/sev, so every other level is provably untouched. Full trail: `findings.md` D236 pass 26; generalised as `porting-notes.md` §D12. | **D249** | Streets and Egyptian (the two biggest/most-open levels) show culling problems on far-away geometry at *default* FOV — distant geometry drops/pops incorrectly. Also a QoL ask: improve LOD in general, but gate it behind a toggle so N64-accurate ("old school") LOD stays selectable. | **Observed, not investigated** (M-119, user QA report). Distinct from D222 (high-FOV near-edge culling) and D218 (fog/far-clip tint). Two candidate mechanisms: (a) portal/room-visibility cutting off too aggressively at range on large multi-room layouts (D233/D234 neighbourhood — check for a far-plane/depth-range sibling to D233's w<0 trivial-reject fix), or (b) GE's own LOD/detail-swap system not ported faithfully (mip-tile / simple-model substitution at distance, `gfx_lod_tile_offset` family). First checks: `GE_PCDUMP` a long sightline on Streets/Egyptian where geometry visibly pops; determine which of (a)/(b) is firing before designing the fix or the requested toggle. §F D249. | | **D217** | 1P weapon viewmodel + character-model CI textures render with a wrong palette on many levels / multiple weapons — PP7 grip + hand render flat green on Facility & Depot, "weird or missing" gun textures elsewhere; Bond's watch face too. Surfaced by D215 (viewmodel now draws). Clean on Bunker1. | **Defect â‘  FIXED (M-90), pending human playtest eyeball — full detail in `findings.md` §H "D217 — M-90" and §F D217.** Root cause: weapon texpool arena bases land %8==4 on PC (memp bump drift from pointer bloat); `texAlignIndices` absolute 8-byte row padding then shifts every palette 4 bytes early while model-DL `G_LOADTLUT` reads the N64 offset (grip count=1 load lands on next tenant's header slot → `0x1A00` green; larger loads read a 2-entry zero pad — the M-86 tell). Fix: `texInitPool` rounds the base up to 8 under `#ifdef PORT` (no-op on N64); 21/21 sweep PASS. Two defects: â‘  **visible green = misaligned texpool arena** (fixed as above). â‘¡ **CI texture-cache collision** (no palette-content key) — proven, **fix landed on branch `fix/d217-model-texture-palette` `f6fc0399`** (palette FNV-1a hash in the CI cache key), owes a 21-level `verify.sh sweep`; does not fix â‘ . | | **D174** | "No blood effect" — user reports no visible blood when shooting guards / into surfaces. | **User QA report; likely D120, unverified.** Decals: the D120 `PointUsage[]` converter fix (M-30) was never interactively verified and only takes effect after a full sidecar regen (`d43 && d69 && d88 --regen`) — check whether the playtest build had regenerated sidecars before concluding the fix failed. Spray: D172 observed the particle spray *drawing* (wrong color, magenta/cyan) on Silo — if the user sees no spray either, that's new info (different weapon/range, or the D172 decode is worse than "wrong color"). First step: BUNKER1 firefight with regenerated sidecars; if decals still absent, instrument the `chr.c:3328` decal walk. | | **D173** | Third-person Bond model (the player-representing figure shown at level start) spawns too high — floats well above the ground "a lot of the time" while the actual player spawn is correct. Same symptom on the Cuba end credits: JB stands ~6 ft in the air above Natalya. | **Confirmed still present across the full 21-level playthrough (M-49).** **Observed, not investigated** (user QA report). D75 animated-model family but a *position* defect, not absence — M-33 confirmed the gun-barrel Bond renders fine upright. Suspects: (a) intro/credits puppet chr spawn position vs the model's base transform; (b) anim root-motion accumulation (`modelSetAnimFrame2WithChrStuff`, D156 area) offsetting the rendered joints above the body origin; (c) a `coord3d`/packed-float decode in the intro camera/model setup. "Player spawning is OK" localises it to the render-side model, not `g_CurrentPlayer` state. | -| **D182** | SELECT FILE screen degrades after navigating away and back (main menu → file select, or backing out of file select and re-entering). On the fresh screen the 007 gun-barrel spiral / Bond silhouette renders behind the file boxes; after a round-trip **the Bond image stops drawing entirely** and the whole background gains the **comb / interlacing** artifact (horizontal line scramble). File boxes, text and the Copy/Erase icons still draw. | **Observed, screenshot committed** (M-35, user QA; `docs/img/bugs/d182-file-select-backout-20260902.png`). Two symptoms, likely linked: **(1)** background 3D image stops rendering after re-entry → a front-end model/DL whose `render_pos` / segment binding is only valid on first construction (D149 / D75 `constructor_menuXX` family — the dossier/wallet models have the same "renders once" behavior). **(2)** interlacing = **M-36 triage: Family A**, the `import_texture_*` `line` vs `full_image_line` shear (see D176(b) / `docs/dev/M-36-TRIAGE.md`) — **not** the D159 `texSwapAltRowBytes` path (that is a full PORT no-op). Open question: why only *after* a round-trip — first import may land `line == width`, re-import may not, or the tile descriptor is stale on re-entry. Repro: `GE_STARTMENU` to file-select, back out with ESC/B, re-enter; `GE_DTEX` on the spiral texture across both visits. Shares defect (2) with D176(b). | +| **D182** | SELECT FILE screen degrades after navigating away and back (main menu → file select, or backing out of file select and re-entering). On the fresh screen the 007 gun-barrel spiral / Bond silhouette renders behind the file boxes; after a round-trip **the Bond image stops drawing entirely** and the whole background gains the **comb / interlacing** artifact (horizontal line scramble). File boxes, text and the Copy/Erase icons still draw. | **Observed, screenshot committed** (M-35, user QA; `docs/img/bugs/d182-file-select-backout-20260902.png`). Two symptoms, likely linked: **(1)** background 3D image stops rendering after re-entry → a front-end model/DL whose `render_pos` / segment binding is only valid on first construction (D149 / D75 `constructor_menuXX` family — the dossier/wallet models have the same "renders once" behavior). **(2)** interlacing = **M-36 triage: Family A**, the `import_texture_*` `line` vs `full_image_line` shear (see D176(b) / `docs/dev/M-36-TRIAGE.md`) — **not** the D159 `texSwapAltRowBytes` path (that is a full PORT no-op). Open question: why only *after* a round-trip — first import may land `line == width`, re-import may not, or the tile descriptor is stale on re-entry. Repro: `GE_STARTMENU` to file-select, back out with ESC/B, re-enter; `GE_DTEX` on the spiral texture across both visits. Shares defect (2) with D176(b). **2026-09-27 (D397):** user re-reported the comb/interlace on the file-select background AND on the **intro gun-barrel** — the latter is a *first-time* render, so the defect is persistent (not only after a round-trip), refuting the "stale descriptor on re-entry" hypothesis. D159 (odd-row swap) is no-op'd and D183 pitch shear is disproven for level_36 + a no-op for the 299×440×1 row-strip. Next step is a live `GE_PCDUMP` capture + ROM ground-truth decode of the gun-barrel RLE image to separate ground-truth dithered content from a GL sampling/texture-cache artifact. See findings.md D397. | | **D197** | Character face/head textures wrap wrong — the face texture repeats around the side of the head "and continues weird" instead of clamping to the front. Seen on **Silo** (M-49 playtest), intermittent ("sometimes"). Reads as **`G_TX_CLAMP` not honoured** for the character-skin tile in fast3d (→ `GL_REPEAT`, so the face UV region tiles around the skull), or wrong `shifts`/`shiftt` / LOD-tile selection on the head texture, or the D167 non-PoT wrap-period issue (head textures are often non-PoT) hitting skinned models. | **Observed, not investigated** (M-49, user bug report — needs a capture + which chr/level). Distinct from D176(b)/D182(2) (those are *bg wall* pitch shear). Related: RC3/D167 (`Video.WrapFix`, non-PoT wrap period) + the `texWriteTileLods` `shifts=shiftt=lod` note in findings §F "D176(b) → 4. candidate 2". First checks: does `gfx_pc.cpp` map N64 `cms/cmt` `G_TX_CLAMP` → `GL_CLAMP_TO_EDGE` for character DLs; is the head tile getting the base tile vs an LOD tile (D107); try `Video.WrapFix=1` on a repro. | | **D195** | Transparency / alpha-blended surfaces render wrong — reported on **Control** (Severnaya bunker basement — Natalya's console room; heavy on glass partitions and the translucent projected/holographic wall displays). Exact failure mode not yet pinned by the reporter (M-49 playtest): candidates are alpha-blended geometry drawing fully opaque, translucent panels drawing invisible, or draw-order/sort so translucent surfaces occlude what's behind. | **Observed, not investigated** (M-49, user bug report — needs a `GE_PCDUMP` capture + which surfaces). fast3d render-mode / combiner path. Same neighborhood as D161 (CI8 tile + `G_TT_NONE` → garbage), D172 (particle-sprite channel order), D176(b) (texture pitch shear) — but this is a *blend/alpha* defect, not a texture-decode one. Note D128 already touched Control's `specialportalarray` (portal struct stride) — check whether the affected surfaces are portal-adjacent. First checks: does `gfx_pc.cpp` honour `G_RM_*` XLU render modes + `G_AC_THRESHOLD`/`G_CC` alpha for these DLs; is `gDPSetRenderMode` `w1` being parsed (cf. findings §F "zbuf-clear w1 scribble"); is Z-write-vs-blend ordering right for XLU surfaces. Get the reporter to say: opaque-where-should-be-clear, invisible, or wrong edges? | | **D252** | Particle effects cycle through a rainbow palette — bullet-impact sparks and lingering smoke/explosion residue drift through the full hue wheel over time (~10 shots to sweep it) instead of holding their intended grey/orange palette (M-124, user playtest). Full mechanism + suspect in findings §F **D252**: colour comes from 15 distinct `IMAGE_SMOKE_N`×`IMAGE_FIRE_N` (RGBA16) pairs the particles age through; prime suspect is `import_texture_rgba16` deriving width/height from the tile line size (32×7 instead of 16×14 for the fire tiles), scrambling each stage's sprite into a different wrong hue. | **Documented only — not fixed for v0.2.0-pre** (user instruction). Confirm with one dumped fire tile vs raw `IMAGE_FIRE_N` bytes, then derive width/height from the LOADBLOCK dtx/dlh. D172/M-82 lineage. | +| **D323** | Distant geometry on 15 levels samples driver-generated mips from the finest texture level; N64 samples Rare's hand-authored mip chain for explicit-LOD textures bound under `G_TL_LOD` (heaviest: Statue 378 binds, Aztec 806). Overwhelmingly CI8/IA8 — subtle fidelity gap, no reported artifact; the only RGBA5551 cut-outs are the D236 set (already handled). | **OPEN / parked** — mechanism confirmed, census in `tools_pc/bg_lod_audit.py`, fix path documented in findings §F **D323** (upload GL mip chains for TEXTLOD-on bindings). Not worth doing speculatively; revisit only if a playtest flags mushy distant detail. | +| **D335a** | **RESOLVED 2026-09-23.** Gun-barrel backdrop (Bond's spotlight) leaked into the pillarbox bars while sliding in from off-canvas. Fix: `menu_jump_constructor_handler` emits a full-canvas `gDPSetScissor` right after the CENTER wrap (only when wider than 4:3), which fast3d squeezes to the pillarbox, so off-canvas draws clip at the 4:3 edge as the N64 RDP clips at the screen edge. Verified with a 16:9 barrel capture. | +| **D335b** | Native widescreen: the F10 options overlay still stretches with the window (legible, pre-existing look). Pillarboxing it needs `optionsoverlay.c`'s click mapping (`gfx_get_ui_screen_rect`) remapped into the centred region in step. Polish, post-playtest. | +| **#105** | Dam: uncovered water-reflection pixels show the procedural cyan fill instead of the stage sky (residual after the water/wall-reflection work; visible boundary between reflected geometry and sky in the settled Enhanced/Remaster capture). | Backlogged 2026-09-28 from [issue #105](https://github.com/jkdansereau/goldeneye-pc-port/issues/105); post-v0.4.0 graphics queue. Note: the issue's "D329" cross-ref is the issue author's own numbering — our D329 is the anim-pinning fix. | | **1P weapon viewmodel** | **RESOLVED (M-83, D215).** In-level first-person weapon models drew nothing. Root cause: `gunRenderFirstPersonGunModels` built its `ModelRenderData` via `*(ModelRenderData*)&D_80035CC0` — a reinterpret across two separately-declared adjacent globals that only lands right on the N64 linker + 4-byte pointers; on x86-64 `renderdata.flags` read 0, gating every weapon DL node off in `modelRenderNodeGundl`. All 3 ranked suspects in VIEWMODEL-RESEARCH.md were ruled out first by the `GE_DVM=1` probe. Fixed `#ifdef PORT` (explicit field init). `subdraw` 1→153 gfx cmds; owed one BUNKER1 eyeball. | ## Resolved (for reference) diff --git a/docs/dev/M7-ALLUNLOCKED-PLAN.md b/docs/dev/M7-ALLUNLOCKED-PLAN.md new file mode 100644 index 00000000..3c78d17e --- /dev/null +++ b/docs/dev/M7-ALLUNLOCKED-PLAN.md @@ -0,0 +1,45 @@ +# M7 — Reversible AllUnlocked: investigation and fix plan + +**v0.4.0 release decision (2026-09-28, user):** ship AllUnlocked as a +**loudly-documented, EXPERIMENTAL, irreversible** option for this release rather +than hiding/disabling it or landing a safe architecture. The toggle stays (default +OFF); the in-game row is now labelled **"All unlocked (EXPERIMENTAL)"** with the ON +value displayed as **"ON - UNSAFE"** in both the F10 overlay and the front PC +Options screen, and the README Known-issues bullet already tells users to back up +`data/ge007.eep` before enabling it. A proper fix (Candidate A port-only shadow or +Candidate B non-mutating virtual query) remains the v0.5.0 work item. + +Status: **investigation, NO FIX APPROVED OR LANDED**. Root cause/repro: `docs/dev/findings.md` D387; original feature: D257, fresh-save repair: D281, old-CRC migration: D297. This plan supersedes the M7 "no-code proof" in `docs/dev/notes/MODERN-OPTIONS-PLAN.md`. Follow `AGENTS.md` rule 2 and `docs/dev-process.md` §7 before any `src/game` edit. + +## User-visible contract to settle + +Recommended: OFF (after restart, as presently documented) must display only genuinely earned mission progress/cheats; ON may *temporarily* expose every level, difficulty, 007 mode and cheat, without modifying durable progress. Achievements **earned while ON without an active cheat** should survive OFF, including difficulty inheritance, timed cheats and profile operations. Enabling/disabling must never silently delete existing genuine save records. Do not auto-repair an EEPROM already containing baked fake unlocks: the bytes lack provenance. The user has not explicitly signed off on a game-code alternative. + +## Confirmed facts (source + isolated data) + +- `port/src/libultra.c:geEepromRW` first migrates legacy CRCs on the five-slot block-4 read, then `geEepromPatchAllCheats` fills unlocked-cheat bits and every zero completion time with 0x3FF and re-CRCs a **temporary copy**. `fileValidateSaves` (`src/game/file2.c:527`) accepts that copy into `saves[]`. `fileWriteSave` (`file2.c:86`) writes the entire 96-byte live slot, not only the setting/progress that changed. +- Private data under `AppData/Local/Temp/ge-m7-20260927/`: the older `data/ge007 copy.eep` has two genuine times and only cheat `04 00 00` in active folder 0 (slot 4). ON or OFF boot without a save yields identical EEPROM bytes after the same legacy CRC migration. ON plus existing `GE_WSPROBE_FRONT=600` performs an ordinary Music-volume save to slot 4: cheats become `ff ff 0f`, 58 empty times become 0x3FF, two real times remain. OFF + restart validates the written CRC and retains all fakes. Full repro/logs: D387. Current `data/ge007.ini` and `data/ge007.eep` were never opened for write; copies and SHA-256s in that private test directory. +- `main.c` seeds debug unlock flags only at boot; an in-session OFF toggle leaves those RAM flags set until restart. `Game.AllUnlocked` defaults OFF; the relevant UI currently gives no explicit restart warning. +- More than a write-mask problem: a successful mission runs `end_of_mission_briefing` (`src/game/file.c:41–68`) → `fileUnlockStageInFolderAtDifficulty` (`file2.c:828`) → `fileOverwriteSaveSlotWithNewSave` (`file2.c:794`), **rotating the profile to a free EEPROM slot** and freeing the old slot. `fileCheckSaveStageDifficultyTime` only updates if the real time beats the synthetic 0x3FF. Genuine lower-difficulty completions and long (>1023-second) runs can also have legitimate 0x3FF times; value alone is not provenance. Fake cheat bits cause `fileGetIsCheatUnlocked` to return true, so `end_of_mission_briefing` skips a genuinely earned timed-cheat award. A port-only sanitizer keyed by physical slot or by `time==0x3FF` is incorrect. +- The current user EEPROM already has two nonfree slots with 60/60 max times and all cheat bits, consistent with an earlier write-back. Two separate older EEPROM backups exist in `data/`, but replacing or auto-merging them would risk later real progress; do not touch or auto-repair originals. + +## Architecture decision before code + +**Candidate A — port-only EEPROM shadow/merge:** keep authoritative *raw* progress per logical profile outside patched `saves[]` and translate every write to durable real-only state. Must cover direct settings writes, profile rotation, copy/delete/reset, earned times (including 0x3FF), timed cheat grants currently suppressed by fake reads, five-slot CRCs, and old-CRC migration. Physical-slot-only interception demonstrably fails. A read/write-only adapter may have insufficient information to recognize some genuine completions or cheat awards; prove a full event/provenance model in an isolated prototype before promising this path. No new sidecar that can be lost/desynced without a recovery design. + +**Candidate B — non-mutating virtual query:** retire `geEepromPatchAllCheats`, keep raw `saves[]` and EEPROM intact, and make the *opt-in* UI/gameplay availability queries report AllUnlocked without changing saved bits/times. Likely small PORT-gated touch points: `fileGetIsCheatUnlocked` (`src/game/file2.c:391`, existing D225 developer-only query override), `fileIsStageUnlockedAtDifficulty` (`file2.c:681`; cheat-enabled mission selection requires `STAGESTATUS_COMPLETED`, not merely the debug-flag `UNLOCKED`), and the raw earned-cheat check in `end_of_mission_briefing` (`src/game/file.c:41–68`), which must still record real cheat awards while virtual availability is ON. Audit every caller and the Rare/PD analogue first. A live-only toggle may require restoring debug flags safely; simplest contract is **takes effect on restart** and an explicit UI label. This alters opt-in `src/game` behavior and is **not** the ABI/layout exception: no implementation or implicit sign-off. If port-only is ruled out, prepare the exact file/line, reference, reason, scope and precedent required by `docs/dev-process.md` §7; obtain explicit user approval and document the grant in a new RULE-2-SIGNOFF finding before edits. + +**Release fallback:** if neither safe architecture is ready, do not present the current toggle as a reversible enhancement. Get an explicit release decision on hiding/disabling it vs shipping a loudly irreversible option; default-off alone does not protect users who toggle it on. Keep existing saves intact. + +## Test matrix (private files ONLY) + +1. Capture baseline hashes of copied current ini/EEPROM; separate valid older-progress, fully fresh zero EEPROM, already-contaminated, legacy-CRC, and mixed/multiple-folder fixtures. Do not copy old saves back into `data/`. +2. No-write ON boot then OFF restart: raw five slots unchanged except *explicit* D297 migration; correct UI availability in both states. +3. ON settings writes via `GE_WSPROBE_FRONT` (normal slot), in-stage F10 watch writes, and save-file chooser/reset: only real settings/intentional reset persist; CRC valid; no synthetic completions/cheats in raw file. +4. Real mission completion at agent and higher difficulty (including inherited lower difficulty), fast timed-cheat award, slow/capped 0x3FF completion, and cheat-enabled mission selection. Verify actual new earned records after OFF restart, no artificial ones. Build an isolated gameplay harness if manual replay is impractical; do not use a settings-write test as a substitute for mission completion. +5. Save-slot rotation (`fileOverwriteSaveSlotWithNewSave`), profile copy/delete, all four folders plus free slot, and reopen across process restart. Confirm all five CRCs via `GE_SAVELOG=1` and raw bit-level diff; compare copied baseline profile identity rather than just physical slot index. +6. Default-OFF NTSC/PAL/JP builds and post-landing 21-level default-render comparison; config/EEPROM originals hash-verified **after** all tests. Human test of ON/OFF UI text and behavior. Existing contaminated saves need an explicit *separate*, opt-in recovery plan; never silently rewrite them. + +## Other wave item + +M5 remains under user review: FPS cap and MSAA are independently exposed already; dropping the two-setting Low-end preset is a separate decision, not a dependency of this M7 investigation. diff --git a/docs/dev/OPTIONS-MENU-PLAN.md b/docs/dev/OPTIONS-MENU-PLAN.md index be3c7eed..cfadcc5a 100644 --- a/docs/dev/OPTIONS-MENU-PLAN.md +++ b/docs/dev/OPTIONS-MENU-PLAN.md @@ -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 diff --git a/docs/dev/QOL-INVENTORY.md b/docs/dev/QOL-INVENTORY.md index cf528d02..590ae158 100644 --- a/docs/dev/QOL-INVENTORY.md +++ b/docs/dev/QOL-INVENTORY.md @@ -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. | diff --git a/docs/dev/RUMBLE-PLAN.md b/docs/dev/RUMBLE-PLAN.md new file mode 100644 index 00000000..a7375309 --- /dev/null +++ b/docs/dev/RUMBLE-PLAN.md @@ -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 || !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.** diff --git a/docs/dev/V0.4.0-PLAN.md b/docs/dev/V0.4.0-PLAN.md new file mode 100644 index 00000000..135ac554 --- /dev/null +++ b/docs/dev/V0.4.0-PLAN.md @@ -0,0 +1,515 @@ +# v0.4.0 Plan — local working document (NOT pushed; update as items move) + +**2026-09-27 user release decision:** defer M7 / AllUnlocked save-safety work +for v0.4.0 (D387); do not ask for a live-save test or treat the feature as +safe. Before shipping, hide/disable the unsafe toggle or explicitly agree +on a release limitation — default OFF alone does not prevent a user from +turning it ON. The user will do a full campaign playtest *instead of* the +21-level automated sweep; record the campaign result as the release gate, +not as a sweep PASS. **Playtest interruption D393/D394:** physical-pad +A use / X reload / Y weapon cycle was user-tested. After the user supplied +the StrategyWiki Xbox 1.1 Jinx mapping, D394 moved crouch to either stick +click, LB to aim, B to a port-side owned-gadget cycle, and retired LB/RB +weapon cycling. Build linked; user physically confirmed the new button roles work, including +stick-click crouch, LB aim, RB no weapon cycling and B gadget cycling (the +N64 port has no separate gadget HUD icon). **D395:** both controller-accessible +binding menus now refuse pad-initiated keyboard/mouse capture and allow B to +cancel an active modal, pending pad-only retest; this is not gamepad rebinding. +**D396:** F10 B now backs/closes, A selects/toggles without changing sliders; +left/right adjusts sliders. NTSC linked; physical pad retest owed. Inspect B's item category/order during campaign play if multiple mission +items are owned: the current ID-range filter includes some mission objects. +RB's remastered-graphics toggle cannot be replicated with N64 assets, and +Back remains the PC options shortcut. + +Created 2026-09-21 from a fresh triage of `findings-index.csv`, +`GRAPHICS-BACKLOG.md`, `docs/BACKLOG.md`, `ROADMAP-1.0.md` (M-139 phase order), +`QOL-INVENTORY.md`, and the 2026-09-20 HANDOFF. + +**Release goal (user-set):** fix everything that still makes the port not 1:1 +with the N64 original; *then* improvements outside the OG game, plus stability. +Sequencing follows M-139's R → Q → F order, split into four phases below. + +## Baseline + +- **v0.3.0 is shipped** (tag on origin at `4827bc86`); `main` is 7 docs-only + commits ahead. Branch `release/v0.4.0` off `main` when Phase 1 starts. +- All 20 solo missions (plus the ending-credits sequence) load/render/crash-free; campaign completable end-to-end + (Agent, playtested). Steady 60 fps. Audio done. Saves done. +- Known-issue list = README "Known issues" section (D303, D245, D308/ + D306, D307, D311, D318 disclosure, widescreen-stretch, D249, + D240/D241, D257/D259/D281 AllUnlocked caveat). ~~D252~~ retired (M-188), + ~~D75~~ fixed (M-197, user-confirmed), ~~D236~~ fixed (M-198, pass 26 — + awaiting play-test sign-off). + +## Triage of every open item (as of this triage) + +### Bucket A — Fidelity (port diverges from N64 behavior/appearance/sound) + +| ID | Item | State / notes | +|----|------|---------------| +| D320 | 17 aim-hold AI lists in 8 levels carry the as-authored D318 softlock race; Facility `ai_19`, Control `ai_9`, Depot `ai_12` high/med-high | Static sweep done; **live probe-verification owed** (GE_D318T-style timeline on the 3 lists). If one freezes: generalize `d318watchdog.c` (parameterize list/offset/chr/entityid), not per-list copies. N64 softlocks permanently — watchdog stays a port-side recovery, disclosed like D318. | +| D252 | Rainbow particles still intermittent on both platforms after the vertex-buffer fix | Second cause unrooted; prime suspect `import_texture_rgba16` fire-tile width/height from tile line size (32×7 vs 16×14). README known issue. | +| D245 | Water seam between two patterns, moves with view (REOPENED) | M-142 probe: tc-shift k flips during ordinary rotation; opt-in `GE_D245_FIXEDSHIFT` candidate build-verified, needs A/B vs N64 reference before making it default. | +| D303 | Wrong extra long muzzle flash (M16 + others) | **Investigated — pipeline + data ruled out.** Flash path (`gunUpdateAndFire` → `rwmtx[1]/[2]`, `dorottex` DLPRIMARY arms) is byte-identical game code; M16's `MuzzleFlashExtension`=6.0 is the legitimate max stat; Gm16Z flash-node data byte-faithful through d43. Headless runs can't fire weapons (guards don't shoot an idle player), so a TEMP `GE_D303=1` probe in `gfx_pc.cpp` logs every very-tall-thin emitted triangle with full RDP state — **blocked on a live firing capture** (playtest brief item 17). | +| D306/D308 | Z-fighting: Dam intro/truck wheels + general/broader instances | **Investigated — real fighting confirmed, port-vs-faithful open.** Depth precision is equivalent to N64 (24-bit both sides; GE's RSP writes full 16.16 post-transform coords, not s16), so level-authored coincident geometry is the prime suspect and likely fights on N64 too. TEMP `GE_ZF=1` flicker probe in `gfx_pc.cpp` (recurrence-filtered; `GE_ZFBOX` region filter) shows sustained oscillation (counts ~1000) in the Dam-intro truck region. Closing it as faithful needs an N64 reference of the same spot; a PC-only mitigation (float depth) would diverge from N64 and needs a rule-2-style decision. | +| D307 | In-level security camera props face backwards on some levels (Bunker) | **Investigated 2026-09-21 — no port-side bug found.** Full orientation path traced (`setupCctv` → per-frame `RotY(unkC8)×unk84`); every input verified N64-faithful against the ROM (PcctvZ switch data, boundpads incl. the legitimate load-time room-scale ×1.8542, record config words); all math helpers are byte-identical `src/game` code. Remaining suspect if it genuinely differs from N64: fast3d handling of the non-orthonormal lens matrix. **Blocked on user repro detail** (which camera/level + screenshot + ideally N64 compare). `GE_D307=1` diagnostic kept in-tree. | +| D236 | Surface 1 billboard trees render as a solid wall of texture | **FIXED (M-198, pass 26, `425237f9`):** root cause was a texture-BINDING bug — `TEXTURETYPE_DETAIL` cards drawn from the detail tile (opaque IA8 noise) instead of the base image; `gfx_lod_tile_offset` now samples tile 1 when the pair differs (`Video.DetailBaseTile`, default on). Live-verified Surface 1/2 + Depot; other levels provably untouched. Awaiting user play-test sign-off; probes stripped (`0503d335`). | +| D235 | Wrong textures after death/restart (level reload), multiple levels | **Headless verification 2026-09-23 (`2a6bc2bb`)**: fix clean on reload; bug NOT reproduced even with the fix off. Needs the real die → mission select → restart A/B (`GE_D235_OFF=1`). | +| D288 | Silo intro: huge flat untextured triangle sweeps the frame (frames ~562–741 of bare `-level_20`) | **Does NOT reproduce on the current build (2026-09-23, `30cf76b8`)**; D271 A/B ruled out as the fixer. Close if the Silo playtest does not see it. | +| D294 | Statue: CCCP monument ground renders solid black, self-recovers in 10–30 s | **FIXED pending user playtest** (2026-09-22). Note: its "3× pool corrupts anim" symptom was D336, so the 2× cap can be revisited. | +| D291 | Dam: texture glitches when the truck drives past | Not investigated. | +| D249 | Distant geometry drops out on Streets/Egyptian at default FOV | M-142 ruled out far-clip + LOD-multiplier; narrowed to portal/room-visibility BFS. **First check overlap with D271's portal-culling fix** (branch state, see Phase 0). Needs live screenshot+location. | +| D195 | Control: transparency/alpha-blended surfaces render wrong (glass partitions, holographic displays) | **FIXED** (index: gfx_sp_vertex fix) — plan row was stale (2026-09-23 reconcile). | +| D197 | Character face textures wrap around the head instead of clamping (Silo, intermittent) | **CLOSED** (user playtest v0.2.0-pre) — plan row was stale (2026-09-23 reconcile). | +| D182(2)/D176(b) | File-select background degrades after round-trip (comb/interlacing); Surface tree-line static — shared "texture pitch/shear" defect | M-36 Family A; `import_texture_*` line-size assumption. Distinct from the disproven D183 hypothesis. | +| D176(a) | Sky: RDPHALF tri-raster decode; horizon-band coverage (Defect 2) still open | Path B draft in `scratchpad/sky.patch`, phase-fold fix applied, **never visually verified** — one `-level_22` screenshot owed. Branch `land/d176a-sky-perspective-scroll` exists (forgejo remote) — audit + port to main. | +| D75 | Front-end logos: Nintendo logo degenerate (tiny orbiting fragment); Rareware filtering wrong | **FIXED (M-197, user-confirmed live 2026-09-21)**: D50 sidecar converter dropped embedded texture blobs. Remaining: pal/jpn sidecar regen on ROM-holding machines. | +| D290 | Watch in-game "controller" section shows no control-pad graphic | **FIXED 2026-09-23 (`a5804d10`)**: D264-pattern template read in `watchRenderController` + 3 pointer-width/raw-Gfx sites. Sibling sweep also found + fixed **D331** (shell casings never rendered, `94dff811`). | +| D174/D172 | Blood: decals never interactively verified (D120 needs sidecar regen); spray color fixed but owed an eyeball | BUNKER1 firefight with regenerated sidecars (`d43 && d69 && d88 --regen`). | +| D240/D241 | Gunshot SFX cadence drifts; PP7/AK47 goes silent under heavy fire near looping sounds | D241 high-confidence = D207 reproduced (check D207 entry status first — index row is stale/ambiguous). Fold into the D207 voice-cap family. | +| D322 | Long-session audio degradation (issue #87, v0.3.0 full campaign): from Silo onward audio worsens; by Caverns/Cradle OST inaudible + SFX "come and go"; restart cures → session-accumulating state | OPEN — static triage done (teardown audit clean, mixer stateless, D202 leak class covered only for ownerless `count==-1` loops and only validated single-level; D305 already records a pool/list desync). Ranked: voice-pool exhaustion/counter drift > queue starvation > evtq saturation. `GE_D322` pool-telemetry probe shipped 2026-09-21; needs a campaign capture with `GE_D322=1 GE_D204=1`. | +| D311 (residual) | Upstream anim-poison source still open (freeze class guarded) | **Likely root-caused by D336** (head-bob alias read); headless Caverns clean at 2× pool. Close after one live Caverns combat session. | +| D258 | PAL/JP asset conversion broken at the source-data level | Repair path verified, not applied. Region fidelity — NTSC-only users unaffected, but it blocks "faithful in every region" and any JP/PAL ROM use. | +| D160 (residual) | Cutscene reliability PARTIAL (D243 family mostly fixed via M-189/M-190) | Residual = whatever the live GE_D160/GE_D243 campaign pass shows. | + +### Bucket B — Stability (crashes/freezes/hangs; not N64-parity per se) + +| ID | Item | State / notes | +|----|------|---------------| +| D289 | Dam computer-room crash (Deck, one occurrence, never repro'd) | Needs a human re-test on a current build (D255+). If it never recurs → close as untracked. | +| D287 | Windows: real, non-self-recovering freeze after ~15–20 min idle + click-back | One confirmed occurrence; `gfxFrameMsgQ` full is a lead. Consider crash-dump wiring on Windows for the next hit. | +| D286 | Deck: ~8 s self-recovering frame freeze on Caverns | One confirmed occurrence, no repro trigger. | +| D175 | Brief stutter/hang (door-open on Runway/Surface) | Matches known transient-hang classes; reproduce under `tools_pc/debug.ps1` idle-box before believing it's new. | +| D285 (residual) | Audio-thread race fix was PARTIAL | Re-check the thread-lock state vs D305's NULL guard coverage. | +| D281 | AllUnlocked breaks audio + RMB aim when enabled with zero saves written | User-facing mitigation shipped; proper fix = make the feature robust or gate it harder (see Phase 3 decision). | +| — | getenv caching in hot paths (**DONE 2026-09-23, `d9d7cd35`**) (post-release cleanup, HANDOFF M-192) | D302 fixed one instance; sweep for stragglers. Cheap perf/stability hygiene. | +| D117 | Frame nondeterminism (GE_DETERM attempted, not achieved) | Parked as a research item; do not block the release on it. | + +### Bucket C — Improvements / QoL (outside OG game; after fidelity) + +| ID | Item | Notes | +|----|------|-------| +| Widescreen | Currently **stretched 4:3**, not native — README discloses this | **DONE 2026-09-23 (D334 world `d5d9a36a`, D335 HUD/front end `f62bceaf`)**: native at any aspect, default on, F10 toggle; Rule-2 approved. Owed: user HUD playtest at 16:9/21:9; D335a cosmetic. | +| D237 | F10 overlay: mouse-wheel scroll, more settings, category grouping | **DONE 2026-09-23 (`91bd0f84`)**: F10 category headers + 5 more rows (wheel scroll already existed). | +| D314/D315 | F10 flicker (user-deprioritized) + main-vs-in-game menu scaling | D315 deferred from v0.3.0; bigger design question — lands naturally with the overlay work. | +| D238 | Mouse sensitivity rework (master sens, RMB aim sens, Y-gain parity) | Partly superseded: aim modes folded (D337, N64 default via PD integration; Centred (PC) opt-in), Aim range PC/N64 (D338). Remaining: sensitivity feel tuning. | +| D239 | Click-to-lock parity: delete always-grab mode 0 + config key | Small; closes D192 by deletion. | +| GEPD controls (M3) | **IMPLEMENTED, targeted playtest owed (D378/D380/D383–D386).** One effective keyboard/mouse layout, no `Input.Layout` selector: Ctrl crouches without aim; E interacts without reloading; R reloads without interacting. Input → Bindings → Movement/Actions edits two Primary/Secondary keyboard-or-Mouse 1–5 slots in both UIs (D388). Mouse 3 (right) is Aim default; wheel still cycles weapons. Menu arrows/Enter/Escape and mouse clicks remain fixed. See `MODERN-OPTIONS-PLAN.md` M3; earlier preset/B-button text there is historical. | +| Modern controller presets (D394) | **1.1 Jinx button roles physically confirmed; precise gadget category still open**: A action, X reload, B cycle owned gadgets, Y cycle weapons, L3/R3 crouch, RT fire, LT/LB aim, D-pad movement, Start pause. RB updated-graphics and Back scores are XBLA remaster-specific: no HD assets/score view in the N64 port; reserve RB, keep Back→PC options. **Planned** named selector: 1.1 Jinx (L move+strafe, R look+turn), 1.2 Christmas (L move+turn, R look+strafe), 1.3 Frost (L look+strafe, R move+turn), 1.4 Elektra (L look+turn, R move+strafe). Face/trigger roles remain fixed across schemes. Add an `Input.ControllerPreset` enum with default 1.1, F10/front-options selector, preserve per-pad inversion/deadzone, separate keyboard `NaturalPitch` from the pad selector, adapt both stick axes and D-pad per scheme, and verify in watch/menu/tank and physical controller before labeling each selectable. Do not treat these four presets as already implemented. | +| Modern options wave (parity set, 2026-09-27) | **In progress, not a single opt-in Modern page (D388).** M1: Input → Bindings and Gameplay → HUD, shared by F10/front; M2: Original red crosshair plus White/custom RGB, style, size and visibility implemented (D379/D381/D382); M3: single effective layout and rebinding (above). **M5: redundant Low-end toggle removed (D388); Frame rate cap and Anti-aliasing remain independently adjustable, stored values preserved on migration.** M4 GE+ knockback/hitstun/damage-sound/death-cam toggles parked for v0.5.0, not v0.4.0 blockers. **M7 BLOCKED (D387):** AllUnlocked ON can persist synthetic cheats/times after any profile-settings save; OFF does not remove them. Do not run on live saves; a safe design and multi-profile fixture tests are owed. PNG screenshot upgrade dropped from this wave; dev PPM dump remains. The M1–M7 detailed notes retain their historical design record in `MODERN-OPTIONS-PLAN.md`. | +| D226 | Player-adjustable HUD scale (ammo counter, pickup text, dialogue) | **DONE 2026-09-24 (`30720354`)**: Game.HudScale 75–150% (user-verified), fast3d rect scaling about anchors, dialogue/bottom width-capped. | +| Controller rebinding UI (D395) | **Not implemented.** The two Input → Bindings → Movement/Actions pages edit keyboard/mouse slots only. Pad-only users could previously enter a capture modal and get stuck; pad-triggered capture is now disabled in both UIs and B cancels a capture started by keyboard/mouse (NTSC build linked; physical pad-only retest owed). Controller presets (D394) are separate from arbitrary pad rebinding; design an explicit controller-actions page, persistence/migration and capture/cancel semantics before enabling pad capture. | Post-v0.4 scope decision; no promise that keyboard binding slots can hold pad buttons. | +| Rumble (D224) | Wire `osMotor*` → `SDL_GameControllerRumble` (PD precedent exists) | Deprioritized M-87; pick up if gamepad support is in scope for v0.4.0 — user call. | +| Per-pad tuning / hot-plug | PD incremental hot-plug + remembered seats; deadzone/scale/rumble per pad | Medium; real-controller users. | +| Window polish | HiDpi, center window, maximize/default-fullscreen | Small P-class wins. | +| Unlocked FPS | Plan parked; premise changed (D248/D250 fixed — default is now a true 60) | Phase 0 re-measurement before any work; motion interpolation stays out. | + +### Bucket D — Infra / housekeeping + +- **D244** self-hosted CI runner link failure — fix before the release tag so + CI can build win+linux bundles (or confirm GitHub runners suffice). +- Post-release v0.3.0 cleanup (HANDOFF): remove GE_OBJT probes + (`chraction.c` chrSet/UnsetStageFlags, `propobj.c` GAS line); M-183/M-185 + clamp removal pending full-cutscene re-verification; D309 probe decision + (keep for v0.4.0 debugging or remove). +- Working-tree archaeology: ~~uncommitted D236 second-pass state~~ **RESOLVED + before this plan was written — `532092c7`/`6807555c` (Sep 15) already landed + the D266 IA4/texedge decode + D271 portal-culling fixes on main and dropped + the D267–D280 probes.** Consequence: Phase 1 item 9 (D249) can proceed with + the D271 fix in-tree; the D236 tree-wall work continues from D276's oml/fmt=3 + lead on top of the landed decode fixes. + +## Session additions (2026-09-23/24, local) + +- **D336** Frigate "everything breaks but the gun" (uninitialised head-bob alias fields, D100 follow-up): FIXED, user-confirmed. +- **D337/D338** aim modes folded + Aim range option: user-approved ("Seems good"). +- **D339** #92 low-end perf: `GE_PERFSTAT` probe; CPU budget measured (~2 ms/frame), GPU the likely bottleneck; needs target hardware. +- **#105** (Enhanced/Remaster water reflection) refers to a `feature/graphics-final-stack` branch that isn't on origin or local; not v0.4.0 scope. Its "D329" label collides with ours, so watch for that if that work merges. + +## Phases + +### Phase 0 — Hygiene (½–1 session; do before any fidelity work) +1. Branch `release/v0.4.0` off `main`. +2. ~~Resolve the uncommitted D236/D271 tree state~~ already landed (`532092c7`). +3. Remove GE_OBJT probes + playtest-folder `.bat`; decide D309 probe fate; + getenv-cache sweep. +4. Fix/confirm CI runner (D244) so release tooling works end-to-end. +5. Carry-over verifications from the v0.3.0 handoff that were owed: + Runway AllUnlocked toggle re-test, menu-click spot check (D316/D317 both + closed — just confirm no regression on the new tree). + +### Release gate (independent of the phases below) + +**D322 (long-session audio degradation, issue #87)** is a campaign-breaking +defect in shipped v0.3.0 and does not wait for the phase ladder: the +`GE_D322` probe is already built and smoke-tested — request the campaign +capture (`GE_D322=1 GE_D204=1`) from the reporter now, and run its decision +rules as soon as the log lands. If the capture implicates a port-side leak, +the fix slots into Phase 2; if it's a rule-2 behavioral difference, it gets +its own sign-off item. + +### Phase 1 — Fidelity wave (the release's core) +Ordered by (crash risk → visibility → effort); each item ends with its own +verification (headless where possible, human eyeball/ear as merge gate): + +1. **D320 live verification + watchdog generalization** — the only open + item that can hard-lock progression on 8 levels; probes already exist. +2. **D289 re-test** (crash class; one human pass, then close or chase). +3. **Batch "one-off geometry/texture anomalies": D288 + D294 + D291** — + all are single-location, bounded, visually documented; likely share a + fast3d import/culling root cause. One investigation thread. +4. ~~**D252 rainbow particles second cause**~~ — **retired (M-188):** the + user's symptom closed as a duplicate of the D219 fix (live retest clean); + decode + dimensions proven correct (M-141/M-156). Re-open only on a + fireball-rainbow re-report. +5. **D245 water seam** — candidate fix exists; A/B against N64 reference, + then flip default (A1-class fidelity call). +6. **D303 + D306/D308 z-fighting family** — small visual parity items; batch + per commit cadence. (~~D307 camera flip~~ investigated 2026-09-21: no + port-side bug found, all inputs verified N64-faithful — blocked on user + repro detail; see Bucket A row.) D303 + D306/D308 both investigated + 2026-07-19: no port-side bug found in either (pipeline/data/precision all + verified N64-faithful); capture probes `GE_D303` and `GE_ZF` are in-tree — + both now blocked on user-side captures (firing repro / N64 reference). + Nothing further to do agent-side until those land. +7. ~~**D236 tree wall**~~ — **done (M-198, pass 26):** `TEXTURETYPE_DETAIL` + tile-binding fix in fast3d (`Video.DetailBaseTile`); live-verified, + probes stripped. Only the user play-test sign-off remains. +8. ~~**D235 texture reload leak**~~ — **done (M-193):** port-side + `TextureCacheMap` now cleared on every stage transition via + `videoResetTextureCache()` in boss.c; 21/21 sweep pixel-identical; live + death/restart confirm is a playtest item. +9. **D249 culling** — *after* the D271 overlap check; live screenshot+location + first, BFS instrumentation second. +10. **Audio parity: D322 long-session degradation first** (issue #87 — the + only audio report that breaks a full campaign; `GE_D322` probe already + shipped, get the campaign capture, then apply the decision rules in the + finding), **then D240/D241** (fold into D207 family) + verify D230's bass + fix by ear on the elevator track. +11. **Front-end fidelity batch: D290 watch pad graphic, D182(2)/ + D176(b) pitch-shear defect, D176(a) sky Defect 2** (verify the patch + visually first — it's been unverified for weeks). (~~D75 logos~~ fixed + M-197; ~~D195 Control alpha~~ FIXED per findings; ~~D197 face wrap~~ + CLOSED at v0.2.0-pre playtest — both were stale in this list.) +12. **D174/D172 blood verification** (regen sidecars, BUNKER1 firefight). +13. **D258 PAL/JP asset conversion repair** — apply the verified repair path; + re-run the region sweep. Decide scope: ship JP/PAL-accurate or document + NTSC-only for v0.4.0 (user call). +14. **D311 upstream anim-poison source** — close out the last open thread of + the freeze class so the known-issue bullet can be dropped. + +Exit criteria: every README "Known issues" bullet either fixed or explicitly +re-disclosed with a v0.5.0 pointer; no new fidelity regressions (21-level +sweep after each landing). + +### Phase 2 — Stability wave +1. **D287/D286 freeze investigation** — wire Windows crash dumps (or an + in-process fault log) so the next occurrence is diagnosable; pattern-match + against `gfxFrameMsgQ` lead and the D309 teardown-race class. +2. **D175 stutter** — idle-box repro under `tools_pc/debug.ps1`; likely a + known transient-hang class, confirm and close or chase. +3. **D285 residual** — re-audit audio thread-locking vs D305's guard. +4. **D281 AllUnlocked no-save path** — make it robust (root-cause the + sndHandleEvent/aim interaction) or gate the toggle until a save exists. + User call on which. +5. Long-idle soak test (both platforms) as the stability acceptance bar. + +### Phase 3 — Improvements / QoL (only after Phases 1–2 are green) +Order by user-visible value (2026-09-27 refresh — several rows since DONE, marked): +1. ~~**Native widescreen**~~ — **DONE 2026-09-23** (D334/D335; playtest sign-off owed). +2. ~~**F10 overlay wave: D237 + D314/D315**~~ — D237 **DONE 2026-09-23**; D314/D315 still open (user-deprioritized; lands with further overlay work). +3. **Input feel: D238 sensitivity rework, D239 delete always-grab, window + HiDpi/center; rumble + per-pad tuning if gamepad is in scope (user call).** +4. ~~**D226 HUD scale**~~ — **DONE 2026-09-24**. +5. Unlocked FPS: re-measure Phase 0 against the post-D248/D250 baseline and + decide whether v0.4.0 or v0.5.0. +6. **Modern options wave — substantially implemented, NOT release-complete** + (D377–D389). Five functional roots; Input → Bindings → Movement/Actions + and Gameplay → HUD work in both menus. One effective PC layout with + dedicated E/R/Ctrl; two bind slots including Mouse 1–5; crosshair + visibility, Original/White/colour, style and size. Redundant Low-end + preset retired, but FPS cap/MSAA remain. M4 GE+ toggles deferred to + v0.5.0, M6 PNG screenshot removed from this release. **M7 blocked by + persisted fake save progress (D387); do not test with a live save**. + Physical acceptance, M7 release decision, 21-level visual gate and + documentation/commit reconciliation are still open. Earlier "one + opt-in section/default-off/preservation proof" estimates were superseded; + the original research remains in `MODERN-OPTIONS-PLAN.md`. + +### Phase 4 — Release ritual +1. `./build-pc.sh ntsc-final` clean; `/linkcheck`; full 21-level + `tools_pc/verify.sh sweep --json`. +2. Full campaign playtest sign-off (user), incl. the D320-probe levels and a + water level + Surface + Silo (the Phase 1 visual fixes). +3. Refresh README Known Issues, `.github/release-notes.md`, findings index; + regenerate `agentic-development.md` stats from git (still owed since + v0.3.0 prep). +4. Push `release/v0.4.0` → tag `v0.4.0` → CI draft bundles → validate both + artifacts → fast-forward main. + +## Explicitly out of scope for v0.4.0 +- macOS/ARM, LAN multiplayer. +- Motion interpolation / sim-rate changes (UNLOCKED-FPS non-goals). +- D117 frame determinism (research-parked). +- **Mod support and HD texture packs — both post-1.0, both researched; see below.** + +### Post-1.0, researched and tracked (do not start in v0.4.0) + +Both were investigated 2026-09-22 against the Perfect Dark PC port fork +`perfect-dark-dabs-mod-dabs-mod` (**MIT**, Ryan Dwyer — same license as this +tree; `AGENTS.md` already names the PD port a standing reference, so code may be +adapted with attribution). Fuller sequencing notes live in the local +`docs/dev/notes/MODERN-PORT-FEATURES-BACKLOG.md` §5/§6, which is +gitignored — the +durable record is `docs/dev/findings.md` and the summary here. The +drop-in mod support design itself is expanded in the local +`docs/dev/notes/MOD-LOADER-PLAN.md` (2026-09-27: C1–C4 components, v1 +scope, acceptance bar). + +**Mod support (ROM patches) — viable, medium–large.** Findings **D325–D328**. +- Community mods *repack* the ROM, so vanilla offsets are useless: Goldfinger 64 + (12→24 MB, 797/803 slices differ) and even the content-neutral Random Eye + randomizer (identical ROM size, yet 803/803 slices differ — content shifted + +7392). Both die in `tools_pc/d43_emit.py:273` on a vanilla-offset zlib slice. +- **The fix is not a loader rewrite.** PD's `port/src/modimport.c` states our own + constraint verbatim and instead diffs the patched ROM against stock, extracting + what differs into a **mod directory** its loader already reads. The game keeps + running its stock ROM at fixed compiled offsets. +- GE's file table is in the ROM after all (D327 said otherwise; corrected by + D328): 1172-compressed data segment at ROM `0x21990` — one raw inflate, which + `port/src/rzdecomp.c` already wraps — table at `0x252c4`, 727 records of 12 + bytes, pointers `0x80020d90`-based, size by differencing adjacent addresses, + exactly `obInit` (`src/game/ob.c:126`). Verified: **681 exact offset+size + matches vs `filelist.u.csv`, 0 mismatches**; discovery needs **no hardcoded + offsets** and reads Random Eye's own table to **704/726 files byte-identical**. +- Remaining work: extraction in C (proven), patch decoding (PD's `rompatch.c` is + an MIT VCDIFF/BPS/IPS decoder — **removes the GPLv2 xdelta3 constraint**), + converters taking the extracted table, and the real effort: a + **mod-directory override layer** — `port/src/romdata.c` reads the ROM only and + has no analogue of PD's `romfile.moddir` file slots. +- **Hard limit:** mods that *add levels* need new stage-table entries in + `src/game` — rule 2, not the ABI/layout exception. Either a sign-off or scope + down to the existing 20 slots. ASM/code mods stay impossible (ROM code is never + executed here). + +**HD texture packs — medium, well-scaffolded. Answers issue #100** ("High +Resolution Textures", open, `enhancement` — a user asking whether HD textures can +be used with the port). Architecture studied from PD's +`port/include/texpack.h` + `port/fast3d/gfx_pc.cpp`; no finding written yet, so +this entry and backlog §6 are the record. +- **Identity is the whole problem, not decoding.** The renderer only ever sees + the pointer `gDPSetTextureImage` was handed, so PD keeps an address → + texture-number registry populated where `texLoad()` runs and invalidates it + when the pool drops (a stale entry names the *wrong* texture). Then three-tier + matching: texture number → texel checksum (for textures embedded in model + files) → per-record. Packs are `textures/.png`; decode happens on a + worker so loading does not hitch; the hook is inside `import_texture`, gated so + it costs one lookup when off. +- Most scaffolding exists here: our `gfx_pc.cpp` is the same lineage + (`gfx_texture_cache`, `:92,579`), `g_Textures[].texturenum` exists + (`src/game/chr.c:3894`), and **`GE_TEXDUMP` already dumps textures** + (`port/fast3d/gfx_pc.cpp:1205`) — it just names them by sequence + (`r000_f3_s1_64x64.bin`) rather than texture number, so dumps cannot round-trip. + Renaming that is the single change turning the probe into a pack dumper. A PNG + decoder is the one new dependency, and zlib is already required. +- Step 1 (the identity registry) is the only novel design decision, and it + records identity where textures load — a `src/game` touch needing `#ifdef PORT` + plus a §F note. Settle that at design time, not mid-implementation. + +## Session log (agent-driven, 2026-09-20 evening) + +**Phase 0 — done except user-side items:** +- `release/v0.4.0` cut off `main` (which already carried the D266/D271 fixes, + `532092c7` — the "uncommitted tree state" item was stale; plan updated). +- D322 probe work committed (`7bb43260`); GE_OBJT probes removed (D321 closed, + M-192 item 7, `56ac2ab8`); D309 probe **kept** (inert, cheap, recurrence + value for the D309/D310 freeze class). +- getenv hot-path sweep (D302 follow-up): function-level scan of all GE_* + sites found no frame/triangle-rate stragglers in fast3d; cached four + per-frame/per-hit guards in `src/game/bg.c` (GE_D104 room pass, GE_D63 + primary+secondary render logs, GE_D154 inner GDL walk) — all `#ifdef PORT`, + mechanical. +- D244 (self-hosted CI runner): needs `pacman -Syu` on the box itself — + **user-side action**, not locally fixable; fallback (`#ifdef PORT` thin + wrapper for the `#pragma weak` alias) only if the toolchain update fails. +- 21-level sweep gate: running on the new tree (see `scratchpad/sweeps/`). +- Carry-over D316/D317 + Runway AllUnlocked re-tests are interactive → folded + into the big playtest brief below. + +**Phase 1 — in flight:** +- Item 1 (D320): generic pin-signature detector added to `d318TimelineTick` + (`GE_D318T=1` now covers any chr: ACT_ATTACK + live-fire attacktype + + byte-stable animframe for 10 s → one `D320T: PIN?` line per chr, repeating + every 10 s while pinned). Detection only — the recovery watchdog stays + hardcoded to ai_22 per D320's own instruction until a second list freezes. + Live verification of Facility ai_19 / Control ai_9 / Depot ai_12 beats = + playtest work (see brief). +- Item 3 (D288/D294/D291 batch): D288 probe refined per M-155 follow-up + (any_behind_camera==0 only — the w<0 bbox math was a false lead; oml word + now logged for asset ID). D294: `GE_D104` room-visibility trace confirmed + sufficient (curRoom/roomsDrawn/budget + per-room load state every 10 s); + needs live Statue repro at FovScale≈130 + max draw distance. D291: still + needs live Dam truck details — lowest value of the three. +- Item 13 (D258 PAL/JP): **deferred pending user scope call** (ship + multi-region or document NTSC-only for v0.4.0) — no PAL/JP ROMs available + here to verify a blind CSV rename against. +- Item 4 (D235 wrong textures after death/restart): **mechanism found + fix + applied (M-193).** M-108's refutation of the game-side CI-cache theory + stands, but the *port* has its own `TextureCacheMap` (source-address keyed, + no content check) that was never invalidated on stage transitions — + `videoResetTextureCache()` existed with zero callers. Now wired into the + per-stage reset block in boss.c (`#ifdef PORT`, after `memaReset`). + Pixel-identical re-uploads; golden sweep running to confirm. Live + death/restart confirmation + a `GE_D235` >0 capture are playtest items. +- D311 upstream poison source (M-194): static pass done — all + `sub_GAME_7F06FE90` callers pass arg2=16.0f (denominator safe); the + old→new lerp only circulates values; `bheadAdjustAnimation` is the one + path where a live player-velocity corruption reaches `model->speed`. Both + speed-write choke points (`modelSetAnimSpeed`, `modelSetAnimation2`) now + log `D311: … ANOMALY` on |v|>1e4/NaN — next Caverns occurrence is a + one-line diagnosis. No env var (D156-guard pattern). +- Item 4 (D252 rainbow particles): **effectively closed — no agent work + left.** Re-read the finding this session: M-141 refuted the width/height + suspect (line=4 → correct 16×14, cross-confirmed by the GE_TEXDUMP + is_fire_shape filter), M-156 proved the ROM bytes decode to a clean + white-hot→ember progression, and **M-188 closed the user's actual symptom + (lingering residue sprites) as a duplicate of the D219 fix** — user + retested live: "seems fine now." Only a *fireball* rainbow re-report would + re-open it. Plan item 4 retired. +- Item 10 (D207 alarm klaxon): **blocked on user repro info, not agent- + workable.** M-77b implemented the designed last-resort LOOPED-preemption + pass + `[D207-DROP]`/`[D207-YIELD]` counters, BUNKER1 forced-alarm soak + showed zero drops / zero yields (pass 1 always finds a victim), and the + change was reverted. The finding now asks for: which level, which + weapon/situation, and a `GE_AUDIOTRACE=1` capture (or clip) of the failing + alarm — then decide between heavy-level 8/8 starvation (re-apply the + reverted pass), an alarm-stop-path leak, or pure perception (klaxon mix + volume vs N64). Added to the playtest brief as a low-effort ask. +- Full 21-level golden sweep: **21/21 PASS** on the final tree (`bb508e5c`), + confirming D235 fix + D311 probe + all Phase 0 changes are pixel-identical. +- **D75 (Nintendo logo) investigation — M-195, no root cause yet but two + suspects eliminated.** (a) Fresh `GE_D63` run: all 23 logo Primary GDLs + emitted every frame, zero unknown-opcode warnings → emission + DL + consumption fine; PC GDLs byte-faithful to N64 source. (b) New `GE_D75V` + per-batch vertex probe: transformed coords match the wordmark geometry + exactly (MP translation matches the lookat) → basematrix / f32→s32 / + matrix-list suspects eliminated. **Leading hypothesis:** every `G_VTX` + batch encodes v0=0, so multi-batch nodes overwrite shared vertex slots + 0..n−1 (only the last batch survives; higher-indexed triangles read stale + slots) — consistent with the single tiny orbiting fragment observed. Next: + ROM-dump one node's GDL stream and cross-reference `G_TRI4` index ranges + against per-batch counts; fix would be port-layer fast3d slot management. + Probe code (`gfx_pc.cpp`, `videoGetFrameCount()` in video.c/h) uncommitted + at end of session — commit with the doc updates. +- **D307 (CCTV facing backwards) investigated — no port-side bug found.** + Env-gated `GE_D307=1` probe in `setupCctv` dumps every input + derived + orientation per record. Bunker1 (`UsetupsevbunkerZ`) decode vs live: model + switch offset = `PcctvZ` (NOT `GcameraZ`, that's the gun) sw[0] + bit-for-bit; boundpads match the pccg sidecar once the legitimate load-time + room-scale (`pad->pos *= get_room_data_float2()`, ≈1.8542 here) is applied; + record config words (incl. `unkCC`) match; all matrix/atan2 helpers are + byte-identical `src/game` code. Conclusion: PC orientation values == what + the N64 computes; if the symptom is real vs N64, the remaining suspect is + fast3d's handling of the non-orthonormal lens matrix (`unk84` row 0 scaled + 0.1×). Blocked on user repro detail (which camera/level + screenshot). + Findings entry updated; probe kept in-tree (D60/D88 pattern). +- **D303 (extra long muzzle flash) investigated — no port-side bug found yet,** + blocked on live capture. Mapped the full flash pipeline (`gunUpdateAndFire` + builds `rwmtx[1]` = yaw×scale×flashext and `rwmtx[2]` from `Switches[2]`; the + flare arms are `dorottex` DLPRIMARY nodes) — all byte-identical game code, + matrix helpers verified index-for-index. Ruled out: weapon stats (M16's + flashext=6.0 is the legitimate max, sniperrifle matches; the ext≥3 ⊆ + CAN_DUAL_WIELD overlap is design coincidence) and model conversion (Gm16Z + DLPRIMARY records + vertex blocks byte-faithful per d43 round-trip). Added a + TEMP `GE_D303=1` probe in `gfx_pc.cpp` (cached-getenv; logs very-tall-thin + emitted triangles with blend/oml/comb/prim/env/w) — 75 s unattended runs on + Bunker1/Dam/Depot show only static scene geometry, so the artifact needs a + live firing repro. Next: user fires the M16 under `GE_D303=1` and shares the + log lines; the spurious quad's RDP state will identify its source. +- **D306/D308 (z-fighting family) investigated — real fighting confirmed, cause + framed.** Verified GE's RSP writes post-transform x/y/z/w as full 32-bit 16.16 + (not s16 8.8), so the port's float pipeline + `GL_DEPTH24_STENCIL8` matches N64 + depth precision — no port-side precision deficit exists. Built a TEMP + `GE_ZF=1` flicker probe in `gfx_pc.cpp` (96×54 screen-cell grid, per-cell depth + history, requires non-monotonic a-b-a oscillation within GE_ZFTOL recurring ≥5× + at the same cell; `GE_ZFBOX` region filter) — earlier center-pair and simple + flicker variants saturated on tessellation noise. Dam (`-level_33`) unattended: + sustained hits in the lower-left truck region with per-cell oscillation counts to + ~970 → D306's "transparent areas" are real depth-test alternation. Prime suspect + is level-authored coincident geometry (fights on N64 too); confirming port-only + needs an N64 reference of the same spot, and a mitigation (e.g. float depth) + would diverge from N64 — parked pending that. +- **Plan/doc refresh:** D75 (M-197) and D236 (M-198) marked fixed throughout + (Bucket A rows, Phase 1 items 6/7/11, playtest brief); findings index + regenerated; README Known Issues to match. + +### Big playtest brief (for the user, when the tree is final) + +Build: `./build-pc.sh ntsc-final`. Set **`GE_D322=1 GE_D204=1`** for the whole +campaign (D322 pool telemetry — the release gate). For the three D320 beats, +add **`GE_D318T=1`** on: Facility (the captive-scientists monologue/execution +beat), Control (the ai_9 guard beat), Depot (the execution beat) — watch for +`D320T: PIN?` lines; a frozen run keeps printing every 10 s. + +Checklist beyond the normal play: +1. **D322**: full campaign, share `ge007.log` after (decision rules in the + finding). +2. **D316/D317 regression confirm**: click menu rows at a few window sizes, + esp. low rows (crop overscan bars) with SafeAreaCrop on/off; toggle + Game.AllUnlocked back and forth in-game. +3. **D289**: Dam computer room — interact with the terminals (was a one-off + crash, likely D255-class, already fixed — confirm it stays gone). +4. **D294**: Statue, set FovScale ≈130 + max DrawDistance/LodDistance, walk + past the CCCP monument at ground level; if the black-ground window hits, + note the time (a `GE_D104=1` run there is the follow-up capture). +5. **D291**: Dam — watch for texture glitches as the truck drives past; + screenshot + which surface. +6. **D175**: door-open on Runway/Surface — note any stutter/hang. +7. **D287**: leave a level idle+unfocused 15–20 min, click back (the + non-self-recovering freeze; one prior occurrence). +8. **D320 beats** as above with `GE_D318T=1`. +9. Audio by-ear: elevator track bass (D230 verify), PP7/AK47 under heavy fire + near looping sounds (D240/D241). +10. **D235**: on any level, die and restart (or reload a save) in the same + session, then look around — textures should be correct (no swapped/wrong + stage textures). Run once with `GE_D235=1` and share the log if anything + looks off (a `>0` count means a stale-cache hit slipped through). +11. **D288 (Silo)**: play Silo normally; if you see the wrong-color target / + geometry anomaly, set `GE_D288=1 GE_PCDUMP=1` for a 30–60 s run around it + and share log + captures (the probe logs the offending oml word for asset + ID). +12. **D311 (Caverns)**: play Caverns ~2 min of normal combat; if the anim- + speed freeze/poison recurs, the log will now carry a one-line + `D311: … ANOMALY` naming the corrupting call site — share it. +13. **D207 (alarm klaxon)**: if gunfire goes quiet during an alarm, note the + level + weapon + situation; ideally re-run that spot with + `GE_AUDIOTRACE=1` and share the log (or a short clip). +14. **D75 (front-end logos)**: confirm the Nintendo/Rareware logos + legal + page still render correctly (fixed M-197; user-confirmed once already). +15. **D236 (Surface trees)**: Surface 1/2 — confirm the treeline renders as + discrete tree sprites, not a wall (fixed M-198; probes stripped). +16. **D307 (security cameras)**: if any in-level CCTV camera looks like it's + facing backwards, note which level + which camera (screenshot) — the + orientation math is verified N64-faithful, so a concrete repro is what's + needed to keep chasing it. +17. **D303 (extra long muzzle flash)**: run with `GE_D303=1` in your terminal, + fire the M16 (and any other gun that shows the extra upward flash) until + you see it, then share the `D303:` log lines from around that moment — + each line carries the RDP state of a candidate quad and will identify the + spurious draw call's source. +18. **AllUnlocked preservation (M7) BLOCKED — D387.** Do not run this + against a real save: even changing a profile setting while ON persists + synthetic cheats and 0x3FF completion times; OFF requires a restart and + does not undo already-written fakes. First define safe earned-progress + semantics, fix in `port/`, and verify on private copies with settings + writes, mission completion, OFF restart and multi-slot CRC checks. + M4 modern toggles are parked to v0.5.0. + +## Conventions reminder +- One findings entry per investigation; the index tail moves constantly — + it ended at **D370** as of 2026-09-27 (the F10 drag entry), so new + labels start **D371+** (the old "after D322" note is long stale; always + confirm the tail of `findings-index.csv` before filing). +- Fidelity fixes: port-layer or documented A1/ABI exception only; genuine + behavioral differences need the rule-2 sign-off (`docs/dev-process.md`). +- Every subagent reads `docs/porting-notes.md` first and appends to it. diff --git a/docs/dev/WATCH-SETTINGS-PLAN.md b/docs/dev/WATCH-SETTINGS-PLAN.md new file mode 100644 index 00000000..d0993580 --- /dev/null +++ b/docs/dev/WATCH-SETTINGS-PLAN.md @@ -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(©)`, 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. diff --git a/docs/dev/agentic-development.md b/docs/dev/agentic-development.md index 2e6caee4..6bba6d5c 100644 --- a/docs/dev/agentic-development.md +++ b/docs/dev/agentic-development.md @@ -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 Background section carries the final project numbers.* + ## Contents - [Why this project exists](#why-this-project-exists) diff --git a/docs/dev/findings-index.csv b/docs/dev/findings-index.csv index 9f9ba4d1..4e1c35b0 100644 --- a/docs/dev/findings-index.csv +++ b/docs/dev/findings-index.csv @@ -7,36 +7,37 @@ 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,PARTIAL,front-end 3D model transforms (D77 audio split out below —,PARTIAL (M-197 fixed the logo texture blobs; v0.4.0 campaign 2026-09-28: D76 · D164,FIXED,Legal screen only draws line 1 —,"FIXED — this row and the ## D164 entry below were stale; the proposed one-line #ifdef PORT fix was actually committed 8dfc578f4 (2026-08-31), well before this correction (2026-09-18)." D159,FIXED,"front-end wallet-Bond photo ""interlaced""/combed (RC1 / D149) —",FIXED (#ifdef PORT no-op the swap in image.c) D161,FIXED,Depot (-level_30) ceiling = bright-blue speckle + radial rays (B2).,FIXED (port/fast3d/gfx_pc.cpp) D165 · D166,FIXED,"Input-layer polish (M-31, port-only):",FIXED -D180 · D181,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,OPEN,"Video.FpsCap throttles the sim, not just presentation (M-39 diag, M-42 clamp).","OPEN (2026-09-26, user direction:" D187,FIXED,"HUD ammo-type/weapon icon vanished at -O2 (M-46 root-cause, M-47 fix + verify).","FIXED (src/game/gunfire.c, #ifdef AVOID_UB); PR #19" D188,FIXED,Linux -O2 boot crash:,"FIXED (src/libultrare/libc/xprintf.c, src/sprintf.c, #ifdef PORT); PR #22" D189,FIXED,Linux -fstack-protector aborts on the decomp's latent stack-buffer overruns (M-48) —,"FIXED (CMakeLists.txt, src/game/stan.c #ifdef PORT); PR #23" D190,CLOSED,"Linux -level_45 SIGSEGV at load in add_ptr_to_objective (objective.c:64) (M-48, open).","CLOSED — not a port bug; level 45 is MP-only and has no solo setup file in the ROM (root-caused M-105, reproduced on Windows)." D191,FIXED,Windows -O2 release-bundle crash:,FIXED + PLAYTEST-VERIFIED (M-79).… D193,FIXED,NPC AI locomotion is too slow —,FIXED + user-playtest-verified (M-81) —… -D194,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,PARTIAL,"QoL gap (M-87, from a PD-PC-port survey):","LANDED via D401 (2026-09-27, re-promoted from DEPRIORITIZED)." D225,FIXED,"Dev-tooling QoL (M-87, user ask):","FIXED (2026-09-18) — GE_DEBUG_UNLOCKALL=1 short-circuits fileGetIsCheatUnlocked() (src/game/file2.c, #ifdef PORT, cached getenv) to always return TRUE; default off, no save mutation." -D226,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…" @@ -44,9 +45,9 @@ D228,FIXED,AK47 white missing-texture patches after D217's texpool fix —,"FIXE 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,FIXED,"Alarm klaxon starves combat SFX and doesn't recover after it stops (surfaced by D206, M-77).",MONITORING — user 2026-09-26: D208,FIXED,Player weapon-fire sound goes permanently silent mid-firefight (all other SFX unaffected) —,FIXED + user playtest-verified (M-78d) — D209,FIXED,Every AI character in the game is locked to a *walk* animation regardless of the speed the ailist commands —,FIXED + user-playtest-verified (M-81). D210,FIXED,chrToPatrol() leaves act_patrol.lastvisible60 uninitialised on PC —,"FIXED (chraction.c:3872, #ifdef PORT → self->act_patrol.lastvisible60 = -1;, N64 line under #else).…" @@ -55,33 +56,33 @@ 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" +D175,CLOSED,In-game stutter / brief hang during normal play —,"CLOSED (v0.4.0 campaign sign-off 2026-09-28, user:" D176,FIXED,"Surface exterior renders wrong (-level_36), two independent defects.","(a) scroll-speed FIXED (fidelity pass; HIGH confidence on root cause, MODERATE on N64 parity); M-93 playtest + M-98 Defect-2 starburst screenshot in the sections." D177,FIXED,Ladders non-functional —,FIXED (M-36) — high confidence; interactive climb test owed D172,FIXED,"Bullet-impact / blood / spark particles render magenta or cyan instead of dark red (M-82, ROOT CAUSE FOUND + FIXED).",FIXED (port/fast3d/gfx_pc.cpp gfx_lod_tile_offset)… -D174,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 +D174,CLOSED,"""No blood effect"" (user QA report).","CLOSED — faithful (2026-09-26, user side-by-side on 1964:" +D183,CLOSED,"M-36 Family A (""texture line/pitch shear"") is DISPROVEN for the Surface repro (-level_36).","CLOSED (2026-09-26, user-approved:" D173,FIXED,Third-person Bond model spawns too high —,"MOSTLY FIXED as a side effect of D243 (2026-09-18, user-confirmed)." D179,FIXED,Packaged build crashes after the logos —,"Diagnosed, not fixed —" -D180,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: 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) — 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 @@ -90,20 +91,20 @@ D113-D116,FIXED,portal BFS ok · matrix chain ok · player raw-offset audit + gu D117,OPEN,"Frame nondeterminism root-caused; GE_DETERM attempted, not achieved.","OPEN (mode attempted, not achieved)" D118,RESOLVED,"SDL input layer; M-24 mouse-look rework (mode-aware map, real INI); D118b/c FIXED.",resolved (D118a residual) D150,FIXED,watch OBJECTIVES / BRIEF page crash:,FIXED (src/str.c #ifdef PORT); interactive re-verify pending -D260-D265,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 +D260-D265,CLOSED,"v0.2.1 hotfix batch: D260 wheel direction swap (fixed) · D261 watch-menu nav auto-repeat (fixed, feel-check owed) · D262 watch item-preview model missing → roo…",CLOSED (2026-09-26 bookkeeping: +D266-D269,CLOSED,"D236/D265 wall, second pass:",CLOSED (2026-09-26 bookkeeping: +D270,CLOSED,Backfilled (M-132) — this label was never given its own row; only code comments and the HANDOFF archive documented it.,CLOSED (2026-09-26 bookkeeping: D271,FIXED,"D236 wall, third pass —",FIXED-but-insufficient (src/game/bg.c #ifdef PORT) — -D276,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:" +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" +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).…" +D152,FIXED,"Mission-failed / objective-failed → permanent black screen (M-28, user bug report —","FIXED (user, 2026-09-26:" D154,?,"bg.c room-geometry ray/hit-test GBI parser port (M-28, WRITTEN / UNVERIFIED / UNCOMMITTED).",M-30 re-audit + fixes (still playtest-gated for final verify). -D156,PARTIAL,"Facility outro-cutscene ""hang"" —","GUARDED (src/game/model.c, #ifdef PORT)…" +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,OPEN,"LOD/culling breaks down on large open levels (Streets, Egyptian named specifically) at *default* FOV —","OPEN — M-142 static review ruled out the unscaled-far-clip and LOD-multiplier suspects (both already correct); CLOSED (v0.4.0 campaign sign-off 2026-09-28, user:" D250,FIXED,"FPS is inconsistent/low on many levels after 60 Hz shipped (M-119, user QA; D248 follow-on).","FIXED — per-call getenv() cost was the bottleneck; rock-steady 60-61 fps after, user re-tested 1440p/4K clean." D251,FIXED,F10 options overlay QoL pass:,"FIXED (port-only, port/src/optionsoverlay.c)." -D252,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,44 +160,128 @@ 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,PROPOSED,PAL/JP asset conversion is broken at the source-data level —,DEFERRED (user call 2026-09-28: D284,FIXED,port/src/random.c's MIPS64 dsll32/dsrl32 shift helpers were backwards —,"FIXED, 21-level sweep clean, no crashes —" -D285,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,FIXED,"Statue: ground geometry near the large CCCP/hammer-and-sickle monument briefly renders solid black over a wide area, then self-recovers 10-30 seconds later… —","FIXED, user playtest confirmed (2026-09-25:" D295,FIXED,"Campaign-halting crash at the end of Control Center on 00 Agent difficulty, then a crash on every subsequent relaunch/resume (GitHub #87, user campaign playtes…",FIXED (M-149) — M-148 root-caused + reproduced on HEAD; fix landed: -D296,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…" -D321,CLOSED,D318 trigger chain fully mapped:,CLOSED — trigger chain mapped and faithful; D318 watchdog validated in live play. -D322,OPEN,Long-session audio degradation:,"OPEN — static triage done (teardown audit, mixer statelessness, D202-coverage check); ranked hypotheses:" +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 stub declares nothing, so TUs that include it get NO CRT prototypes —",FIXED (2026-09-22) — proven impact: +D325,CLOSED,ROM-mod (xdelta) compatibility:,CLOSED as a question — +D326,CLOSED,Random Eye randomizer repacks the ROM too —,CLOSED as a question — +D327,RESOLVED,"Mod support, resolved plan:",RESOLVED — superseded by D328; approach (a) is NOT ruled out (these scans missed the 1172-compressed data segment). +D328,CLOSED,"Mod support is viable and D327's ""ruled out"" was wrong:",CLOSED as a question — +D329,FIXED,The D318/D320 guard anim pin is a tick-granularity divergence:,"FIXED — Rule-2 high-FPS fix in model.c; clk=1 no longer pins (WSL + native Windows), 0 triggers at clk=2; bunker1 golden failure predates it (on/off A/B); user playtest confirms N64-matching Ourumov…" +D330,FIXED,"#103 (v0.3.0, Win 11):",FIXED (port/src/input.c) — +D331,FIXED,Ejected shell casings never rendered on PC:,"FIXED (gunfire.c #ifdef PORT, explicit template); PP7 casing verified on native Windows (before:" +D332,FIXED,Reticule jitter in aim mode (RMB) —,FIXED (fix/mouse-pd-model) — +D333,FIXED,"#104 (v0.3.0, Linux):",FIXED (2026-09-26 bookkeeping: +D334,FIXED,"Native widescreen, world side (WIDESCREEN-FOV-PLAN Option B Phase 2):",FIXED (2026-09-26 bookkeeping: +D335,FIXED,"Native widescreen, HUD + front end (WIDESCREEN-FOV-PLAN Phase 3):",FIXED (2026-09-26 bookkeeping: +D336,FIXED,"Frigate (and any level) ""all polygons break except the gun"" after turning:","FIXED (src/game/bondhead.c #ifdef PORT, ABI/layout); headless repro (Runway → exit → Frigate → turn, D294 2× room pool) clean, D156 0 fires; golden 3/3; Win + Linux builds." +D337,FIXED,Aim modes folded (user decision):,FIXED (2026-09-26 bookkeeping: +D338,FIXED,"Input.AimRange (F10 ""Aim range""):",FIXED (2026-09-26 bookkeeping: +D339,OPEN,"#92 (low-end ~30 fps, Celeron N3060 / HD 400) —","OPEN (2026-09-26, user:" +D341,FIXED,"Intro gun-barrel blood drip draws as static blocks/lines instead of dripping (user, v0.4.0, 2026-09-26; death blood fine).","FIXED (M-201, user-verified 2026-09-27):" +D342,FIXED,"File select: folder bodies and Bond photos vanish after selecting a file and backing out (user, v0.4.0, 2026-09-26).","FIXED (M-201, user-verified):" +D343,FIXED,"File select: PC ""Options"" label opens the settings overlay (v0.4.0 feature, M1 of the route-B options screen, 2026-09-27); also fixes F10 on file select jumpin…","FIXED (M1, closed): one Rule-2 #ifdef PORT draw hook (user sign-off 2026-09-27); headless + user-verified (mouse, F10, >30 s, widescreen, physical controller on the bottom-bar label and options scree…" +D344,FIXED,Host PC bugchecks (BSOD 0x119 VIDEO_SCHEDULER_INTERNAL_ERROR) during game test runs; probable cause:,"FIXED (probable cause; orderly quit + GE_QUITFRAME), headless + user-verified; no dump, watch for recurrence." +D345,FIXED,"Front-end crosshair snaps away from the pointer on screen/page changes (file select → mode select setCursorPOSforMode(0), front.c:2516; every MENU_PC_OPTIONS p…","Port-only, user-verified (parts a–g):" +D346,CLOSED,PC-settings wording pass:,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest. +D347,FIXED,F10 overlay controller:,"FIXED (port-only, optionsoverlay.c):" +D348,?,"File-select ""PC Options"" label:",SUPERSEDED by D398 (glyph replaced with the IMG_DOT texture; no new art). +D349,CLOSED,GE watch settings are not normally saved to the Bond file:,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest. +D350,CLOSED,Watch-backed PC options:,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest. +D351,FIXED,Existing D346 value/unit rows had positional initializers shifted into hidePtr:,"FIXED (port-only): designated row initializers restore unit, deadzone divisor and hide-if-auto fields; build clean, headless smoke clean." +D352,FIXED,D350 review of 1303c0eb:,"FIXED (port-only, port/src/watchsettings.c):" +D353,FIXED,Turok-style settings regrouping:,"FIXED (port-only, optionsoverlay.c/frontoptions.c):" +D354,FIXED,Front-end F10 Bond rows silently disabled:,"FIXED (port-only, port/src/watchsettings.c):" +D355,FIXED,F10/options Music slider inaudible in-stage:,"FIXED (port-only, port/src/watchsettings.c):" +D356,FIXED,"Settings regroup: Turok-style pages renamed to functional sections (INPUT/GAMEPLAY/GRAPHICS/AUDIO/VIDEO) with PD-style ""(File N)"" scoping annotations, the redu…","FIXED (port-only, port/src/optionsoverlay.{h,c} + port/src/watchsettings.{h,c} + port/src/frontoptions.c; plan docs/dev/D356-SETTINGS-REGROUP-PLAN.md):" +D358,FIXED,D356 level-1 overlap:,"FIXED (port-only, port/src/frontoptions.c):" +D359,CLOSED,"Settings UI file terminology settled on the end-user term ""Profile"":",CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest. +D360,CLOSED,F10 INPUT heading scrolled away; F10 navigation and distance defaults (2026-09-27).,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest. +D361,FIXED,Main-menu F10 Audio freezes the game:,"FIXED (port-only, port/src/watchsettings.c + F10 probe in optionsoverlay.c):" +D362,CLOSED,F10 GoldenEye dossier-style visual refresh (2026-09-27).,CLOSED (superseded by D363 after user playtest): +D363,FIXED,F10 return to a dark overlay with the N64 watch's green/Bank Gothic visual language (2026-09-27).,"FIXED (palette/font user-approved, 2026-09-27); D364 follows up on profile tags, click alignment and slider calibration." +D364,FIXED,Settings UI feedback:,FIXED (user mostly accepted; D365 follows up on annotation size and click targets). +D365,CLOSED,F10 annotation hierarchy and visible mouse change targets (2026-09-27).,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest. +D366,CLOSED,Input calibration label:,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest. +D367,?,F10 full-width control boxes misaligned with their contents (2026-09-27).,SUPERSEDED by D368 after user playtest: +D368,?,Simplify F10 click targets:,"INPUT RULE KEPT; tinted strips user-rejected, removed in D369." +D369,FIXED,Remove F10 row boxes/strips entirely (2026-09-27).,FIXED (awaiting live acceptance): +D370,CLOSED,F10 distance/volume drags hurt frame rate (2026-09-27).,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest. +D371,FIXED,"GEPD key-layout preset + reload/crouch bindings, v0.4.0 modern options wave M3 (2026-09-27).",FIXED (headless-verified; awaiting user live accept): +D372,FIXED,"Low-end perf preset (v0.4.0 modern options wave M5, 2026-09-27).",FIXED (headless-verified; awaiting user live accept): +D373,FIXED,"In-game crosshair on/off + colour (v0.4.0 modern options wave M2, 2026-09-27).",FIXED (user live-tested; rule-2 sign-off recorded 2026-09-27): +D374,FIXED,"GEPD becomes the default key layout; preset layering fix (v0.4.0 M3 playtest feedback, 2026-09-27).",FIXED (headless + user live-tested): +D375,CLOSED,Crouch wired to the wrong pad bit:,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest. +D376,CLOSED,Crouch follow-up + modern-options spot check (2026-09-27).,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest. +D377,FIXED,GEPD independent crouch (2026-09-27).,FIXED (scripted stance verified; user says live crouch was good; pad/hold-vs-toggle specifics owed): +D378,FIXED,GEPD use/reload separation (2026-09-27).,FIXED (headless edges tested; user says E/R seemed good; targeted door/partial-mag cases owed): +D379,FIXED,Crosshair colour: authored texture is red (2026-09-27).,FIXED (cyan frame capture; live yellow/blue recheck owed): +D380,FIXED,One PC keyboard layout; PD-style rebind design (2026-09-27).,FIXED (follow-up D383): +D381,CLOSED,"RULE-2-SIGNOFF: opt-in RGB, size and Beta crosshair (2026-09-27).",CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest. +D382,CLOSED,Crosshair menu placement and slider hitches (2026-09-27).,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest. +D383,PARTIAL,PD-style keyboard rebinding in both options UIs (2026-09-27).,PARTIAL (2026-09-28). +D384,CLOSED,Front PC Options key-bind navigation feedback + two-slot UI (2026-09-27).,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest. +D385,CLOSED,Real mouse 1–5 action rebinding in the two-slot editor (2026-09-27).,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest. +D386,CLOSED,Correct Aim default from SDL middle (Mouse 2) to right (Mouse 3) (2026-09-27).,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest. +D387,OPEN,M7 AllUnlocked's read-time fake unlocks leak into saved EEPROM (2026-09-27).,OPEN (isolated user-data-copy save/untoggle repro; no fix): +D388,CLOSED,Nest functional binding/HUD menus and remove redundant M5 toggle (2026-09-27).,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest. +D389,CLOSED,F10 footer clipping and ambiguous submenu links (2026-09-27).,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest. +D390,CLOSED,F10 pinned Back selection did not visibly change (2026-09-27).,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest. +D391,CLOSED,F10 click at glyph bottom could select next row (2026-09-27).,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest. +D392,FIXED,Train intro soldier hunched; hand intersects torso vs 1964 —,FIXED (#ifdef PORT ABI/layout in chraction.c); Train aim-angle probe confirms correction; user visually verified the soldier looks fixed against 1964. +D393,FIXED,Controller B/Y both crouch; no in-game use button —,FIXED + user live-tested A use / X reload / Y weapon cycle. +D394,PARTIAL,Xbox 1.1 Jinx controller parity —,PARTIAL (2026-09-28). +D395,RESOLVED,Controller trapped in keyboard/mouse-only binding capture —,"RESOLVED (v0.4.0 campaign sign-off 2026-09-28, user:" +D396,RESOLVED,"F10 pad B/A used as slider decrement/increment, not back/select —","RESOLVED (v0.4.0 campaign sign-off 2026-09-28, user:" +D397,RESOLVED,SELECT FILE background + intro gun-barrel show an interlaced/comb look —,RESOLVED (2026-09-28): +D398,FIXED,PC Options label: D348's procedural slider glyph replaced with an existing mainfolderimages texture (2026-09-27).,"SUPERSEDED by D400 (icon removed, text-only label; purple-hover bug moot; the D400 textMeasure arg-swap bug was found and fixed there)." +D399,PARTIAL,File-select bottom row (SELECT FILE text + Copy/Erase + PC Options dot/label) shifted 15px left as a block (user sign-off 2026-09-27).,"LANDED (port-only, D343 class):" +D400,PARTIAL,"File-select ""PC Options"" label:","LANDED (port-only, frontoptions.c):" +D401,CLOSED,Rumble Pak → real gamepad haptics (re-promotes D224/M-87):,CLOSED (2026-09-28). Rumble shipped in v0.4.0 (D401); Steam Deck real-pad validation: +D402,RESOLVED,First clean Linux (glibc / SteamOS) build of release/v0.4.0 broke in two port-layer include-resolution spots MinGW tolerated (bondconstants.h first-include cyc…,RESOLVED (Linux/Ubuntu-24.04 build → glibc-2.41-compatible binary for the Steam Deck): +D403,RESOLVED,Deck v0.4.0-pre test:,"RESOLVED (2026-09-27, user-confirmed on the Deck:" +D404,RESOLVED,"""Aim style: Centred (PC)"" (Input.AimMode=1, D333/D337) has no effect when aiming with a gamepad; the row's own doc comment over-promises (""applies to every aim…","RESOLVED (2026-09-28, port-layer only):" +D405,CLOSED,"fast3d/OpenGL: MSAA above the driver max black-screens; two glBindFramebuffer calls pass an index where a GL name is required (PR #106, italoarruda, 2026-09-25…","CLOSED (2026-09-28). Merged from PR #106 (code file only; security-reviewed clean - port-layer GL state fixes, no external input, no allocations)." +D406,RESOLVED,front-end PC Options screen:,"RESOLVED (2026-09-28, port-layer only, Steam Deck-validated):" +D407,PARTIAL,Tanks cannot be boarded/exited on PC —,"PARTIAL (fix landed in v0.4.0, port-only):" +D408,OPEN,Game.SkipIntro skips the post-mission failure dossier:,"OPEN (cosmetic, not root-caused past the handoff site)." diff --git a/docs/dev/findings.md b/docs/dev/findings.md index d70589a9..f23ee5ee 100644 --- a/docs/dev/findings.md +++ b/docs/dev/findings.md @@ -391,36 +391,37 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D59–D68 | intro render: blood-RLE clobber, DMA validate, OSMesgQueue, HEADS/BODIES sentinels, romCopy width, image_entry, Globalimagetable BE→LE | resolved | | D69 · D78–D84 | stage load (`load_bg_file`): bg/stan offline sidecar (`d69_emit.py`) + StandTile/bg_room_data ABI | resolved | | D70–D74 | intro-logo pixels: C-array bswap, UV path, sinf/cosf `DVAL()`, texture-import truncation | resolved | -| D75 | front-end 3D model transforms (D77 audio split out below — resolved, unrelated cause) | OPEN (parked). **M-32 triage (see §F "D75 ADDENDUM"): (a) D73-scope-gap RULED OUT (gu tree fully endian-clean). Splits in two: Bug 1 = logo/photo transform = the parked D114/D116 fast3d viewport mirror (not game code). Bug 2 = absent animated models = category (b). M-32b runtime probe (`GE_D75=1`): `render_pos`/`dynAllocate`-arena hypothesis RULED OUT (render_pos valid + fresh each frame), model instances valid (nMtx 21/1), zero fast3d DL warnings — failure is downstream in `drawjointlist`/`dotube` vtx/node-DL resolution or an off-screen `basemtx`. Needs a drawjointlist-level probe. M-33 (upright captures, D168 fixed): "Bug 1 = D114/D116 mirror" retracted; **gun-barrel Bond RENDERS fine** (walk + fire, upright) — the "absent" reports were the flipped capture; **Nintendo logo genuinely broken** (renders as 2 white blobs, shifted left — a real `logoinst` transform/geometry bug, not a flip); cast roll not re-captured. See §F "D75 Bug 2 — M-33 UPDATE". **v0.2.0-pre playtest (user):** Nintendo logo still broken; Rareware logo nearly right but its texture filtering looks off; MISSION COMPLETE / mode-select screens have no 3D models at all, so the earlier "absent mode-select model" framing was inaccurate.** | +| D75 | front-end 3D model transforms (D77 audio split out below — resolved, unrelated cause) | **PARTIAL (M-197 fixed the logo texture blobs; v0.4.0 campaign 2026-09-28: Nintendo + legal pages confirmed clean, but the Rareware logo still shows a minor texture-filtering artifact -- re-opened as a cosmetic known issue, README-disclosed): the D50 sidecar converter dropped the models' embedded texture blobs (`PnintendologoZ`/`PgoldeneyelogoZ`/`PlegalpageZ`) — `d43_emit.py` now raw-copies + round-trip-validates them; NTSC sidecars regenerated. Remaining: pal/jpn regen on ROM-holding machines.** Historical trail — M-32 triage (see §F "D75 ADDENDUM"): (a) D73-scope-gap RULED OUT (gu tree fully endian-clean). Splits in two: Bug 1 = logo/photo transform = the parked D114/D116 fast3d viewport mirror (not game code). Bug 2 = absent animated models = category (b). M-32b runtime probe (`GE_D75=1`): `render_pos`/`dynAllocate`-arena hypothesis RULED OUT (render_pos valid + fresh each frame), model instances valid (nMtx 21/1), zero fast3d DL warnings — failure is downstream in `drawjointlist`/`dotube` vtx/node-DL resolution or an off-screen `basemtx`. Needs a drawjointlist-level probe. M-33 (upright captures, D168 fixed): "Bug 1 = D114/D116 mirror" retracted; **gun-barrel Bond RENDERS fine** (walk + fire, upright) — the "absent" reports were the flipped capture; **Nintendo logo genuinely broken** (renders as 2 white blobs, shifted left — a real `logoinst` transform/geometry bug, not a flip); cast roll not re-captured. See §F "D75 Bug 2 — M-33 UPDATE". **v0.2.0-pre playtest (user):** Nintendo logo still broken; Rareware logo nearly right but its texture filtering looks off; MISSION COMPLETE / mode-select screens have no 3D models at all, so the earlier "absent mode-select model" framing was inaccurate.** | | D76 · D164 | **Legal screen only draws line 1 — root-caused (M-31)**: linker-adjacency assumption broken by mingw. — full `## D164` entry at file tail | **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). Docs simply never caught up to the merge. | | D159 | front-end wallet-Bond photo "interlaced"/combed (RC1 / D149) — `texSwapAltRowBytes` odd-row 8-byte pre-swap (N64 RDP odd-line TMEM XOR compensation) not reversed by fast3d | FIXED (`#ifdef PORT` no-op the swap in `image.c`) | | D161 | Depot (`-level_30`) ceiling = bright-blue speckle + radial rays (B2). A CI8 tile drawn with `gsDPSetTextureLUT(G_TT_NONE)` was decoded against the stale `rdp.palette` → garbage. Fix: `#ifdef`-free narrow route CI→I when `palette_fmt == G_TT_NONE` in `gfx_pc.cpp import_texture()`. | FIXED (`port/fast3d/gfx_pc.cpp`) | | D165 · D166 | **Input-layer polish (M-31, port-only)**: cursor P-controller + hipfire C-pulse. — full `## D165` / `## D166` entries at file tail | FIXED | -| D180 · D181 | **Native-PC input pass (QoL run)**: click-to-lock capture + ScreenShakeIntensity hook. — full `## D180` / `## D181` entries at file tail | LANDED, config-gated, feel-checks owed | -| D186 | **`Video.FpsCap` throttles the sim, not just presentation (M-39 diag, M-42 clamp).** The frame-pacing `sysSleep`+busy-wait in `sync_framerate_with_timer` (`port/fast3d/gfx_sdl2.cpp`) runs inline on the scheduler thread (`osSpTaskStartGo`→`gfx_run`→`swap_buffers_begin`), so a low cap blocks VI-retrace delivery and drags every game thread down to the cap rate (no N64 analogue — RSP/RDP are separate silicon). **Landed:** `0 < FpsCap < 30` → treated as `0` (uncapped) + warning, at both `videoInit` (normalises a bad `ge007.ini` for the next `configSave`) and `gfx_sdl_set_target_fps` (covers the F10-overlay live path); caps ≥ 30 unchanged. Verified: default `FpsCap=0` framediff 3/3 golden-identical; `FpsCap=10` now runs at the uncapped rate; `FpsCap=60` still paces. **Owed:** move pacing off the scheduler thread so any cap only drops presented frames. See §F "D186" + porting-notes.md §E. | CLAMP LANDED (`port/fast3d/gfx_sdl2.cpp`, `port/src/video.c`); real fix owed | +| D180 · D181 | **Native-PC input pass (QoL run)**: click-to-lock capture + ScreenShakeIntensity hook. — full `## D180` / `## D181` entries at file tail | FIXED (2026-09-26 bookkeeping: shipped; user reports it feels good in v0.4.0 play). Earlier: LANDED, config-gated, feel-checks owed | +| D186 | **`Video.FpsCap` throttles the sim, not just presentation (M-39 diag, M-42 clamp).** The frame-pacing `sysSleep`+busy-wait in `sync_framerate_with_timer` (`port/fast3d/gfx_sdl2.cpp`) runs inline on the scheduler thread (`osSpTaskStartGo`→`gfx_run`→`swap_buffers_begin`), so a low cap blocks VI-retrace delivery and drags every game thread down to the cap rate (no N64 analogue — RSP/RDP are separate silicon). **Landed:** `0 < FpsCap < 30` → treated as `0` (uncapped) + warning, at both `videoInit` (normalises a bad `ge007.ini` for the next `configSave`) and `gfx_sdl_set_target_fps` (covers the F10-overlay live path); caps ≥ 30 unchanged. Verified: default `FpsCap=0` framediff 3/3 golden-identical; `FpsCap=10` now runs at the uncapped rate; `FpsCap=60` still paces. **Owed:** move pacing off the scheduler thread so any cap only drops presented frames. See §F "D186" + porting-notes.md §E. | OPEN (2026-09-26, user direction: target an "original" framerate/timing mode, with 60 fps + options on top; pacing must stop throttling the sim). Earlier: CLAMP LANDED (`port/fast3d/gfx_sdl2.cpp`, `port/src/video.c`); real fix owed | | D187 | **HUD ammo-type/weapon icon vanished at `-O2` (M-46 root-cause, M-47 fix + verify).** — full `## D187` entry at file tail | FIXED (`src/game/gunfire.c`, `#ifdef AVOID_UB`); PR #19 | | D188 | **Linux `-O2` boot crash: `_Printf` passes `&args` of a by-value `va_list` param (M-48).** — full `## D188` entry at file tail | FIXED (`src/libultrare/libc/xprintf.c`, `src/sprintf.c`, `#ifdef PORT`); PR #22 | | D189 | **Linux `-fstack-protector` aborts on the decomp's latent stack-buffer overruns (M-48) — FIXED.** — full `## D189` entry at file tail | FIXED (`CMakeLists.txt`, `src/game/stan.c` `#ifdef PORT`); PR #23 | | D190 | **Linux `-level_45` SIGSEGV at load in `add_ptr_to_objective` (`objective.c:64`) (M-48, open).** — full `## D190` entry at file tail | **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).** Full chain… | | D191 | **Windows `-O2` release-bundle crash: SIGSEGV in `modelGetNodeRwData` (`src/game/model.c:478`) — the `root->Parent` node-tree walk (M-49, user playtests of the…** — full `## D191` entry at file tail | FIXED + PLAYTEST-VERIFIED (M-79).… | | D193 | **NPC AI locomotion is too slow — characters don't move to their destinations at the original game's rate (M-49, user playtest; clarified after the FULL 21-level…** — full `## D193` entry at file tail | **FIXED + user-playtest-verified (M-81) —… | -| D194 | **Mouse input needs another tuning pass — RMB aim over-sensitive + sensitivity couples to frame/sim rate (M-49, user playtest).** — full `## D194` entry at file tail | **PARTIAL — M-120 human playtest… | +| D194 | **Mouse input needs another tuning pass — RMB aim over-sensitive + sensitivity couples to frame/sim rate (M-49, user playtest).** — full `## D194` entry at file tail | FIXED (2026-09-26: user reports the v0.3.0→v0.4.0 mouse changes feel solid and glitch-free; code-side validation owed). Earlier: **PARTIAL — M-120 human playtest… | | D195 | **Transparency / alpha surfaces broken on Control (M-49, user playtest)** — full `## D195` entry at file tail | **FIXED (`port/fast3d/gfx_pc.cpp`, `gfx_sp_vertex`) —… | | D197 | **Character face/head textures wrap around the head, seen on Silo (M-49, user playtest).** — full `## D197` entry at file tail | CLOSED — user playtest v0.2.0-pre | | D198 | **`alAdpcmPull`/`_decodeChunk` used `K0_TO_PHYS` (an unconditional `& 0x1FFFFFFF` mask) on 64-bit heap pointers (`src/libultra/audio/load.c`).** — full `## D198` entry at file tail | FIXED (`src/libultra/audio/load.c`) — part of the Phase-3 mixer landing, not yet build/runtime-verified in isolation | -| D199 | **Phase-3 software audio mixer: macro-swap `aXxx` opcode execution + reverse-engineered `aSetBuffer` persistent-context semantics (`port/src/mixer.c`…** — full `## D199` entry at file tail | LANDED (`port/src/mixer.c`, `port/include/mixer.h`, `include/PR/abi.h`) — functional on `-level_09`, context-inference risk noted, needs a wider level sweep | +| D199 | **Phase-3 software audio mixer: macro-swap `aXxx` opcode execution + reverse-engineered `aSetBuffer` persistent-context semantics (`port/src/mixer.c`…** — full `## D199` entry at file tail | FIXED (2026-09-26 bookkeeping: shipped; user reports it feels good in v0.4.0 play). Earlier: LANDED (`port/src/mixer.c`, `port/include/mixer.h`, `include/PR/abi.h`) — functional on `-level_09`, context-inference risk noted, needs a wider level sweep | | D200 | **`-level_09` segfault: reverb's `ALDelay.input/output` are u32 — `-d->output` zero-extends to a +4GB pointer offset on 64-bit (D3x class, FIXED).** `src/libultrare/audio/reverb.c` back-references the delay ring as `&r->input[-d->output]`; on N64 (s32 `ptrdiff_t`) the u32 negation wraps to a small negative index, on PC x86-64 it zero-extends to +4294967136 samples → `_loadBuffer`/`_saveBuffer` write 13–52KB wild, trampling the delay array / `r->base` / adjacent heap in a self-propagating loop until unmapped memory. Fixed with `(s32)` negation casts at 5 sites (PD ground truth `n_reverb.c` does exactly this); ramalign line gets `(s64)`. Verified: `-level_09` runs 120 s+ crash-free, DRAM guard window over the FX region sees zero OOB writes. | FIXED — 5-line cast in reverb.c (this session) | | D196 | **OS mouse cursor stays visible after closing the F10 options overlay in click-to-lock mode (M-49, user playtest).** — full `## D196` entry at file tail | **FIXED (`6e758f1d`, merged to `main`) —… | | D192 | **Front-end mission/level-select grid pointer still can't reach the outer cells — but only under `Input.MouseCaptureMode=0` (legacy always-grab) (M-49, user…** — full `## D192` entry at file tail | 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 | **SELECT FILE mouse hit-box offset — compiler stack-layout dependency, not coordinate mismatch (M-87 QA / M-91 fix).** — full `## D221` entry at file tail | **FIXED (M-91)** — explicit `{min,max}` `coord2d` pairs under `#ifdef PORT`; human mouse-playtest confirmed. Distinct from D192 (mode-0 clamp range, still open). | | D222 | **High `Video.FovScale` breaks culling — NPCs and some world objects stop rendering near the screen edges (M-87, user QA).** — full `## D222` entry at file tail | **FIXED, awaiting visual verification (M-121).** Root cause traced one step further than the original write-up… | | D223 | **QoL ask: mouse-wheel weapon cycling should be directional like a normal PC shooter — wheel up = next weapon, wheel down = previous — instead of both directions…** — full `## D223` entry at file tail | **FIXED (M-105, `port/src/input.c`) —… | -| D224 | **QoL gap (M-87, from a PD-PC-port survey): the N64 Rumble Pak is fully dropped instead of modernized.** — full `## D224` entry at file tail | **DEPRIORITIZED (M-87, user call).** Gamepad-only —… | +| D224 | **QoL gap (M-87, from a PD-PC-port survey): the N64 Rumble Pak is fully dropped instead of modernized.** — full `## D224` entry at file tail | **LANDED via D401 (2026-09-27, re-promoted from DEPRIORITIZED).** Rumble Pak now drives real SDL gamepad haptics (`SDL_GameControllerRumble`) via the port-layer `inputRumble*` API + `CONT_CARD_ON`/`PFS_ERR_DEVICE` gating; single global `Input.RumbleScale` (0..1) config + one `Rumble strength` slider row. Real-pad playtest **done 2026-09-28 (Steam Deck: rumble works and is validated; user: include in v0.4.0)**. Gamepad-only —… | | D225 | **Dev-tooling QoL (M-87, user ask): a debug-build/env toggle that unlocks all missions + all cheats, for testing.** — full `## D225` entry at file tail | **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. Build-verified; headless boot with `GE_STARTMENU=7` (mission select) crash-free. | -| D226 | **QoL ask: player-adjustable HUD scale for the ammo counter, bottom-left pickup/item status text, and top-of-screen dialogue/subtitle text.** — full `## D226` entry at file tail | **OPEN — root cause / call sites identified, not designed or implemented.** Two viable directions… | +| D226 | **QoL ask: player-adjustable HUD scale for the ammo counter, bottom-left pickup/item status text, and top-of-screen dialogue/subtitle text.** — full `## D226` entry at file tail | **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 | **Sky renders as two independently-moving halves — "two sky processes" (M-93 QA; root cause M-100b: unit error, `unk20`/`unk24` are S10.5 not texels).** — full `## D227` entries (M-94→M-100b) at file tail | **FIXED (M-100b, PR #43)** — also fixed "scrolls too fast" (M-96) + over-tiling; human play-test confirmed. Full history in the sections. | | D229 | **IsWater-level water renders green, pulses blue<->green (M-101; Frigate `-level_26`).** — full `## D229` entry at file tail | **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. Full trail in the section. | | D230 | **In-level music quality: a bass instrument sounds corrupted on PC, correct on N64 reference (M-105, user reports; repro refined M-106).** — full `## D230` entry at file tail | **FIXED (M-121, v0.2.0) — RAW16 wavetable byte-order bug, fixed via in-place bswap16 in `romdataFinishCartMap()`; owes a by-ear pass** (Control elevator+track, Cavern elevator, Runway ~5-7s in). | +| D231 | **QoL batch: mute-on-focus-loss + screenshot-hotkey productization (M-105, roadmap Tier 3, port-only).** `Audio.MuteOnFocusLoss` (drop mixed blocks while the window is unfocused; pause-on-focus-loss deferred because of the D204-family AI feedback loop) plus `Video.ScreenshotKey`/`Video.ScreenshotDir` (scancode-based key, timestamped `screenshots/` PPMs). Implemented M-105 as PR #56 (`port/src/audio.c`, `port/src/video.c`, `port/include/audio.h`), never merged. | **CLOSED — PARKED/DEPRIORITIZED (user, 2026-09-25).** PR #56 closed and the `feat/qol-mute-screenshot` branch was deleted from GitHub. The code is in no release branch; it's kept locally only (local branch `feat/qol-mute-screenshot`, commit `3f6e8d98`, plus a patch in the gitignored `docs/dev/notes/parked/`). If it's revived: rebase onto the current release branch and playtest alt-tab mid-level plus the custom screenshot key/dir. | | D233 | **All levels (intermittent): indoor level geometry invisible near doors — you can see through walls/floors into rooms you can't see into, while world objects…** — full `## D233` entry at file tail | **FIXED (port/fast3d/gfx_pc.cpp, `gfx_sp_tri1`) —… | | D234 | **Dam: guard towers along the dam structure invisible except the first one (M-106, user QA report).** — full `## D234` entry at file tail | **VISUALLY VERIFIED FIXED (M-108) —… | | D244 | **Self-hosted CI runner (`corpyvt6agent01`) link failure: `undefined reference to 'objectiveGetStatus_WEAK'` (2026-09-12, first-ever build attempt on the…** — full `## D244` entry at file tail | **OPEN, not yet fixed.** Next… | @@ -428,9 +429,9 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D204 | **PC audio ran ~2 % below real time permanently (AI feedback loop's 3 ms setpoint vs PC jitter).** — full `## D204` entry at file tail | FIXED (`port/src/audio.c`, `port/src/libultra.c`, `port/include/audio.h`) — measured; a by-ear pass on a real playthrough still owed | | D203 | **Steam Deck (SteamOS, x86_64 Linux) v0.1.0 release bundle: Facility (`-level_34`) crashes "after loading in as James Bond" (user bug report).** — full `## D203` entry at file tail | CLOSED — root cause + fix in D253. | | D201 | **`src/libultra/audio/bnkf.c` bank/instrument/sound/wavetable relocation offsets were `s32`, truncating the real 64-bit base pointer.** — full `## D201` entry at file tail | FIXED (`src/libultra/audio/bnkf.c`) — part of the Phase-3 mixer landing, not yet build/runtime-verified in isolation | -| D202 | **Silenced PPK "slap" + broader real-time mixer corruption (M-52→M-67).** — full `## D202` entry at file tail | ROOT CAUSE ESTABLISHED (M-66); Disposition C implemented + measured (M-66b) — stuck loop is sound 203, a faithful N64 quirk; PC-side expiration fades it. M-67 static analysis exhausted. Full trail in the section. | +| D202 | **Silenced PPK "slap" + broader real-time mixer corruption (M-52→M-67).** — full `## D202` entry at file tail | FIXED (2026-09-26, user by-ear: silenced PPK sounds good, no slap or lingering loop; M-66b expiry + later audio fixes). Earlier: OPEN — by-ear check owed (2026-09-26): fire the silenced PPK a lot, listen for a slap/crackle or a lingering loop. Earlier: ROOT CAUSE ESTABLISHED (M-66); Disposition C implemented + measured (M-66b) — stuck loop is sound 203, a faithful N64 quirk; PC-side expiration fades it. M-67 static analysis exhausted. Full trail in the section. | | D206 | **PC played every SFX one bank slot too high — pointer-width layout shift in `struct ALInstrumentAlt_s` (M-76).** Root cause of D205 (3)/(4) + D202's "slap". — full `## D206` entry at file tail | **FIXED (M-76, `src/snd.c:1001`, `#ifdef PORT`)** — ABI/layout-only (D3x class); user by-ear A/B vs N64 = PASS (M-77c) → D202 closed. | -| D207 | **Alarm klaxon starves combat SFX and doesn't recover after it stops (surfaced by D206, M-77).** — full `## D207` entry at file tail | MONITORING (M-77c: user reports the alarm is acceptable in play after D206; no fix shipped) — voice-starvation hypothesis did NOT hold up (M-77b); designed pass reverted. | +| D207 | **Alarm klaxon starves combat SFX and doesn't recover after it stops (surfaced by D206, M-77).** — full `## D207` entry at file tail | MONITORING — user 2026-09-26: "fixed possibly"; M-201 live captures showed no drops (see D241). Earlier: MONITORING (M-77c: user reports the alarm is acceptable in play after D206; no fix shipped) — voice-starvation hypothesis did NOT hold up (M-77b); designed pass reverted. | | D208 | **Player weapon-fire sound goes permanently silent mid-firefight (all other SFX unaffected) — `struct` field `bondview.h:200 s32 field_A48` is used as a second…** — full `## D208` entry at file tail | FIXED + user playtest-verified (M-78d) — Bunker firefight: player gunshots persist through the whole engagement and survive the klaxon stopping. Committed 7601b9db (PR #29, merged). | | D209 | **Every AI character in the game is locked to a *walk* animation regardless of the speed the ailist commands — `act_ubytes.padding[45]`, a raw-byte alias into…** — full `## D209` entry at file tail | **FIXED + user-playtest-verified (M-81).** `chraction.c:3665`, `#ifdef PORT` → `get_sound_at_range(self, self->act_gopos.unk59, ...)`; the N64 line kept verbatim under `#else`.… | | D210 | **`chrToPatrol()` leaves `act_patrol.lastvisible60` uninitialised on PC — a raw-byte union alias (`act_init.padding[0x13]`) that shifts when `act_patrol` widens…** — full `## D210` entry at file tail | **FIXED (`chraction.c:3872`, `#ifdef PORT` → `self->act_patrol.lastvisible60 = -1;`, N64 line under `#else`).… | @@ -440,25 +441,25 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D168 | **`GE_PCDUMP` / F12 PPM captures were vertically flipped — the entire source of the bogus D114/D116 "HUD/text X-mirror" (M-33, developer-confirmed).** — full `## D168` entry at file tail | FIXED (`port/fast3d/gfx_opengl.cpp`); D114 / D116 reclassified as capture-orientation artifacts (below). | | D169 | **Front-end mouse pointer can't reach the outer cells of the mission/level-select grid — only an inner ~3×3 selectable (M-33, developer bug report).** — full `## D169` entry at file tail | **FIXED (M-33, `port/src/input.c`, port-only, no `#ifdef PORT`).** The `menuMode` pointer branch now derives its clamp bounds from the live virtual screen —… | | D178 | **Pre-mission briefing screen: objectives blank / missing, briefing pages blank (also the D143 "briefing text blank" side effect) — FIXED (M-36).** — full `## D178` entry at file tail | **FIXED (M-36)** — high confidence | -| D175 | **In-game stutter / brief hang during normal play** — full `## D175` entry at file tail | OPEN — observed, not investigated | +| D175 | **In-game stutter / brief hang during normal play** — full `## D175` entry at file tail | CLOSED (v0.4.0 campaign sign-off 2026-09-28, user: no stutter over the full campaign -- fixed). Earlier: OPEN (user, 2026-09-26: still happens occasionally; keep). Earlier: OPEN — observed, not investigated. **Steam Deck (Linux, 2026-09-27/28): occasional frame-timing hitches at low CPU/GPU (~15–30 %) — a frame-pacing/present profile, not a throughput bottleneck (see entry).** | | D176 | **Surface exterior renders wrong (`-level_36`), two independent defects.** (a) black sky = RDPHALF no-op, root-caused M-37; scroll-speed FIXED (perspective-correct tc in `skyPortRenderPoly`). (b) cliff/rock walls = grey diagonal static — NOT a decode bug (D183: 0/166 loads strided); re-scope from ROM ground truth of the wall texnum (M-37 inconclusive, leaning tiling-density). — full `## D176(a)` entries at file tail; see also GRAPHICS-BACKLOG.md D176 | **(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. **(b) open.** | | D177 | **Ladders non-functional — progression blocker (user QA) — FIXED (M-36): `count`/`rooms` land in the high half of a widened pointer.** — full `## D177` entry at file tail | **FIXED (M-36)** — high confidence; interactive climb test owed | | D172 | **Bullet-impact / blood / spark particles render magenta or cyan instead of dark red (M-82, ROOT CAUSE FOUND + FIXED).** — full `## D172` entry at file tail | **FIXED (`port/fast3d/gfx_pc.cpp` `gfx_lod_tile_offset`)… | -| D174 | **"No blood effect" (user QA report).** — full `## D174` entry at file tail | OPEN — observed; first step = BUNKER1 firefight with regenerated sidecars | -| D183 | **M-36 Family A ("texture line/pitch shear") is DISPROVEN for the Surface repro (`-level_36`).** The importers' pitch assumption is real but never fires there; the Surface cliff "grey static" is not a shear. Full evidence + what the walls actually are: §F "D183" below. A defensive, provably-no-op de-stride landed in `import_texture()` anyway (covers the `gfx_dp_load_tile` case the `SUPPORT_CHECK`s assert against), plus `GE_DTEX` STRIDED marker + `GE_TEXRAW` raw-source dump. | Hypothesis DISPROVEN + diagnostics shipped; D176(b)/D182(2) still OPEN | +| D174 | **"No blood effect" (user QA report).** — full `## D174` entry at file tail | CLOSED — faithful (2026-09-26, user side-by-side on 1964: blood matches N64). Earlier: Blood renders (user, 2026-09-26); OPEN only for an N64-accuracy comparison on 1964. Earlier: OPEN — observed; first step = BUNKER1 firefight with regenerated sidecars | +| D183 | **M-36 Family A ("texture line/pitch shear") is DISPROVEN for the Surface repro (`-level_36`).** The importers' pitch assumption is real but never fires there; the Surface cliff "grey static" is not a shear. Full evidence + what the walls actually are: §F "D183" below. A defensive, provably-no-op de-stride landed in `import_texture()` anyway (covers the `gfx_dp_load_tile` case the `SUPPORT_CHECK`s assert against), plus `GE_DTEX` STRIDED marker + `GE_TEXRAW` raw-source dump. | CLOSED (2026-09-26, user-approved: disproven hypothesis; the Surface sky issue it served was fixed under D176). Earlier: Hypothesis DISPROVEN + diagnostics shipped; D176(b)/D182(2) still OPEN | | D173 | **Third-person Bond model spawns too high** — full `## D173` entry at file tail | **MOSTLY FIXED as a side effect of D243 (2026-09-18, user-confirmed).** User: "9/10 times he's in the right place now, if he's off it's only by a small amount, no more floating in midair or glitching out spinning like before." The M-160/M-165 shared-mechanism hypothesis (D243's animation-state/position desync class) holds for this bug too, confirmed by outcome even though the exact code path was never separately traced for D173 specifically. Small positional-offset residual remains, not investigated further — no more full floating/spin. | | D179 | **Packaged build crashes after the logos — the D43/D69 model + bg sidecars are ROM-derived and absent from any build that has no ROM (M-34, alpha-release QA).** — full `## D179` entry at file tail | Diagnosed, not fixed — release packaging gap + latent `romdata.c` fallback | -| D180 | **Native-PC input pass (M-XX QoL run, `qol/native-pc-input-menu`, port-only).** — full `## D180` entry at file tail | LANDED, port-only, no `#ifdef PORT`.… | -| D181 | **`Game.ScreenShakeIntensity` — first route-(b) `src/` gameplay-cosmetic hook (M-XX QoL run).** — full `## D181` entry at file tail | **Menu row pulled (v0.2.1, user testing).** The hook is correct on its own narrow terms —… | -| D184 | **F10 port-layer options overlay — the approach-(C) surface from `OPTIONS-MENU-PLAN.md` (M-37).** — full `## D184` entry at file tail | **LANDED, port-only, no `#ifdef PORT` in `src/`.** Build links 241/241.… | -| D211 | **PC FOV slider — `Video.FovScale` (M-83 QoL wave, port-only, `WIDESCREEN-FOV-PLAN.md` Phase 4).** — full `## D211` entry at file tail | **PARTIAL — M-84 refix… | -| D212 | **Anisotropic-filtering config wiring — `Video.Anisotropy` (M-83 QoL wave, port-only).** — full `## D212` entry at file tail | **LANDED, port-only.** Build clean. **Owed:** eyeball at 16× on a grazing surface (Depot roof / Dam walkway) vs 1×. | -| D213 | **On-screen FPS readout — `Video.DisplayFPS` (M-83 QoL wave, port-only, PD parity).** — full `## D213` entry at file tail | **LANDED, port-only.** Build clean; `bunker1` no-op confirmed. **Owed:** eyeball that the counter reads a sane ~30 and doesn't overlap the HUD. | -| D214 | **Keyboard rebinding — `[Input.Bind]` config section (M-83, port-only, PD parity, Phase 4 debt).** — full `## D214` entry at file tail | **PARTIAL — M-84: code PASS (alt/ctrl); one doc-only fix.** LANDED, port-only, no `#ifdef PORT`; build clean.… | +| D180 | **Native-PC input pass (M-XX QoL run, `qol/native-pc-input-menu`, port-only).** — full `## D180` entry at file tail | FIXED (2026-09-26 bookkeeping: shipped; user reports it feels good in v0.4.0 play). Earlier: LANDED, port-only, no `#ifdef PORT`.… | +| D181 | **`Game.ScreenShakeIntensity` — first route-(b) `src/` gameplay-cosmetic hook (M-XX QoL run).** — full `## D181` entry at file tail | FIXED (2026-09-26 bookkeeping: shipped; user reports it feels good in v0.4.0 play). Earlier: **Menu row pulled (v0.2.1, user testing).** The hook is correct on its own narrow terms —… | +| D184 | **F10 port-layer options overlay — the approach-(C) surface from `OPTIONS-MENU-PLAN.md` (M-37).** — full `## D184` entry at file tail | FIXED (2026-09-26 bookkeeping: shipped; user reports it feels good in v0.4.0 play). Earlier: **LANDED, port-only, no `#ifdef PORT` in `src/`.** Build links 241/241.… | +| D211 | **PC FOV slider — `Video.FovScale` (M-83 QoL wave, port-only, `WIDESCREEN-FOV-PLAN.md` Phase 4).** — full `## D211` entry at file tail | FIXED (2026-09-26 bookkeeping: shipped; user reports it feels good in v0.4.0 play). Earlier: **PARTIAL — M-84 refix… | +| D212 | **Anisotropic-filtering config wiring — `Video.Anisotropy` (M-83 QoL wave, port-only).** — full `## D212` entry at file tail | FIXED (2026-09-26 bookkeeping: shipped; user reports it feels good in v0.4.0 play). Earlier: **LANDED, port-only.** Build clean. **Owed:** eyeball at 16× on a grazing surface (Depot roof / Dam walkway) vs 1×. | +| D213 | **On-screen FPS readout — `Video.DisplayFPS` (M-83 QoL wave, port-only, PD parity).** — full `## D213` entry at file tail | FIXED (2026-09-26 bookkeeping: shipped; user reports it feels good in v0.4.0 play). Earlier: **LANDED, port-only.** Build clean; `bunker1` no-op confirmed. **Owed:** eyeball that the counter reads a sane ~30 and doesn't overlap the HUD. | +| D214 | **Keyboard rebinding — `[Input.Bind]` config section (M-83, port-only, PD parity, Phase 4 debt).** — full `## D214` entry at file tail | FIXED (2026-09-26 bookkeeping: shipped; user reports it feels good in v0.4.0 play). Earlier: **PARTIAL — M-84: code PASS (alt/ctrl); one doc-only fix.** LANDED, port-only, no `#ifdef PORT`; build clean.… | | D215 | **1P weapon viewmodel never drew in-level — `gunRenderFirstPersonGunModels` builds its `ModelRenderData` from a reinterpret across separately-declared adjacent…** — full `## D215` entry at file tail | **FIXED (`src/game/gunfire.c`, `#ifdef PORT`).** Build clean.… | -| D216 | **`Game.SkipIntro` — boot straight to the file-select menu (M-83, port + one `#ifdef PORT` game-code line, PD parity, Phase 4 quick win).** — full `## D216` entry at file tail | **LANDED + VERIFIED this session.** Build clean.… | +| D216 | **`Game.SkipIntro` — boot straight to the file-select menu (M-83, port + one `#ifdef PORT` game-code line, PD parity, Phase 4 quick win).** — full `## D216` entry at file tail | FIXED (2026-09-26 bookkeeping: shipped; user reports it feels good in v0.4.0 play). Earlier: **LANDED + VERIFIED this session.** Build clean.… | | D217 | **1P weapon viewmodel + 3P Bond model textures garbled — wrong/stale palette on model-DL CI textures (M-84).** Two defects: â‘  texpool arena misalignment (the green), â‘¡ CI texture-cache collision. — full `## D217` entries (M-86→M-97) at file tail | **FIXED + merged** (PR #47 + PR #49); white residual split out as D228. | -| D218 | **Wide FOV exposes the per-level draw-distance / fog boundary — "blue artifacting" down long peripheral sightlines (M-84, user QA on the D211 FOV slider).** — full `## D218` entry at file tail | **Logged (M-84), fix proposed + parked.** Plan… | +| D218 | **Wide FOV exposes the per-level draw-distance / fog boundary — "blue artifacting" down long peripheral sightlines (M-84, user QA on the D211 FOV slider).** — full `## D218` entry at file tail | FIXED (2026-09-26 reclassified: the proposed fix shipped as Video.DrawDistance + DrawDistanceAutoFov, default 150). Earlier: **Logged (M-84), fix proposed + parked.** Plan… | | D219 | **PPK explosions render purple on Dam (M-87, user QA, "not 3D but noting").** — full `## D219` entry at file tail | **RESOLVED (M-115, 2026-09-12)** for the original PPK-purple symptom. **The adjacent bullet-spark-rainbow investigation logged under this label (M-157/M-158) is also FIXED (2026-09-18 session)** — `glass2.c`'s `bullet_spark_render` read the spark's RGBA colour via a hardcoded raw byte offset instead of the named `s_bullet_spark` struct fields, landing in the wrong memory on 64-bit PC; fixed by reading the named fields. User live-confirmed. D252's duplicate "lingering particle residue" report closes as the same bug (M-188). | **M-111 (2026-09-12, live session, 6 build/relaunch cycles): G_LIGHTING lead RULED OUT; texture-cache-collision lead RULED OUT; ROOT CAUSE FOUND — R/B channel swap in the decoded fire particle texture data itself.** Live-captured 4 rounds against a real user playtest (not scripted) with the user confirming explosions were still blue/purple in every capture: @@ -472,14 +473,14 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). 2. **Confirmed empirically, not just by inspection:** dumped raw decoded RGBA5551 words at the exact point `texChannelsToPixels`/`texInflateLookup` write them (`GE_D219RAW=1`, Dam `-level_33`, scripted PPK fire via `GE_INPUTSCRIPT`) — e.g. pre-fix texnum 2111 decoded word `0xf1c2` (R=30,G=3,B=1 under standard RGBA5551 — briefly looks *plausible* red until you realize this is the value BEFORE the byte-swap fast3d applies; the two-stage mis-cancellation is what produced the reported blue/purple, not a one-step swap — see the fix verification below for the real signal). The conclusive test is the rendered/uploaded texture, not the intermediate value in isolation. 3. **Fix (`src/game/image.c`, `#ifdef PORT`/`#else` macro, no `src/` logic changed):** added `PORT_PIXEL16`/`PORT_PIXEL32` — `__builtin_bswap16`/`32` under `#ifdef PORT`, identity under `#else` (so the N64 build is byte-for-byte unaffected, verified by inspection: the macro is a no-op expansion, not a code path change) — and wrapped every wide-pixel (`RGBA32`/`RGB24`/`RGBA16`/`IA16`/`RGB15`) store in the five functions listed above. CI/I4/I8/IA4/IA8 branches (single-byte, no byte order) are untouched. This is the D3x/AGENTS.md#2 narrow ABI/byte-layout exception class: mechanical, semantics-preserving (same numeric color, corrected byte order for the LE host), scoped to exactly the format cases that need it. 4. **Verified, numerically and visually:** `GE_TEXDUMP=1` PPM capture of the 15 `IMAGE_FIRE_N` textures post-fix — average RGB flipped from B-dominant (M-111: `fire000 (102,67,138)`) to R-dominant (post-fix `fire000 (141,127,53)`, `fire003 (147,81,24)`, `fire005 (47,10,2)`); a 10x-upscaled render of one frame and a 6-frame animation strip (frames 000-006, session scratchpad) show a clean, unambiguous fire-sprite animation — bright yellow-white core cooling through orange to red/black ash, exactly the expected in-game fire look, no purple/blue anywhere. Sanity-checked a montage of 12 ordinary (non-fire) world textures from the same Dam capture (clouds, hazard stripes, pipes, grating, concrete) — all render correctly, no regression from the format-gated change. `-level_09` (BUNKER1) and `-level_33` (Dam) both boot and render crash-free post-fix (10s+ each, no `ge007.crash.log`). **Not yet done:** a full 21-level `verify.sh sweep` and a genuine live human playtest confirming the fix in real (non-scripted) gameplay — the scripted-fire + texdump evidence here is strong but the row should stay open until a human eyeballs a real firefight on this build. `docs/porting-notes.md` gets a new entry for this bug class (runtime-decoded multi-byte pixel formats need explicit byte-order handling; static C-array textures already get this via D71/`gfx_tex_normalize_source`, runtime-decompressed ones did not). **M-114 — REGRESSION, reverted.** User playtest after M-113 landed reported 1P muzzle flash rendering blue and the ammo HUD counter rendering pink — colors inverted on textures that were fine before. Root cause: M-113's `PORT_PIXEL16`/`PORT_PIXEL32` swap is applied unconditionally to every call site in `texReadUncompressed`, `texChannelsToPixels`, `texInflateLookup`, and `texInflateLookupFromBuffer` — i.e. to every RGBA16/RGB15/IA16/RGBA32/RGB24 texture decoded through these shared functions, not just `IMAGE_FIRE_N`. This is the exact failure mode already logged above against the sibling function ("do NOT attempt a blanket R/B swap in `import_texture_rgba16` — it's shared by the whole game and would break every correctly-rendering RGBA16 texture"), just repeated one file over. Muzzle flash and the ammo HUD icon evidently decode through this same shared path without needing the swap. **Fix applied:** `PORT_PIXEL16`/`PORT_PIXEL32` reverted to identity (no-op) in `src/game/image.c`; the macro/call-site wrapping is left in place as scaffolding. **D219 is OPEN again** — the fire-particle color bug is unfixed. **Next, concrete, not yet done:** find the actual property that distinguishes `IMAGE_FIRE_N`'s decode from a working same-format sibling (M-112 already narrowed one candidate: FIRE is baked directly into a DL rather than going through the `sImageTableEntry` table that flare/ammo/impact sprites use — confirm whether that DL-embedded path is what needs the swap, or whether it's specifically `compmethod`, per M-112's still-open next step) and scope the macro to that property, not to a hardcoded texnum range. Not runtime-verified this session (static revert only, done to stop an active regression fast) — a human should confirm muzzle flash/ammo colors are back to normal on this build. **M-115 (2026-09-12, delegated session, 3 build/run cycles): fix RE-APPLIED, scoped by GBI pixel format — the M-114 regression resolved by construction, fire fixed again.** Per M-114's brief, found the property that distinguishes `IMAGE_FIRE_N`'s decode from working same-format siblings and scoped the macros to it. (1) **The discriminator is the decoded pixel FORMAT, not compmethod or texnum.** Full offline census of all 2698 ROM image-table entries (`scratch/d219_headers.py`, parsing real bitstream headers with the same MSB-first `texReadBits` contract as `src/game/image_bank.c`; zlib images skipped after the 2-byte `rz` preamble per `decompressdata`): only 94 non-zlib wide-pixel images exist — RGBA16=19, RGB15=14, IA16=1, RGBA32=60. The fire frames span compmethods RLE(4)/LOOKUP(5)/RLELOOKUP(7)/HUFFMANBLUR(8) (confirmed offline AND at runtime), so compmethod cannot scope the fix; a texnum range is a hardcoded landmine. Format IS structural: every 16-bit GBI-size format (RGBA16/RGB15/IA16) is consumed by importers that read the pool as **manually big-endian bytes** (`import_texture_rgba16`: `col16=(addr[0]<<8)|addr[1]`; `import_texture_ia16`: `intensity=addr[0], alpha=addr[1]`), while every 32-bit format (RGBA32/RGB24) is consumed by `import_texture_rgba32`, which does `PD_BE32()` on a **native** u32 load — it expects host-order words. So bswap-at-store is required for exactly the 16-bit family and must be identity for the 32-bit family; M-113's blanket swap hit both (M-114), M-114's full revert broke the 16-bit family again. (2) **Change (`src/game/image.c`, `#ifdef PORT` only, no call-site edits):** `PORT_PIXEL16(x)` = `__builtin_bswap16((unsigned short)(x))`; `PORT_PIXEL32(x)` = `(x)`. Every existing call site already routes by `TEXFORMAT_*` to the right macro, so M-114's scaffolding was already format-scoped per site — only the macro definitions were wrong (both bswapped). 30-line comment at the definition records the importer contract + census. (3) **Runtime verification (Dam `-level_33`, scripted Z-pulse fire via `GE_INPUTSCRIPT`, `GE_D219RAW=1` + `GE_TEXDUMP`/`GE_TEXRAW`):** all 15 fire frames decode at runtime as format=1 (RGBA16) with compmethods 4/5/7/8 exactly matching the offline census (texnum 2085 is genuinely an nc=1 LOOKUP transparent first frame — A=0 everywhere; the visible frames are 2087–2113); the raw pool dumps decode as warm RGBA5551 sprites — mean RGB over opaque texels **(255,238,139)**, R-dominant, correct vertical-blob shape (rows 3–10 of the 16×14 tile); e.g. texnum 2111's pool word is now `0xc2f1`, which the importer reads back as `0xf1c2` = R30,G3,B1 — the true decoded value M-113 recorded pre-fix, i.e. exactly the pool state whose rendered PPMs M-113 proved correct (mean 141,127,53, clean animation). (4) **Regression safety is structural, not empirical:** RGBA32/RGB24 pool bytes are bit-identical to the M-114-revert build (`PORT_PIXEL32` = identity in both), so muzzle flash / ammo HUD / flares / crosshairs — all RGBA32 — render exactly as in the state the user last confirmed fine; no 32-bit path was touched at all. The other non-zlib 16-bit images (texnum 1198–1201 RGBA16, 2430 IA16, 2510–2523 RGB15) were equally byte-reversed before this change and are now corrected by the same importer contract; they do not load during a Dam run, so their on-screen state is owed to the next human playtest in whatever level uses them. (5) **Probes left in tree** (env-gated, inert): `GE_D219RAW` generalized in `texInflateNonZlib` — header line for every non-zlib wide-pixel decode (`format <= TEXFORMAT_IA16`), first-row pixel dump for RGBA16/RGB15/IA16, no texnum filter; plus the pre-existing D172/D219 and `GE_TEXDUMP` fire-dumper probes. **Status: RESOLVED (2026-09-12)** — user playtested this build and confirmed it works (fire correct, no muzzle-flash/ammo-HUD regression); D219 closed. | -| D220 | **1P muzzle flash position shifts upward at higher `Video.FovScale` (M-87, user QA, Jungle/M16).** — full `## D220` entry at file tail | **ANALYZED (M-91) — the suspected unscaled-viewmodel root does NOT hold; this is correct perspective behavior, likely cosmetic/QoL.** The 1P hand + flash are model nodes in ONE gun DL (`gunfire.c` builds `rwmtx[1..4]` for hammer/flash/billboard nodes) drawn through the single per-frame projection from `viSetupCurrentPlayerView` —… | -| D85 | **Room primary/secondary DL → `texLoadFromGdl` garbage.** — see §H (D85 entries: root cause + widen/texture-pool fixes) | OPEN (safety-netted; widen/pool fixes landed, geometry now renders) | +| D220 | **1P muzzle flash position shifts upward at higher `Video.FovScale` (M-87, user QA, Jungle/M16).** — full `## D220` entry at file tail | CLOSED — merged into D303 (2026-09-26, user: the M16/AR15 muzzle flash rises regardless of FOV, so the FOV framing was wrong; tracked as D303 with a 1964 comparison). Earlier: **ANALYZED (M-91) — the suspected unscaled-viewmodel root does NOT hold; this is correct perspective behavior, likely cosmetic/QoL.** The 1P hand + flash are model nodes in ONE gun DL (`gunfire.c` builds `rwmtx[1..4]` for hammer/flash/billboard nodes) drawn through the single per-frame projection from `viSetupCurrentPlayerView` —… | +| D85 | **Room primary/secondary DL → `texLoadFromGdl` garbage.** — see §H (D85 entries: root cause + widen/texture-pool fixes) | CLOSED (2026-09-26, user-approved: widen + texture-pool fixes landed long ago, safety net in place, no current symptoms). Earlier: OPEN (safety-netted; widen/pool fixes landed, geometry now renders) | | D86 · D87 | **modelInitRwData truncated ptr · attract-demo BE `ramromfilestructure`.** — see §H (D86, D87) | resolved | | D88.1–D88.3 · D88.5–D88.6 | `Usetup*Z` header + sub-table width/endian conversion (`d88_emit.py`) | resolved | | D88.4 | **`propDefs` polymorphic record stream not byteswapped → `setupDoor` crash.** — see §H (D88.4) + `## D132` | resolved (`d88_propdefs.py`); layout audit M-20 → **D132** | | D132 | D88 propDefs layout audit (M-20): converter cursor confirmed vs real PC struct layout for all types the 21 levels emit. DOOR/OBJECT-prefix/VEHICHLE/AIRCRAFT/TANK/AUTOGUN/AMMO/TINTED_GLASS/objective sub-records all MATCH. **Divergence found:** types 14 LINK / 19 SWITCH / 38 LOCK_DOOR / 44 SAFE_ITEM — each has `s32 IndexN` fields sharing a union with a pointer (`LinkRecord.first`/`Index1`), so on PC the field sits in an 8-byte, 8-aligned union slot, but the converter emits it at N64 tight-4-byte-word offsets → `pdef->Index1` reads `Index2`'s value, `Index2` reads 0 → switch-doors / dual-weapons / locked-doors / safes silently fail their validity guard (non-crash; guard failure also prevents the under-sized-record `->next` overflow). | proposed fix (not applied) — see below | | D157 | **Campaign never unlocks the next level (M-30, user bug — "complete Dam on Agent, Facility stays locked, no save").** — full `## D157` entry at file tail | FIXED (`src/bondtypes.h`, `#ifdef PORT` —… | -| D160 | **Dam level-end cutscene (Bond off the dam / bungee) never plays — cuts straight to the MISSION COMPLETE report (M-30/M-31, user bug D148).** — full `## D160` entry at file tail | DIAGNOSTIC SHIPPED, not root-caused (`c95713f5`, `#ifdef PORT` + `GE_D160=1`)… | +| D160 | **Dam level-end cutscene (Bond off the dam / bungee) never plays — cuts straight to the MISSION COMPLETE report (M-30/M-31, user bug D148).** — full `## D160` entry at file tail | FIXED (user, 2026-09-26: the Dam ending cutscene plays since v0.3.0). Earlier: DIAGNOSTIC SHIPPED, not root-caused (`c95713f5`, `#ifdef PORT` + `GE_D160=1`)… | | D161 | **Depot (`-level_30`) ceiling renders as bright-blue speckle + radial "light-ray" streaks converging to a point instead of a dark corrugated roof (M-30/M-31…** — full `## D161` entry at file tail | FIXED (`port/fast3d/gfx_pc.cpp` `import_texture`, `port/fast3d/gfx_opengl.cpp` diag). porting-notes.md §D. | | D89–D92 | stage-load→frame: stan zero-fill overrun, portal address trunc, chr/AI spawn ptrs | resolved | | D93–D102 | struct-pun / hardcoded-size pass: player alloc, player.model inline Model, weapon Model pun, master-DL buffer | resolved (D95 pool bump partial) | @@ -489,20 +490,20 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D117 | **Frame nondeterminism root-caused; `GE_DETERM` attempted, not achieved.** — see §H (D117 M-8 root cause + M-52 addenda) | OPEN (mode attempted, not achieved) | | D118 | **SDL input layer; M-24 mouse-look rework (mode-aware map, real INI); D118b/c FIXED.** — see §H (D118 + M-24 entries) | resolved (D118a residual) | | D150 | watch OBJECTIVES / BRIEF page crash: `strcat(buf, langGet(id))` with `langGet` → NULL (unloaded bank) → NULL deref; `str.c` str* builtins elide a plain param NULL check, so guard via asm-laundered `GE_IS_NULL` | FIXED (`src/str.c` `#ifdef PORT`); interactive re-verify pending | -| D260–D265 | 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 → root-caused to D264 (fixed at Gfx level, visual check pending) · D263 Facility Ourumov/Trevelyan beat — **resolved: not a bug**, proximity/LOS-triggered as on N64; full chain incl. execution shot verified firing on PC (teleport probe + user playtest) · D264 watch preview renderdata.flags zeroed by cross-global template read on PC (ABI/layout class; fixed) · D236/D265 Surface 1 tree "wall": IA16 importer dimensions transposed (fixed); decode attempt reverted after raw-byte proof that byte-pair is the authored image; root cause = `import_texture_ia4` decoding RDP I2:A2 as I3:A1, inverting the tree strip's alpha ramp → opaque repeating silhouettes; IA4 I3:A1→I2:A2 decode fix applied and user re-tested — **still a wall; reverted, parked** with evidence + suspect list in D265 | partial — see rows | -| D266–D269 | D236/D265 wall, second pass: **the wall is CI8 32×32, not IA** (D269) — band-gated probe finds two draw classes per room stream (OPA oml=0xc4112078 + XLU oml=0xc41049d8), both BILERP/TEXTLOD/2-cycle/**FOG-on**; cards are soft gradient blobs, stripe period ≈ one card per visible room. D266: re-applied the reverted I2:A2 IA4 decode + replaced the hardcoded 0.19 edge quantize with AC-aware handling (census shows zero IA4 draws at boot). D267: IA8 dump infra — the three IA8 images are noise/mast sprites, not the wall. D268: force-alpha A/B proves the blend pipeline works. Ranked suspects now: fog range/application (D218 family) > TEXTLOD/mip sampling > draw density/culling > palette | OPEN (the wall itself) — probes live in tree (`GE_D266`/`GE_D266W`/`GE_D267`/`GE_D268_FORCEA`); the D266 decode + D271 portal-culling fixes **landed** (`532092c7`, M-139 R2 extraction) | -| D270 | **Backfilled (M-132) — this label was never given its own row; only code comments and the HANDOFF archive documented it.** — full `## D270` entry at file tail | dead-ended into D271/D272; infrastructure kept in tree | +| D260–D265 | 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 → root-caused to D264 (fixed at Gfx level, visual check pending) · D263 Facility Ourumov/Trevelyan beat — **resolved: not a bug**, proximity/LOS-triggered as on N64; full chain incl. execution shot verified firing on PC (teleport probe + user playtest) · D264 watch preview renderdata.flags zeroed by cross-global template read on PC (ABI/layout class; fixed) · D236/D265 Surface 1 tree "wall": IA16 importer dimensions transposed (fixed); decode attempt reverted after raw-byte proof that byte-pair is the authored image; root cause = `import_texture_ia4` decoding RDP I2:A2 as I3:A1, inverting the tree strip's alpha ramp → opaque repeating silhouettes; IA4 I3:A1→I2:A2 decode fix applied and user re-tested — **still a wall; reverted, parked** with evidence + suspect list in D265 | CLOSED (2026-09-26 bookkeeping: v0.2.1 hotfix batch shipped; D265 is a D236 sub-pass, closed M-198). Earlier: partial — see rows | +| D266–D269 | D236/D265 wall, second pass: **the wall is CI8 32×32, not IA** (D269) — band-gated probe finds two draw classes per room stream (OPA oml=0xc4112078 + XLU oml=0xc41049d8), both BILERP/TEXTLOD/2-cycle/**FOG-on**; cards are soft gradient blobs, stripe period ≈ one card per visible room. D266: re-applied the reverted I2:A2 IA4 decode + replaced the hardcoded 0.19 edge quantize with AC-aware handling (census shows zero IA4 draws at boot). D267: IA8 dump infra — the three IA8 images are noise/mast sprites, not the wall. D268: force-alpha A/B proves the blend pipeline works. Ranked suspects now: fog range/application (D218 family) > TEXTLOD/mip sampling > draw density/culling > palette | CLOSED (2026-09-26 bookkeeping: sub-pass of D236, which closed in M-198). Earlier: OPEN (the wall itself) — probes live in tree (`GE_D266`/`GE_D266W`/`GE_D267`/`GE_D268_FORCEA`); the D266 decode + D271 portal-culling fixes **landed** (`532092c7`, M-139 R2 extraction) | +| D270 | **Backfilled (M-132) — this label was never given its own row; only code comments and the HANDOFF archive documented it.** — full `## D270` entry at file tail | CLOSED (2026-09-26 bookkeeping: sub-pass of D236, which closed in M-198). Earlier: dead-ended into D271/D272; infrastructure kept in tree | | D271 | **D236 wall, third pass — suspect #3 (draw density/culling) CONFIRMED as a real over-draw; the D106 guard was the cause (M-129).** — full `## D271` entry at file tail | FIXED-but-insufficient (`src/game/bg.c` `#ifdef PORT`) — **landed** `532092c7` (M-139 R2 extraction, probes stripped); wall still open, see D236 | -| D276 | **D236 wall, eighth pass — the true painter of the visible weave is a fully-opaque IA8 blocky-noise texture, not either of D269's named CI8 classes (M-134).** — full `## D276` entry at file tail | 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`…** — full `## D279` entry at file tail | undocumented — artifacts only | -| D281 | **`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…** — full `## D281` entry at file tail | OPEN — user-reported regression, not root-caused; mitigation is user-facing guidance only (release notes: don't enable until a save exists, treat as experimental). | +| D276 | **D236 wall, eighth pass — the true painter of the visible weave is a fully-opaque IA8 blocky-noise texture, not either of D269's named CI8 classes (M-134).** — full `## D276` entry at file tail | CLOSED (2026-09-26 bookkeeping: sub-pass of D236, which closed in M-198). Earlier: 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`…** — full `## D279` entry at file tail | CLOSED (2026-09-26 bookkeeping: sub-pass of D236, which closed in M-198). Earlier: undocumented — artifacts only | +| D281 | **`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…** — full `## D281` entry at file tail | FIXED (2026-09-23, port/src/libultra.c): the AllUnlocked patch now reproduces the game's fresh-slot result (folder saves with DEFAULT_OPTIONS + one free slot) instead of five folder-1 slots with options 0 (no aim sight). Verified by fresh-eep SAVELOG; audio half **confirmed 2026-09-28 (campaign, user: good)**. v0.4.0 decision (user-delegated, 2026-09-28): keep the shipped mitigation + README disclosure; the zero-saves root cause (sndHandleEvent/aim interaction) is v0.5.0. | | D282 | **QoL ask (v0.2.1 user testing, 2026-09-15): front-end menu navigation (main menu, file select, mission-select map) currently requires the RIGHT analog stick —…** — full `## D282` entry at file tail | FIXED (`port/src/input.c`, `#ifdef` not needed — port-layer-only file), feel unconfirmed. | | D283 | **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…** — full `## D283` entry at file tail | 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 — fog math VERIFIED N64-faithful; the IA8 noise class VISUALLY confirmed as the weave's painter (M-135).** No quick win for trees. — full `## D280` entry at file tail | REFRAMED again — no fix; next steps in detail section, see D236 | -| D275 | **D236 wall, eighth pass — probe upgrades (`GE_D270PIX` list, `GE_D270KEEPOML` counter) (M-133).** — full `## D275` entry at file tail | done — probes live, uncommitted; see D276 for what they found | -| D274 | **D236 wall, seventh pass — probe identity (`seq`) unreliable; per-slot address stamp. Content-vs-decode question was a bug in our own offline analysis (M-132).** — full `## D274` entry at file tail | `d267_slot_addr` fix live (always-on); content/decode question CLOSED; mip-presence CLOSED; wall still open, see D236/D276 | -| D273 | **D236 wall, fifth pass — N64 ground-truth screenshot obtained; `GE_D273` palette-decode A/B RULED OUT (M-131).** — full `## D273` entry at file tail | RULED OUT (`port/fast3d/gfx_pc.cpp` `palette_to_rgba32`, pre-existing/uncommitted `GE_D273`) — wall still open, see D236/D272 | -| D272 | **D236 wall, fourth pass — per-card S/T audit RULES OUT "identical UV window" + converter bug; cards sample a wide authored UV range (M-130).** — full `## D272` entry at file tail | OPEN — probe `GE_D272` added to `port/fast3d/gfx_pc.cpp` (env-gated, uncommitted); wall still open, see D236 | +| D280 | **D236 wall, ninth pass — fog math VERIFIED N64-faithful; the IA8 noise class VISUALLY confirmed as the weave's painter (M-135).** No quick win for trees. — full `## D280` entry at file tail | CLOSED (2026-09-26 bookkeeping: sub-pass of D236, which closed in M-198). Earlier: REFRAMED again — no fix; next steps in detail section, see D236 | +| D275 | **D236 wall, eighth pass — probe upgrades (`GE_D270PIX` list, `GE_D270KEEPOML` counter) (M-133).** — full `## D275` entry at file tail | CLOSED (2026-09-26 bookkeeping: sub-pass of D236, which closed in M-198). Earlier: done — probes live, uncommitted; see D276 for what they found | +| D274 | **D236 wall, seventh pass — probe identity (`seq`) unreliable; per-slot address stamp. Content-vs-decode question was a bug in our own offline analysis (M-132).** — full `## D274` entry at file tail | CLOSED (2026-09-26 bookkeeping: sub-pass of D236, which closed in M-198). Earlier: `d267_slot_addr` fix live (always-on); content/decode question CLOSED; mip-presence CLOSED; wall still open, see D236/D276 | +| D273 | **D236 wall, fifth pass — N64 ground-truth screenshot obtained; `GE_D273` palette-decode A/B RULED OUT (M-131).** — full `## D273` entry at file tail | CLOSED (2026-09-26 bookkeeping: sub-pass of D236, which closed in M-198). Earlier: RULED OUT (`port/fast3d/gfx_pc.cpp` `palette_to_rgba32`, pre-existing/uncommitted `GE_D273`) — wall still open, see D236/D272 | +| D272 | **D236 wall, fourth pass — per-card S/T audit RULES OUT "identical UV window" + converter bug; cards sample a wide authored UV range (M-130).** — full `## D272` entry at file tail | CLOSED (2026-09-26 bookkeeping: sub-pass of D236, which closed in M-198). Earlier: OPEN — probe `GE_D272` added to `port/fast3d/gfx_pc.cpp` (env-gated, uncommitted); wall still open, see D236 | | D137 | **right-mouse (aim-sight) crash:** — full `## D137` entry at file tail | FIXED (`src/game/gunfire.c` `#ifdef PORT` — `sp54` is `Gfx *`) | | D119 · D120 | guard-attack `weapons_held[]->chr` pun crash · blood-stain `PointUsage[]` chain hang | D119 fixed · **D120 FIXED (M-30)** | | D120 | **Blood-decal `PointUsage[]` chain — converter now emits it (M-30).** — full `## D120` entry at file tail | FIXED (`tools_pc/d43_emit.py`). Interactive verify pending — blood decals on a BUNKER1 firefight should now appear + no hang. | @@ -522,10 +523,10 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D118d | **Watch-menu list over-scrolls with keyboard (M-27, user bug report).** `game_options_inventory_navigation` / `sub_GAME_7F0A611C` (`options.c`) have a "slam the stick" fast-scroll: a raw *level* check `joyGetStickY(PLAYER_1) < -0x46` / `>= 0x47` that steps `watch_inventory_cursor_pos` by 1 **every frame** it's held. An N64 stick rarely sustains past ~0x46 on-axis so a human taps it; keyboard W/S (and SDL pads pegged at 0x80) sit there every frame → one keypress skips several items (breaks item selection needed to finish the game). Fix: `#ifdef PORT` macro `GE_WATCH_STICK_FAST{UP,DOWN}` → `0` for the port — stick list-nav then goes only through the latched single-step path (`watch_stick_y_pressed_*`, one step per press, existing game mechanism) and the -0x1e..-0x45 smooth-scroll band; D-pad/C-buttons keep edge-repeat. N64 `#else` = verbatim. | FIXED (`src/game/options.c`, `#ifdef PORT`). Build green, `-level_09` unregressed. **Feel to be confirmed in playtest** (probe couldn't reach the INVENTORY watch page headlessly). D118 input family. Latent sibling: `front.c` menu nav uses `joyGetStick*InRange` level checks the same way. | | D141 | **Watch-menu item-model crash (M-27), exposed by D140.** — full `## D141` entry at file tail | FIXED (`src/game/gunfire.c`, `#ifdef PORT`). Watch page now renders (weapon page verified — mirrored text D114/D116, dark weapon model D75, both parked cosmetic). `-level_09`/`-level_20` unregressed. | | D150 | **Watch objective/briefing page crash (M-28).** — full `## D150` entry at file tail | FIXED (`src/str.c`, `#ifdef PORT`). Build green (`ntsc-final`); `-level_09` 600+ frames @ 91.67% unregressed. Watch-page repro is interactive — user to re-verify. porting-notes.md §C. | -| D152 | **Mission-failed / objective-failed → permanent black screen (M-28, user bug report — LIVE process inspected, not yet fixed).** — full `## D152` entry at file tail | MITIGATED (`port/src/libultra.c`, M-28, `#ifdef PORT`/port-only).… | +| D152 | **Mission-failed / objective-failed → permanent black screen (M-28, user bug report — LIVE process inspected, not yet fixed).** — full `## D152` entry at file tail | FIXED (user, 2026-09-26: no black screen after mission/objective failure). Earlier: MITIGATED (`port/src/libultra.c`, M-28, `#ifdef PORT`/port-only).… | | D152+ | **D152 addendum — static audit + narrow fix (M-31).** — full `## D152+` entry at file tail | FIXED (`src/snd.c`, `port/src/libultra.c`, `#ifdef PORT`). porting-notes.md §D4. | | D154 | **`bg.c` room-geometry ray/hit-test GBI parser port (M-28, WRITTEN / UNVERIFIED / UNCOMMITTED).** — full `## D154` entry at file tail | **M-30 re-audit + fixes (still playtest-gated for final verify).** Cross-checked the ported G_TRI1 + all 12 G_TRI4 nibble recoveries twice against the N64 `#else` and the raw BE-word bit derivation —… | -| D156 | **Facility outro-cutscene "hang" — the actual fix (M-29, user bug report, 2nd occurrence after D155).** — full `## D156` entry at file tail | GUARDED (`src/game/model.c`, `#ifdef PORT`)… | +| D156 | **Facility outro-cutscene "hang" — the actual fix (M-29, user bug report, 2nd occurrence after D155).** — full `## D156` entry at file tail | FIXED (user, 2026-09-26: no outro hang seen). Earlier: GUARDED (`src/game/model.c`, `#ifdef PORT`)… | | D155 | **Facility outro-cutscene end → "hang" = unclamped `deltaFrames` spiral (M-29, user bug report).** — full `## D155` entry at file tail | FIXED (`src/game/frametiming.c`, `#ifdef PORT` —… | | D153 | **`-level_09` frame ~900–1400 `0xc0000005` = D117/D134 load flakiness, NOT a bug (M-28).** — full `## D153` entry at file tail | NOT A BUG — closed. Run anchors on a quiet machine. | | D151 | **Watch data all blank — objectives / mission background / M / Q / Moneypenny (M-28, user bug report).** — full `## D151` entry at file tail | FIXED (`src/bondtypes.h`, `#ifdef PORT` —… | @@ -538,23 +539,23 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D166 | **Hipfire mouse pitch is digital while yaw is analog — inconsistent feel (user bug B7 / D118a, "cheaper alternative").** — full `## D166` entry at file tail | FIXED (`port/src/input.c`).… | | D233 | **All levels (intermittent): indoor level geometry invisible near doors — you can see through walls/floors into rooms you can't see into, while world objects…** — full `## D233` entry at file tail | **FIXED + VISUALLY VERIFIED (M-107 fix, M-108 verify).** See the other D233 row for full detail. | | D234 | **Dam: guard towers along the dam structure invisible except the first one (M-106, user QA report).** — full `## D234` entry at file tail | **VISUALLY VERIFIED FIXED (M-108), mechanism unexplained.** See the other D234 row for detail. | -| D235 | **Wrong textures load after dying/restarting a level — not Facility-specific, seen on multiple levels (M-106, user QA report; clarified same session).** — full `## D235` entry at file tail | 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 | **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).** — full `## D236` entry at file tail | OPEN, sixteenth pass (2026-09-18) — new `GE_D236RAW` headless dump PROVES every room-background static vertex (all ~40 rooms sampled) is alpha=255 on load, ruling out "static ROM-authored low alpha" outright; a `vtxseg` extension to `GE_D236ALPHA` shows the tree class's vertices come from a raw/unsegmented pointer (not `SPSEGMENT_MODEL_VTX`/`BG_VTX`), consistent with a dynamically-built quad — but all 4 known `model.c` dynamic-quad builders (gunfire/shadow/rottex/tube) and propobj.c's screen-sprite builder were read in full and ruled out by content/addressing mismatch. The actual write site remains genuinely unidentified after a real search; next step is a stack-backtrace-style probe, not more signature-matching. Not v0.3.0-blocking, user is continuing anyway. | -| D237 | **QoL ask (M-106, user QA): F10 options overlay — make it scrollable with the mouse wheel, expose more settings into it (e.g. screen-flash / no-hit-flash…** — full `## D237` entry at file tail | OPEN — not designed; port-only | -| D238 | **QoL ask (M-106, user QA): mouse sensitivity rework toward normal PC-shooter feel.** — full `## D238` entry at file tail | OPEN — not designed; port-only (`port/src/input.c` + `config.c`) | +| D235 | **Wrong textures load after dying/restarting a level — not Facility-specific, seen on multiple levels (M-106, user QA report; clarified same session).** — full `## D235` entry at file tail | CLOSED (user, 2026-09-26: not seen since the v0.3.0–v0.4.0 work; fix wired; reopen if it recurs). Earlier: OPEN — fix wired, headless reload clean (2026-09-23); NOT reproduced with the fix off (headless) nor in the 2026-09-25 user playtest (die → mission select → restart). Keep the fix; close if it stays unreproduced through the release playtests. | +| D236 | **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).** — full `## D236` entry at file tail | **CLOSED (M-198, pass 26, `425237f9`; probes stripped `0503d335`): root cause = `TEXTURETYPE_DETAIL` tile binding — fast3d drew the detail tile (opaque IA8 noise) instead of the base image; `gfx_lod_tile_offset` now samples tile 1 when the pair differs (`Video.DetailBaseTile`, default on). Live-verified Surface 1/2 + Depot; awaiting user play-test sign-off.** Historical trail — pass 24 (2026-09-21, static) DECODED THE TREE TEXTURE** (new `tools_pc/texdecode.py` ports image.c's LOOKUP/HUFFLOOKUP/RLELOOKUP codecs). **`tex1198` LOD 0 = 64×17 RGBA5551, 44.2% opaque, and its 1-bit alpha renders as an unmistakable treeline silhouette** — pass 23's identification confirmed by decode, not inference. **Rare authored a six-level LOD pyramid that PRESERVES the silhouette and its alpha coverage (44.2% → 43.1% → 38.8%), each level a separately-authored image with its own compmethod.** Since `XLU_SURF` has no alpha test (pass 22), averaging that 0/1 alpha into fractional mip alpha would smear the minified treeline into exactly the reported band — so 'authored LOD chain replaced by driver-generated mips' is now the leading mechanism, with hard ground truth behind it. Corrects pass 23: only `tex1198` is a treeline (`tex1199` 89.4% opaque denser foliage; `tex1200`/`tex1201` 100% opaque detail textures sharing the class). Two decoder bugs found while porting (missing per-image byte-align, `image.c:1096`; a GUESSED rather than ported RLE fudge) — both produced confident wrong pictures. **Pass 25 (live) answered all three questions: (1) YES 64×17 RGBA16 intact, pool ≡ offline decode; (2) YES alpha survives (44.2% opaque); (3) NO — no code path uploads authored chains as GL mips.** New: `tex1198`–`1201` are **never drawn from an idle spawn** (zero RGBA16 imports in 4800 frames) — the spawn wall is painted by other classes (cf D276/D280); the drawn tree-class card is **`tex149` IA8 32×64 hard cutout, byte-exact import, no authored chain**. Mip mechanism located: `texGetDepthAndSize` sizes LOADBLOCKs from the header `lods` field even for single-image textures (live A/B: 2808 B vs 2048 B real data), and the port's RC2 clip + `glGenerateMipmap`/trilinear (observed live) box-filters hard 0/F alpha into fractional mips → XLU smear. Pass 26 fix directions in entry (cheapest: no mip gen for no-chain textures). | +| D237 | **QoL ask (M-106, user QA): F10 options overlay — make it scrollable with the mouse wheel, expose more settings into it (e.g. screen-flash / no-hit-flash…** — full `## D237` entry at file tail | FIXED (2026-09-23): category headers + 5 more settings (SkipIntro, Show FPS, pad invert/deadzone/trigger); wheel scroll already existed. Hands-on pass owed. | +| D238 | **QoL ask (M-106, user QA): mouse sensitivity rework toward normal PC-shooter feel.** — full `## D238` entry at file tail | FIXED (2026-09-26: covered by D337/D338 aim work; user reports mouse feel good; code-side validation owed). Earlier: OPEN — not designed; port-only (`port/src/input.c` + `config.c`) | | D239 | **QoL ask (M-106, user QA): click-to-lock vs always-grab parity.** — full `## D239` entry at file tail | CLOSED — executed in the v0.2.0 prep pass (port-only)… | -| D240 | **Player gunshot SFX timing/cadence reportedly does not match the N64 original's rate (M-106, user QA report).** — full `## D240` entry at file tail | **OPEN — not root-caused; static call-site tracing only, no runtime verification possible in this environment.** Confidence LOW-MEDIUM that this is a *new* bug distinct from the D155/D186/D193/D204/D209 timing-keystone family (all of which gate the identical `deltaFrames→speedgraphframes→g_ClockTimer→g_GlobalTimer` chain this SFX trigger consumes).… | -| D241 | **Player gunshot SFX goes silent under some conditions — reported on Bunker with the alarm sounding, "or some other factor" — but only for PP7 and AK47, Klobb…** — full `## D241` entry at file tail | **OPEN — not newly root-caused; high confidence this is D207 reproduced, not an independent bug.** Recommend folding this repro into D207 (or closing D241 as a duplicate once D207 is reopened) rather than tracking separately.… | -| D242 | **Player-death sequence UX regression vs N64 original: PC requires an LMB click to proceed to the mission-failed/file-select screen; user recalls N64 auto-plays…** — full `## D242` entry at file tail | **NOT A PORT-LOGIC BUG as far as static analysis can tell.** Confidence HIGH that the LMB-click skip path is faithful original behavior (Z_TRIG-mapped, exists unmodified in `bondview2.c`), not a defect to remove.… | +| D240 | **Player gunshot SFX timing/cadence reportedly does not match the N64 original's rate (M-106, user QA report).** — full `## D240` entry at file tail | **CLOSED (v0.4.0 campaign sign-off 2026-09-28, user: cadence drift not heard over the full campaign -- fixed). Never root-caused -- if re-reported, the D155/D186/D193/D204/D209 timing-keystone family is the prime suspect.** Earlier: OPEN — not root-caused; static call-site tracing only, no runtime verification possible in this environment. Confidence LOW-MEDIUM that this is a *new* bug distinct from the D155/D186/D193/D204/D209 timing-keystone family (all of which gate the identical `deltaFrames→speedgraphframes→g_ClockTimer→g_GlobalTimer` chain this SFX trigger consumes).… | +| D241 | **Player gunshot SFX goes silent under some conditions — reported on Bunker with the alarm sounding, "or some other factor" — but only for PP7 and AK47, Klobb…** — full `## D241` entry at file tail | **CLOSED as the D207 family (v0.4.0 campaign sign-off 2026-09-28, user: no silence under heavy fire + alarms -- fixed).** Earlier: OPEN — not newly root-caused; high confidence this is D207 reproduced, not an independent bug. Recommend folding this repro into D207 (or closing D241 as a duplicate once D207 is reopened) rather than tracking separately.… | +| D242 | **Player-death sequence UX regression vs N64 original: PC requires an LMB click to proceed to the mission-failed/file-select screen; user recalls N64 auto-plays…** — full `## D242` entry at file tail | FIXED/CLOSED (2026-09-26, user: death -> mission failed flow works without input, matches N64). Earlier: OPEN — N64 reference obtained (2026-09-26, user on 1964: after death the game returns to mission select and shows the "killed in action" screen); PC behaviour still to validate. Earlier: OPEN — 1964 comparison owed (2026-09-26, user). Earlier: **NOT A PORT-LOGIC BUG as far as static analysis can tell.** Confidence HIGH that the LMB-click skip path is faithful original behavior (Z_TRIG-mapped, exists unmodified in `bondview2.c`), not a defect to remove.… | | D243 | **Dam end-of-level cutscene (the abseil/bungee sequence, D148/D160's subject): the camera violently shakes/"spazzes" during each of the scripted camera-angle…** — full `## D243` entry at file tail | **FIXED, live-confirmed by the user (M-189/M-190, 2026-09-18).** Root cause found: `bondview2.c:598-599`'s manual buffer-layout math for Bond's third-person cutscene body model reserved space using a hardcoded 32-bit `sizeof(Model)` literal (`0xfb`) that never accounted for pointer widening — real 64-bit `sizeof(Model)` is 232 bytes, 44 more than reserved, so the animation-data buffer placed right after it overlapped the model's own `playspeed`/`animrate`/`unkac`/`unkb0`/`unkb4` fields, corrupting them (this is what M-183/M-185's clamps were band-aiding). Fixed by computing the real `sizeof(Model)` instead of the stale literal, matching the identical idiom already used one line above for `ModelFileHeader`. **M-190 follow-up:** M-170's camera-shake freeze was replaced with a filter re-seed-on-teleport (camera now tracks Bond continuously within each shot instead of holding a frozen angle), and an unrelated bug in the same review pass — mouse-look never gated during scripted/no-control camera states (D300's own documented-but-unimplemented safety gate) — was found and fixed in `port/src/input.c`. User confirmed both fixes live, full intro-flyover + Dam abseil playthrough: "ok yeah looks good." | -| D245 | **IsWater water: two competing patterns with a visible seam that moves as the player looks around (mouse/aim) — "looks almost exactly like skybox" (M-116, user…** — full `## D245` entry at file tail | 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 a live A/B, not shipped as default.… | +| D245 | **IsWater water: two competing patterns with a visible seam that moves as the player looks around (mouse/aim) — "looks almost exactly like skybox" (M-116, user…** — full `## D245` entry at file tail | FIXED (M-201, 2026-09-26, user-verified vs 1964): water texture imported/normalised at 32x43/16-wide instead of the 32x32 CI8 mask period (resets, 2x speed) + s16 sky/water vertices replaced by G_FLOATVTX_EXT floats (jitter). Earlier: OPEN — CONFIRMED PORT BUG (2026-09-26, user vs 1964: PC water displays glitched with a weird yellow line; view-dependent shift is subtle; animation otherwise matches). Earlier: 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 a live A/B, not shipped as default.… | | D246 | **Tiny pixel strips at the left and right edges of the screen, "like overscan" — look like raw pixels, not 3D graphics (M-116, user QA report).** — full `## D246` entry at file tail | **FIXED (2026-09-18 session, port-only, `Video.SafeAreaCrop`).** Measured precisely (exactly 1.0 logical unit missing on both edges, every resolution); underlying clip-precision root cause not isolated, but `gfx_adjust_viewport_or_scissor` now trims a fixed 1-unit margin from both edges — same toggle/mechanism as D247. Live user playtest confirmed clean edges. | | D247 | **Black bars top and bottom of screen — possible aspect-ratio defect (M-116, user QA report).** — full `## D247` entry at file tail | **FIXED (2026-09-18 session, port-only, `Video.SafeAreaCrop`).** Root cause: faithful, unmodified N64 TV-safe-area letterboxing that real CRT overscan used to hide; this port showed the full VI frame. Per user request, given a real fix (not left as "correct original behaviour"): `gfx_adjust_viewport_or_scissor` now remaps against the active gameplay viewport bounds so it fills the window edge-to-edge. New `Video.SafeAreaCrop` toggle (default on). Live user playtest confirmed clean edges. | | D248 | **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).** — full `## D248` entry at file tail | **FIXED, root-caused to a genuine pointer-width ABI bug in `src/sched.c` (compiled for the PC build, not excluded).** `osScAddClient()` (`src/sched.c:187-199`) stashes a per-client "present every retrace vs.… | -| D249 | **LOD/culling breaks down on large open levels (Streets, Egyptian named specifically) at *default* FOV — far-away geometry has visible culling issues (M-119…** — full `## D249` entry at file tail | 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 code reading.… | +| D249 | **LOD/culling breaks down on large open levels (Streets, Egyptian named specifically) at *default* FOV — far-away geometry has visible culling issues (M-119…** — full `## D249` entry at file tail | OPEN — M-142 static review ruled out the unscaled-far-clip and LOD-multiplier suspects (both already correct); CLOSED (v0.4.0 campaign sign-off 2026-09-28, user: no distant-geometry drop-out seen on Streets/Egyptian over the full campaign -- fixed); narrowed to the portal/room-visibility BFS, needs a live screenshot+location, not more code reading.… | | D250 | **FPS is inconsistent/low on many levels after 60 Hz shipped (M-119, user QA; D248 follow-on).** — full `## D250` entry at file tail | FIXED — per-call `getenv()` cost was the bottleneck; rock-steady 60-61 fps after, user re-tested 1440p/4K clean. | | D251 | ****F10 options overlay QoL pass:** — full `## D251` entry at file tail | **FIXED (port-only, `port/src/optionsoverlay.c`).** (a) The panel now computes how many rows fit the *actual* current 2D height (`maxVisibleRows()` from `viGetY()`) and scrolls… The last open symptom — the bottom row deterministically duplicating the selected item (v0.2.0-pre playtest) — is resolved by D304's `overlayUpdateScroll()` minimal-scroll rewrite; user-confirmed no longer occurring (2026-09-18). | -| D252 | **Particle effects cycle through a rainbow palette (M-124, user playtest of the v0.2.0-pre candidate).** — full `## D252` entry at file tail | **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).** `explosionRenderPart()`'s 4-vs-5-vertex overrun (`src/game/explosion.c`) is fixed and stays in. Initial testing suggested "fixed on Windows, recurs on Deck," but further user testing found it also recurs intermittently on Windows (same Jungle repro, not always reproducing) and is level-dependent (same repro method doesn't trigger it on every level) — not a clean platform split. Repro method (`Game.AllUnlocked` + cheats for an immediate grenade launcher) is flagged as a possible, unconfirmed factor. The unconfirmed secondary lead from this session (stale TMEM-slot `src_fmt` CI8 reroute, `port/fast3d/gfx_pc.cpp` ~line 1250) remains the best-named candidate for the residual. Not re-investigated further this session. | +| D252 | **Particle effects cycle through a rainbow palette (M-124, user playtest of the v0.2.0-pre candidate).** — full `## D252` entry at file tail | **FIXED (M-201, 2026-09-26, user-verified): port-side D68 shim `gimgSyncCompiledGlobalDLs` only synced the compiled explosion DLs on the first stage; every later stage kept the title stage's freed texture pointers. Earlier history: 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).** `explosionRenderPart()`'s 4-vs-5-vertex overrun (`src/game/explosion.c`) is fixed and stays in. Initial testing suggested "fixed on Windows, recurs on Deck," but further user testing found it also recurs intermittently on Windows (same Jungle repro, not always reproducing) and is level-dependent (same repro method doesn't trigger it on every level) — not a clean platform split. Repro method (`Game.AllUnlocked` + cheats for an immediate grenade launcher) is flagged as a possible, unconfirmed factor. The unconfirmed secondary lead from this session (stale TMEM-slot `src_fmt` CI8 reroute, `port/fast3d/gfx_pc.cpp` ~line 1250) remains the best-named candidate for the residual. Not re-investigated further this session. | | D253 | **Facility (and any level with a FORCECROUCH/LADDER linked special tile) SIGSEGV on Linux/Steam Deck — `stanCheckLinkedSpecialTile` stores to a garbage…** — full `## D253` entry at file tail | FIXED — Deck-verified: user got past the crouch tile with the re-cut bundle (a *new*, later crash is D255). | | D254 | **Linux crash handler destroyed its own logs: `ge007.crash.log` only ever contained the SIGABRT block, never the original SEGV.** — full `## D254` entry at file tail | FIXED — field-verified. | | D255 | **Facility firefight SIGSEGV on Steam Deck (v0.2.0-pre CI bundle; D253 fix confirmed working).** — full writeup directly below this table (row status was stale; see M-140 update) | **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 originally suspected. Verified live on real Steam Deck hardware (Facility + Caverns, zero crashes); Windows 21-level sweep clean. | @@ -567,47 +568,132 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D262 | **v0.2.1 (user report): "3D models missing on watch menu screen item select" — the selected-item 3D preview does not appear on the watch inventory page.** — full `## D262` entry at file tail | FIXED (primary cause, D264) — pixel-level confirmation pending user run. | | D264 | **D262's root cause (ABI/layout class, cf. D140/D52): the watch item preview's `ModelRenderData` template is read across two adjacent globals that no longer sit…** — full `## D264` entry at file tail | FIXED — build green, probes removed; visual confirmation pending. | | D263 | **v0.2.1 (user report): Facility — "Ourumov not shooting Trevelyan"; the scripted opening beat never triggers. RESOLVED: not a port bug — the beat is…** — full `## D263` entry at file tail | RESOLVED (not a bug), refined (v0.2.1 further user testing, 2026-09-15). The D318 re-report (2026-09-18) turned out to be a **different** failure mode (derailment race → softlock, see D318 RESOLVED) — D263's proximity-trigger mechanism map stands. | -| D265 | **D236 investigation (port-layer, `port/fast3d/gfx_pc.cpp`): the tree cards are IA8/IA4, not IA16 — and `import_texture_ia4` decoded the RDP's I2:A2 texel as…** — full `## D265` entry at file tail | PARKED — reverted to baseline; evidence + suspect list above. | -| D258 | **PAL/JP asset conversion is broken at the source-data level — `scripts/filelist.e.csv` and `filelist.j.csv` carry naming defects, so `gen_romassets.py e/j` (and…** — full `## D258` entry at file tail | OPEN — repair path verified, not applied (out of v0.2.0 scope). | +| D265 | **D236 investigation (port-layer, `port/fast3d/gfx_pc.cpp`): the tree cards are IA8/IA4, not IA16 — and `import_texture_ia4` decoded the RDP's I2:A2 texel as…** — full `## D265` entry at file tail | CLOSED (2026-09-26 bookkeeping: sub-pass of D236, which closed in M-198). Earlier: PARKED — reverted to baseline; evidence + suspect list above. | +| D258 | **PAL/JP asset conversion is broken at the source-data level — `scripts/filelist.e.csv` and `filelist.j.csv` carry naming defects, so `gen_romassets.py e/j` (and…** — full `## D258` entry at file tail | DEFERRED (user call 2026-09-28: v0.4.0 ships NTSC-only, README-disclosed; the verified repair path is post-v0.4 backlog). Earlier: OPEN — repair path verified, not applied (out of v0.2.0 scope). | | D284 | **`port/src/random.c`'s MIPS64 `dsll32`/`dsrl32` shift helpers were backwards — PC PRNG desynced from the N64's from draw 1, in every release to date (M-138 review; fix M-139b).** — full `## D284` entry at file tail | FIXED, 21-level sweep clean, no crashes — RNG stream changes from every prior build; existing replays will diverge. **"Savegames unaffected" was FALSE (M-148): `fileGenerateCRC` runs off a local constant seed via `randomGetNextFrom`, so the fix invalidates every stored slot CRC — v0.3.0 silently wipes all pre-fix saves on first launch (release-blocker candidate, see D295 M-148)** | -| D285 | **Steam Deck SIGSEGV during D255 live-testing = SEPARATE bug: the `sndHandleEvent` preempt-scan walks the active-sound list unlocked (M-140; fixed M-141).** — full `## D285` entry at file tail | **PARTIALLY FIXED, RECURRED PAST THE FIX (see D305, 2026-09-18).** `osSetIntMask` lock added around the preempt-scan (M-141), live-verified crash-free on real Deck hardware (2026-09-16) — but the exact same fault (same FAULT ADDR, same site) recurred on the v0.3.0 pre-release build, disproving the lock as a *complete* fix: it stops concurrent mutation but not a scan that starts on an already-empty list. D305 adds the actually-missing NULL guard. Do not conflate with D255 — different thread, different subsystem. | -| D286 | **User-confirmed real ~8s frame freeze on Caverns, Steam Deck — self-recovered, not a crash (M-140).** — full `## D286` entry at file tail | OPEN — one confirmed occurrence, no repro trigger identified, not root-caused; distinct from D255/D285. Do not chase a repro without a better lead. | -| D287 | **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…** — full `## D287` entry at file tail | OPEN — one confirmed occurrence, real (not self-recovering), not root-caused; `gfxFrameMsgQ` full is a lead, not a confirmed cause. | -| D288 | **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 —…** — full `## D288` entry at file tail | 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 | **Dam: a crash in the computer room, not reproducible on a second attempt (M-141, Steam Deck user playtest).** — full `## D289` entry at file tail | 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 | **Watch's in-game "controller" section shows no control-pad graphic (M-141, Steam Deck user playtest).** — full `## D290` entry at file tail | OPEN — observed, not investigated. Needs a live repro (open the watch, navigate to the controller-config page) plus a screenshot for comparison against N64 reference. | -| D291 | **Dam: texture glitches when the truck drives past (M-141, Steam Deck user playtest).** — full `## D291` entry at file tail | OPEN — observed, not investigated. Needs a live repro with the truck's exact position/timing and, ideally, a screenshot. | +| D285 | **Steam Deck SIGSEGV during D255 live-testing = SEPARATE bug: the `sndHandleEvent` preempt-scan walks the active-sound list unlocked (M-140; fixed M-141).** — full `## D285` entry at file tail | FIXED (user, 2026-09-26: fixed around v0.2.1/v0.2.2; D305 guard also landed; not seen since). Earlier: **PARTIALLY FIXED, RECURRED PAST THE FIX (see D305, 2026-09-18).** `osSetIntMask` lock added around the preempt-scan (M-141), live-verified crash-free on real Deck hardware (2026-09-16) — but the exact same fault (same FAULT ADDR, same site) recurred on the v0.3.0 pre-release build, disproving the lock as a *complete* fix: it stops concurrent mutation but not a scan that starts on an already-empty list. D305 adds the actually-missing NULL guard. Do not conflate with D255 — different thread, different subsystem. | +| D286 | **User-confirmed real ~8s frame freeze on Caverns, Steam Deck — self-recovered, not a crash (M-140).** — full `## D286` entry at file tail | CLOSED (2026-09-26, user-approved: single occurrence on a v0.2.x-era Deck build, before the D255/D285/D305/D329/D336 fixes; not seen since; reopen on recurrence). Earlier: OPEN — one confirmed occurrence, no repro trigger identified, not root-caused; distinct from D255/D285. Do not chase a repro without a better lead. | +| D287 | **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…** — full `## D287` entry at file tail | FIXED (user playtest 2026-09-26: idle-unfocused → click-in resumes, mouse checks fine). Earlier: FIX APPLIED, pending user re-test (2026-09-25) — likely mechanism found by code review: SDL mouse-mode/cursor calls (`SDL_SetRelativeMouseMode`, `SDL_ShowCursor`) were made from the scheduler thread (every controller poll) concurrently with the host (window) thread's focus/click handling; the idle+refocus trigger is exactly when both flip grab state. Now queued and applied only on the host thread (`inputApplyMouseRequests`). Not reproduced; confirm with an idle 20 min + refocus test. **Steam Deck (Linux/Wayland, 2026-09-27/28): idle + Deck settings and resume-from-sleep both fine — idle/refocus confirmed on the Deck.** | +| D288 | **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 —…** — full `## D288` entry at file tail | CLOSED — not reproduced on the current build (2026-09-23 capture) and the 2026-09-25 user Silo playtest confirms it gone ("silo fixed"). Fixer not isolated (D271 A/B ruled out). | +| D289 | **Dam: a crash in the computer room, not reproducible on a second attempt (M-141, Steam Deck user playtest).** — full `## D289` entry at file tail | FIXED (user, 2026-09-26: no recurrence on post-D255 builds). Earlier: 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 | **Watch's in-game "controller" section shows no control-pad graphic (M-141, Steam Deck user playtest).** — full `## D290` entry at file tail | 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 | **Dam: texture glitches when the truck drives past (M-141, Steam Deck user playtest).** — full `## D291` entry at file tail | CLOSED — duplicate of D306, faithful (M-201, 2026-09-26: user-captured, Dam intro camera 5 only). Earlier: OPEN — observed, not investigated. Needs a live repro with the truck's exact position/timing and, ideally, a screenshot. | | D292 | **Death animations are glitched — Bond is often sunk halfway into the floor (M-141, Steam Deck user playtest).** — full `## D292` entry at file tail | **MOSTLY FIXED as a side effect of D243 (2026-09-18, user-confirmed alongside D173) — same fix, same outcome: right 9/10 times, small residual offset only when off, no more full sinking/glitching.** See D173 for the user's exact confirmation wording; not separately re-tested, reported together. | | D293 | **QoL ask: no discoverable way to quit the game from the UI (M-141, Steam Deck user playtest — "couldn't see a way to exit the game. Maybe build this into the…** — full `## D293` entry at file tail | FIXED (M-141) — "Quit to desktop" row added to the F10 overlay; build-verified Win+Linux, not yet live-confirmed.… | -| D294 | **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…** — full `## D294` entry at file tail | OPEN — not investigated.… | +| D294 | **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…** — full `## D294` entry at file tail | **FIXED, user playtest confirmed (2026-09-25: not reproduced)** — root cause: room-model POOL exhaustion (authored `-ma220`=220 KB < ~250 KB monument-plaza demand at FovScale 130 + max DD/Lod); `memaAlloc` NULL → silent skip → undrawn room shows the night-sky clear colour. Stochastic (wall-clock seed → load order). Fix: `portRoomPoolScale()` (video.c, identity at N64-faithful settings) applied in `bossMainloop`; verified headless (faithful→exact authored size; user settings→0 failures; `GE_ROOMPOOL=0.25`→exhaustion on demand). Playtest: main patch gone; residual corner flicker traced to full-level residency (487 KB = 2.2× authored) starving the 2× pool; the 3×/linear fix REGRESSED the renderer on the menu-driven path (D156 NaN anim state, not a bank overflow) → reverted to sqrt + 2.0 cap (ma=450560). Residual open; probes in place for next capture. High-DD horizon edge on night levels = setting-inherent, documented only.… | | D295 | **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 playtest).** — full `## D295` entry at file tail | FIXED (M-149) — M-148 root-caused + reproduced on HEAD; fix landed: port-side JPN-cache seed (`port/src/main.c`) + `#ifdef PORT` buffer widen in `front.c` (D8 class). Verified: previously-crashing input now clean, 21/21 sweep PASS. Companion save-wipe regression → D297. History — M-148 fully root-caused + **reproduced on current HEAD**: `difficultytext[4]` (`front.c:2616`) overflow — `strcpy("00 Agent")`/`strcat("\n")` spills onto `textpos.p[0]` (y, ≥0x80 in normal geometries) → high-bit byte → `langGetJpnCharPixels` NULL deref (`language.c:320`, `g_JpnCacheCacheItems` NULL outside JPN mode — likewise unallocated on N64 non-JP builds, crash potential is decomp-native). The user's save is a *valid pre-D284* save (all 5 slot CRCs bit-exact under the old PRNG); post-fix builds wipe it at validation, which is why the unmodified artifact no longer triggers it — NOT a fix. No OOB exists (M-147 guess retracted: 007 difficulty has no stored times). Fix options flagged for sign-off (port-side JPN-cache init preferred; `difficultytext` enlarge = D8 class). Side finding: v0.3.0 silently wipes all pre-fix saves on first launch (D284 side effect — own D entry owed). Separate still-open issue found: unconditional `[EVTQ-DROP]` audio-pool-exhaustion logging, `src/libultra/audio/event.c:92`.… | -| D296 | **Control Center, 00 Agent: noticeable stutter, user's own read is "maybe from the drone guns" (M-146, same D295 investigation session).** — full `## D296` entry at file tail | OPEN — single live report, un-triaged; observed only under a gdb-attached debug-harness launch (`tools_pc/debug.ps1`), not the packaged release artifact — user flagged that harness overhead has caused false stutter before. Needs a plain-exe / real-release repro check before any perf tracing.… | +| D296 | **Control Center, 00 Agent: noticeable stutter, user's own read is "maybe from the drone guns" (M-146, same D295 investigation session).** — full `## D296` entry at file tail | CLOSED (2026-09-26, user-approved: stutter seen only under gdb (debug.ps1); D302, fixed two days later, removed a per-triangle uncached getenv costing ~15 fps; reopen if seen on a normal build). Earlier: OPEN — single live report, un-triaged; observed only under a gdb-attached debug-harness launch (`tools_pc/debug.ps1`), not the packaged release artifact — user flagged that harness overhead has caused false stutter before. Needs a plain-exe / real-release repro check before any perf tracing.… | | D297 | **v0.3.0 silently wipes ALL pre-fix player saves on first launch (D284 side effect; found during D295/M-148).** — full `## D297` entry at file tail | FIXED (M-150) — migration shim landed: `port/src/legacycrc.c` (isolated, deliberately-wrong pre-D284 PRNG, reviewer-verified bit-exact against `0de99b4d^`) + eeprom-shim read-path hook in `libultra.c` (block 4 only; persists pristine+migrated bytes before the read-time-only AllUnlocked patch). Verified: user's #87 artifact → `D297-migrated 5`, all slots OK, idempotent on relaunch; native post-fix save untouched; corrupt slot left to the game's own wipe path; fresh eep untouched; 21/21 sweep PASS. Block-0 header needed no migration (factory seal, verified). | -| D298 | **Full HUD (not just the aim-mode crosshair) missing in a v0.3.0 dev build — user suspects a debug-launch artifact and asked NOT to raise it until seen in a regular release-targeted build; parked** — full `## D298` entry at file tail |s — regression reported 2026-09-16 during v0.3.0 work (build ce8427cf); user says "don't fix, just for context"; not yet investigated** — full `## D298` entry at file tail | +| D298 | **Full HUD (not just the aim-mode crosshair) missing in a v0.3.0 dev build — user suspects a debug-launch artifact and asked NOT to raise it until seen in a regular release-targeted build; parked** — full `## D298` entry at file tail | CLOSED (2026-09-26, user: HUD is fine now). Earlier: s — regression reported 2026-09-16 during v0.3.0 work (build ce8427cf); user says "don't fix, just for context"; not yet investigated** — full `## D298` entry at file tail | | D299 | **Game.SkipIntro crashed selecting any save slot except one — reproduced on a fresh save and on a clean Release rebuild, ruling out stale save data (found + fixed 2026-09-17).** — full `## D299` entry at file tail | 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 PORT`, zero game-logic change). Build-verified both `RelWithDebInfo`/`Release`; live user re-test confirms all 4 slots now selectable with `SkipIntro=1`. Not yet run through a full 21-level sweep. | | D300 | **Hipfire look feels non-uniform (issue #89): mouse px routed through the N64 analog stick's quadratic natural-turn curve, so slow motion is "almost unrecognised" and fast motion saturates at full turn speed by ~13 px/poll.** — full `## D300` entry at file tail | FIXED, user-confirmed live — WI-1 per `docs/dev/GEPD-INPUT-PLAN.md`: new `hipDirectCompute()` (`port/src/input.c`) writes `vv_theta`/`vv_verta` directly, mirroring GEPD's hipfire formula and D194's existing aim-mode direct-write pattern — linear px→degree, FOV-scaled, dt-normalized, bypassing the stick curve entirely. Safety-gated on `bonddead`/`outside_watch_menu`/`pause_state` plus (M-190, 2026-09-18) the scripted-camera-mode check the original safety-gates writeup named but never implemented — mouse could freely move the camera during POSEND/INTRO cutscenes until this session. `Input.MouseDirectLook` (default 1) is the escape hatch back to the legacy stick path. Build/link-verified; user playtested live: "input already feels better," then again post-M-190: "ok yeah looks good." | | D301 | **`fileGetIsCheatUnlocked()` NULL-deref, unguarded on every call site, not just the D299/SkipIntro one (GitHub #87, two reports with an identical crash PC/fault address).** — full `## D301` entry at file tail | FIXED locally on `release/v0.3.0` (not pushed) — NULL-guarded `fileGetIsCheatUnlocked()` itself (`src/game/file2.c`, `#ifdef PORT`) rather than only its one known trigger route; build-verified, crash-free smoke test. Live confirmation against the actual #87 repro still owed — a synthetic `GE_STARTMENU=6` repro attempt did not reproduce the crash even pre-fix, so the exact #87 trigger condition (why the save lookup fails) remains unconfirmed. | | D302 | **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) — a reintroduction of D250's own uncached-`getenv()`-in-the-hot-path bug class.** — full `## D302` entry at file tail | 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 elsewhere in the file. Verified via `GE_D193=1 -level_33` (Dam): rock-steady `render=59-61` post-fix, matching D250's own post-fix baseline exactly (pre-fix D250 pinned this level at 45-50fps). Not yet re-confirmed by the user's own hands-on playtest. | -| D303 | **Some muzzle flashes (M16 named specifically, "and some others") draw wrong — the flash renders very long, extending straight up from the gun (user QA on the v0.3.0 pre-release build, 2026-09-18).** — full `## D303` entry at file tail | OPEN — reported only, not yet investigated. | +| D303 | **Some muzzle flashes (M16 named specifically, "and some others") draw wrong — the flash renders very long, extending straight up from the gun (user QA on the v0.3.0 pre-release build, 2026-09-18).** — full `## D303` entry at file tail | FIXED (M-201, 2026-09-26): d43_emit sized op-22 star-flash vertex arrays as count, not count*4 arms; regenerated sidecars render a clean star. Earlier: OPEN — CONFIRMED PORT BUG (2026-09-26, user vs 1964: M16/AR33 muzzle flash shoots up and draws much higher on PC; N64 shows a normal star/burst at the muzzle). Earlier: OPEN — 1964 comparison owed; absorbs D220 (user 2026-09-26: M16/AR15 flash rises regardless of FOV). Earlier: OPEN — investigated: pipeline + weapon stats + model conversion ruled out; GE_D303 capture probe in-tree (`port/fast3d/gfx_pc.cpp`), blocked on a live firing repro. | | D304 | **Mouse sensitivity menu overhaul (user request, 2026-09-18): aim speed/turn speed sliders weren't "1:1", had too little range, and (follow-up) risked decoupling into a bad state; F10 menu also had a click-jump bug.** — full `## D304` entry at file tail | 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, the actual cause of the reported "menu jumps to the setting below on click"). Build-verified (Windows) both passes; headless overlay smoke test clean after each. **Live F10 playtest done (2026-09-18): user confirmed the collapsed single-sensitivity menu works, the click-jump is gone, and the D251 bottom-row duplication no longer occurs.** | | D305 | **Steam Deck SIGSEGV on the v0.3.0 pre-release build (Cavern, early in the level, `Game.AllUnlocked` previously enabled on the session/save) — same exact FAULT ADDR 0x62 / `sndHandleEvent` signature as D285, reproduced AFTER D285's own lock fix was already live-verified crash-free on real hardware.** — full `## D305` entry at file tail | FIXED (defensive guard) — `sndHandleEvent`'s preempt-scan (`src/snd.c`) dereferenced its list-walk pointer before ever checking it for NULL (a do-while always runs its body once); D285's `osSetIntMask` lock only prevents another thread from emptying the list mid-scan, not a scan that starts with an already-empty list, so the crash could still happen even with that fix in place. Added the missing `iterState != NULL` guard before the loop, matching D255's established skip-instead-of-dereference precedent. Root desync mechanism (why pool-exhaustion accounting and the list can disagree) still not identified; `Game.AllUnlocked` correlation not proven, only reported. **Live-verified on the Deck (2026-09-18):** user ran two test sessions on Cavern with the updated build, plus toggled `Game.AllUnlocked` off then back on and relaunched — no crash in any of them. | -| D306 | **Dam intro/truck wheels render with odd transparent-looking areas, possibly z-fighting (user QA, 2026-09-18).** — full `## D306` entry at file tail | OPEN — reported only, not investigated. Explicitly not for v0.3.0. | -| D307 | **In-level security camera entities can be flipped facing the wrong direction (backwards) on some levels, e.g. Bunker (user QA, 2026-09-18) — NOT the player's view camera.** — full `## D307` entry at file tail | OPEN — reported only, not investigated. Explicitly not for v0.3.0. | -| D308 | **Occasional z-fighting confirmed on some levels' geometry, general/broader than the D306 truck-wheel instance (user QA, 2026-09-18).** — full `## D308` entry at file tail | OPEN — reported/confirmed by the user, not investigated. Explicitly not for v0.3.0; D306 may be one instance of this. | +| D306 | **Dam intro/truck wheels render with odd transparent-looking areas, possibly z-fighting (user QA, 2026-09-18).** — full `## D306` entry at file tail | CLOSED — faithful, confirmed on 1964 (M-201): all PC-specific leads refuted (FOV, depth precision, depth state, near plane, wheel transforms); it is the authored truck geometry seen from intro camera 5 under the truck. Absorbs D291. Earlier GE_ZF "real fighting" was a probe-quantisation artefact. | +| D307 | **In-level security camera entities can be flipped facing the wrong direction (backwards) on some levels, e.g. Bunker (user QA, 2026-09-18) — NOT the player's view camera.** — full `## D307` entry at file tail | FIXED (M-201, 2026-09-26, user-verified): renamed duplicate member `CCTVRecord.pad`→`lookpad` made setupCctv read the MOUNT pad as the look-at pad; prop.c reads lookpad on PC. Earlier: OPEN — CONFIRMED PORT BUG (2026-09-26, user vs 1964: ALL Bunker 1 security cameras face backwards on PC, correct on N64). Earlier: OPEN (investigated 2026-09-21) — no port-side bug found: orientation path traced, every input verified N64-faithful vs ROM (`PcctvZ` switch data, room-scaled boundpads, config words), all math byte-identical `src/game`. Blocked on user repro detail; `GE_D307=1` diagnostic in-tree. | +| D308 | **Occasional z-fighting confirmed on some levels' geometry, general/broader than the D306 truck-wheel instance (user QA, 2026-09-18).** — full `## D308` entry at file tail | CLOSED — faithful (2026-09-26, user: Statue ground z-fighting reproduces on 1964 GEPD bundle). Earlier: OPEN (user, 2026-09-26: possibly faithful; validate Statue ground z-fighting on 1964 vs port). Earlier: OPEN — investigated: PC depth precision == N64 (24-bit both sides; RSP writes full 16.16), so level-authored coincident geometry is the prime suspect; GE_ZF flicker probe in-tree. | | D309 | **Complete non-self-recovering freeze (Windows) returning from Caverns to the intro screen — boss thread spins forever in `ai()` on `m_RunToBondPersistent` via a phantom `GotoNext(lblZero)` created by `chraiitemsize`'s `AI_PRINT` NUL-scan mis-sizing a stringless PRINT record (user report + live GDB diagnosis, 2026-09-18).** — full `## D309` entry at file tail | **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). The teardown-ordering open question (why PC presents the unreachable-Bond state) remains documented but is now harmless. | | D310 | **RULE-2-SIGNOFF: `chraiitemsize()` mis-sizes 1-byte `AI_PRINT` records (NUL-scans forward), corrupting AI execution for any list containing an active PRINT — fixes D309's freeze mechanism and the Caverns standing-still guards (`Stop`+`PRINT("wait")` mis-lands on EndList, re-executed every tick; user report 2026-09-18).** — full `## D310` entry at file tail | **FIXED — user-playtest-confirmed 2026-09-18** (v2: global-list-only sizing via `d310ItemSize`; guards resume, Trevelyan pathing intact, Caverns→intro clean; probe log verified both list classes). PD precedent: PRINT removed, slot 0xAD reassigned. | -| D311 | **Complete freeze in `modelSetAnimFrame2WithChrStuff` root-motion loop — D156 guard hole: poisoned `framea` (`model->animframe1`) passes the frameb-only clamp and saturates to a ~3.1e9-iteration spin (Caverns, live GDB diagnosis, 2026-09-18).** — full `## D311` entry at file tail | GUARDED (`#ifdef PORT`, D156-guard extension in the same file) — freeze class neutralized; upstream speed/frame poison source OPEN (level reload clears displaced model state). No AI involvement — distinct from D309/D310. | +| D311 | **Complete freeze in `modelSetAnimFrame2WithChrStuff` root-motion loop — D156 guard hole: poisoned `framea` (`model->animframe1`) passes the frameb-only clamp and saturates to a ~3.1e9-iteration spin (Caverns, live GDB diagnosis, 2026-09-18).** — full `## D311` entry at file tail | FIXED — root cause D336 (`bheadAdjustAnimation` read never-written placeholder fields); the `#ifdef PORT` guard is retained as a safety net. 2026-09-25 user playtest incl. ~2 min of Caverns combat: 0 D311 ANOMALY / D156 lines in a ~46-min session. | | D312 | **Throwable items (mines, grenades, etc.) can clip through walls in certain spots (user report, v0.3.0 pre-release playtest, 2026-09-18).** — full `## D312` entry at file tail | FIXED — `bgBuildRoomVtxBounds` mis-reads the G_VTX vertex count on PC (`Gfx_le.dma.par` bit-layout shift): n=16 batches collapse to single-point bounds and fail the per-batch AABB pre-test, so their wall triangles are never ray-tested. One-line `#ifdef PORT` fix; verified by repro. | | D313 | **Bullet impacts do not appear on some walls/surfaces — the projectile appears to pass through them; impacts DO work on some surfaces such as doors/windows (user report, v0.3.0 pre-release playtest, 2026-09-18).** — full `## D313` entry at file tail | FIXED — same root cause as D312, merged diagnosis; same one-line fix. Doors/windows use the separate working `bgTestHitOnObj` path, hence the "surface-dependent" appearance. | -| D314 | **F10 menu items flicker randomly/intermittently (reported on the main-menu context, user report, v0.3.0 playtest build, 2026-09-18).** — full `## D314` entry at file tail | OPEN — confirmed still present post-D316 fix (2026-09-18); user deprioritized chasing it further for now. Diagnostic probe (`GE_D314=1`) landed in `optionsOverlayHandleInput()` if revisited. | -| D315 | **Menu scaling differs between main menu and in-game — main menu scales OK, in-game menu "seems kinda big sometimes" (user report, v0.3.0 playtest build, 2026-09-18).** — full `## D315` entry at file tail | OPEN — disclosed as a known issue, deferred to v0.3.1 (bigger design question, not a mechanical fix). | +| D314 | **F10 menu items flicker randomly/intermittently (reported on the main-menu context, user report, v0.3.0 playtest build, 2026-09-18).** — full `## D314` entry at file tail | FIXED (user playtest 2026-09-26) — M-200 wheel-queue race fix + D287 window-thread cursor calls. Earlier: confirmed still present post-D316 fix (2026-09-18). Diagnostic probe (`GE_D314=1`) landed in `optionsOverlayHandleInput()` if revisited. | +| D315 | **Menu scaling differs between main menu and in-game — main menu scales OK, in-game menu "seems kinda big sometimes" (user report, v0.3.0 playtest build, 2026-09-18).** — full `## D315` entry at file tail | CLOSED (2026-09-26, user: menu scaling looks fine; likely resolved by the v0.4.0 widescreen/HUD-scale work D334/D335/D226). Earlier: OPEN — disclosed as a known issue, deferred to v0.3.1 (bigger design question, not a mechanical fix). | | D316 | **Mouse-clicking some menu items glitches and lands on the item below instead (user report, v0.3.0 playtest build, 2026-09-18).** — full `## D316` entry at file tail | 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`. Build-verified (golden diff PASS, worst_cell=0); interactive click verification owed. | | D317 | **Crash when toggling Game.AllUnlocked back and forth in-game (user report, v0.3.0 playtest build, 2026-09-18) — no dump left behind.** — full `## D317` entry at file tail | 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). Same fix resolves it. | -| D318 | **Facility (regular play path): after Objective C (destroying the bottling tanks) completes, Ourumov freezes and does not shoot Trevelyan as he should (user report, 2026-09-18).** — full `## D318` entry at file tail | RESOLVED (2026-09-19): as-authored latent race (gas bit mid-monologue derails c78's pre-aim hold into a permanent ACT_ATTACK anim pin; N64-identical), NOT a port bug; port-side watchdog `port/src/d318watchdog.c` re-inits the attack on confirm. D263 mechanism map stands (different failure mode). | +| D318 | **Facility (regular play path): after Objective C (destroying the bottling tanks) completes, Ourumov freezes and does not shoot Trevelyan as he should (user report, 2026-09-18).** — full `## D318` entry at file tail | FIXED via D329 (2026-09-26 bookkeeping: tick-granularity root cause; Rule-2 fix in model.c, user-verified 2026-09-23). Earlier: OPEN — re-classified 2026-09-23 by D329 (tick-granularity divergence at 1 tick/frame, not a platform-invariant lock); fix decision pending, watchdog stays shipped. Earlier resolution text kept in the full entry. | | D319 | **Frame cap slider was mostly a dead zone; replaced with a plain 30/60 toggle (user ask, 2026-09-18).** — full `## D319` entry at file tail | FIXED — new `ROW_FPSCAP` cycle (30/60 only) in `port/src/optionsoverlay.c`; default changed to 60 in `port/src/video.c`. Root cause was D186 (sim tick ceiling + low-cap guard); this just fixes the menu's exposure of it. Build-verified; interactive confirmation owed. | -| D320 | **D318-class sweep: aim-hold → `_update` AI lists in 8 levels carry the same as-authored softlock race (static analysis, 2026-09-20).** — full `## D320` entry at file tail | 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-watchdog recommendation documented. | -| D321 | **D318 trigger chain fully mapped: tanks → combat bit is an authored ~3.5 s gas-cascade delay (chr 254 script); derail lands same-tick; PC behavior confirmed faithful to N64 (probe captures, 2026-09-20).** — full `## D321` entry at file tail | CLOSED — trigger chain mapped and faithful; D318 watchdog validated in live play. Optional deferred tuning: `D318_DEADLOCK_TICKS` 600→300 (user's call). | -| D322 | **Long-session audio degradation: full campaign on v0.3.0 — audio progressively worsens from Silo, by Caverns/Cradle the OST is inaudible and SFX "come and go"; restarting the game restores it (issue #87, user report, 2026-09-21).** — full `## D322` entry at file tail | OPEN — static triage done (teardown audit, mixer statelessness, D202-coverage check); ranked hypotheses: voice-pool exhaustion/counter drift > queue starvation > evtq saturation. `GE_D322` pool-telemetry probe shipped; needs a campaign capture with `GE_D322=1 GE_D204=1`. | +| D320 | **D318-class sweep: aim-hold → `_update` AI lists in 8 levels carry the same as-authored softlock race (static analysis, 2026-09-20).** — full `## D320` entry at file tail | FIXED via D329 (2026-09-26 bookkeeping: tick-granularity root cause; Rule-2 fix in model.c, user-verified 2026-09-23). **v0.4.0 campaign (2026-09-28): no D320-beat freeze seen (user: good).** Earlier: OPEN — re-classified 2026-09-23 by D329 (tick-granularity divergence at 1 tick/frame, not a platform-invariant lock); fix decision pending, watchdog stays shipped. Earlier resolution text kept in the full entry. | +| D321 | **D318 trigger chain fully mapped: tanks → combat bit is an authored ~3.5 s gas-cascade delay (chr 254 script); derail lands same-tick; PC behavior confirmed faithful to N64 (probe captures, 2026-09-20).** — full `## D321` entry at file tail | FIXED via D329 (2026-09-26 bookkeeping: tick-granularity root cause; Rule-2 fix in model.c, user-verified 2026-09-23). Earlier: OPEN — re-classified 2026-09-23 by D329 (tick-granularity divergence at 1 tick/frame, not a platform-invariant lock); fix decision pending, watchdog stays shipped. Earlier resolution text kept in the full entry. | +| D322 | **Long-session audio degradation: full campaign on v0.3.0 — audio progressively worsens from Silo, by Caverns/Cradle the OST is inaudible and SFX "come and go"; restarting the game restores it (issue #87, user report, 2026-09-20).** — full `## D322` entry at file tail | FIXED (Windows) for the periodic "garbled" bursts — root cause SDL2's WASAPI backend, not the device: side-by-side monitor runs on the same USB interface show WASAPI draining 81–88 % for ~30–50 s every ~317 s (1.4 % playback lost overall) while DirectSound stays at exact real time. `port/src/audio.c` now prefers `directsound,wasapi` (SDL_AUDIODRIVER env still overrides). Remaining: SFX drop-outs in firefights, if still heard after this fix (8-voice cap, D207/D241); #87 progressive decay: **v0.4.0 campaign (2026-09-28) -- user has not seen it again, "probably good"; closed on user trend, `GE_D322=1` capture recipe remains in the entry for any recurrence.** | +| D323 | **G_TL_LOD bindings of explicit-LOD textures: PC samples driver mips from LOD0, N64 samples the authored mip chain (static census, 2026-09-21).** — full `## D323` entry at file tail | CLOSED — parked as a fidelity enhancement, not a bug (2026-09-26, user-approved: no reported artifact). Earlier: OPEN / parked — mechanism confirmed in code; census (`tools_pc/bg_lod_audit.py`) shows 15 levels with explicit-LOD textures bound under TEXTLOD-on classes (stat 378, azt 806 binds heaviest); overwhelmingly CI8/IA8 (subtle), the only RGBA5551 cut-outs are the D236 set (already fixed). No reported artifact; fix path documented (upload GL mip chains for TEXTLOD-on bindings) but not worth doing speculatively. Also documents GE's OMH field-write encoding (op 0xBA, value at absolute bit position in w1). | +| D324 | **GE's stub declares nothing, so TUs that include it get NO CRT prototypes — implicit `int f()` declarations silently corrupt non-int returns: `atof` garbage in every float ini parse (D38 class).** — full `## D324` entry at file tail | FIXED (2026-09-22) — proven impact: `Game.ScreenShakeIntensity` (the only float ini option) silently parsed to garbage whenever a user set it; caught via the D294 `GE_ROOMPOOL` hook. Fix: `port/shim/stdlib.h` now declares atof/strtod/strtol/atexit/getenv unconditionally (K&R form); pc_protos.h gains atof/strtod for game TUs; glass2.c PORT block uses `__builtin_memset`. Residual harmless implicit decls (atoi/floorf/sqrtf/memset builtins, lround) documented. | +| D325 | **ROM-mod (xdelta) compatibility: relocating/total-conversion mods cannot work while the file table is compiled in; in-place data-only mods are one sidecar-staleness fix away (Goldfinger 64 probe, 2026-09-22).** — full `## D325` entry at file tail | CLOSED as a question — mechanism measured (GF64 relocates 797/803 vanilla file slices; 12→24 MB expanded ROM; `d43_emit` dies on a vanilla-offset zlib slice). ASM mods never work; relocating mods need a runtime-parsed file table (rule-2 collision for added levels); in-place data-only mods need only a ROM-identity stamp in `manifest.csv` (`port/src/romconvert.c`). | +| D326 | **Random Eye randomizer repacks the ROM too — the hypothesised "in-place data-only" mod tier is empty in practice; the in-ROM file table is the single keystone for any mod support (2026-09-22).** — full `## D326` entry at file tail | CLOSED as a question — identical ROM size but 96.5% of bytes and 803/803 file slices differ (content preserved, shifted +7216/+7392/−229888); same `d43_emit` zlib failure. D325's cheap tier disproven. Two bounded scans failed to find the in-ROM `fileentry` table (likely inside the compressed code image); that recovery question gates everything. Backlogged (local notes §5, extends T3.6). | +| D327 | **Mod support, resolved plan: reading the file table out of the ROM is ruled out by measurement; content-addressed offset remapping (gap inference over contiguously-packed files) is the viable route (1964GEPD fork review, 2026-09-22).** — full `## D327` entry at file tail | RESOLVED — superseded by D328; approach (a) is NOT ruled out (these scans missed the 1172-compressed data segment). `hw_address` absent from the ROM in all 4 encodings tested (0 hits for the segment-relative form); `file_resource_table.inc.c` is a decomp reconstruction. Viable route: locate unchanged files by content, bound changed ones by neighbour gaps (`obInit` differencing proves contiguity). Verified primitive: GE's `0x7F000000` segment maps uncompressed from ROM `0x34b30`, uniquely signature-findable. 1964 fork is GPL-2.0 — facts only, no code reuse. | +| D328 | **Mod support is viable and D327's "ruled out" was wrong: GE's file table IS in the ROM, inside the 1172-compressed data segment, and locating it needs no hardcoded offsets (PD dabs fork review + verification on vanilla + 2 mods, 2026-09-22).** — full `## D328` entry at file tail | CLOSED as a question — table at `0x252c4` inside the segment inflated from ROM `0x21990` (727 rows; 681 exact offset+size matches vs filelist, 0 mismatches). Hardcode-free scan reads Random Eye's own table -> **704/726 files byte-identical** (vs 0/803 at vanilla offsets in D326) and GF64's 24 MB table correctly. PD's architecture is an offline importer + mod-dir override, not a runtime ROM loader; its MIT `rompatch.c` removes the GPL xdelta3 constraint. Remaining work is the mod-dir override layer. | +| D329 | **The D318/D320 guard anim pin is a tick-granularity divergence: it exists only at 1 sim tick per rendered frame (port at 60 fps); at 2+ ticks/frame (GE's normal N64 regime) it cannot persist (lockstep A/B, 2026-09-23).** — full `## D329` entry at file tail | 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 timing; watchdog stays shipped for now. | +| D330 | **#103 (v0.3.0, Win 11): Auto-Aim / Look Ahead "won't stay on" in the in-game watch options: the mouse drove the analog stick inside the watch, and a small leftward flick selected OFF.** — full `## D330` entry at file tail | FIXED (port/src/input.c) — user-confirmed 2026-09-25: Auto-Aim set ON in the watch stays ON while moving the mouse. | +| D331 | **Ejected shell casings never rendered on PC: `g_DefaultCasingModelRenderData` (u32[15] template) read as the wider PC `ModelRenderData` gave flags 0 (third instance of the D264 pattern; found by the D290 sibling sweep, never user-reported).** — full `## D331` entry at file tail | FIXED (gunfire.c `#ifdef PORT`, explicit template); PP7 casing verified on native Windows (before: none; after: brass casing ejects and tumbles); golden 3/3; Win + Linux builds clean. | +| D332 | **Reticule jitter in aim mode (RMB) — ROOT-CAUSED from live captures; fixed by adopting the Perfect Dark port's mouse-aim model behind `Input.PdMouseAim`.** The game's damped crosshair update runs once per SIM TICK while the port's GEPD overwrite runs once per input POLL, so the value displayed is `write * damp^k` with k varying (k=1 66%, k=2 18%, k=0 17% of frames). At damp 0.8 that stepped the drawn reticule 20% whenever k changed | **FIXED (`fix/mouse-pd-model`)** — `Input.PdMouseAim` (default off, live via `GE_PDMOUSEAim`): the port accumulates only and the game's integrator is driven by the port-supplied turn with PD's near-zero damp (0.01), via a `#ifdef PORT` hook in `sub_GAME_7F067FBC`. Two earlier fixes were tried and reverted (see the `## D332` entry). **Awaiting feel-test.** | +| D333 | **#104 (v0.3.0, Linux): FPS-style centred aim on RMB, delivered as `Input.AimStyle` (F10 "RMB aim style": N64 / PD DAMP / CENTRED), folding in D332's toggle.** — full `## D333` entry at file tail | FIXED (2026-09-26 bookkeeping: shipped; user reports it feels good in v0.4.0 play). Earlier: IMPLEMENTED (port/src/input.c + optionsoverlay.c, port-only); default N64 = unchanged; build + golden clean; feel check owed. | +| D334 | **Native widescreen, world side (WIDESCREEN-FOV-PLAN Option B Phase 2): the world is projected at the real window aspect (Hor+) by generalising GE's own N64 16:9 mode; `Video.NativeWidescreen` (default on).** — full `## D334` entry at file tail | FIXED (2026-09-26 bookkeeping: shipped; user reports it feels good in v0.4.0 play). Earlier: IMPLEMENTED (Rule-2 approved 2026-09-23); 4:3 identity (golden 3/3); native 16:9 centre matches the 4:3 frame (mean diff 4.3 vs 12.4 stretched); 21:9 clean on Bunker/Facility/Dam. HUD still stretched → Phase 3. | +| D335 | **Native widescreen, HUD + front end (WIDESCREEN-FOV-PLAN Phase 3): HUD elements anchored left/right/centre and the front end pillarboxed via the fast3d EXT aspect modes (PD pattern), `port/include/hudaspect.h`.** — full `## D335` entry at file tail | FIXED (2026-09-26 bookkeeping: shipped, user-verified at 16:9/21:9; F10-overlay pillarbox remains as D335b in GRAPHICS-BACKLOG). Earlier: IMPLEMENTED (Rule-2 approved 2026-09-23); 4:3 byte-identical (no emission at ≤4:3); verified at 16:9/21:9 on HUD, watch, title card, legal/file-select/gun-barrel. Menu pointer mapped into the pillarbox. One cosmetic leak → GRAPHICS-BACKLOG D335a. | +| D336 | **Frigate (and any level) "all polygons break except the gun" after turning: Bond's head-bob anim read two never-written placeholder fields (`field_5C0`, `animFlipFlag`) that on N64 are raw-offset aliases into the inline `player.model` (D100 follow-up; A1 class). Likely the D311 upstream anim-poison source.** — full `## D336` entry at file tail | 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. **User-confirmed 2026-09-24.** | +| D337 | **Aim modes folded (user decision): N64 (default) = the N64 aim model with the mouse fed through the game's integrator at PD's damp (D332 made default); CENTRED (PC) opt-in; Q/L and Toggle-aim now route mouse aim like RMB; pad/keyboard aim no longer swallowed by the mouse integration.** — full `## D337` entry at file tail | FIXED (2026-09-26 bookkeeping: shipped; user reports it feels good in v0.4.0 play). Earlier: IMPLEMENTED (port/src/input.c + optionsoverlay.c); headless-measured with new scripted-mouse harness; golden 3/3; Win + Linux builds. Open: N64-range calibration decision. | +| D338 | **`Input.AimRange` (F10 "Aim range"): PC (default, GEPD/mouse-injector full-screen crosshair) or N64 (original stick limits: 65% of half-width, camera turn from ~49%).** — full `## D338` entry at file tail | FIXED (2026-09-26 bookkeeping: shipped; user reports it feels good in v0.4.0 play). Earlier: IMPLEMENTED + measured (PC 98%/72%, N64 65%/49%); golden 3/3; Win + Linux builds. | +| D339 | **#92 (low-end ~30 fps, Celeron N3060 / HD 400) — measured CPU budget: game thread ~0.2–0.35 ms/frame, render-thread display-list pass 0.4–1.8 ms/frame on this box; CPU very likely not the bottleneck. New `GE_PERFSTAT=1` probe (render phases, tris/batches, game-thread busy, GPU timer query).** — full `## D339` entry at file tail | OPEN (2026-09-26, user: review toward the end of v0.4.0; needs a test machine with similar HD 400-class hardware). Earlier: INVESTIGATING — probe landed; target-hardware numbers needed before any fix. | +| D341 | **Intro gun-barrel blood drip draws as static blocks/lines instead of dripping (user, v0.4.0, 2026-09-26; death blood fine).** — full `## D341` entry at file tail | FIXED (M-201, user-verified 2026-09-27): two fast3d defects — the 1-cycle blood texrect sampled a stale tile-1 declaration via the D236 LOD rule, and the in-place-regenerated dyn-pool texture hit a stale address-keyed cache entry. | +| D342 | **File select: folder bodies and Bond photos vanish after selecting a file and backing out (user, v0.4.0, 2026-09-26).** — full `## D342` entry at file tail | FIXED (M-201, user-verified): scratch placed at the N64 offset +0xA000 of `ptr_logo_and_walletbond_DL` landed inside the larger PC wallet model; moved past its 0x17000 reservation. | +| D343 | **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 jumping to the legal screen after 30 s.** — full `## D343` entry at file tail | 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 screen). | +| D344 | **Host PC bugchecks (BSOD 0x119 VIDEO_SCHEDULER_INTERNAL_ERROR) during game test runs; probable cause: quit called exit() while the render thread could be inside the GL driver (2026-09-27).** — full `## D344` entry at file tail | FIXED (probable cause; orderly quit + GE_QUITFRAME), headless + user-verified; no dump, watch for recurrence. | +| D345 | **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` page hop), and the options screen's keyboard navigation was missing/mis-modelled (2026-09-27).** — full `## D345` entry at file tail | Port-only, user-verified (parts a–g): (a) `input.c` re-assert window on menu change; (b) `cursorToItem` skips its snap while the pointer is live; (d) W/S emit stick in menus under NaturalPitch; (e/f) F10-style discrete row stepping + arrow left/right value adjust replace raw drift (first pass shipped broken, fixed); (g) screen-placed cursor parks in the left gutter, off the row text. Game-side one-liner deferred: physical-controller pass was good, no Rule-2 sign-off sought. | +| D346 | **PC-settings wording pass: option labels/values aligned to Nightdive/Turok + PD-port conventions (title-case On/Off, "Frame rate cap", "Anti-aliasing"/"None", units in values not labels, self-explanatory auto-FOV toggles); VIEW section merged into VIDEO (2026-09-27).** — full `## D346` entry at file tail | **CLOSED (2026-09-28).** Shipped in v0.4.0; accepted in the user's final review / campaign playtest. | +| D347 | **F10 overlay controller: left/right did nothing (only A/Y adjusted values) and no input had hold-to-repeat (2026-09-27).** — full `## D347` entry at file tail | FIXED (port-only, `optionsoverlay.c`): D-pad left/right + stick-X wired into value adjust (A/X/B/Y kept); 18/4-frame hold-to-repeat on adjust and up/down nav, matching the options screen's D345(e)/(f) cadence. User validated physical-controller pass. | +| D348 | **File-select "PC Options" label: no settings icon exists in the game art (mainfolderimages = copy/del/select-arrow/X/check/dot only) -- a mini slider glyph is drawn instead (2026-09-27).** — full `## D348` entry at file tail | SUPERSEDED by D398 (glyph replaced with the IMG_DOT texture; no new art). PCDUMP pixel-verified when shipped (135 knob px vs 9 paper). | +| D349 | **GE watch settings are not normally saved to the Bond file: erase is the only caller of the settings serializer, and stage start reloads the file's old values.** — full `## D349` entry at file tail | **CLOSED (2026-09-28).** Shipped in v0.4.0; accepted in the user's final review / campaign playtest. | +| D350 | **Watch-backed PC options: shared front/F10 Bond-file rows, explicit chooser, game-thread F10 bridge and field-scoped PC-only persistence.** — full `## D350` entry at file tail | **CLOSED (2026-09-28).** Shipped in v0.4.0; accepted in the user's final review / campaign playtest. | +| D351 | **Existing D346 value/unit rows had positional initializers shifted into `hidePtr`: %/x rows hidden or formatted without units.** — full `## D351` entry at file tail | FIXED (port-only): designated row initializers restore unit, deadzone divisor and hide-if-auto fields; build clean, headless smoke clean. Manual settings visual check owed. | +| D352 | **D350 review of 1303c0eb: stage transitions silently drop queued F10 setting changes (queue drained before the `stageActive` check); the UI thread's chooser state is unsynchronized; rapid slider drags push one RSP command per detent and re-persist the same value each frame; persist failures are silent (2026-09-27).** — full `## D352` entry at file tail | FIXED (port-only, `port/src/watchsettings.c`): hold-not-drop at stage transitions, spinlock on chooser state + front-screen guard, per-field coalesced queue commits, warning logs on all no-op paths, `PersistField` made static. NTSC build clean, headless menu smoke clean. | +| D353 | **Turok-style settings regrouping: the D346 VIDEO mega-section (14–15 rows, at the MAX_PROWS limit) re-split into DISPLAY + GRAPHICS, the "AUDIO (BOND FILE)" hack header dropped (music/FX sliders fold into BOND FILE), all remaining rows converted to designated initializers, `buildPages` overflow now logs, and the Bond-file page title annotates the selected file (2026-09-27).** — full `## D353` entry at file tail | FIXED (port-only, `optionsoverlay.c`/`frontoptions.c`): 6 sections of 3–11 rows, all rows designated-initialized, `optionsRowIsBondChooser` accessor, build clean + headless smoke clean. Master-volume row, per-section reset (Gate E) and key rebinding remain in the M3 backlog. | +| D354 | **Front-end F10 Bond rows silently disabled: `watchSettingsAvailable()` mis-routed F10-on-file-select through the stage path (returned 0), so with the hidden chooser stuck at "none" the volume sliders read "Select file" and did nothing; maintainer-reported (2026-09-27).** — full `## D354` entry at file tail | FIXED (port-only, `port/src/watchsettings.c`): all non-stage contexts route through the front path with a lazily-resolved default folder (explicit pick > `selected_folder_num` > first valid), one-shot game-thread `fileBuildWriteNewSave(FOLDER1)` fallback, GE_WSPROBE/GE_WSPROBE_FRONT env probes headless-verified (stage: live+save changed; front: default file 1 + save changed). | +| D355 | **F10/options Music slider inaudible in-stage: the slider drives only the X-track (track 2) via `set_mTrack2Vol`, but in-stage BGM is track 1, which the game pins to `VOLUME_MAX` at level start (lv.c:364) and never rescales from `mTrack2Vol` in solo -- so neither the N64 watch nor the F10 could change the music the player was hearing; maintainer-reported (2026-09-27).** — full `## D355` entry at file tail | FIXED (port-only, `port/src/watchsettings.c`): the Music commit now also calls `musicTrack1ApplySeqpVol(v)` (the exact resync the MP path already does, mpmenu.c:354) and a once-per-activation resync runs on stage entry; by-ear confirmed by the maintainer. N64 watch path untouched. | +| D356 | **Settings regroup: Turok-style pages renamed to functional sections (INPUT/GAMEPLAY/GRAPHICS/AUDIO/VIDEO) with PD-style "(File N)" scoping annotations, the redundant N64-watch rows (`Bond.Look`, `Bond.AimControl`) off the menu surface, and a real per-section Reset-to-defaults (two-step arm->confirm, watch rows to BLANKSAVEDATA via the D352 commit path, ini rows to the port's C initializers; `__Resolution` a documented exclusion) replacing the Turok reset rows the PC build never honored (2026-09-27).** — full `## D356` entry at file tail | 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`): 5 sections all below MAX_PROWS, hidden rows D181/D216-style (keys + watch paths stay live), edge-triggered reset with 3 s arm window + maintain/clear disarm, GE_WSPROBE_RESET front + in-stage probes headless-verified (all sections failures=0, scope isolation OK, saved bytes 0xFF), GE_WSPROBE/GE_WSPROBE_FRONT regressions clean. | +| D358 | **D356 level-1 overlap: the front options save-file row (item 0) painted under the first content row, and `s_hl` (item-numbered) was indexed straight into `s_rowIdx` (content-numbered) -- wrong-row adjusts, a dead last row, a one-past read, wrong highlight (2026-09-27).** — full `## D358` entry at file tail | FIXED (port-only, `port/src/frontoptions.c`): content rows draw at `rowY(k+1)`; all `s_hl` -> `s_rowIdx` uses take `s_hl-1`; A-press guards `s_hl >= 1 && s_hl <= s_rowN`; hit-test/cursor untouched (already item-numbered). Companion: "(save)" tag -> "(profile)". | +| D359 | **Settings UI file terminology settled on the end-user term "Profile": the screen mixed five terms ("Save file"/"File N"/"(profile)"/"Select file"/"(no file)"), and the manual's "game file" reads as the program's own files in modern PC usage (2026-09-27).** — full `## D359` entry at file tail | **CLOSED (2026-09-28).** Shipped in v0.4.0; accepted in the user's final review / campaign playtest. | +| D360 | **F10 INPUT heading scrolled away; F10 navigation and distance defaults (2026-09-27).** — full `## D360` entry at file tail | **CLOSED (2026-09-28).** Shipped in v0.4.0; accepted in the user's final review / campaign playtest. | +| D361 | **Main-menu F10 Audio freezes the game: EEPROM save from joyPoll deadlocks waiting for its own poll ack (2026-09-27).** — full `## D361` entry at file tail | FIXED (port-only, `port/src/watchsettings.c` + F10 probe in `optionsoverlay.c`): queue front F10 save edits to game thread; actual poll-thread Music+FX commits tested with existing and fresh EEPROM, orderly quit, user's ini/save restored. | +| D362 | **F10 GoldenEye dossier-style visual refresh (2026-09-27).** — full `## D362` entry at file tail | CLOSED (superseded by D363 after user playtest): paper/Zurich/gold look rejected. The pinned section heading and shared layout/hit geometry remain. | +| D363 | **F10 return to a dark overlay with the N64 watch's green/Bank Gothic visual language (2026-09-27).** — full `## D363` entry at file tail | FIXED (palette/font user-approved, 2026-09-27); D364 follows up on profile tags, click alignment and slider calibration. | +| D364 | **Settings UI feedback: replace P legend; F10 slider click band misses drawn bar; input defaults visually off-centre (2026-09-27).** — full `## D364` entry at file tail | FIXED (user mostly accepted; D365 follows up on annotation size and click targets). | +| D365 | **F10 annotation hierarchy and visible mouse change targets (2026-09-27).** — full `## D365` entry at file tail | **CLOSED (2026-09-28).** Shipped in v0.4.0; accepted in the user's final review / campaign playtest. | +| D366 | **Input calibration label: call 50 the default, not original (2026-09-27).** — full `## D366` entry at file tail | **CLOSED (2026-09-28).** Shipped in v0.4.0; accepted in the user's final review / campaign playtest. | +| D367 | **F10 full-width control boxes misaligned with their contents (2026-09-27).** — full `## D367` entry at file tail | SUPERSEDED by D368 after user playtest: per-value boxes remained confusing. | +| D368 | **Simplify F10 click targets: setting name selects, the rest changes (2026-09-27).** — full `## D368` entry at file tail | INPUT RULE KEPT; tinted strips user-rejected, removed in D369. | +| D369 | **Remove F10 row boxes/strips entirely (2026-09-27).** — full `## D369` entry at file tail | FIXED (awaiting live acceptance): no row backgrounds, control rectangles or hover paint; selected setting distinguished by brighter text, slider tracks remain. Input rule from D368 unchanged. | +| D370 | **F10 distance/volume drags hurt frame rate (2026-09-27).** — full `## D370` entry at file tail | **CLOSED (2026-09-28).** Shipped in v0.4.0; accepted in the user's final review / campaign playtest. (user: fixed.) | +| D371 | **GEPD key-layout preset + reload/crouch bindings, v0.4.0 modern options wave M3 (2026-09-27).** full `## D371` entry at file tail | FIXED (headless-verified; awaiting user live accept): `Input.Layout` (0=FPS, 1=GEPD) + `Input.CrouchMode` (hold/toggle) in F10 INPUT; GEPD preset acts as per-key effective default under `[Input.Bind]` overrides; new IA_RELOAD/IA_CROUCH emit B (unbound by default). MODERN section dropped per D356 Turok-standard rule (rows land in functional INPUT). Boot/ini-round-trip/F10-page verified; in-game feel-check owed. | +| D372 | **Low-end perf preset (v0.4.0 modern options wave M5, 2026-09-27).** full `## D372` entry at file tail | FIXED (headless-verified; awaiting user live accept): one-row `Video.LowEndMode` in F10 VIDEO; ON writes FpsCap 30 + MSAA x1 (restart), OFF restores compiled defaults (60/2). Registered key persists through orderly exit. | +| D373 | **In-game crosshair on/off + colour (v0.4.0 modern options wave M2, 2026-09-27).** full `## D373` entry at file tail | FIXED (user live-tested; rule-2 sign-off recorded 2026-09-27): `Video.CrosshairHide` (0 = on, N64) + `Video.CrosshairColor` (7 tints, 0 = white) in F10 GRAPHICS; #ifdef PORT hook in gunfire.c gunDrawSight (the wave's only rule-2 item). | +| D374 | **GEPD becomes the default key layout; preset layering fix (v0.4.0 M3 playtest feedback, 2026-09-27).** full `## D374` entry at file tail | FIXED (headless + user live-tested): GEPD playtest found Ctrl fired the gun (stale persisted [Input.Bind] Fire=Left Ctrl masked the preset) and the FPS default made no sense for a mouse FPS. D374: GEPD is now Input.Layout=0 (the default; N64 layout = 1); only EXPLICIT per-key overrides beat the preset. One-off ini flip: pre-D374 `Layout = 1` (ex-GEPD) is now 0. | +| D375 | **Crouch wired to the wrong pad bit: B is not the crouch input (v0.4.0 M3 playtest, 2026-09-27).** full `## D375` entry at file tail | **CLOSED (2026-09-28).** Shipped in v0.4.0; accepted in the user's final review / campaign playtest. | +| D376 | **Crouch follow-up + modern-options spot check (2026-09-27).** full `## D376` entry at file tail | **CLOSED (2026-09-28).** Shipped in v0.4.0; accepted in the user's final review / campaign playtest. | +| D377 | **GEPD independent crouch (2026-09-27).** full `## D377` entry at file tail | FIXED (scripted stance verified; user says live crouch was good; pad/hold-vs-toggle specifics owed): Ctrl/B/Y now drive game stance from the port with no aim requirement. | +| D378 | **GEPD use/reload separation (2026-09-27).** full `## D378` entry at file tail | FIXED (headless edges tested; user says E/R seemed good; targeted door/partial-mag cases owed): R reload only, E interaction only in gameplay, menu/watch/tank retains B. | +| D379 | **Crosshair colour: authored texture is red (2026-09-27).** full `## D379` entry at file tail | FIXED (cyan frame capture; live yellow/blue recheck owed): nonwhite tints use alpha silhouette instead of multiplying red RGB; default unchanged. | +| D380 | **One PC keyboard layout; PD-style rebind design (2026-09-27).** full `## D380` entry at file tail | FIXED (follow-up D383): one PC layout and migration remain; keyboard capture UI is implemented separately, with live acceptance owed. | +| D381 | **RULE-2-SIGNOFF: opt-in RGB, size and Beta crosshair (2026-09-27).** full `## D381` entry at file tail | **CLOSED (2026-09-28).** Shipped in v0.4.0; accepted in the user's final review / campaign playtest. | +| D382 | **Crosshair menu placement and slider hitches (2026-09-27).** full `## D382` entry at file tail | **CLOSED (2026-09-28).** Shipped in v0.4.0; accepted in the user's final review / campaign playtest. | +| D383 | **PD-style keyboard rebinding in both options UIs (2026-09-27).** full `## D383` entry at file tail | **PARTIAL (2026-09-28).** Keyboard/mouse rebinding shipped + accepted in v0.4.0; pad-rebinding UI is the v0.5.0 controller-options wave (CONTROLLER-INPUT-PLAN). | +| D384 | **Front PC Options key-bind navigation feedback + two-slot UI (2026-09-27).** full `## D384` entry at file tail | **CLOSED (2026-09-28).** Shipped in v0.4.0; accepted in the user's final review / campaign playtest. | +| D385 | **Real mouse 1–5 action rebinding in the two-slot editor (2026-09-27).** full `## D385` entry at file tail | **CLOSED (2026-09-28).** Shipped in v0.4.0; accepted in the user's final review / campaign playtest. | +| D386 | **Correct Aim default from SDL middle (Mouse 2) to right (Mouse 3) (2026-09-27).** full `## D386` entry at file tail | **CLOSED (2026-09-28).** Shipped in v0.4.0; accepted in the user's final review / campaign playtest. | +| D387 | **M7 AllUnlocked's read-time fake unlocks leak into saved EEPROM (2026-09-27).** full `## D387` entry at file tail | OPEN (isolated user-data-copy save/untoggle repro; no fix): a profile-settings save persists fake cheats/0x3FF times; OFF does not undo them. Rotating writes and cheat-award suppression make simple write masking unsafe. **v0.4.0 (2026-09-28): shipped as a loudly-documented EXPERIMENTAL / irreversible option** (row = "All unlocked (EXPERIMENTAL)", ON shows "ON - UNSAFE", README backs it up); safe architecture deferred to v0.5.0. **User-confirmed 2026-09-28: the warning is sufficient -- ship.** | +| D388 | **Nest functional binding/HUD menus and remove redundant M5 toggle (2026-09-27).** full `## D388` entry at file tail | **CLOSED (2026-09-28).** Shipped in v0.4.0; accepted in the user's final review / campaign playtest. | +| D389 | **F10 footer clipping and ambiguous submenu links (2026-09-27).** full `## D389` entry at file tail | **CLOSED (2026-09-28).** Shipped in v0.4.0; accepted in the user's final review / campaign playtest. | +| D390 | **F10 pinned Back selection did not visibly change (2026-09-27).** full `## D390` entry at file tail | **CLOSED (2026-09-28).** Shipped in v0.4.0; accepted in the user's final review / campaign playtest. | +| D391 | **F10 click at glyph bottom could select next row (2026-09-27).** full `## D391` entry at file tail | **CLOSED (2026-09-28).** Shipped in v0.4.0; accepted in the user's final review / campaign playtest. | +| D392 | **Train intro soldier hunched; hand intersects torso vs 1964** — firing-table float-offset alias across an 8-byte pointer union. Full `## D392` entry at file tail. | FIXED (`#ifdef PORT` ABI/layout in `chraction.c`); Train aim-angle probe confirms correction; user visually verified the soldier looks fixed against 1964. | +| D393 | **Controller B/Y both crouch; no in-game use button** — gameplay pad face map conflated modern buttons and native N64 A/B. Full `## D393` entry at file tail. | FIXED + user live-tested A use / X reload / Y weapon cycle. D394 supersedes B crouch and shoulder cycling to match the user-supplied Xbox 1.1 chart. | +| D394 | **Xbox 1.1 Jinx controller parity** — user supplied the Xbox 1.1–1.4 control table; B is gadgets, stick clicks crouch, LB aim, RB HD toggle (not weapon cycling). | **PARTIAL (2026-09-28).** Xbox 1.1 / GEPD presets shipped and validated in the campaign playtest; presets 1.2-1.4 -> v0.5.0 wave. | +| D395 | **Controller trapped in keyboard/mouse-only binding capture** — pad could enter the binding modal but could not finish or cancel without keyboard. | RESOLVED (v0.4.0 campaign sign-off 2026-09-28, user: pad capture-block + B-cancel confirmed on the physical pad). Both options UIs block pad-initiated capture; pad B can cancel an active modal; **Steam Deck retest 2026-09-27: a pad enters bindings and is not stuck — the "trapped" symptom is not reproduced on the Deck's Steam Controller**. Distinct from the still-open controller-rebinding *feature* — PR #109 / D383.) | +| D396 | **F10 pad B/A used as slider decrement/increment, not back/select** — B/Y fed Left and A/X fed Right on every row. | RESOLVED (v0.4.0 campaign sign-off 2026-09-28, user: F10 B/A/slider behaviour confirmed on the physical pad). B backs one F10 page, closes at root; A/X accept/toggle on edge but never shift a slider; D-pad/stick left/right still adjust. | +| D397 | **SELECT FILE background + intro gun-barrel show an interlaced/comb look** — D182(2) family, but present on FIRST display, not only after re-entry (2026-09-27). Full `## D397` entry at file tail. | **RESOLVED (2026-09-28):** root cause = **H2** (confirmed by a 3-scale PCDUMP test — the comb period is fixed at ≈3 *native* rows across 640/1024/1280, i.e. window-period = 3×scale: the `(<<2)-1` 0.75-native-pixel row-strips rely on RDP floor-based scanline stepping that `gfx_draw_rectangle`'s continuous-float rasterization did not replicate). **Fix (port-layer, surgical):** `gfx_draw_rectangle` now floors the top edge to its scanline and the bottom edge to the last covered scanline (the RDP scanline range `[top,bot+1]`) before rasterizing — a no-op for whole-pixel rects, so only sub-pixel strips change; §D15 in `docs/porting-notes.md` documents the mechanism. H1 (ENV_ALPHA 5-bit CC scaling) REFUTED by a byte-identical PD-port cross-check (the darkening it predicted is faithful, not a bug). `GE_D397SH`/`D397SHM` probes removed. Verified by playtest: gun-barrel + file-select gradient render clean, comb gone. | +| D398 | **PC Options label: D348's procedural slider glyph replaced with an existing mainfolderimages texture (2026-09-27).** Full `## D398` entry at file tail. | SUPERSEDED by D400 (icon removed, text-only label; purple-hover bug moot; the D400 `textMeasure` arg-swap bug was found and fixed there). | +| D399 | **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).** Full `## D399` entry at file tail. | LANDED (port-only, D343 class): `selectpos.f[0]` 110→95 (new `#ifdef PORT` in front.c), Copy 200/222→185/207, Erase 282/304→267/289 (D343 PORT block), `LABEL_X` 358→343 (`frontoptions.c`); hit bounds recompute from the shifted positions. Clean build, PCDUMP frame 195 no overlaps; by-eye check **done 2026-09-28 (user: good) -- RESOLVED**. | +| D400 | **File-select "PC Options" label: icon removed, text-only; fixed a swapped textMeasure() arg bug and a 7px vertical misalignment vs Copy/Erase (user sign-off 2026-09-27).** Full `## D400` entry at file tail. | LANDED (port-only, `frontoptions.c`): D398's dot icon dropped on user instruction ("most straightforward design"); label now a plain `textRender` word, GAP 28 after Erase. Two measurement bugs fixed: (1) `textMeasure(textheight, textwidth)` args passed swapped -> label 35px above the bar, hot band transposed; (2) bare-label height measures 0 (vs 14 for newline-terminated Copy/Erase) -> 7px lower tops; fixed by restoring the `kLabelNL` height measure. PCDUMP frame 195: tops aligned 279.8 vs 279.3, 31px clearance from Erase; by-eye check **done 2026-09-28 (user: good) -- RESOLVED**. | +| D401 | **Rumble Pak → real gamepad haptics (re-promotes D224/M-87): `CONT_CARD_ON` gate + `osPfs*`/`osMotor*` shims rewired to `SDL_GameControllerRumble`, single global `Rumble strength` slider on `Input.RumbleScale` (2026-09-27/28).** Full `## D401` entry at file tail. | **CLOSED (2026-09-28).** Rumble shipped in v0.4.0 (D401); Steam Deck real-pad validation: works, user confirmed for release. | +| D402 | **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 cycle; `video.c` `_SHIFTL`); fixed in `port/shim/` only, no `src/` edits, no rule-2 (2026-09-27).** Full `## D402` entry at file tail. | RESOLVED (Linux/Ubuntu-24.04 build → glibc-2.41-compatible binary for the Steam Deck): three `port/shim/` fixes — `PR/ucode.h` + `bondconstants.h` defer `pc_protos.h`/`bondtypes.h` during `bondconstants.h`'s own `ultra64.h` pass (breaks the `bondconstants.h`→`ultra64.h`→`pc_protos.h`→`bondtypes.h`→`bondconstants.h` re-entry); `PR/gbi.h` defines `_SHIFTL`/`_SHIFTR` when absent (the `gbi.h` macros in `video.c` lost their `mbi.h` definitions on Linux). Builds + links clean on Ubuntu-24.04; bundled `libSDL2` with `rpath=$ORIGIN`; pushed to the Deck. Shim files uncommitted at log time. | +| D403 | **Deck v0.4.0-pre test: Nintendo logo + copyright/legal page render broken, broader graphics "not reflecting new updates" — stale 0.3.0-era sidecars seeded into the bundle (2026-09-27).** Full `## D403` entry at file tail. | RESOLVED (2026-09-27, user-confirmed on the Deck: a stale-sidecar **test artifact**, never a code regression): the Deck bundle was seeded with `pcmodels`/`pccg` sidecars from the `0.3.0-local` install (data dated 2026-09-15), which predate the D75/M-197 embedded-blob raw-pixel fix (commit `425237f9`, 2026-09-21) and the D303 muzzle-vertex converter fix (2026-09-26) → the `P*` front-end logo/legal textures come out broken while the in-exe fast3d fixes (D397 gun-barrel/file-select) render correctly. Disambiguation performed (2026-09-27): `data/pcmodels-ntsc-final` + `data/pccg-ntsc-final` regenerated from the Python-source converter (`prepare-assets.py` + the current `d43/d69/d88_emit.py`, `ALL CHECKS PASSED`) on the NTSC ROM and pushed to the Deck (sha256-verified), preserving the user's `ge007.eep`/`ge007.ini`. **RESOLVED (user-confirmed on the Deck, 2026-09-27): the front-end logo / copyright / legal page now render correctly — confirming this was the stale-sidecar test artifact, not a code regression.** | +| D404 | **"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 input … pad trigger") but the centred path is mouse-only (Steam Deck, 2026-09-27).** Full `## D404` entry at file tail. | RESOLVED (2026-09-28, port-layer only): the toggle now genuinely works on a pad. First logged as a communication bug and closed by relabel ("Aim style (mouse)"), then reopened on user challenge the same day: the pad path emits stick *deflection* into the game's crosshair integrator, so the pad was actually always in the **N64** model (a flick travels the crosshair; edge-scroll + spring-back only made a hold *feel* direct), and the "stick has no crosshair to travel" rationale was wrong. Fix: new `padDirectCompute()` in `input.c` (the pad twin of `hipDirectCompute`'s CENTRED branch -- same dead/watch/pause/cutscene gates, minus the mouse-grab gate; revised same day to the N64 path's verbatim quadratic curve -- `sign(v)\u00b7clamp(defl/70,\u00b11)\u00b2 \u00b7 (fov/60) \u00b7 3.5 deg/poll` (bondview2.c canNaturalTurn/canNaturalPitch), full-stick parity with the N64 path, pending retest on the refreshed build); the natural-pitch pad branch routes CENTRED through it and keeps N64's classic deflection behaviour (also the default, so no pad default changes). Labels reverted to device-neutral ("Aim style" / "Aim range"). **Steam Deck (2026-09-27/28): "centered aim is broken on deck too" -- fixed for real; the first-pass linear gain was measured as a dramatic sensitivity loss (2026-09-28) and replaced with the N64 path's verbatim quadratic curve (fov/60 base, 3.5 deg/poll top rate); retest on the refreshed build: user "good". RESOLVED.** | +| D405 | **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; label rebased from the PR's D323, which collides with our D323 LOD finding) — full `## D405` entry at file tail | **CLOSED (2026-09-28).** Merged from PR #106 (code file only; security-reviewed clean - port-layer GL state fixes, no external input, no allocations). `resolve_msaa_color_buffer` clamps `msaa_level` to `GL_MAX_SAMPLES`; the resolve/copy paths bind `current_framebuffer ? framebuffers[i].fbo : 0` (the default framebuffer is GL name 0). Both Windows (opengl32) and Linux use this backend. Post-merge sweep 21/21 PASS + MSAA=16 boot check render-verified; link ritual clean. | +| D406 | **front-end PC Options screen: long sections (INPUT, 17 visible rows) run off the bottom of the 440x330 paper, and rows past the silent 14-row page cap (Crouch mode, Reset to defaults, Bindings…) are unreachable from the front screen** (user report 2026-09-28, v0.4.0 pre-push review) — full `## D406` entry at file tail | RESOLVED (2026-09-28, port-layer only, Steam Deck-validated): in-section pagination in `frontoptions.c` (11 rows/page; the vertical stepper crosses page boundaries both ways; dim 'Page p/N' marker + bottom hint on every page; slider bars start past long labels; F10 overlay untouched). D406b/c/d same-cycle refinements (label/track overlap, hint visibility/wording, hint position). | +| D407 | **Tanks cannot be boarded/exited on PC — the v0.4.0 use/reload split (D378/D393) removed the B-button tap the engine's tank handlers in `bondview2.c` consume (user report 2026-09-28, Runway/Streets)** — full `## D407` entry at file tail | PARTIAL (fix landed in v0.4.0, port-only): present `GE_CONT_B` on the use (E / pad A) rising edge only while `g_PlayerIsInTank == 1` or `g_BondCanEnterTank != 0`; E keeps its D378 no-reload-fallback semantics elsewhere, N64 layout unaffected. D407(b) same cycle: front PC Options page-edge highlight clamp + mouse-wheel / W-S paging (wheel queue consumed on the menu, D223); its wheel mapping shipped inverted and was fixed to match W/S (wheel up = step up). D407(c): board-animation lockout, menu-accept B gate in tank states, in-tank aim routed through the legacy velocity stick + `Input.TankAimScale` knob. RESOLVED 2026-09-28: all port-only (src/game zero-diff), 10 TANKDBG probes stripped, release binary verified clean, user live tank drive signed off (board + exit OK, aim feel good). | +| D408 | **`Game.SkipIntro` skips the post-mission failure dossier: with it on, entering a level then aborting (watch Z+A) or dying (KIA) returns straight to the menus — no REPORT / "Mission status: KILLED IN ACTION / ABORTED" screen** (user report 2026-09-28; save/AllUnlocked ruled out) — full `## D408` entry at file tail | OPEN (cosmetic, not root-caused past the handoff site). v0.4.0 decision: SkipIntro stays EXPERIMENTAL / not recommended for regular users — F10 row relabelled "Skip intro (EXPERIMENTAL)". | + Phase 2 replaced the Phase-1 demo loop with the real `mainproc()` on real OS threads, compiled GE's real `src/sched.c`, and brought in PD's fast3d software @@ -2426,6 +2512,12 @@ Confidence overall: **medium** (triage solid, no runtime confirmation). > renders as degenerate white blobs" plus the unverified cast roll; the > gun-barrel half is retired. The M-32b probe text below is left for history. +> **M-195 UPDATE (v0.4.0 Phase 1, 2026-09-21) — Nintendo logo: vertex transform proven CORRECT; leading hypothesis now = G_VTX slot overwrite.** Three-pass investigation this session: **(1) Emission is fine.** Fresh `GE_D63=1` bare-front-end run (log `/c/tmp/d75_d63.log`, 32k lines): all 23 nintendologo Primary GDLs are emitted **every frame** of the logo screen (~1002 hits each, BaseAddr=0x70157a98), and the log contains **zero** `D146: unknown GBI opcode` lines — the software RSP consumes every DL to completion. PC-side GDLs verified byte-faithful to the N64 source via the d43 toolchain (same custom GE opcode sequences, same vertex counts). **(2) Transform is fine.** New `GE_D75V="lo-hi"` probe in `gfx_sp_vertex` (per-batch transformed-vertex min/max + MP translation row; uses new `videoGetFrameCount()` accessor) shows the logo's vertices land exactly where the wordmark geometry says they should: x∈[−2500,+2500], y∈[±644], w≈3.9k–5k, MP translation (0,0,3878.7) matching `lookat(0,0,4000)` — this eliminates basematrix, the f32→s32 conversion loop, and the matrix-list suspects. **(3) Leading hypothesis: vertex-slot overwrite.** Every `G_VTX` batch in these GDLs encodes **v0=0** (fast3d decodes `n=C0(0,16)/sizeof(Vtx)`, `dest_index=C0(16,4)`; probe confirms di=0 for every batch), so a node whose vertices span multiple 16-vertex batches writes all of them to slots 0..n−1 of the shared buffer — each later batch **overwrites** the earlier ones and only the last batch survives. Triangles referencing indices beyond the last batch's count read stale slots from prior nodes (likely clipped, not visible as junk — consistent with the observed single ~25×6px orbiting fragment matching the smallest node's 4-vert quad at x≈1020 → ~155px orbit radius vs observed ~200px). The N64 RSP must handle multi-batch nodes differently (per-batch ring or relative indexing) — GE's own ucode is not in this repo, so the next step is empirical: dump one full node's GDL stream from the ROM and cross-reference its `G_TRI4`/`G_TRI1` index ranges against per-batch vertex counts. If confirmed, the fix is port-layer only (fast3d vertex-slot management) — no rule-2 sign-off needed. Probe registered in `GE-ENV-PROBES.md` as `GE_D75V`. + +> **M-196 UPDATE (v0.4.0 Phase 1, 2026-09-21) — slot-overwrite hypothesis REFUTED; bug narrowed to the color-combiner/texture path.** New `GE_D75D="lo-hi"` probe in `port/fast3d/gfx_pc.cpp`: per-frame G_VTX/G_TRI token stream + per-triangle rejection/emission accounting (trivial-reject, backface-cull, NDC bbox, invisible-flag, vertex-color range, oml, depth z/w, combine mode, prim/env color). Front-end boot (no `-level`), logo window = frames ~244+ where a large model appears (nv=93 G_VTX batches, **nt=1018** tris/frame; the earlier frames 5–243 are the title-screen textured quads, nv=2/nt=12, which render fine at 8.36% coverage). **(1) Slot-overwrite is refuted.** The logo model's DL is **streamed**: `seq` = `VTTTT…VTTT…` (each G_VTX batch immediately followed by its own triangles), and `maxtri=15` — no triangle ever references a vertex index beyond 15, i.e. within a single 16-slot batch. M-195's "all batches write to slot 0, only the last survives" mechanism **cannot apply** to this model; there is no cross-batch clobbering. **(2) Rejection gates are not the cause.** Of the 1018 tris/frame: `rej_triv=0`, `rej_cull≈520` (a normal ~half for a closed mesh), **~500 reach OpenGL**; and all ~500 are on-screen (`emit_on≈500, emit_off=0`, NDC bbox ⊂ [−1,1]²). Not the invisible flag (`invis=0`). Vertex colors span `[0,255]` (not black). Depth `z/w ∈ [−1, 0.97]`, no NaN. **(3) Symptom quantified.** PPM pixel analysis of the logo window: **0–33 content px/frame** (a ~19×2px fragment right of centre) vs the title quads' 8.36% — this is M-195's "tiny orbiting fragment", now measured. **(4) Narrowed cause = texture-modulate material rendering black.** The model's combine mode is `0x009ffe4f19ffe4f1` (`is_2cyc=0`). A one-shot decode in `gfx_generate_cc` (new `D75DCC:` log, gated to this exact word) shows it parses to **rgb a=`G_CCMUX_TEXEL0`(1), c=`G_CCMUX_SHADE`(4)** — a texture-modulate material — and `gfx_generate_cc` sets `used_textures[0]=1`. `env=(0,0,0,0)`, `prim=(255,255,255,255)`. Intended output = TEXEL0 × SHADE; if TEXEL0 samples black at draw time (no/empty texture bound), the whole model is black-on-black → invisible except AA-edge fragments. **Anomaly to close next:** at emit time BOTH `gfx_rapi->shader_get_info(prg)` and `comb->used_textures[0]` read **0**, even though `gfx_generate_cc` set it to 1 for this exact combine_mode — so the TEXEL0 input is being lost between combiner build and draw (suspects: `shader_id0`→`gfx_lookup_or_create_shader_program`, the `prg[tm]` variant selection at gfx_pc.cpp:~2503, or the GL backend's sampler binding / texture upload for this model's tile). If the TEXEL0 sampler is dropped, `SHADER_TEXEL0` defaults to 0 → MODULATE → black. Port-layer fast3d only; no rule-2 sign-off needed. Probes (`GE_D75D`, `D75DCC`) registered in `GE-ENV-PROBES.md`; both env-gated/inert without the var. + +> **M-197 UPDATE (v0.4.0 Phase 1, 2026-09-21) — ROOT CAUSE FOUND + FIXED: the D50 sidecar converter reserved and remapped the model's embedded texture-blob span but never copied the pixel bytes into the emitted image, so TEXEL0 sampled a zeroed buffer.** The M-196 "utex=0 anomaly" was first closed as a probe artifact: the frame-level `utex`/`cutex` fields were sampled on the *first* emitted triangle of the frame (a title quad, utex=0) while `comb` held the *last* triangle's mode; new logo-mode-specific fields (`lg_utex`/`lg_tex`, gated to combine word `0x009ffe4f19ffe4f1`) show both = 1 for every logo triangle — the combiner and GL shader correctly claim a texture. The real defect is upstream in the asset pipeline: the NINTENDO logo model (`PnintendologoZ`, loaded via `front.c` `init_menu00_nintendoscreen` → `load_object_fill_header(dst=ptr_logo_and_walletbond_DL, 0x3c000)`) carries its texture pixels **embedded in the model file** — GDL `G_SETTIMG` (GE opcode `0xFD`) slots with seg-5 (file-base-relative) targets. The N64 original has a 1024-byte white-wordmark IA8 blob at file offset `0x408`. The D50 offline converter (`tools_pc/d43_emit.py`) detected the blob span (`[0x408, 0x808)`), reserved it in the PC layout (new offset `0x6428`), and remapped all 23 SETTIMG slots to `w1=0x05006428` — **but the emit pass never copied `src[0x408:0x808]` into the output buffer**, so the sidecar's RZ image carried a zero-filled blob region. At runtime `seg_addr(0x05006428)` = file base `0x70157a98` (M-195's BaseAddr) + `0x6428` = **`0x7015dec0`** — exactly the all-zero texture buffer the `GE_TEXDUMP_ADDR` probe measured (both imports, dls=242 and dls=4444: `nonzero=0/1024`). TEXEL0 × SHADE over black pixels = the whole 1018-tri model renders black-on-black. **Three files are affected** (the only model files with seg-5 SETTIMG targets, found by scanning all 512): `PnintendologoZ` (`[0x408,0x808)`), `PgoldeneyelogoZ` (`[0x68,0xb30)`, 2 images), `PlegalpageZ` (`[0x90,0x2070)`, 5 images). **Fix (tooling-only, no `src/game` change, no rule-2 sign-off):** `d43_emit.py` now tracks `blob_regions` through layout, raw-copies each span in the emit pass (`buf[n:n+osz] = src[o:o+osz]` — the RDP reads these bytes as-is, no transform), and round-trip-validates the blob bytes so this cannot silently regress. Sidecars regenerated (`ALL CHECKS PASSED`, incl. the new blob check); offline re-decode confirms all 8 blob targets now carry real pixels (nintendologo: `ffffffff…` white wordmark, 713/1024 nonzero). **Live-verified:** bare front-end boot, `GE_PCDUMP="240-330:10"` — logo window now renders the animated 3D logo: frame 320 = **23,302 non-black px (7.59%)**, wide wordmark bbox `(94,190)-(542,290)`, vs M-196's 0–33 px; frames 340–560 show the logo rotating through its full animation at 3–10% coverage. No placed object offset falls inside any blob span (checked for all three files), so the old-space overlap between a PC-footprint node region and the nintendologo blob start is a bookkeeping artifact with no pointer ambiguity. **User-confirmed live (2026-09-21):** "the images and legal page and Nintendo logo are good now." **Remaining:** `pal-final`/`jpn-final` sidecars must be regenerated on machines holding those ROMs (`python tools_pc/d43_emit.py `). Probes added this pass: `GE_TEXDUMP_ADDR=` (raw source-byte dump + stats for every import from one address, bypassing the 400-cap) and the D75D `lg_*` logo-mode fields; both env-gated. + **D75 Bug 2 — RUNTIME PROBE (session M-32b, `GE_D75=1` in `title.c` `sub_GAME_7F007F30`, kept env-gated).** Booted the bare front end (`GE_D75=1 GE_PCDUMP="1-1200:120"`, no `-level`) and captured through the @@ -10448,6 +10540,8 @@ crash class C2 (Facility `-level_34`, Runway `-level_35`): `import_texture_i8` A **Status (from §F table):** OPEN — observed, not investigated +**Steam Deck (Linux / SteamOS, v0.4.0-pre, 2026-09-27/28 — user playtest):** the same intermittent frame-timing hitches reproduce on the Deck, but with a *different* utilisation profile than #92/D339: both CPU and GPU sit at only ~15–30 % (nowhere near a throughput bottleneck) yet the user perceives occasional "weird frame timing." Low-utilisation hitches like this are a **frame-pacing / present** problem, not a throughput problem. Candidates: the D186 `Video.FpsCap` throttle (the D186 row notes it throttles the *sim*, not just presentation), the D248/D250 pacing path (fixed for the 30-vs-60 case), or the Deck's Wayland/GBM + AMD present/vsync behaviour. Next (if pursued): a `GE_PERFSTAT=1` run plus a per-frame `dl`/`swap-pace` time histogram on the Deck, to see whether the hitches are a pacing-wait spike or a present stall. **Log-only this pass — not investigated yet.** + ## D172 — Bullet-impact / blood / spark particles render magenta or cyan instead of dark red (M-82, ROOT CAUSE FOUND + FIXED). **Bullet-impact / blood / spark particles render magenta or cyan instead of dark red (M-82, ROOT CAUSE FOUND + FIXED).** Particle billboards (`explosion.c` `explosionRenderPart`, `gSPVertex`+`gSP2Triangles`) take their CC/tile state from ROM state records replayed just before them (`g_ExplosionDisplayLists[]` = `assets/oddtextures.c` `globalDL_0x078..`). The dominant record sets `gsDPSetCycleType(G_CYC_2CYCLE)` + `gsDPSetTextureLOD(G_TL_TILE)` + `gsDPSetCombineMode(G_CC_INTERFERENCE, G_CC_MODULATEIA2)` and binds **two** tiles: tile 0 = IA8 "smoke" @ TMEM 0, tile 1 = RGBA16 "fire" @ TMEM 0x188. `G_CC_INTERFERENCE` cycle-0 = `TEXEL0 * TEXEL1`. **fast3d's `gfx_lod_tile_offset()` returns `0` unconditionally** on the `!gfx_detail_textures_enabled` branch — a D107 fix for GE's mip textures (whole chain loaded in one LOADBLOCK at TMEM 0; `port/src/video.c:225` sets the flag `false`). So texunit 1's sampled tile = `first_tile_index + 0` = tile 0 → **TEXEL1 samples the smoke texture, not fire** → `smoke * smoke` → wrong colour. Two earlier M-82 candidates were false trails: (1) "0xB9 unhandled → D146 abort" — 0xB9 is `G_SETOTHERMODE_L` in this non-F3DEX2 build, handled; zero D146 aborts in a sustained-fire run. (2) "records never set 2-cycle" — they do (`0xBA`), and fast3d applies it (probe-confirmed `cycletype=2CYC`). **Probe evidence (`GE_D172=1`, user Silo firefight, `d172_silo.log`):** 40/40 particle tris → `cfg1[tile=1 tmem=392 fmt=RGBA siz=16b]` but `SAMPLED1 = tile0 (tmem=0 fmt=IA)`, `tex_lod=0`. @@ -10688,6 +10782,13 @@ D243 (cutscene shake/duplication, its own CONSOLIDATED NEXT STEPS block appears ## D224 — QoL gap (M-87, from a PD-PC-port survey): the N64 Rumble Pak is fully dropped instead of modernized. +**DONE via D401 (2026-09-27).** Re-promoted from DEPRIORITIZED; implemented per +`docs/dev/RUMBLE-PLAN.md` exactly as the M-87 addendum prescribed — both +upstream blockers (`CONT_CARD_ON` gate + `osPfsInit` → `PFS_ERR_DEVICE`) landed +with the `SDL_GameControllerRumble` wiring, plus `Input.PlayerN.RumbleScale` +config (options-menu slider deferred). Real-pad playtest + `RumbleScale=0` +silence check owed. See `## D401` (file tail) for the full record. + **QoL gap (M-87, from a PD-PC-port survey): the N64 Rumble Pak is fully dropped instead of modernized.** GE genuinely has a rumble subsystem (`src/motor.c`, `src/joy.c`) but the port's `osMotorInit/Start/Stop` (`port/src/libultra.c:1220-1223`) are all hard stubs — "no accessories on the PC," `osMotorStart`/`osMotorStop` return `-1` unconditionally, `osPfsFindFile`-family returns `PFS_ERR_NOPACK`. So every in-game rumble event (weapon recoil, explosions, etc.) computes and calls into a dead path. **PD's PC port wires the equivalent straight through to real gamepad rumble:** `inputRumbleSupported(idx)` checks `SDL_GameControllerHasRumble`/`SDL_JoystickIsHaptic` (with a Windows fallback: some pads report no haptics but rumble anyway), `inputRumble(idx, strength, time)` calls `SDL_GameControllerRumble()` scaled by a per-pad `RumbleScale` config value (options-menu slider, `Input.PadN.RumbleScale`), and `osPfsFindFile` reports a "Rumble Pak" present exactly when a real pad supports rumble (`libultra.c` — same "pretend the N64 accessory is there" pattern GE's port already uses for `osMemSize`/Expansion Pak, just applied to a real capability instead of faked). Cross-ref `QOL-INVENTORY.md` (folded into the existing "per-pad tuning" row). **Status (from §F table):** **DEPRIORITIZED (M-87, user call).** Gamepad-only — irrelevant to mouse/keyboard, which is the primary target for this next release; no keyboard/mouse play is affected by NOT doing this. Root cause + fix still valid whenever gamepad support becomes a priority (no `src/` game-logic touch: GE's `motor.c`/`joy.c` call sites already exist and already call the `osMotor*` shims; the fix is entirely in `port/src/libultra.c` + `input.c` — add `inputRumbleSupported`/`inputRumble` per the PD shape, wire `osMotorStart`/`osMotorStop` to them, add `Input.PadN.RumbleScale` config). **M-87 PD-legacy-survey addendum (`docs/dev/notes/PD-LEGACY-SURVEY.md` candidate 1, high confidence) — the D224 fix as originally stated is NOT sufficient by itself.** Two more upstream blockers sit before the stubbed `osMotorInit/Start/Stop` ever gets reached: (1) `port/src/libultra.c:1096-1098` sets `g_contStatus[i].status = 0` for every pad — `CONT_CARD_ON` is never set, and `src/joy.c:184` gates the *entire* rumble-pak init path on that bit, so `joyRumblePakInit()` never even calls `osPfsInit`. (2) `src/joy.c:186-190` only attempts `osMotorInit` if `osPfsInit`'s return is `PFS_ERR_ID_FATAL` or `PFS_ERR_DEVICE` — but the port's stub (`libultra.c:1214`) returns `PFS_ERR_NOPACK`, so motor init is never attempted even once (1) is fixed. PD's exact trick (`libultra.c:213-227,349-351`): set `CONT_CARD_ON` for connected pads, and make `osPfsInitPak()` return `PFS_ERR_DEVICE` specifically when the pad supports rumble. **Whenever D224 gets implemented, both of these must land alongside the `SDL_GameControllerRumble` wiring** or the game's state machine parks at `RUMBLEPAKINITSTATE_NOT_READY` forever and nothing audibly changes despite looking wired up correctly. @@ -10704,6 +10805,10 @@ D243 (cutscene shake/duplication, its own CONSOLIDATED NEXT STEPS block appears **Status (from §F table):** **OPEN — root cause / call sites identified, not designed or implemented.** Two viable directions: (a) a `#ifdef PORT` wrapper at just these 2-3 `bondview2.c` call sites that scales the emitted `gSPTextureRectangle` geometry (`xl/yl/xh/yh`) by a new `f32 portHudScale` (default 1.0 ⇒ byte-identical) while inversely scaling `dsdx/dsdy` to keep the same source glyph texels mapped onto the larger/smaller rect, re-anchored so the opposite edge (bottom for the lower-left block, top for dialogue) doesn't move; or (b) thread a scale parameter into `textRenderOutlined`/`textDrawGlyphQuad` itself, defaulted to 1.0 at every other call site. (a) is more surgical (matches the "don't touch shared menu text" constraint) but duplicates a little geometry math; (b) is more central but touches a much more widely shared function. No PD precedent found (PD's port has no equivalent HUD-scale option). F10 overlay slider candidate once implemented, alongside `Video.FovScale`. +**Design refinement (2026-09-23, not implemented).** Per-glyph scaling in `textDrawGlyphQuad` (option a/b above) is insufficient. Both HUD text sites first draw a background box sized to the measured *unscaled* text (lower-left: `draw_blackbox_to_screen`; top dialogue: `microcode_constructor_related_to_menus` over `msg.y..msg.bottom`), NTSC's dialogue path is `textRender`, not `textRenderOutlined`, and the ammo counter pairs text with an icon image (`set_rgba_redirect_generate_microcode`). Scaling glyphs alone would overflow the boxes and split the ammo readout. **Recommended:** a port-side span transform in fast3d, analogous to D335's aspect modes: a new EXT command carrying (scale, anchor x/y) that scales *all* rect and vertex positions (x and y, plus texrect dsdx/dsdy) about the anchor until cleared. The game-side edit is then just a wrap around each HUD call (box + text + icon together), anchored bottom-left for status text, top-centre for dialogue and bottom-right for ammo. The D335 LEFT/RIGHT/CENTER anchoring composes with it. Slider `Game.HudScale` 75–200%, default 100 (no emission, byte-identical). + +**IMPLEMENTED (2026-09-24, v0.4.0) as designed above: `Game.HudScale` (F10 "HUD scale %", 75–150, default 100; capped at 150 after the user playtest: "past that not super effective").** fast3d: new extension command `G_HUDSCALE_EXT` (opcode 0x46, free in GE's command space; w0 low 16 = scale×256, w1 = anchor x/y in 10.2): while active, `gfx_dp_texture_rectangle` / `gfx_dp_fill_rectangle` scale rect coordinates about the anchor and divide texrect dsdx/dtdy by the scale (glyphs stay crisp, same texels). Rects clip at the canvas origin (negative rects aren't drawn). The state resets every frame. It composes with the D335 aspect anchors (scaling happens in logical space before the aspect mapping). Game (`#ifdef PORT`, Rule-2 approved presentation, `port/include/hudaspect.h` macros): bottom messages / title card anchored at the view's bottom-left; top dialogue anchored at its text origin (`msg.x` = view left + 0x1e); ammo per hand at the bottom-right / bottom-left. **Width cap:** dialogue and bottom-message line breaks are authored into the text (no runtime wrap), so those two scale only as far as their widest line still fits (`textMeasure` with the renderer's fonts; long lines stay at ~100% instead of clipping). At 100% nothing is emitted (byte-identical; golden 3/3). **Verified (1280×720 captures):** title card and ammo at 150%/200% correctly enlarged about their corners; Archives dialogue no longer clips (an earlier centre-anchor attempt cut off "Guard:", and an origin anchor without the cap overflowed the right edge). Windows + Linux builds clean. Commit `30720354`. + ## D230 — In-level music quality: a bass instrument sounds corrupted on PC, correct on N64 reference (M-105, user reports; repro refined M-106). **In-level music quality: a bass instrument sounds corrupted on PC, correct on N64 reference (M-105, user reports; repro refined M-106).** Filed to give the post-D77 residual its own label — D77 (in-level music *absence*) is RESOLVED (PR #42, missing `return` in `sub_GAME_7F0C0BF0` zeroed the seqp volume on the only in-level trigger path), so "music doesn't play" claims in tracked docs are stale; what actually remains is this quality gap. **M-106 repro, deterministic (not intermittent):** Control's elevator track and its regular in-level track both carry the bad instrument throughout; Cavern's elevator track has it too; Runway's track is otherwise fine but the same instrument enters ~5–7s after the track starts and sounds bad from then on. User confirmed against original N64 reference audio that a bass instrument belongs there on Runway — this is **not** a wrong-instrument/wrong-bank selection (rules out the D77 trigger-path family and the seqp-volume fix's neighborhood); the instrument choice is correct, only its *sound* is wrong on PC. Determinism (same track, same spot, every time) points at a static decode/synthesis bug for this instrument's wavetable/ADPCM data — most likely low-frequency-specific (pitch-shift/downsample artifacting, a wrap/loop-point error, or a clipping/gain issue that only bites bass-range samples) — rather than a live mixing/voice-stealing issue (D147/D152/D202 family), which would be intermittent. The mixer layer and the trigger layer are both proven working by every track that sounds right, so the suspect neighborhood is the seq/instrument-bank decode path (`csplayer.c`/`synthesizer.c`, bank loading), not `port/` audio plumbing. @@ -10728,11 +10833,22 @@ D243 (cutscene shake/duplication, its own CONSOLIDATED NEXT STEPS block appears **Status (from §F table):** 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. **M-108 static-analysis pass: the leading "stale CI-cache not reset on restart" hypothesis is likely REFUTED, don't re-propose it as-is.** The theory was that `g_TexCacheItems`/`g_TexCacheCount` (`src/game/image.c:38-40`, cleared only in `set_mt_tex_alloc()`, `src/game/initmttex.c:8`) survives a death/restart because the restart path skips `set_mt_tex_alloc`. But tracing the actual call graph: `set_mt_tex_alloc()` runs inside `lvlStageLoad()` (`src/game/lv.c:369`), which has exactly **one** call site (`src/boss.c:485`) inside `bossMainloop`'s outer `while (!done)` loop; that loop re-runs on *every* stage transition, driven by `g_StageNum = g_MainStageNum` (`boss.c:693`) which is set by `bossSetLoadedStage()` (`boss.c:722`) — the same function the front-end's level-select/continue path calls (`front.c:6947`). Nothing in this call graph is level-select-specific vs. restart-specific; both look like they go through the identical full reload (including the `set_mt_tex_alloc` cache reset). Have not yet found the actual death/restart trigger's call site to be 100% certain it doesn't have its own bypass, so this isn't fully closed out — but the single-reload-path structure makes the "cache survives restart" mechanism unlikely as stated. Next: find the actual death-handler call site (search for where a life/continue/retry decision is made, likely `chr.c`/a game-state machine, not yet located) and confirm whether it calls `bossSetLoadedStage`/goes through `bossMainloop`'s reload path same as normal transitions, or has a distinct lighter-weight path — if it's the same path, the real D235 mechanism is still unknown and this CI-cache theory should be dropped. +**M-193 (v0.4.0 Phase 1, 2026-09-21): port-side GL texture cache found — the M-108 refutation stands for the *game-side* CI cache, but the port has its own cache with the exact same structural hazard, and it was never invalidated on stage transitions.** The PC renderer keeps a `TextureCacheMap` (`port/fast3d/gfx_pc.cpp`) mapping `(source address, palette addrs, fmt, siz, palette_index, size_bytes, palette_hash)` → uploaded GL texture, with **no content check** — a hit trusts the key. Stage textures' source addresses live in the `MEMPOOL_STAGE` arena, which the per-stage `while (!done)` block in `boss.c` resets via `mempResetBank`/`memaReset` on *every* transition (death/restart included — M-108's call-graph trace already established both paths re-run that block). After the reset, any cache entry keyed into that arena is stale; if the texpool places a different image at an address that held another image in the previous run of the level, the lookup hits and returns the old pixels = "wrong textures after dying/restarting", never on a cold boot (empty cache). The N64 has no such cache (the RSP re-reads DRAM every frame), so this is a pure port-side defect — fixable in `port/`-adjacent code with no rule-2 sign-off. **Fix applied:** the orphaned `videoResetTextureCache()` (`port/src/video.c`, zero callers) is now wired into that per-stage block right after `memaReset`/`reset_play_data_ptrs` (`#ifdef PORT`) — a full clear on every stage load. Re-uploads are pixel-identical (same content → same GL upload), cost is one-time during the loading screen; ROM-segment textures (menu/HUD) just re-upload too, harmlessly. `GE_D235=1` logs the purged entry count per transition (cold boot = 0, death/restart > 0) as direct mechanism evidence. **Remaining:** live death/restart confirmation at playtest (wrong textures gone?) + a `GE_D235` capture showing >0 on a restart; if symptoms persist after this, the next suspect is intra-level texpool eviction reusing addresses mid-play (nothing emits `G_INVALTEXCACHE_EXT`, so per-address staleness is possible there too — the handler exists at `gfx_pc.cpp:3624` but has no emitter). + +**v0.4.0 verification pass (2026-09-23, native Windows, headless): fix wired and clean on reload; the bug itself did NOT reproduce, even with the fix disabled. Needs the user's original trigger to close.** Harness: `-level_09`, scripted watch Abort → title stage → new TEMP `GE_D235_RELOAD=` re-enters Bunker 1 via `bossSetLoadedStage()` (the front end's mission-start call) after a front-end dwell; new TEMP `GE_D235_OFF=1` skips the reset for an A/B. Results: +- Fix ON: cold boot purges 0, the abort into the title purges 527–833, and re-entry purges 63 (front-end entries). The reloaded level renders correctly; settled gameplay matches the fix-OFF run 7/9 frames, and the 2 fails are intro-fade/camera timing (D117), not textures. +- Fix OFF, identical load order (intro both times, 300-frame legal-screen dwell): 588–788 stale entries survive into the reload; **all textures correct**. +- Fix OFF, divergent load order (first load skips the intro, so only the shaft is seen; the reload plays the full control-room flyby): 589 stale entries survive; **all textures correct** at full resolution (`scratch/d235_1750.png`). + +So on this path, stale keys either miss or hit the same image. Either GE's placement of Bunker 1's bg textures is order-independent here, or the key (`palette_hash`, `size_bytes`, fmt) disambiguates. The fix stays correct by construction: an address-keyed cache with no content check outlives an arena reset, and clearing it at every stage load costs only re-uploads during the loading screen. What's untested is the reported trigger: a real **death → mission failed → mission select → restart** (the front end's image-heavy briefing/folder screens and 3D models go through the stage arena in between) on the level where it was seen (Facility first). **Human test:** reproduce with `GE_D235_OFF=1` (expect wrong textures) and without (expect clean); note the level and surfaces. Confidence that the fix addresses the report: medium, since the mechanism is real but the trigger is not captured. + +**2026-09-25 playtest (user, Windows, one ~46-min session Silo → Caverns, `GE_D322=1 GE_D204=1`, build `7baf4900`):** die → mission select → restart did not reproduce wrong textures (fix on). Still unreproduced in every configuration; stays OPEN only because the original trigger was never observed locally. + ## D236 — 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). **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).** Eight passes so far (D265, D266–D270, D271, D272, D273, D274, D275–D276). Ruled out: IA4/IA8 decode (as originally scoped), the blend pipeline, fog combine (as originally scoped), portal-culling over-draw (D271, real bug, fixed, not the cause), converter/UV-conversion bugs (D272, code-read clean), the CI palette bit-layout (D273/D274, tested twice against real content), "mips absent" (D274, confirmed present/enabled), "identical UV window" *for the CI8 classes* (D272). **Current leading picture (D276, M-134): the actual visible weave at confirmed weave pixels is painted by a different, previously-dismissed class — an IA8 blocky-noise texture (`oml=0xc81049d8`/`fmt=3`) sampled with a fixed, byte-identical 2×-wrapped UV window and low fog, reproducing D265's original "same window" observation exactly (D269's CI8-classes framing had reframed away from that lead prematurely).** A third, unattributed CI8 class (`0x0c184b50`) may be the reference's actual discrete-tree layer and shows an erratic z/w suggesting its own occlusion/billboard bug — D236 may split into two root causes. **Ninth pass (D280, M-135): fog math verified N64-faithful (±2 truncation over 2505 verts), the IA8 noise class visually confirmed as the weave's painter (HF 14.8→2.9 when collapsed), `0xc8102078` ruled out (terrain, 1.4M tris), and the depth-race read inverted — `0x0c184b50` tree cards are IN FRONT of the noise quad (z/w smaller=closer) yet still don't visibly win; under deterministic `-level_36` boot they barely instantiate at all (~4 tris vs 720 in a free run). No quick win: remaining work is CPU-side instantiation/room-streaming, not a port fix.** **Tenth pass (M-155): that CPU-side-instantiation framing itself is REFUTED — three independent deterministic re-runs of the same `-level_36` boot (one with sustained camera rotation) proved the class's per-frame count is byte-identical, not run-to-run dependent; the earlier "~4 vs 720" comparison was an outdoor-vs-indoor room mismatch (the boot's scripted intro walks the camera out of the tree-bearing room at a fixed frame), not nondeterminism. No fix; the original wall-vs-discrete-sprites symptom is unresolved and untouched by this pass.** Distinct from D176(b)'s painted tree-line backdrop. Logged in `GRAPHICS-BACKLOG.md`. -**Status (from §F table):** OPEN — tenth pass refuted the "instantiation is nondeterministic" theory (ordinary room-based occlusion, not a bug); original wall-vs-sprites symptom still unresolved; see D276/D280/M-155 +**Status (from §F table):** OPEN — **pass 24 (2026-09-21, static) DECODED THE TREE TEXTURE** (new `tools_pc/texdecode.py` ports image.c's LOOKUP/HUFFLOOKUP/RLELOOKUP codecs). **`tex1198` LOD 0 = 64×17 RGBA5551, 44.2% opaque, and its 1-bit alpha renders as an unmistakable treeline silhouette** — pass 23's identification confirmed by decode, not inference. **Rare authored a six-level LOD pyramid that PRESERVES the silhouette and its alpha coverage (44.2% → 43.1% → 38.8%), each level a separately-authored image with its own compmethod.** Since `XLU_SURF` has no alpha test (pass 22), averaging that 0/1 alpha into fractional mip alpha would smear the minified treeline into exactly the reported band — so 'authored LOD chain replaced by driver-generated mips' is now the leading mechanism, with hard ground truth behind it. Corrects pass 23: only `tex1198` is a treeline (`tex1199` 89.4% opaque denser foliage; `tex1200`/`tex1201` 100% opaque detail textures sharing the class). Two decoder bugs found while porting (missing per-image byte-align, `image.c:1096`; a GUESSED rather than ported RLE fudge) — both produced confident wrong pictures. Next: one `-level_36` run filtered on **`fmt=0 siz=2`**, diffing the live import against this offline reference on (1) 64×17 RGBA5551 dims/format, (2) ~44% alpha transparency, (3) whether all six authored LODs reach GL or only LOD 0. **Next pass framing (post rule-2-clarification):** @@ -10977,12 +11093,336 @@ User played live with `GE_D236ORDER=1` set, walked toward Surface1's tree line, **Not v0.3.0-blocking, user is continuing anyway.** `GE_D236RAW` and the `vtxseg` extension to `GE_D236ALPHA` are both in tree, env-gated, zero cost unset. Raw headless dump preserved at `scratch/d236_raw_headless.log`. +**Pass 17 (2026-09-21, fresh session) — WRITER IDENTIFIED: `doshadow`. Plus a false alarm resolved.** + +1. **Vertex struct is 16 bytes, not 32**: GE's RSP uses a custom vertex format `{s16 ob[3]; u16 flag; s16 tc[2]; cn[4]}` (alpha at byte 0xF) — no screen-space sx/sy/st/ct. `vtxallocator(n)` = `dynAllocate(n<<4)` = n×16B; the observed +0x40 arena increments are doshadow/dogfnegx's 4-vertex (64B) allocations, not 2×32B Vtx. +2. **Decisive probe**: env-gated backtrace at allocation time (`GE_D236BT`, `get_ptr_allocated_block_for_vertices` in `chr.c`, calling a new C-linkage `port_d236bt_dump()` extracted from the RSP-side unwind logic in `gfx_pc.cpp`). All allocator hits resolve to **`doshadow`** (`model.c:5397`) via `drawjointlist → chrRenderProp → chrpropRender → chrpropsRenderPass → lvlRender`. Zero dotube activity. First allocation's physical address exactly matches the RSP-side rawvtx capture. Pass 16's exclusion of doshadow was based on a wrong claim ("dotube/doshadow use segment-tagged addressing") — only `dorottex` tags G_VTX; both use raw pointers. +3. **doshadow alpha mechanism**: `cn[3] = (envcolour.a * D_800363F0)/255` for CHR/SMOKE props, else `= D_800363F0` — per-frame global shadow intensity, explains the varying low alphas (observed 1→2→12→13→14; probe saw 13 and 80 alternating). All 4 vertices are copies of a constant template (`D_800363F8`). Textured quad via `shadow->image` → `texSelect`. +4. **FALSE ALARM (pass 17c) — the d43_emit.py op-13 "ShadowRecord bug" does not exist.** A ROM scan of all 42 op-13 records (all in C*Z character files, none in P*Z props) initially suggested the converter writes HeaderNode to r+24 / Scale to r+32 / omits BaseAddr — misaligned vs the C struct's *commented* offsets (image@0x10, HeaderNode@0x14, Scale@0x18, BaseAddr@0x1C). But a runtime dump of the live record (`GE_D236BT` probe in `doshadow`) shows **HeaderNode@0x14 = 0 and the valid pointer at 0x18** — because on PC (64-bit) `image` widens to 8 bytes, shifting HeaderNode→0x18, Scale→0x20, BaseAddr→0x28. The converter's r+16/r+24/r+32 layout **matches the PC struct exactly**; the C comments describe the N64 32-bit layout (the D52/A1 pointer-widening class). Live values all sane: image→valid tconfig (32×32, fmt=0), HeaderNode valid, height≈110, size≈(389,413), BaseAddr set by the loader's promotion pass (`modelPromoteNodeOffsetsToPointers` SHADOW case overwrites it with fileramaddr anyway). **Do not "fix" d43_emit.py op-13.** +5. **Conclusion so far**: the faint textured sheets ARE `doshadow`'s drop-shadow quads — large (≈390×413 world units) flat ground quads sampling a tiny 32×32 texture at low per-frame alpha. N64 draws the identical quads (same code, same data). **The user-visible bug is therefore almost certainly that the trees' actual sprite geometry is missing/broken on PC**, leaving only the shadow sheets visible — not a shadow bug. + +**Next steps (fresh session):** (a) look at the dumped frames (`ppm/frame_0003xx.ppm`, converted PNGs in /tmp) to characterize what's actually on screen; (b) identify which prop/character model owns the shadow node (node 0x7020ef44, file image base 0x7020eda0 — all 42 op-13 records are in C*Z character files, so check which characters are present on Surface 1 and whether *their* main geometry renders); (c) if the tree sprites are a separate prop class, verify that file's GDL/vertex emission through `d43_emit.py` (D75 blob-bug class is the standing suspect for missing geometry). **TEMP probes in tree (env-gated, zero cost unset):** `GE_D236BT` backtrace probe in `chr.c` + live-record dump in `model.c:doshadow` (writes `d236sh.bin` to CWD on first hit — delete both when D236 concludes), `port_d236bt_dump()` helper in `gfx_pc.cpp`. Untracked scan script: `tools_pc/scan_op13.py`. + +**Pass 18 (2026-09-21, static ROM analysis session) — the tree-class RM word is a runtime LUT rewrite; exact source pair identified.** + +1. **All tree/plant prop files carry zero renderable geometry — confirmed with the real node parser, not just an opcode histogram.** `PpalmtreeZ`, `PpalmZ`, `Pjungle3_treeZ`, `Pjungle5_treeZ`, `Pplant1`–`Pplant11Z`, `Pgas_plant_*Z`: node set is only {Group(2), BBox(10), DLCollision(24)} — **no op-4 DisplayListRecord anywhere**. Same on N64. The visible tree sprites are therefore drawn by the room background (`bg_sevx_all_p.seg`, ROM 0x66F700, 116176 B; Surface1=level_36, Surface2=level_43), not by prop models. +2. **The measured tree-class RM word `0x0c184b50` exists in no ROM data.** Scanned all 25 non-empty bg files (decompressed RZ GDLs) and all 512 model GDLs: zero hits. The only `0x0c18xxxx` word in ROM is `0x0c182048` (menu logo bg). It is generated at runtime. +3. **Exact source found — `DL_LUT_CLOUD`.** Compiled GE's LUT pairs (`scratch/d236/lutdump.c`, output in `scratch/d236/lutvals.txt`): CLOUD = from `G_RM_PASS|G_RM_AA_ZB_XLU_SURF2` = **`0x0c1849d8`** → to `G_RM_PASS|G_RM_ZB_CLD_SURF2` = **`0x0c184b50`** — exact match with the measured tree-class word. The "from" word is one of the six bg RM words present in ROM (×24 in sevx). `G_RM_PASS = 0x0c080000` explains the high bits: `0x0c08 | 0x0010 (ZB_CLD_SURF2 hi) = 0x0c18`. So: room GDLs contain `SETOTHERMODE_L 0x0c1849d8`, and a runtime `bgApplyDynamicCCRMLUT` pass rewrites them to `0x0c184b50` before the RSP sees them. Note `DL_LUT_BILLBOARD` shares the same "from" word (`0x0c1849d8`) but maps to `0x0c193078` (TEX_EDGE2) — which LUT wins depends on the applied index. +4. **Pass 17's doshadow finding stands but is a different sheet.** doshadow emits `G_RM_ZB_CLD_SURF|G_RM_ZB_CLD_SURF2 = 0x00504b50` (no PASS bits) — a *different* word from the measured tree class. The faint character drop-shadow quads (doshadow, 32×32 shadow texture) and the tree-texture billboards (bg CLOUD-rewritten quads) are two separate draw sources; the user-visible "wall of tree texture" is the latter. +5. **N64 cutout mechanism (hypothesis to verify):** `0x0c184b50` = ZMODE_XLU + FORCE_BL + CVG_WRAP/FULL + IM_RD, alphacompare bits 0–2 = 000 (NONE), no CVG_X_ALPHA. No alpha *test* — the silhouette must come from plain alpha **blending** of the IA texture (`out = src*A + dst*(1-A)`; transparent texels have A=0). The port's `use_alpha` gate in `gfx_pc.cpp` (~line 2386: requires `(oml & (3<<20)) == G_BL_CLR_MEM<<20 && (oml & (3<<16)) == G_BL_1MA<<16`) must be checked against this word's blender bits (bits 16–17 = 00, bits 20–21 = 01) — if the gate rejects it, fast3d draws the quad opaque → exactly the reported "wall/sheet" symptom. + +**Next steps (fresh session):** (a) read `bgApplyDynamicCCRMLUT`'s implementation + the `enum CCRMLUT` in `src/game/bg.h`; map the literal indices 1/5/6/7 used at `bg.c:2797–2819` and `CCRMLUT_WALLETBOND` (`front.c:2177`) to tables, and find which index applies `DL_LUT_CLOUD` plus **all** call sites of that index (the room-load path uses 1/5/6/7 only — the CLOUD application may live in a second call path); (b) verify the port's `use_alpha`/blend handling in `port/fast3d/gfx_pc.cpp` for RM `0x0c184b50` (it is already special-cased at ~lines 2166/2217/2259/2279/2356 — D236 probes + zfix; review what they do); (c) if the gate rejects this RM, fix in `port/fast3d` (port layer — rule 2 allows it); (d) when concluded, remove TEMP probes listed above. Scratch tools: `scratch/d236/{lutdump.c,lutbuild.bat,lutvals.txt,scan_bg_rm.py,rmvals.txt}`, `tools_pc/scan_op13.py`. + +**Pass 19 (2026-09-22, static session) — both pass-18 leads REFUTED; complete runtime-emitter map for `0x0c184b50` established; the discrete-sprite mechanism identified as `dogfnegx` (op-12 GUNFIRE node).** + +1. **`CCRMLUT_CLOUD` (index 4) has ZERO call sites — pass 18's "runtime LUT rewrite" mechanism is dead.** Complete call-site map for `bgApplyDynamicCCRMLUT` (`src/game/bgapply.c:77`): `bg.c:2797/2804/2812/2819` (room load; literal indices 1=`CCRMLUT_PRIMARY_ADDFOG`, 5=`CCRMLUT_SECONDARY_ADDFOG` when `g_FogSkyIsEnabled`, else 6=`CCRMLUT_PRIMARY`, 7=`CCRMLUT_SECONDARY`) and `front.c:2177` (`CCRMLUT_WALLETBOND`). `CCRMLUT_BILLBOARD` (2), `CCRMLUT_WATER` (3) and `CCRMLUT_CLOUD` (4) are defined (`bondconstants.h:2931-2942`) but never applied anywhere in the compiled set — the `DL_LUT_CLOUD` table itself exists (`bg.c:2174`, comment: "Transparent to Cloud (Saves AA - Stops Jaggies from appearing behind BillBoard)") mapping exactly `0x0c1849d8 → 0x0c184b50`, but its enum index is never passed to `bgApplyDynamicCCRMLUT`. Corroborating: the ADDFOG LUTs that ARE applied map the same "from" word `0x0c1849d8` → `G_RM_FOG_SHADE_A|G_RM_AA_ZB_XLU_SURF2` = **`0xc81049d8`** (`bg.c:2039-2040`, `2061-2062`) — exactly D276's confirmed noise-weave class. So the sevx room-bg quads that carry `0x0c1849d8` in ROM end up as `0xc81049d8` at runtime (fog enabled), NOT as tree cards. +2. **The only code that emits `0x0c184b50` is `texSetRenderMode` mode 4 ("cloud"), 2-cycle, zbuffer-on** (`othermodemicrocode.c:300`: `gDPSetRenderMode(G_RM_PASS, G_RM_ZB_CLD_SURF2)` = `0x0c184b50` exactly), reached from its callers via `sub_GAME_7F073038` (`model.c:4940-4942`) → `texSelect(..., arg2=4, ...)` → `texSetRenderMode`. Mode-4 `texSelect` call sites: **`model.c:5369` `dogfnegx`** (op-12 `MODELNODE_OPCODE_GUNFIRE`), **`model.c:5545` `doshadow`** (op-13 `MODELNODE_OPCODE_SHADOW`), `glass2.c:732` bullet sparks, `gunfire.c:1512/1518/1524` flares, `explosion.c:1890` smoke (zbuf=1), `front.c` menu/crosshair (zbuf=0 → emits `0x0c184340` instead). Note the 1-cycle variant (emitted when `tconfig==NULL` or `tconfig->level==0`, `othermodemicrocode.c:415`) emits `G_RM_ZB_CLD_SURF|G_RM_ZB_CLD_SURF2` = `0x00504b50` with zbuffer on (`0x00504340` without) — so pass 18's "doshadow emits `0x00504b50`" holds only for the 1-cycle case; with a mipmapped texture + zbuffer on, **both doshadow and dogfnegx emit exactly `0x0c184b50`** (2-cycle combine: `G_CC_TRILERP`/`G_CC_MODULATEIA2` for RGBA/IA → A = texel.a × shade.a). Pass 17's backtrace attributed the tree class's vertex allocations to `doshadow`, but `dogfnegx` is a co-emitter through the identical `drawjointlist → chrRenderProp` path and the same `vtxallocator(4)` + raw-pointer pattern — the attribution may have resolved only the more frequent of the two. +3. **`dogfnegx` IS the discrete camera-facing sprite mechanism** (`model.c:5225-5380`): it computes the view direction from `rodata->Offset` through the node's model matrix, builds a rotation aligning the quad to face the camera, applies per-frame random scale (0.75–1.25) and random UV rotation around the texture centre, and draws a `rodata->Size`-sized textured quad with mode-4 cloud RM. Gated on `(renderdata->flags & 2) && rwdata->Gunfire.visible`. `ModelRoData_GunfireRecord` (`bondtypes.h:1151`): Offset@0x0, Size@0xC, Image@0x18, Scale@0x1C, RwDataIndex@0x20. This is almost certainly what N64 shows as the "discrete tree sprites". +4. **The port's `use_alpha` gate ACCEPTS `0x0c184b50` — pass 18's prime suspect is refuted by static decode.** Against this repo's gbi.h blender layout (`include/PR/gbi.h:720-723`: `GBL_c2` puts c2-modifier at bits 16-17, c2-secondary-colour at bits 20-21 of other_mode_l): the gate (`gfx_pc.cpp` ~line 2386) requires bits 16-17 == `G_BL_1MA`(0) and bits 20-21 == `G_BL_CLR_MEM`(1); the word has byte2=`0x18` → bits 16-17=0 ✓, bits 20-21=1 ✓ → `use_alpha=true`. Full decode: ALPHACOMPARE=NONE, no CVG_X_ALPHA/ALPHA_CVG_SEL, FORCE_BL set, c1=(IN, A=0), c2=(IN, A_IN over MEM, 1MA) = standard src-over alpha blend with A from the combiner (`texSelect` mode-4 2-cycle sets `G_CC_TRILERP`/`G_CC_MODULATEIA2` for IA → A = texel.a × shade.a). No port-side opaque-draw gate explains the wall. +5. **Implication / reframed picture:** with the tree class alpha-blended correctly in the port, the consistent story across D276 (visible weave painted by the `0xc81049d8` room-bg noise class), D280 (tree cards in front yet not visibly winning) and pass 15 (tree-class vertex alpha capped ≤36/255 ≈ 14%, bimodal 25/255) is that the tree cards blend at ~10% opacity over the opaque noise wall — nearly invisible, exactly "the wall shows through". The remaining open questions are (a) whether that low alpha is ROM-faithful (doshadow's `cn[3] = envcolour.a × D_800363F0/255`, `D_800363F0` set per-chr from shade alpha at `chr.c:3319`, init 0x50; dogfnegx uses template `D_800363E0`) or a port-fed wrong input, and (b) whether the discrete billboards (`dogfnegx`) actually instantiate on PC at all — its gate inputs (`renderdata->flags & 2`, `rwdata->Gunfire.visible`) are unverified. + +**Next steps (fresh session):** (a) ROM-holding session: run new `tools_pc/scan_op12.py` (added this pass; counts op-12/op-13 nodes per model file, dumps GunfireRecord Offset/Size/Image) to identify which models own Surface 1's tree billboards/shadows and cross-ref against pass 17's D236SH live-record data (32×32 fmt=0 shadow texture); (b) runtime: `GE_D236ALPHA` + `GE_D236RM` on `-level_36`, correlate the tree class's per-vertex alpha with `D_800363F0`/envcolour.a, and compare against N64 reference footage (faint vs solid sprites decides faithful-faint vs port bug); (c) verify `dogfnegx`'s gate inputs on PC (`renderdata->flags & 2`, `rwdata->Gunfire.visible`) — if billboards never fire, the discrete sprites are missing entirely (rule-2 bucket (a)/(b) question); (d) on conclusion remove TEMP probes: `GE_D236RM/ORDER/ALPHA/ZFIX/BT` in `gfx_pc.cpp`, `GE_D236BT` + D236SH dump in `model.c:doshadow`, `port_d236bt_dump()`, `tools_pc/scan_op1{2,3}.py`. Scratch tools: `scratch/d236/{lutdump.c,lutbuild.bat,lutvals.txt,scan_bg_rm.py,rmvals.txt,rmdump.c,rmbuild.bat,rmnames.txt}`. + +**Pass 20 (2026-09-22, ROM-holding session) — pass-19 step (a) executed: full 512-file `scan_op12.py` run; the `dogfnegx`=trees identification is REFUTED by the asset data.** + +1. **Verified scan result** (re-ran `tools_pc/scan_op12.py ntsc-final` this pass). *Note: the script's final line "total files with op12: 62" is a reporting artifact — that counter accumulates every file with EITHER opcode, so it equals the op13 file count; the real per-opcode totals are below.* Across all 512 converted model files: + - **op-12 (`MODELNODE_OPCODE_GUNFIRE` / `dogfnegx`) — 21 nodes total, ALL in weapon/gun/tank models.** 16 one-node weapon files (`PchrautoshotZ`, `Pchrfnp90Z`, `PchrgoldenZ`, `PchrgrenadelaunchZ`, `PchrkalashZ`, `Pchrm16Z`, `Pchrmp5kZ`, `Pchrmp5ksilZ`, `PchrrugerZ`, `PchrshotgunZ`, `PchrskorpionZ`, `PchrspectreZ`, `Pchrtt33Z`, `PchruziZ`, `PchrwppkZ`, `PchrwppksilZ`) + `PgroundgunZ`(2), `Pgun_runway1Z`(1), `ProofgunZ`(1), `PtankZ`(1). The dumped records confirm muzzle-flash semantics: barrel-relative offsets (e.g. shotgun `(-375,0,16)`), modest sizes, small embedded-image indices (`img` 0x18–0x6c, `vma?False`). `PtankZ`'s single node is a large smoke/flash quad (`(0,5,0)`, size 2143×1026). + - **op-13 (`MODELNODE_OPCODE_SHADOW` / `doshadow`) — 42 nodes total, ALL in C*Z character files** (one per character; matches pass 17's "42 op-13 records, all in C*Z"). + - **No tree/plant model carries either node.** The 16 foliage models present (`Pjungle3_treeZ`, `Pjungle5_treeZ`, `PpalmZ`, `PpalmtreeZ`, `Pplant1Z/2Z/2bZ/3Z/11Z`, `Pgas_plant_*Z`, `Pgasplant_clear_doorZ`) contain **only** `{GROUP(2), BBOX(10), DLCOLLISION(24)}` nodes — static geometry + collision, zero billboard/shadow nodes. + +2. **Refutation of pass 19's central identification.** Pass 19 concluded "`dogfnegx` (op-12 GUNFIRE) is the discrete camera-facing billboard [that draws the trees]" and that the `0x0c184b50` class is emitted by dogfnegx/doshadow. But `dogfnegx` has **zero tree instances** in the entire asset set — every one of its 21 nodes is a weapon muzzle flash or tank effect. So the `0x0c184b50` class that passes 17/19 chased (pass 17's backtrace attributed its vertex allocations to `doshadow`) is, on this level set, most plausibly **character drop-shadows + weapon muzzle flashes**, *not* the trees. Passes 17–19 appear to have been instrumenting the wrong draw class for the reported symptom. + +3. **Redirection: the visible trees are static geometry, not dynamic billboards.** Given (1), the Surface 1 trees must be drawn from static data — either (a) the foliage PROP models (`PpalmZ`/`Pjungle3_treeZ`/etc., which DO load and contain GROUP/BBOX/DLCOLLISION nodes) or (b) the level background file `bg_sevx_all_p.bin`. Both are static; neither uses the dogfnegx/doshadow path. **Honest caveat — not yet disambiguated:** my offline decode of `bg_sevx_all_p`'s room secondary-DL streams found billboard-shaped cards (G_VTX+G_TRI4 with a 0xC0/G_SETTEX texture-select), but their OML words (`0x0c1849d8`/`0x0c184dd8`) do *not* match the runtime `0x0c184b50`, and pass 19 established those exact bg quads become the **noise wall** (`0xc81049d8`) at runtime (fog-enabled ADDFOG LUT). So it is NOT yet confirmed whether the visible tree sprites come from the foliage prop models or the bg file — that needs a runtime attribution. + +4. **Next step, concrete (supersedes pass 19's b/c):** a single runtime probe that attributes the on-screen tree-region triangles to their draw source — tag `gfx_sp_tri*` triangles by whether their vertices came from a **model-node GDL** (prop/model path) vs a **bg room DL** (`g_BgRoomInfo[].ptr_*_expanded_mapping_info`, SPSEGMENT_BG_VTX=14 / the bg-widened arena), and for the visible tree pixels log the active RM + texture/tile. That one capture decides (a)-vs-(b) and ends the "which class is actually the trees" question 19 passes have been circling. If it's the foliage prop models, the standing D75 blob-conversion suspect (missing/mis-converted mesh or texture in `d43_emit.py`) becomes the prime fix target; if it's the bg file, the D85/D276 bg-DL + noise-wall thread resumes. + +**TEMP probes still in tree (env-gated, zero cost unset) — remove when D236 concludes:** `GE_D236RM/ORDER/ALPHA/ZFIX/BT` (`gfx_pc.cpp`), `GE_D236BT` backtrace + D236SH dump (`model.c:doshadow`, writes `d236sh.bin`), `port_d236bt_dump()` helper, `tools_pc/scan_op1{2,3}.py`. + + +**Pass 21 (2026-09-21, static session) — pass-20's step (a)-vs-(b) ANSWERED STATICALLY, no runtime probe needed: the Surface 1 trees cannot be prop models, because the level places none. Prop/`d43_emit.py` conversion is retired as a D236 fix target; the bg-file branch is the only one left.** + +Pass 20 closed by proposing a runtime draw-source attribution probe (tag `gfx_sp_tri*` by model-node GDL vs bg room DL) to decide whether the visible trees are (a) foliage prop models or (b) static `bg_sevx_all_p` geometry. That probe is **not necessary** — the setup data answers it, and two independent static lines of evidence agree. + +1. **No Severnaya-type tree model exists in the asset set at all.** All 16 foliage models in `data/pcmodels-ntsc-final/manifest.csv` are jungle-themed (`Pjungle3_treeZ`, `Pjungle5_treeZ`, `PpalmZ`, `PpalmtreeZ`, `Pplant1Z/2Z/2bZ/3Z/11Z`, `Pgas_plant_*Z`), and the `PROP` enum in `src/bondconstants.h` describes each one as "Jungle …" (`PROP_JUNGLE3_TREE` = "Jungle Large Tree", `PROP_PALM` = "Jungle Palm Tree", `PROP_PLANT2B` = "Jungle Plant, Low Shrub", …). There is no pine/fir/snow-tree prop anywhere in the 512-model set. + +2. **Surface 1's setup places zero foliage props — and neither does any other setup in the repo.** New tool `tools_pc/scan_setup_props.py` parses the `PROP` enum out of `src/bondconstants.h` and the object records out of `assets/obseg/setup/Usetup*Z.c`, then reports which props each setup instantiates. For `UsetupsevxZ.c` (Surface 1 — `bg.c:201` maps `LEVELID_SURFACE` → `bg/bg_sevx_all_p.seg` + this setup) it matches **219 object records**, of which **all 83 `StandardProp` records are individually accounted for** (`PROP_WOOD_SM_CRATE4`×31, `PROP_BOOK1`×16, `PROP_OIL_DRUM7`×12, `PROP_WOODEN_TABLE1`×4, `PROP_BOOKSHELF1`×4, `PROP_PADLOCK`×4, `PROP_DESK_LAMP2`×2, `PROP_METAL_CHAIR1`×2, `PROP_JERRY_CAN1`×2, plus one each of `PROP_MOTORBIKE`, `PROP_CARBMW`, `PROP_STOOL1`, `PROP_HATPEAKED`, `PROP_KEYBOARD1`, `PROP_CHRMAP`) — **not one foliage prop**. Same result for all 14 setups present in the repo (`arch ark azt cave control cryp dam depo pete run sevbunker sevb sevxb sevx`), including Dam/Runway/Streets, which also have visible outdoor greenery. GE's outdoor trees are never props. + + *Positive control (the zero is meaningful).* A "0 foliage" count is only evidence if the detector can fire. The tool now self-tests and prints it: `PROP enum: 341 entries; foliage props recognised (9): PROP_JUNGLE3_TREE, PROP_JUNGLE5_TREE, PROP_PALM, PROP_PALMTREE, PROP_PLANT1, PROP_PLANT11, PROP_PLANT2, PROP_PLANT2B, PROP_PLANT3`. The matcher resolves every foliage name in the enum and still reports none placed. The record parser is independently sanity-checked by the distinctive props it *does* find in Surface 1 — motorbike, safe + safe door, the map objective item, the Yale and bolt keys, `PROP_HATCHSEVX`, `PROP_SEVDISH` — all of which really are in that level. + + *Tooling bug caught during this pass, worth recording:* the first version of `load_prop_enum()` located the enum with `l.strip().startswith("typedef enum PROP")`, which matches `typedef enum PROPFLAG` (`bondconstants.h:294`) hundreds of lines earlier. That yields an enum containing **no `PROP_*` members at all**, so every `obj` resolved to `?` and every foliage count was a meaningless 0. The run *looked* exactly like a clean negative result. Fixed to an exact `l.strip() == "typedef enum PROP"` match, and the self-test above exists specifically so this failure mode cannot masquerade as a finding again. This is the D219 probe lesson in a different costume (**a diagnostic must be shown capable of producing the positive before its negative is trusted**). + +3. **What this settles.** Pass 20's alternative (a) is dead: **the visible Surface 1 trees are static background geometry in `bg_sevx_all_p.seg`**, reached through `bgLoadRoomPrimaryGdl` / `bgLoadRoomSecondaryGdl`. The standing D75 blob-conversion suspect (`tools_pc/d43_emit.py` mis-converting a prop mesh or texture) is therefore **not** a D236 fix target and should not be pursued for this bug. This independently corroborates D280's ninth-pass reframing ("the remaining work is CPU-side instantiation/room-streaming, not a port fix") and explains why 19 passes of model-node instrumentation (op-12/op-13, `dogfnegx`/`doshadow`) found nothing tree-shaped: **there was never a tree model to find.** + +4. **Render-mode path re-checked while here (no bug found; recorded so pass 22 does not re-derive it).** The bg-authored foliage/billboard cards carry `G_RM_AA_ZB_XLU_SURF` (`0x…49d8`), and `DL_LUT_BILLBOARD` (`bg.c:2151`, `CCRMLUT_BILLBOARD` = index 2) exists precisely to rewrite `XLU_SURF` → `G_RM_AA_ZB_TEX_EDGE` ("Transparent 1Cycle to BillBoard 1Cycle") — the alpha-cutout mode that would give discrete silhouettes instead of a blended sheet. **But index 2 is never applied**, on N64 or PC: the only call sites are `bg.c:2797/2804/2812/2819` (room load, literals 1/5/6/7 = `PRIMARY_ADDFOG`/`SECONDARY_ADDFOG`/`PRIMARY`/`SECONDARY` by `g_FogSkyIsEnabled`) and `front.c:2177` (`WALLETBOND`). So `BILLBOARD`, `WATER` and `CLOUD` are all dead LUTs in the original game too — this is **not** the PC/N64 divergence, and "apply the BILLBOARD LUT" is not a legitimate fix (it would be a game-logic change, rule 2). + + Also checked and **cleared**: `bgApplyDynamicCCRMLUT`'s scan bound. `Gfx` is 16 bytes on PC (`include/PR/gbi.h`: `Gwords { uintptr_t w0, w1; }`), and `end` is computed as `ptr + usize_primary_DL_binary` in *bytes* — but `usize_*` is assigned from `expanded_size` **after** `bgWidenRoomGdl()` has already rewritten it to the widened PC byte count (`bg.c:2526`/`2559`), so the scan covers the whole room DL, not half of it. No 32→64-bit truncation here. The LUT tables themselves are layout-safe too: their scalar initializers (`DL_LUT_WATER`'s raw `0xB900031D, 0x00552078, …`) brace-elide into `Gfx.words.w0/w1` and zero-extend correctly at either width. + +5. **Next step for pass 22 (bg branch only).** The question is now narrow: *within `bg_sevx_all_p`'s room display lists, which draw class carries the tree cards, and why does it not reach the screen?* D280 established that the `0x0c184b50` class sits in front of the noise quad, has CPU-baked bimodal fog (25/255) and "barely draws at all" under a deterministic `-level_36` boot — but pass 20 proved that word is only ever emitted at runtime by `texSetRenderMode` mode 4 (shadows/muzzle flashes/sparks), so it is **not** the bg tree class either. The remaining candidates are the bg-authored classes D269/D272 catalogued (`oml=0xc4112078` OPA and `0xc41049d8` XLU, both CI8) versus D276's `0xc81049d8` IA8 noise painter. Concretely: dump the Surface 1 room GDLs with the existing `GE_D85DUMP` probe (no new code) and census the distinct `oml`/texture pairs per room, then match against a room the user can stand in front of trees in. That is a static-plus-one-run step, not a new instrumentation build. + +**Tooling added this pass:** `tools_pc/scan_setup_props.py` (read-only, no ROM needed, runs against the repo's own sources). Unlike the `GE_D236*` env probes it costs nothing at runtime and is generally useful — keep it rather than removing it when D236 concludes. + + +**Pass 22 (2026-09-21, static session) — pass-21's owed step done OFFLINE, no run: complete render-mode census of Surface 1's bg room DLs, straight out of the ROM. D276's wall painter `0xc81049d8` is now tied to its authored source (bg *secondary* DL, `G_RM_AA_ZB_XLU_SURF`, 24 of 42 rooms). No bg file in any level authors the TEX_EDGE "billboard cut-out" class, so the tree cards are plain alpha-blended quads and the remaining suspicion falls on their texture's alpha, not on geometry, culling, fog or billboard orientation.** + +Pass 21 proposed dumping the Surface 1 room GDLs with `GE_D85DUMP` and censusing the `oml`/texture classes — a build plus one run. That turned out to be unnecessary too: the bg `.seg` files are stored **uncompressed** in the ROM (they have to be — `obLoadBGFileBytesAtOffset` random-accesses them by byte offset), so the whole structure is readable offline. + +**Two new tools, both read-only, no ROM-dependent build, no C compiler:** + +* `tools_pc/bg_gdl_census.py` — decodes `bg__all_p` out of the ROM, walks every room's primary and secondary GDL, and censuses the distinct render modes per room. It reproduces `bg.c`'s own structure exactly: file word[1] → the 24-byte `bg_room_data` array (`bg.c:865`); a room's compressed blob size = distance to the next non-zero mapping-bin offset with primary and secondary treated as one interleaved stream (`bg.c:944-990` + `getPri/SecMappingBinCount`); each blob raw-deflate behind its own 2-byte header (`bgDecompress` → `decompressdata`, which is just "skip 2 bytes, inflate"). + *Validation:* **every** blob in every level tested inflates cleanly with zero failures. Since the sizes are derived, not stored, a single off-by-one in that derivation would corrupt the stream and fail inflate — so a clean sweep is a real check on the derivation, not a vacuous one. +* `tools_pc/rendermode.py` — evaluates the `G_RM_*`/`RM_*(clk)`/`GBL_c1`/`GBL_c2` macros out of `include/PR/gbi.h` and the `DL_LUT_*` tables out of `src/game/bg.c` in pure Python (93 `G_RM_*` composites resolve). It decodes any `oml` word into flags + blender equation, and produces the (from → to) rewrite maps for the LUTs `bg.c` actually applies. *Cross-check:* it independently reproduces pass 19's `0x0c1849d8 → 0xc81049d8` ADDFOG mapping, which pass 19 derived by hand. + +1. **Surface 1 (`bg_sevx_all_p`, 42 rooms) authors exactly six render-mode classes** — the complete set, with their fog-on LUT images: + + | authored | fog-on (LUT 1 pri / 5 sec) | setups | rooms | mode | + |---|---|---|---|---| + | `0x0c182078` | `0xc8102078` | 39 | 39 | `AA_ZB_OPA_SURF` (primary DLs) | + | `0x0c1849d8` | `0xc81049d8` | 24 | 24 | `AA_ZB_XLU_SURF` (secondary DLs) | + | `0x0c184dd8` | `0xc8104dd8` | 15 | 15 | `AA_ZB_XLU_DECAL` | + | `0x0c192d58` | `0xc8112d58` | 7 | 7 | `AA_ZB_OPA_DECAL` | + | `0x00502078` | (unchanged) | 2 | 2 | OPA_SURF, no-fog blender | + | `0x005049d8` | (unchanged) | 1 | 1 | XLU_SURF, no-fog blender | + +2. **D276's wall painter is now attributed to authored data.** `0xc81049d8` — the class D276 identified as the thing actually painting the visible weave — is **exactly** the LUT image of the bg *secondary* DL class `0x0c1849d8` (`G_RM_AA_ZB_XLU_SURF`), present in 24 of Surface 1's 42 rooms. Nineteen passes have logged that word live; this is the first time it has been traced back to a specific authored source in a specific file. It is ordinary bg transparent-surface geometry, and there is a lot of it. + +3. **`LEVELID_SURFACE` == 36**, so the `-level_36` deterministic boot used by D269/D272/D276/D280 really was Surface 1 (`LEVELID_STATUE = 22` + 14 sequential entries; `bg.c:201` then maps it to `bg/bg_sevx_all_p.seg`). Worth stating explicitly — several passes' conclusions rest on it and none of them checked. + +4. **No bg file in ANY level authors the TEX_EDGE "billboard cut-out" class `0x0c193078`.** Censused `sevx sevb sev dam jun run depo azt` — the union of their authored render modes is `{0c182078, 0c182048, 0c1849d8, 0c184dd8, 0c184e50, 0c192d58, 00502078, 005049d8, 00504dd8, 00552d58}`, and `0x0c193078` appears in none of them, **including Jungle and Depot**. That matters because `DL_LUT_PRIMARY_ADDFOG` carries a dedicated rewrite pair for it with Rare's own comment: *"Billboard Cut-out to FOG Billboard Cut-out - eg, Mario Tree or Depot lamp"*. So the engine's TEX_EDGE billboard path is real but is **never fed from bg geometry** — it belongs to the prop/model path (which is where the jungle foliage props live, per pass 21). Combined with pass 21, Surface 1 has neither tree props nor TEX_EDGE bg cards. + +5. **Consequence — the mechanism is narrowed to texture alpha.** Surface 1's tree cards can only be plain `XLU_SURF` textured quads in the secondary DLs. `XLU_SURF` is `GBL_c2(CLR_IN, A_IN, CLR_MEM, 1MA)` — a straight src-over blend with **no alpha test and no coverage-from-alpha**. Under that mode a card is discrete only if its texture's alpha is transparent outside the silhouette. So "sheet instead of discrete trees" is, mechanically, "the alpha of this class's texture is opaque where it should be transparent" — which is precisely what D276 found and measured (*"fully opaque, alpha nibble mostly 0xf"*, 1023/1024 texels at A=F per D267). **This retires, for this class, the geometry/culling/fog/billboard-orientation branches** that D269→D280 cycled through: none of them can turn a correct-alpha XLU_SURF card into a sheet, and none of them are needed to explain it. + + The open sub-question is no longer *"which draw class"* but *"is this texture's format/alpha being decoded correctly at all"*. D265 tried IA4 `I3:A1`→`I2:A2` (reverted), D267 dumped the IA8 set and dismissed it on a single-instance visual judgement, D273 ruled out the CI palette layout, D276 re-identified the IA8 class but never questioned whether `fmt=3 siz=1` is the *right* interpretation of those bytes. A 32×32 image that decodes as "high-frequency blocky gray static with opaque alpha" is exactly what a tree texture looks like when read at the **wrong format or wrong bit-split** — that specific possibility has never been tested. + +6. **Anomaly to resolve before trusting the older logs.** D269 (M-128) recorded the two live Surface 1 classes as `oml=0xc4112078` (OPA) and `0xc41049d8` (XLU). The LUT images derived here are `0xc8112078` and `0xc81049d8`. The difference is exactly one field — cycle-1's fog alpha source, bits 26-27: `G_BL_A_SHADE` (2, what the LUT writes via `G_RM_FOG_SHADE_A`) vs `G_BL_A_FOG` (1, what D269 logged). D276 and D280 logged the `c8` (A_SHADE) form, matching the LUT. So either something rewrites the fog alpha source after `bgApplyDynamicCCRMLUT`, or D269's words were mis-transcribed and three passes of "two draw classes per room stream" rest on them. Decide this by decoding a live log with `python tools_pc/rendermode.py ` rather than by eye — it is a one-bit-field difference that is very easy to read past, and it changes the fog blend if real. + +7. **Also recorded (fog-off path).** `DL_LUT_PRIMARY` (6) and `DL_LUT_SECONDARY` (7) contain **zero** render-mode rewrite pairs — their bodies are entirely `gsDPSetCombineMode`/`gsDPSetCombineLERP`. So when `g_FogSkyIsEnabled` is false, room render modes are left exactly as authored and only combiners are patched. (This is a correct reading of the tables, not a parser gap: the tool finds 8 pairs in LUT 1 and 3 in LUT 5 from the same code path.) + +**Next step for pass 23 (one run, or possibly none):** re-dump the Surface 1 textures for the `0xc81049d8` class with the existing `GE_TEXDUMP`/`GE_TEXRAW`/`GE_D270KEEPOML=1049d8` probe combination (the current `texdump/` in-tree is from a D75 logo capture, not a Surface run), then decode the dumped bytes offline under *every* plausible format hypothesis — IA8 as currently assumed, IA4 both bit-splits, I8, I4, CI4/CI8 with the room's TLUT, RGBA5551 — and look for the one that yields a tree silhouette with a sensible alpha channel. Cheap, offline, and it directly tests the one mechanism left standing. Resolve item 6's `c4`/`c8` discrepancy from the same run's log. + +**Tooling added this pass (keep — read-only, no runtime cost):** `tools_pc/bg_gdl_census.py`, `tools_pc/rendermode.py`. + + +**Pass 23 (2026-09-21, static session) — THE TREE CARDS ARE IDENTIFIED. Surface 1's `0x0c1849d8`/`0xc81049d8` class binds 13 distinct textures, and four of them — `tex1198`–`tex1201`, `RGBA16` (RGBA5551, ONE bit of alpha), 64×17, `HUFFLOOKUP`, six EXPLICIT LODs — are each bound in 21 of the level's 42 rooms, 3.5× the next most widespread. They are the only explicit-LOD textures in the entire Surface 1 bg. They would log as `fmt=0 siz=2`, which is why D265's IA hunt, D269/D272/D273/D274's CI8 hunt and D276's IA8 hunt all missed them: every probe filter in this investigation has been looking at minority members of the right render-mode class.** + +Pass 22 left the census with `tex=0` — Surface 1's room DLs contain no `G_SETTIMG` at all. That turned out to be the key, not a dead end. + +1. **bg room DLs reference textures by NUMBER, not address.** `texLoadFromGdl` (`tex.c:779`) has `case G_NOOP: texnum = in->words.w1 & 0xfff;` — opcode `0xC0`, 329 occurrences in Surface 1's room DLs — and resolves each into real texture commands at room load. So the texture identity of every bg draw is readable offline, by tracking the active render mode across the command stream and recording the `G_NOOP` texnums bound under it. `bg_gdl_census.py` now does this. + +2. **Reading the texture headers offline.** A texture's ROM blob begins with a control byte (`image.c:2568`): bit 7 `hasExplicitLods`, bit 6 `iszlib`, bits 0-5 `lod` (image count); the bitstream starts at **byte 1**. `iszlib=0` → `texInflateNonZlib`'s 24-bit `ffff wwwwwwww hhhhhhhh cccc` header; `iszlib=1` → `texInflateZlib`'s `format u8, numcolours-1 u8, palette[], w/h per image`. New tool `tools_pc/texheader.py`; offsets/sizes come from the existing `imagelist.u.csv`. + *Sanity check, and a near-miss worth recording:* the first attempt forgot the control byte. The resulting parse **still looked plausible** — it produced four "formats" and confident dimensions — but 1020 of 2699 entries were implausible and the format nibble only ever took the values 0/4/8/12, the tell that the field was reading across a boundary. With the byte-1 offset, **1** of 2699 is implausible and the dimension histogram is dominated by 64×32, 64×64, 32×64, 32×32. The tool prints that count every run so the offset can't silently drift again ([[porting-notes §E positive-control rule]] — same class as pass 21's enum bug). + +3. **The full texture list for the wall class** (`oml=0x0c1849d8` → live `0xc81049d8`), by room coverage: + + | texture | rooms | ROM format | dims | compmethod | explicit LODs | logs live as | + |---|---|---|---|---|---|---| + | `tex1201` | **21** | `RGBA16` | 64×17 | HUFFLOOKUP | **yes (6)** | `fmt=0 siz=2` | + | `tex1200` | **21** | `RGBA16` | 64×17 | HUFFLOOKUP | **yes (6)** | `fmt=0 siz=2` | + | `tex1199` | **21** | `RGBA16` | 64×17 | HUFFLOOKUP | **yes (6)** | `fmt=0 siz=2` | + | `tex1198` | **21** | `RGBA16` | 64×17 | HUFFLOOKUP | **yes (6)** | `fmt=0 siz=2` | + | `tex149` | 6 | `IA8` | 32×64 | RLELOOKUP | no | `fmt=3 siz=1` | + | `tex806` | 5 | `IA8` | 64×32 | HUFFLOOKUP | no | `fmt=3 siz=1` | + | `tex844` | 4 | `RGBA16_CI8` | (zlib, 180 col) | — | no | `fmt=2 siz=1` | + | `tex1373` | 3 | `IA8` | 64×32 | RLE | no | `fmt=3 siz=1` | + | `tex239` | 2 | `IA8` | 64×32 | HUFFBLUR | no | `fmt=3 siz=1` | + | `tex241`/`tex151` | 1 | `RGBA16_CI8` | (zlib) | — | no | `fmt=2 siz=1` | + | `tex619` | 1 | `IA8` | 32×64 | RLELOOKUP | no | `fmt=3 siz=1` | + | `tex238` | 1 | `IA8` | 64×32 | RLE | no | `fmt=3 siz=1` | + +4. **This resolves the D269-vs-D276 contradiction.** Three passes argued about whether "the wall" is CI8 (D269/D272: `fmt=2 siz=1`) or IA8 (D276: `fmt=3 siz=1`). **Both are members of this one render-mode class, and both are minority members.** D269 sampled a CI8 card and generalised; D276 sampled an IA8 card and generalised and called D269's reframing premature. Neither was wrong about what it sampled; both were wrong to treat their sample as *the* class. The dominant members — four textures covering half the level — are `fmt=0 siz=2` **RGBA16**, a format nobody has filtered for or dumped in 22 passes. + +5. **Why these four are the tree cards** (inference, stated as such — see the honesty note below). Four consecutive texture numbers; near-identical compressed sizes (1569/2035/1902/1964 bytes); identical geometry (64×17) and codec; each bound in exactly 21 of 42 rooms — i.e. every outdoor room, four variants apiece; drawn in the *secondary* (transparent) pass. That is precisely the signature of a small set of foliage/treeline billboard variants scattered across an outdoor level. Nothing else in the level looks like this. + +6. **Mechanism hypothesis, now concrete for the first time.** `RGBA16` is RGBA**5551** — exactly **one bit** of alpha. A 64×17 strip is a wide, short card: a treeline band whose alpha bit is 0 for sky above the silhouette and 1 for tree. It is drawn under `G_RM_AA_ZB_XLU_SURF`, which pass 22 established is a straight src-over blend with **no alpha test and no coverage-from-alpha**. So the silhouette exists *only* in that one alpha bit, and the card is minified hard at distance — which is why Rare authored **six explicit LOD images** for it, the only explicit-LOD textures in this entire bg. Two ways that becomes a sheet on PC, both untested: + * **the 1-bit alpha is decoded as always-opaque** → a solid rectangle of tree-coloured texture tiled across the horizon; or + * **the authored LOD chain is dropped and GL generates mips by averaging** → the 0/1 alpha becomes fractional, and with no alpha test a minified treeline smears into a translucent band instead of crisp cutouts. + + The second is the concrete, asset-grounded version of the "TEXTLOD/mip sampling" suspicion D269 raised, D272 reframed to, and D274 parked for lack of a mechanism. It now has one. + +7. **Honesty note — what is NOT established.** The pixels have not been decoded: `HUFFLOOKUP` is one of Rare's huffman/RLE/lookup codecs and none of them are implemented offline, so "these are trees" is a strong inference from format, geometry, count and room coverage, **not** a decode. It should be confirmed before any fix is built on it. Equally, no claim is made here that either mechanism in (6) is the actual bug — both are hypotheses that now have a specific, cheap test. + +**Next step for pass 24 — one run, tightly specified:** boot `-level_36` and dump the textures for this class with the probe filter set to **`fmt=0 siz=2`** (RGBA16) rather than the IA/CI filters every previous pass used — `GE_TEXDUMP=1 GE_TEXRAW=1 GE_D270KEEPOML=1049d8`, then match `GE_TEXI[N] addr=` against the dump by address as D275 did. Three things to read off that one capture: (a) do `tex1198`–`tex1201` arrive as 64×17 RGBA5551, or has the width/height/format been mangled on the way in (D265 already found one transposed-dimensions bug in this area); (b) is the alpha bit non-trivial, or opaque everywhere; (c) do all six explicit LOD images arrive, or only LOD 0. Also resolve pass 22's item 6 (`c4` vs `c8` fog alpha) from the same log using `python tools_pc/rendermode.py `. + +**Tooling added/extended this pass (keep — read-only, no runtime cost):** `tools_pc/texheader.py` (new); `tools_pc/bg_gdl_census.py` now tracks `G_NOOP` texnums and prints the decoded ROM header for every texture each render-mode class binds. + + +**Pass 24 (2026-09-21, static session) — THE TREE TEXTURE IS DECODED. `tex1198` LOD 0 is a 64×17 RGBA5551 treeline strip, 44.2 % opaque, and its alpha channel renders as an unmistakable row of trees. Rare authored a six-level LOD pyramid for it that PRESERVES the silhouette and its alpha coverage all the way down (44.2 % → 43.1 % → 38.8 %). Pass 23's mechanism (b) — authored LOD chain replaced by averaged mips — now has hard ground truth behind it, and the reference image to diff a live capture against exists offline. Still no build, still no run.** + +Pass 23 identified the candidate cards but could not decode them (`HUFFLOOKUP` unimplemented). That is now done: `tools_pc/texdecode.py` ports `texReadBits` (`image_bank.c:99`), `texInflateNonZlib`'s image loop, `texBuildLookup`, `texInflateHuffman`, `texInflateRle` and `texInflateLookupFromBuffer` from `src/game/image.c`, covering the `LOOKUP` / `HUFFLOOKUP` / `RLELOOKUP` compmethods. + +1. **`tex1198` LOD 0 — 64×17 RGBA16 (RGBA5551), HUFFLOOKUP, 481/1088 texels opaque (44.2 %).** Its 1-bit alpha channel: + + ``` + ###....###############...############...########....########.### + ###.....##############...############....#.#####.....######..### + ###.....#############....###########......######.....######...## + ##.....#############.....######.####.......#####....######.#..## + ###.....#############....###########.......###......#######...## + ###.....####.########....###########......######.....######...## + #.......############.....###########.......#####.....#####.....# + ##.......###########.....#######..##.......####......#####.....# + ##......#######.####......#..###..#........####......#####.....# + #.......########.###......#..###..#........####......####......# + ##.......###########........####...........#.#.......#####.....# + ##.......###########.........###...........####......#####.....# + ##.......##.###..###.........###...........####.......####.....# + ##........#.###..##..........###............#.#.......###......# + .#...........#...##..........##.............##........###....... + .................##...........#.............##..........#....... + .............................................#.................. + ``` + + Vertical trunks tapering to points, transparent sky between them, thinning to isolated tips at the bottom edge. **These are the trees.** Pass 23's identification is confirmed by decode, not inference. 522 of the 1088 texels are literally `0x0000` (black, alpha 0). + +2. **The authored LOD pyramid preserves the silhouette** — this is the load-bearing new fact: + + | LOD | dims | compmethod | alpha opaque | + |---|---|---|---| + | 0 | 64×17 | HUFFLOOKUP | 481/1088 = **44.2 %** | + | 1 | 32×9 | RLELOOKUP | 124/288 = **43.1 %** | + | 2 | 16×5 | LOOKUP | 31/80 = **38.8 %** | + | 3 | 8×3 | RLE | (channel codec, not ported) | + + LOD 1 and LOD 2 still read as recognisable tree silhouettes, and the opaque fraction holds near 40 % instead of collapsing. Each level uses a *different* compression method, i.e. these are six separately authored images, not a generated chain. **That is exactly what you do to keep a 1-bit-alpha cutout crisp under minification, and exactly what naive box-filter mip generation destroys** — averaging a 0/1 alpha yields fractional alpha, and under `XLU_SURF` (no alpha test, no coverage-from-alpha — pass 22) fractional alpha does not cut out, it blends. A minified treeline then smears into a translucent band across the horizon: the reported "wall/sheet of tree texture". + +3. **Correction to pass 23.** Pass 23 called `tex1198`–`tex1201` "the tree cards". The decode refines that: only **`tex1198`** is unambiguously a treeline strip (44.2 % alpha). **`tex1199`** is the same geometry and codec but 89.4 % opaque with thin vertical gaps — a denser foliage/branch mass or a treed cliff face. **`tex1200` and `tex1201` are 100 % opaque** dense detail textures (138–162 distinct colours, no alpha structure at all) that merely share the render-mode class. The "four textures in 21 of 42 rooms each" observation stands; "all four are trees" does not. + +4. **Two decoder bugs found and fixed while porting — both would have produced confident, wrong pictures.** + * **Per-image byte alignment.** `image.c:1096` ends each LOD image with `if (img_bitcount == 0) img_curpos++; else img_bitcount = 0;` — discard the partial byte, and skip a whole byte when already aligned. Omitting it desynchronised every LOD after the first. It surfaced as LOD 1 parsing as `fmt=0 1x32 cm=0`; a scan of neighbouring bit offsets showed a constant **+8-bit** drift, which pointed straight at the align. + * **`texInflateRle`'s fudge was guessed, not ported.** The first draft invented `fudge = ((1< 0) { cost -= blocksize + 1; fudge++; }`. The guess still decoded *something* — it gave `tex1198` LOD 1 as 66.7 % opaque and `tex149` as 48.3 % — and only the corrected port gives the self-consistent 43.1 % and 34.4 %. **Every RLELOOKUP figure in this entry is from the corrected port; any earlier session notes quoting 48.3 % for `tex149` are wrong.** (Same lesson as passes 21 and 23, third time in four passes: in this codebase a half-right bitstream parse produces plausible output, never an obvious failure. Port the derivation, don't reconstruct it.) + +5. **What this gives pass 25.** There is now an offline, N64-correct reference for the exact texture at the centre of D236: format, dimensions, per-LOD alpha coverage, and the silhouette itself, reproducible with `python tools_pc/texdecode.py 1198`. A live capture can be diffed against it field by field instead of being interpreted on its own. + +**Next step (pass 25) — one run, three questions, all now answerable by direct comparison:** + 1. Does `tex1198` arrive as **64×17 RGBA5551**, or are the dimensions/format mangled? (D265 already found one transposed-dimensions bug in this area.) + 2. Is the imported alpha still **~44 % transparent**, or opaque everywhere? If opaque, the bug is in the 5551 alpha-bit decode and mechanism (a) is confirmed. + 3. Do **all six authored LOD images** reach the GL texture, or only LOD 0 with driver-generated mips? If the latter, mechanism (b) is confirmed and the fix is to upload the authored chain (`glTexImage2D` per level with `GL_TEXTURE_MAX_LEVEL`) rather than letting the driver filter a 1-bit alpha. + Capture with `GE_TEXDUMP=1 GE_TEXRAW=1 GE_D270KEEPOML=1049d8` on `-level_36`, filtering **`fmt=0 siz=2`**. Note `texSwapAltRowBytes` (`image.c:1088`) runs on explicit-LOD textures and swaps bytes on alternate rows — expect that in the raw dump; `texdecode.py` decodes logically and does not apply it. + +**Tooling added this pass (keep — read-only, no runtime cost):** `tools_pc/texdecode.py` (``, `--lod N`, `--ppm out.ppm`; PPM renders transparent texels magenta). + +**Pass 25 (2026-09-21, live session) — all three pass-24 questions answered by direct comparison; the mip mechanism is now located to a line. Q1 YES, Q2 YES, Q3 NO — and the "wall" turns out not to be painted by `tex1198` at all from an idle spawn.** + +**Setup:** live runs of `-level_36` (idle boot, 4800 frames) with `GE_TEXDUMP=1 GE_TEXRAW=1 GE_D219RAW=1`, plus one A/B run flipping the in-tree config key `Video.FixMipTextures` (default 1 → 0) to observe unclipped LOADBLOCK sizes. `texdecode.py` was first verified function-by-function against `image.c`/`image_bank.c` (all pass-24 numbers reproduced: 44.2/43.1/38.8 %, 89.4 %, 100 %, tex149 34.375 %). One latent decoder bug fixed on the way: a `max(1, …)` clamp on `(ncol-1).bit_length()` in the plain-LOOKUP path that C's `texGetBitSize(1)=0` does not have (unreachable for GE assets; no output change). + +**Q1 — YES, dimensions/format intact.** D219RAW shows all six `tex1198` LODs pool-decoded at level load with the exact offline dims/compmethods (64×17 HUFFLOOKUP, 32×9 RLELOOKUP, 16×5 LOOKUP, 8×3/4×2/2×2 RLE), format=1 (RGBA16) throughout. The live C decoder's first row of LOD 0 is **pixel-identical** to the offline decode after the documented `texSwapAltRowBytes` byte-swap — and that swap is a no-op on PORT (D159 early-return), so raw dumps are plain linear pixels. + +**Q2 — YES, alpha survives intact.** Pool bytes ≡ ROM-decoded bytes ⇒ 44.2 % opaque at the pool stage. `import_texture_rgba16` (`gfx_pc.cpp:791`) maps bit0→alpha 255/0; `import_texture_ia8` maps upper nibble=intensity, lower=alpha — both preserve alpha by construction. Data check on the one drawn member of the class: `GE_TEXI[101]` (IA8 32×64, `tex149`) is **byte-for-byte identical** to the offline ROM decode (2048/2048). + +**Q3 — NO, structurally.** `gfx_opengl_upload_texture` (`gfx_opengl.cpp:713`) does `glTexImage2D(level 0)` + optional `glGenerateMipmap`; there is no code path that uploads an authored chain as GL mip levels. Each LOADBLOCK becomes one GL texture; the driver box-filters level 0. + +**New finding N1 — `tex1198`–`tex1201` are never drawn from an idle spawn.** Across all 4800 frames there is not a single `fmt=0 siz=2` (RGBA16) import while CI8/IA8 background draws continue every frame. The tree-like cover visible at Bond's spawn point is therefore painted by the *other* classes — consistent with D276/D280 (the IA8 noise class paints the weave). Pass 24's "leading mechanism" applies to `tex1198`'s cards, which only draw when the player reaches their rooms; the spawn-visible wall is a different painter. + +**New finding N2 — the drawn tree-class texture is `tex149`: IA8 32×64 hard cutout, 34.4 % opaque** (alpha nibble F for opaque pixels, 0 for holes — no ramp). Its ROM blob is **329 bytes = exactly one image**; there is no authored mip chain. + +**New finding N3 — the mip mechanism, located precisely.** `texGetDepthAndSize` (`tex.c:330`) sizes the LOADBLOCK from `maxlod`, and `texInflateNonZlib` sets `maxlod = forcenumimages` = the header's `lods` field (**7** for `tex149`) **even when `explicit_lods=0` and only one image was decoded**. So GE's LOADBLOCK is deliberately larger than the real pool data: live A/B with `Video.FixMipTextures=0` shows the same 32×64 cards at **size=2808** (base 2048 + 760 B "mip region") vs 2048 clipped. On N64 those extra bytes are whatever adjacent stage-pool memory follows the allocation — deterministic, likely neighbouring level textures. The port's RC2 clip (`g_fix_mip_textures`, `gfx_pc.cpp:1089/1142`, default ON) truncates to base height and `glGenerateMipmap` + trilinear minification replaces that region with driver box-filtered mips — **observed live on the 32×64 upload: `gen_mip=1 filter=LINEAR mipmap_filter_mode=MIPMAP_LINEAR`; 380/400 sampled uploads generate mips.** For a hard 0/F alpha cutout, box filtering turns every mip level into fractional alpha, and the XLU blend then smears minified silhouettes — the reported wall/sheet. (For `tex1198`-class cards with real six-level chains, the same clip+regenerate path replaces the authored pyramid — pass 24's mechanism (b), now confirmed at the line level.) + +**Honesty note.** The N64 side of N3 is established for the *load* (block size, pool contents) but not for the *sample*: what the RDP actually read out of that 760 B mip region for minified cards (adjacent-pool bytes vs effectively level-0) cannot be resolved without hardware or an RDP sampler model. Both candidate port fixes below are therefore approximations to be judged by playtest A/B, not by derivation. + +**Fix directions for pass 26 (port-only; no game logic):** +1. **No-chain textures (`explicit_lods=0`, single-image blob — `tex149` class):** do not `glGenerateMipmap`; sample level 0 only (`GL_LINEAR`, non-mip min filter). Matches "RDP with no real mips" and kills the fractional-alpha smear at zero cost. This is the cheapest test of the whole hypothesis: if the spawn wall sharpens into discrete trees, mechanism confirmed end-to-end. +2. **Chained textures (`tex1198` class):** upload the authored levels as real GL mip levels (needs per-LOD dims from the pool header; GE's authored dims are *not* exact halvings — 17→9→5→3→2 vs GL's required 17→8→4→2 — so levels must be padded/cropped to legal sizes, or uploaded into a custom layout). Bigger job; only after (1) is judged. + +**Probe added this pass (TEMPORARY — remove once the mip question is closed):** `GE_D236P25` env-gated block in `gfx_opengl_upload_texture` logging width/height/gen_mipmaps/filter per upload (capped 400 lines). **Process note:** Git-Bash `kill` does not terminate the game here — use `taskkill //F //IM ge007.x86_64.exe` (two idle runs were left alive this session). + +**Pass 26 (2026-09-21, live session) — D236 ROOT-CAUSED AND FIXED. The tree cards were never a decode, alpha or mip problem: the port was binding the wrong texture. Surface 1 now renders discrete snow-covered pines; verified live.** + +**The pass-25 fix direction was tested first and REFUTED.** Implemented cut-out +mip suppression (skip `glGenerateMipmap` + `GL_TEXTURE_MAX_LEVEL 0` for +textures whose alpha is hard 0/255 with real holes), config-gated, and A/B'd it +live on `-level_36` at the idle spawn. **The wall did not change.** Two things +came out of that run and both mattered: + +- The per-upload alpha census showed the spawn view is dominated by **248 + uploads of a fully-opaque 32×32 texture** (`clear=0 solid=1024 mid=0`) — the + "wall" is opaque, so no alpha/blend mechanism could have produced it. That + retires the whole mip / 1-bit-alpha / XLU-smear branch (passes 22–25). +- Correction to pass 25's **N3**: the 760 B "mip region" beyond a no-chain + texture's base image is **not adjacent pool garbage**. `texInflateNonZlib` + (`image.c:1123-1153`) and `texInflateZlib` (`image.c:324-360`) *generate* the + chain themselves via `texShrinkNonPaletted`/`texShrinkPaletted` when + `explicit_lods == 0 && forcenumimages >= 2`. Those shrinkers also re-quantise + alpha to the source's bit depth — for RGBA5551, `a = (a0+a1+a2+a3 + 2) >> 2` + keeps every level a hard 0/1 cut-out, unlike a GL box filter. Worth knowing + for any future mip work; irrelevant to this bug. + +**Root cause (`gfx_lod_tile_offset`, `port/fast3d/gfx_pc.cpp`).** GE's +`texLoadFromGdl` picks a handler per binding from `in->words.w0 & 7`: + +- `TEXTURETYPE_LOD`(0) / `TEXTURETYPE_MIPMAP`(2) — `texHandleType0`/`Type2`: + ONE load block at TMEM 0, tiles 0..n are LOD levels of that one image. +- `TEXTURETYPE_DETAIL`(1) — `texHandleType1` (`tex.c`): **TWO** load blocks — + the *detail* texture at TMEM 0 (`texWriteLoadToTmemZero(tex2)`) and the + **base image** at TMEM `texGetSizeInBytes(tex2,0)` on tiles 1+ + (`texWriteLoadToTmemAddr(tex1,size)` + `texWriteTileLods(tex1,…,1,size)`). + +D107 folded every `G_TL_LOD` draw to tile 0 on the premise "GE loads the whole +mip chain at TMEM 0". True for types 0/2, **false for type 1** — there tile 0 is +the detail texture. So the port drew the detail texture instead of the base +image, and never imported the base image at all. + +**Confirmed against the data, offline and live:** + +1. `GE_D85TEX` on `-level_36`: `tex1198`–`tex1201` each resolve 28× with a + non-NULL pool pointer and **`type=1`** — they were being bound all along. + Pass 25's N1 ("zero `fmt=0 siz=2` imports in 4800 frames") was the symptom, + not evidence the cards weren't referenced. +2. Offline scan of every `G_NOOP` in `bg_sevx_all_p`: the only `type=1` + bindings in Surface 1 are exactly the four tree cards, **21 rooms each, all + sharing `texnum2 = 2465`**. Everything else in the level is `type=2` with + `detail=0`. +3. `tex2465` = **IA8 32×32 HUFFLOOKUP, `lods=0`, fully opaque** — precisely the + 248-upload opaque 32×32 class above, and the same texture D267/D276 measured + at "1023/1024 texels at A=F" and called the noise painter. D276's painter was + never a mystery texture; it is the tree cards' detail tile. + +**Fix (port-layer only, `gfx_lod_tile_offset`).** When `rdp.tex_lod` is set and +tile `fi+1` sits at a different TMEM address *and* declares a different +`fmt`/`siz` than tile `fi`, return tile offset 1 (the base image) instead of 0. +Deliberately asymmetric: 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 +would simply keep today's behaviour. Config `Video.DetailBaseTile`, default on. + +**Two weaker discriminators were tried live and both failed — recorded so they +are not retried:** (a) "tile fi+1 is at a different TMEM address" alone — mip +levels are at different TMEM offsets too, so every ordinary surface started +sampling its first mip (trees correct, all ground/snow textures wrecked; the +user caught this in the same build); (b) additionally requiring +`rdp.loaded_texture[tmem].addr` to be set — same failure, because +`rdp.loaded_texture[]` is persistent state never cleared between binds, so a +stale entry from an earlier type-1 draw satisfies it. + +**Verification.** +- Live `-level_36` idle spawn, frame 560: opaque tiled noise wall → + **discrete snow-covered pines lining the valley**, ground/snow unchanged + against the pre-fix capture. Surface 2 (`-level_43`) likewise shows a discrete + night treeline; Depot (`-level_30`, the only other level with `type=1` + bindings) renders clean. +- `bunker1` golden framediff is **bit-identical with the fix on and off** + (`worst_cell=189.741875` both ways — that delta is pre-existing, from the + other uncommitted work in the tree, not from this change). +- Offline blast-radius scan of every level's bg: `type=1` bindings exist **only** + in `sevx` (84), `depo` (39), `sevb` (4), `sev` (4). Every other level has zero, + so the change is provably inert on them. + +**Generalised to `docs/porting-notes.md` §D12.** + +**Housekeeping:** with D236 closed, every D236 diagnostic is out of the tree +-- the pass-25 `GE_D236P25` probe and the pass-26 cut-out experiment, plus +`GE_D236RM`/`ORDER`/`ALPHA`/`ZFIX` (`gfx_pc.cpp` `gfx_sp_tri1`), `GE_D236RAW` +(`bg.c`), `GE_D236BT` (`chr.c` + `model.c`), the `port_d236bt_dump()` host +backtrace dumper and the `windows.h` include it needed, and the +`g_d236_last_vtx_seg`/`_raw` G_VTX trackers. Their rows are gone from +`GE-ENV-PROBES.md`; `gen_env_probes.py --check` is in sync. Build clean and +the Surface 1 spawn capture is unchanged across the strip. + ## D237 — QoL ask (M-106, user QA): F10 options overlay — make it scrollable with the mouse wheel, expose more settings into it (e.g. screen-flash / no-hit-flash… **QoL ask (M-106, user QA): F10 options overlay — make it scrollable with the mouse wheel, expose more settings into it (e.g. screen-flash / no-hit-flash `Game.NoHitFlash`, and other easily-exposed config keys), and organise rows by category (mouse settings together, video together, etc.).** The overlay (`port/src/optionsoverlay.c`) already renders over the registered `config.c` option list; what's missing is wheel-scroll nav, a broader row set, and grouping headers. Cross-ref `OPTIONS-MENU-PLAN.md` §3 (v1 minimum set) + QOL-INVENTORY "PD has, GE lacks". **Status (from §F table):** OPEN — not designed; port-only +**IMPLEMENTED (2026-09-23, v0.4.0; port-only, `port/src/optionsoverlay.c`).** Wheel scroll already existed (D304 era). Added: (1) **category headers**, a new `ROW_HEADER` kind (not config-backed, never selectable; up/down/wheel step over headers via `overlayStepSel`, clicks on headers ignored, `rowAdjust` no-ops) drawn in the title accent: DISPLAY / VIEW / MOUSE / AIM / CONTROLLER / GAME; (2) **more settings**: `Game.SkipIntro`, `Video.DisplayFPS`, `Input.PadLookInvertY`, `Input.PadDeadzone` (slider, step 500), `Input.PadTriggerPct`. `Game.NoHitFlash` was already exposed. Verified: overlay capture shows the headers and the selection starting on the first real row; no unregistered-key warnings; Windows + Linux builds clean. Owed: a hands-on pass (wheel/arrows skip headers, the new sliders feel right). + ## D238 — QoL ask (M-106, user QA): mouse sensitivity rework toward normal PC-shooter feel. **QoL ask (M-106, user QA): mouse sensitivity rework toward normal PC-shooter feel.** Three parts: (1) a *separate* RMB aim-mode sensitivity in addition to hipfire turn; (2) ideally ONE single master mouse-sensitivity setting driving both, **linked by default**, with an option to set them independently later (today `Input.MouseAimSpeed` and `Input.MouseTurnSpeed` are two independent keys, defaults 16/100 — see `input.c:188-190`); (3) up/down should feel mostly the same as left/right (`Input.MouseYScale` exists but the axis asymmetry in the gain constants `AIM_GAIN`/`MOUSE_TURN_GAIN` + smoothing path is the likely culprit). User's reference for "feels better": GEPD Mouse Injector. Lump with the overall mouse/input fixes (D194/D165/D166 lineage; SOLITAIRE natural-pitch scheme switch may subsume part of this — see D166 M-87 note). @@ -10999,13 +11439,32 @@ User played live with `GE_D236ORDER=1` set, walked toward Surface1's tree line, **Player gunshot SFX timing/cadence reportedly does not match the N64 original's rate (M-106, user QA report).** Static trace only (no ROM/build/display in this worktree). Weapon fire re-triggers its SFX through `bondwalkItemGetSoundTriggerRate()`/`bondwalkItemGetSound()`, gated on `g_GlobalTimer` (`src/game/gunfire.c:3246-3286`: `handptr->field_A4C = bondwalkItemGetSoundTriggerRate(var_s1) + g_GlobalTimer`), alternating between two per-hand `ALSoundState` slots (`audioHandle`/`field_A48`, same block) so a fast-firing weapon's report can overlap itself once before being retriggered. `g_GlobalTimer` advances by `g_ClockTimer = speedgraphframes` every tick (`src/game/lv.c:1023,1037`), and `speedgraphframes` is set from `deltaFrames` in `updateFrameCounters()` (`src/game/frametiming.c:64-76`) — the exact tick-delta plumbing already investigated (and partly fixed) under **D155** (catch-up-spiral clamp, same file), **D186** (`Video.FpsCap` sim-coupling clamp), **D193/D209** (AI-locomotion timing keystone — D209 the actual root cause, a 64-bit union-aliasing bug, now fixed and merged), and **D204** (audio pipeline running ~2% below real time forever, fixed by padding silence at the DAC). Since D209 and D204 both merged before this report, the SFX-cadence complaint is either (a) a genuinely distinct, audio-specific residual downstream of those fixes — e.g. a mismatch between the sim-tick-gated *trigger* (above) and the software mixer's own playback clock (`port/src/audio.c:93,155,221-222` — the synth produces samples at a measured ~21600/s against the 22050 Hz output device, the same gap D204 had to compensate for elsewhere) that specifically desyncs how *often* a shot's sample starts rather than the absolute wall-clock rate D204 measured, or (b) a stale pre-D209/pre-D204 build report. Per-weapon rate constants (`AutomaticFiringRate`/`SingleFiringRate`/`SoundTriggerRate`, `src/game/gun.h:60-82`, values in `assets/obseg/gun/*/gunWeaponStat.inc.c`) are plain compiled C literals, not ROM-serialized data, so no pointer-width/ABI reconciliation issue applies here. -**Status (from §F table):** **OPEN — not root-caused; static call-site tracing only, no runtime verification possible in this environment.** Confidence LOW-MEDIUM that this is a *new* bug distinct from the D155/D186/D193/D204/D209 timing-keystone family (all of which gate the identical `deltaFrames→speedgraphframes→g_ClockTimer→g_GlobalTimer` chain this SFX trigger consumes). Raise confidence by: (1) confirming the user's build/commit postdates the D209 merge; (2) a `GE_AUDIOTRACE`-style capture logging the tick (`g_GlobalTimer`) and/or wall-clock timestamp of consecutive same-weapon fire-SFX requests during sustained auto-fire (AK47/Klobb, `AutomaticFiringRate=0x03`), diffed against the weapon's rate constant and against N64 reference footage's frame-count between shots. +**Status (from §F table):** **CLOSED (v0.4.0 campaign sign-off 2026-09-28, user: cadence drift not heard over the full campaign -- fixed; never root-caused -- if re-reported, the timing-keystone family above is the prime suspect).** Superseded: OPEN — not root-caused; static call-site tracing only, no runtime verification possible in this environment. Confidence LOW-MEDIUM that this is a *new* bug distinct from the D155/D186/D193/D204/D209 timing-keystone family (all of which gate the identical `deltaFrames→speedgraphframes→g_ClockTimer→g_GlobalTimer` chain this SFX trigger consumes). Raise confidence by: (1) confirming the user's build/commit postdates the D209 merge; (2) a `GE_AUDIOTRACE`-style capture logging the tick (`g_GlobalTimer`) and/or wall-clock timestamp of consecutive same-weapon fire-SFX requests during sustained auto-fire (AK47/Klobb, `AutomaticFiringRate=0x03`), diffed against the weapon's rate constant and against N64 reference footage's frame-count between shots. ## D241 — Player gunshot SFX goes silent under some conditions — reported on Bunker with the alarm sounding, "or some other factor" — but only for PP7 and AK47, Klobb… **Player gunshot SFX goes silent under some conditions — reported on Bunker with the alarm sounding, "or some other factor" — but only for PP7 and AK47, Klobb unaffected (M-106, user QA report, concrete repro).** This is very likely **D207 finally reproduced.** D207 (M-77) root-caused that GE's SFX player has a hard **8-voice cap** (`MUSIC_SFX_SEQ_MAYBE_MAX_SOUNDS`, `src/music.c:76`) and that D206's fix made the alarm klaxon (`ALARM3_SFX`=163, triggered from `handle_alarm_gas_timer_calldamage()`, `src/game/propobj.c:14457`) a genuine **infinite ADPCM loop** that now legitimately occupies one of those 8 voices for the alarm's whole duration; the manual preemption scan (`snd.c:~347-392`) refuses to steal any *owned* LOOPED voice (the M-65 carve-out only exempts *ownerless* loops), so once 8/8 voices are held, a new one-shot request is silently dropped via `sndDisposeSound` (`snd.c:391`). D207's own M-77b session built a `GE_FORCEALARM=1` BUNKER1 harness and could **not** reproduce audible drops (`[D207-DROP]=0`), and explicitly left as "owed": *"needs a real repro from the user: which level, which weapon/situation."* This report supplies exactly that (Bunker, alarm). **Investigated why PP7/AK47 and not Klobb, inconclusive at the code level:** Klobb (`ITEM_SKORPION` — confirmed GE's internal name for the Klobb via `src/game/player.c:576`, `skorpion_stats` in `assets/obseg/gun/skorpion/gunWeaponStat.inc.c`) and AK47 (`ak47_stats`, `assets/obseg/gun/ak47/gunWeaponStat.inc.c`) share the **same** `AutomaticFiringRate=0x03` (fast auto), while PP7 (`wppk_stats`, `assets/obseg/gun/wppk/gunWeaponStat.inc.c`) is semi-auto only (`AutomaticFiringRate=0xFF`) — so a naive "fires faster ⇒ more self-inflicted voice contention" theory does not cleanly explain grouping PP7 with AK47 against Klobb. All three weapons' fire SFX (`GUN_B1_MGUN3_3_SFX`=Klobb, `GUN_B2_HEAVY_SFX`=PP7, `GUN_B4_BOLTACTION_SFX`=AK47 — `src/bondconstants.h:2277-2280`) go through the identical one-shot, priority-0x40 `sndPlaySfx` path (`gunfire.c:3246-3286`); no per-weapon distinction exists in the trigger or priority *code*. A real distinction, if one exists, most likely lives in the **sample bank data itself** (per-sample envelope/decay length — a longer-tailed sample stays `PLAYING` and stealable/held longer, changing its odds in the preemption scan) — that data is ROM-derived binary content, not inspectable without the extracted ROM (`./data/`, absent in this worktree). Confirmed the voice-allocation/preemption code is unmodified `src/snd.c` game logic, not a `port/` mixer bug: `port/src/mixer.c`/`port/src/audio.c` implement DSP synthesis and device output only (per D199), with no SDL `Mix_*`-style channel-priority layer in the port to have introduced a new bug. Per AGENTS.md non-negotiable #2, any real fix belongs in D207's already-designed (reverted, M-77b) last-resort LOOPED-preemption pass in `snd.c`, not in `port/`. -**Status (from §F table):** **OPEN — not newly root-caused; high confidence this is D207 reproduced, not an independent bug.** Recommend folding this repro into D207 (or closing D241 as a duplicate once D207 is reopened) rather than tracking separately. Next: re-run M-77's `GE_FORCEALARM=1` harness on `-level_09` with scripted PP7 and AK47 fire (not just generic fire) against Klobb as a control, watching `g_sndAllocatedVoicesCount` / the (currently reverted) `[D207-DROP]` counters to see whether the 8-cap is actually hit for two of the three weapons and not the third — or whether it is really the sample-envelope-length distinction above. No runtime verification possible in this environment (no ROM/display). +**Status (from §F table):** **CLOSED as the D207 family (v0.4.0 campaign sign-off 2026-09-28, user: no silence under heavy fire + alarms -- fixed).** Superseded: OPEN — not newly root-caused; high confidence this is D207 reproduced, not an independent bug. Recommend folding this repro into D207 (or closing D241 as a duplicate once D207 is reopened) rather than tracking separately. Next: re-run M-77's `GE_FORCEALARM=1` harness on `-level_09` with scripted PP7 and AK47 fire (not just generic fire) against Klobb as a control, watching `g_sndAllocatedVoicesCount` / the (currently reverted) `[D207-DROP]` counters to see whether the 8-cap is actually hit for two of the three weapons and not the third — or whether it is really the sample-envelope-length distinction above. No runtime verification possible in this environment (no ROM/display). + +**2026-09-26 (M-201, v0.4.0): user still hears gunshot drop-outs "with rapid fire guns while a lot of other sound effects occur" (D322 garbling itself is gone). Headless measurements, no drop reproduced.** + +- **Static:** player shots (`gunfire.c:3296-3340`) and guard shots (`chraction.c:5630`) both stop the previous shot sound and start a new one, using at most 2 handles per hand, rate-limited by `SoundTriggerRate` against `g_GlobalTimer`. At 1 tick/frame a shot sound can start every `rate+1` ticks; at the N64's 2–3 ticks/frame the check rounds up to frame boundaries (rate 3: every 6 ticks at 3 ticks/frame vs 4 on PC), so the port plays up to ~1.5x more shot sounds (a D13-class divergence) but does not hold more simultaneous voices. `sndDeactivate` only posts `AL_SNDP_DEACTIVATE_EVT`; the slot frees in `sndDisposeSound`. +- **Runtime (Dam, invincibility + all guns, `GE_FORCEALARM=1`, PP7 held in bursts via new `GE_INPUTSCRIPT` `ZHOLD`/`ZREL`, ~100 s each):** `GE_AUDIOTRACE` `[D207-DROP]` = 0 at both FpsCap 60 and 30 (peak SFX slots 8/8 and 7/8); `GE_D322` at 60 fps: worst sample `sfx` 7/8, `pv free` 9/24, `evtq` 9/64, audio `q` 720/2880, `drop=0`. Both drop paths (`preempt-scan` = 8-cap, `no-limit` = physical pool) are covered by `[D207-DROP]`; failing sounds first retry every 33 ms (audible as late, not missing). +- **Harness limits:** `gunRequestHandWeaponChange` / the level-start equip call did not switch weapons mid-level from the harness, so the automatic-weapon case (AK47/Klobb) is untested headless. +- **Next:** capture the user's real scenario with `GE_AUDIOTRACE=1 GE_D322=1 GE_D204=1` (`scratch/playtest_audio.bat`), noting level, weapon and time of each drop-out; any `[D207-DROP]` line names the layer. If none appear, the "drop-out" is the faithful stop-then-restart of the per-hand shot sound, not a lost voice. + +**2026-09-26 (M-201, cont.): live captures. Not reproducible with lightweight logging; the one captured drop-out ran with the heavy GE_AUDIOTRACE probe.** + +- **Run 1 (user, Bunker, AK47, alarm active, `GE_AUDIOTRACE`+D322+D204):** drop-out heard. `[D207-DROP]`=0, pools healthy (sfx <= 6/8, pv free >= 11/24). The per-sound `sndPlaySfx` trace is capped (~630 events) and stopped before the AK47 section, so it could not show the moment. `GE_AUDIOTRACE` does an uncached `getenv` plus a file write from the audio path on nearly every sound event (D250/D302 class), so this drop-out may be probe-induced. +- **New TEMP `GE_D207S`** (`src/game/gunfire.c`, `#ifdef PORT`, read-only): logs every player shot-sound decision (`H1`/`H2`/`SKIP`, where SKIP = both per-hand handles still held because the stop is async) with `g_GlobalTimer`. Uncapped, cheap. +- **Run 2 (FpsCap 30):** 315 decisions, 0 SKIP, no drop-outs heard. AK47 gaps 5–7 ticks. +- **Run 3 (FpsCap 60):** 408 decisions, 0 SKIP, no drop-outs heard. AK47 exactly every 5 ticks (`SoundTriggerRate` 4 in the BUGFIX_R0 build). +- **Run 4 (FpsCap 60, stress test, 7 weapons incl. AK47/ZMG/D5K/RC-P90/Klobb):** 2073 decisions, **0 SKIP**; SFX cap frequently 8/8 but `drop=0`; evtq max 21/64; D204 `rt` 0.999–1.000 throughout, audio queue never empty. User: "seemed okay". +- **Refuted:** voice-cap drops, physical-pool exhaustion, per-hand handle starvation (the 60 fps shot-rate divergence is real, 5 vs 6 ticks for the AK47, but never starves the handles), output underrun during these runs. +- **Open:** the drop-outs the user heard in the untraced 21:9 session remain unexplained and intermittent. Standing capture: `scratch/playtest_audio.bat` (`GE_D207S` + D322 + D204, no audio-thread file I/O); press F12 on a drop-out. + +**Status: OPEN (not reproducible) — drop paths and handle starvation refuted by measurement; awaiting a natural occurrence under lightweight logging.** ## D242 — Player-death sequence UX regression vs N64 original: PC requires an LMB click to proceed to the mission-failed/file-select screen; user recalls N64 auto-plays… @@ -11013,6 +11472,8 @@ User played live with `GE_D236ORDER=1` set, walked toward Surface1's tree line, **Status (from §F table):** **NOT A PORT-LOGIC BUG as far as static analysis can tell.** Confidence HIGH that the LMB-click skip path is faithful original behavior (Z_TRIG-mapped, exists unmodified in `bondview2.c`), not a defect to remove. Confidence LOW (untested) on whether the no-click automatic path is actually healthy on PC. Needs a human playtest: die, then **deliberately touch no input** during the death sequence and time whether/how long until the mission-failed screen appears on its own. If it never arrives without a click, the real bug is a `g_GlobalTimerDelta`/`camera_transition_timer` stall in the D240 clock family — cross-reference D240 — not a wrongly-added input gate, so there is still no `#ifdef PORT` skip-gate to remove per AGENTS.md #2. No runtime verification possible in this environment (no ROM/display). +**M-201 (2026-09-26): headless check, no input after death.** Dam, `GE_D320R="1000:explode=13,0"` (grenade at Bond's feet, no invincibility), `GE_INPUTSCRIPT` with no events: the death camera auto-played its angles (frames ~1400–1900) and the stage exited on its own ~15 s later (stage 33 → 90). **The "PC requires an LMB click" report does not hold on the current build.** A direct `-level_XX` launch has no front-end session, so it then fell into the title/attract loop (stage 90 → 25 → 90) — not comparable with the N64 reference (user on 1964: returns to mission select and shows "Killed in Action"). Owed: user check from a normal menu launch that death lands on mission select + KIA. + ## D243 — Dam end-of-level cutscene (the abseil/bungee sequence, D148/D160's subject): the camera violently shakes/"spazzes" during each of the scripted camera-angle… **Dam end-of-level cutscene (the abseil/bungee sequence, D148/D160's subject): the camera violently shakes/"spazzes" during each of the scripted camera-angle cuts (M-107, live `GE_D160=1` trace, two interactive `-level_33` runs + one fresh-save normal-progression run).** **First, corrects D160's own premise: the cutscene is NOT skipped on the current build.** Both debug-loaded runs show exactly the AI-script-scripted sequence firing: `bondviewSetCameraMode(7)` (`CAMERAMODE_POSEND`, the cutscene camera-lock state) called 2-3 times, matching `ai_17`'s three `camera_switch` commands in `UsetupdamZ.c`'s abseil ailist (`assets/obseg/setup/UsetupdamZ.c:2656`+), followed by `bossReturnTitleStage`. So D160's original (b)/(c) hypotheses (AI trigger never fires / puppet renders invisibly) are **refuted on this build** — the mechanism runs; what's broken is what it renders. **Root cause candidate, directly observed:** right in the camera-cut window, fast3d's D146 catch-all (`port/fast3d/gfx_pc.cpp:3037`, "unknown GBI opcode … ending DL") fired a tight burst — same 4 memory addresses repeating every frame for ~10 frames, then clean — with payload bytes `0xfafbfbfcfcfbfdfc`/`0xfcfafbfbfafbfafb`: a **freed/uninitialized-memory fill pattern**, not real GBI commands. This reads as a stale-pointer display-list read, not a decode bug — some segment/scene buffer backing the outgoing camera shot's geometry got freed or recycled while a still-in-flight display list (queued ahead in the port's double-buffered render pipeline — see D134's "pendingGfx" note, `port/src/libultra.c:733-746`, on how many frames of latency the sched round-trip can carry) still referenced it. **Independent evidence of a race, not a fixed-order bug:** the two `-level_33` debug-load runs show the camera_switch(7) calls landing in a *different order relative to* `bossReturnTitleStage` between runs (run 1: three switches, then the stage-return call; run 2: two switches, the stage-return call, *then* a third switch arriving after) — and the D146 corruption burst appeared in run 1 but not run 2 at all, non-deterministically. That pattern (symptom present/absent, call order shuffling) is the signature of a genuine timing race between the AI-list's cutscene commands and the level-exit/report-transition code, not a deterministic logic bug. **User-confirmed on real (non-debug-boot) play, not just the debug harness:** a fresh-save normal playthrough into Dam still hit the camera glitch on this same cutscene — ruling out "artifact of the `-level_33` debug boot" for this specific symptom (unlike the secondary "no report screen, straight to file-select" symptom seen only on the debug-loaded runs, which did NOT reproduce on the fresh-save run and is therefore most likely a `-level_N` debug-boot artifact per the D190 precedent, not filed as its own bug here). @@ -11229,6 +11690,19 @@ D173 (level-start floating), D292 (death sinking), and the Cradle spin (also und **Untested candidate, explicitly NOT shipped as the default** (per this row's own "don't repeat the same mistake" lesson from the superseded `allowShift=TRUE` fix): `skyPortForceFanShift()` + `GE_D245_FIXEDSHIFT=` env var (`sky.c`, `#ifdef PORT`) lets a live session force a single constant `k` for the whole water draw instead of the per-frame adaptive pick, eliminating the transition this session found — but at the cost of coarser tc quantization *always* (a fixed `k=5` collapses to whole-texel granularity, which could itself look worse on the shimmer cross-fade; that trade-off can only be judged by eyes on the actual water, not another headless capture). Build-verified crash-free with `GE_D245_FIXEDSHIFT=5` on Frigate (300+ frames); `verify.sh frigate`/`verify.sh "surface 2"` both PASS with the env var unset (default path byte-identical to the current reopened behaviour — nothing changes for players until someone opts in). **Next step for whoever has live Frigate/Surface-2 access:** play with `GE_D245_FIXEDSHIFT` unset (current behaviour, confirm the glitch still reproduces) vs. set to 3, 4, and 5 in turn, and report which (if any) looks better — that's the missing data point, not more static tracing. +**M-201 (2026-09-26, v0.4.0): FIXED — two port-layer causes; user-verified against 1964 ("looks accurate").** + +User vs 1964: PC water jittered with player movement, "reset" even standing still, animated too fast (the earlier yellow line was not present on the current build). + +1. **Texture geometry (the resets + speed).** The IsWater image is a 32x32 CI8 texture (mask 5 = 32-texel period) loaded with its mip chain (1400 B) and drawn through an RGBA16 tile (the D229 case). fast3d imported it CI8 as **32x43** (mip rows included) while the triangle path normalised UVs with the RGBA16 reading, **width 16** (line bytes / 2) and height 43 (`GE_D229` probe: `texw=16 texh=43`). So S ran at 2x frequency and T repeated every 43 rows, not 32; the port's per-fan S/T fold (multiples of 64 texels) is not a multiple of 43, so every fold step jumped the pattern. Fix (`gfx_pc.cpp`, D229 case only): import only the `line_size * (1<