v0.4.1 sync: build and CI fixes, settings presets D440-D447, outside PRs

build-pc.sh Windows temp-dir re-exec fixes; CI pin actions and no-cache ROM
handling; original-N64 and Deck presets; D440 preset labelling; D442 All
unlocked as a RAM/query-time override; D443 settings values (MSAA 16x,
Restart game); D444 front-end Previous/Next paging; D447 Original-aspect
pillarbox/letterbox. Outside contributions by dolent: PRs #120 and #121 (relabelled D445,
D446), merged as dolent's original commits.

Refs: D440 D441 D442 D443 D444 D445 D446 D447 D387 D283 #120 #121
This commit is contained in:
James Dansereau committed 2026-10-06 18:35:39 -04:00
72 files changed
+2228 -2080

No files matched your search

+63 -30
View File
@@ -37,7 +37,7 @@ checked against an accurate emulator (1964/GEPD) or real hardware. On top
of that sits a **modern-remaster layer** (the bar set by Nightdive's
Quake/Turok remasters, the XBLA remaster and GE+/Dab's mod). It must never
change game logic by default, and it must always be possible to get back
to the original look and feel (see the *Original N64 preset* in §5).
to the original look and feel (see the *Original N64 preset*, D440).
The port is **feature complete** when all of these hold:
@@ -63,6 +63,35 @@ verification debt cleared.
Where things stand (2026-09-29): 1–7 hold for NTSC-U, with the small
accuracy gaps listed in §2. 8 and 9 are open.
**Parity rule (decided 2026-09-30):** every platform we release for
carries the same sign-off gate: a full 21-level sweep plus a campaign
pass. Windows and Linux now; macOS when it joins.
## Order of work (decided 2026-09-30)
Work continues on `release/v0.4.1`. When to cut it, and what to call it
(v0.5.0 or later), is decided as the work lands. There are two stages:
**Stage A: now → the pre-1.0 build**
1. Clear the §3 verification debt.
2. Split-screen (§1): keep working and testing until it is good.
3. `All unlocked` rework (§5a).
4. D357 settings values (§5a), plus the restart button.
5. Outside PRs (§5a), in the proposed order once the maintainer approves it.
6. Pillarbox with the exact original aspect for the Original N64 preset (§5a).
7. Platform parity: re-base the golden baselines and extend them to both
platforms (§7), then run the gate on each platform.
8. Investigate and triage every §2 fidelity gap to fix or accept. Do not
leave any of them as "open, unexamined".
**Stage B: pre-1.0 build → 1.0**
1. PAL/JP (§1). It goes last on purpose, so that region-specific
regressions are easy to isolate and unwind.
2. macOS/ARM: merge the `macos` branch (§4), with the D441 census
(store-to-u32 tagging) and a high-arena test.
3. The exit gate on every shipped platform.
4. Remove the temporary scaffolding (§7).
## Known issues
What a player will notice in the current release. **This table is the
@@ -75,11 +104,11 @@ below).
|---|---|---|---|
| Multiplayer isn't available yet | Missing feature | — | #99, §1 |
| PAL and JP ROMs aren't supported | NTSC-U only | Use an NTSC-U ROM | D258, §1 |
| **`All unlocked` can permanently write fake unlocks into your save** | Save corruption | Back up `data/ge007.eep` before enabling it; don't use it on a save you care about | D387, §5a |
| `All unlocked` may already have written fake unlocks into a save from an older build | Save corruption | **Fixed on `release/v0.4.1` for new saves** (D387). Back up `data/ge007.eep` before first use; fakes an older build wrote are not repaired; cheats earned while ON are not kept. Delete this row when it ships. | D387, §5a |
| `Game.SkipIntro` also skips the post-mission failure dossier | Minor | Leave SkipIntro off | D408, §2 |
| F10 overlay stretches in native widescreen (menus pillarbox correctly) | Cosmetic | — | D335b, §5a |
| Rareware logo shows a subtle texture-filtering artifact | Cosmetic | — | D75, §2 |
| Steam Deck: a first launch in Desktop Mode skips the Deck preset | Settings | First launch in Game Mode, or delete `ge007.ini` and relaunch | D283, §4 |
| Steam Deck: a first launch in Desktop Mode skips the Deck preset | Settings | **Fixed on `release/v0.4.1`** (real-Deck check owed); delete this row when it ships. Until then: first launch in Game Mode, or delete `ge007.ini` and relaunch | D283, §4 |
| ~30 fps on very low-end GPUs (e.g. Intel HD 400) | Performance | Set `Video.LowEndMode=1` in `ge007.ini` | D339, #92, §4 |
| No macOS or ARM builds | Platform | — | #88, #95, #101, §4 |
| Controller buttons can't be rebound in-game (keyboard/mouse can) | Missing feature | — | #109, §5a |
@@ -91,7 +120,7 @@ below).
| Item | Status | Refs | Notes |
|---|---|---|---|
| **2–4 player split-screen multiplayer** | parked | #99, D416–D423, branch `feat/mp-splitscreen-99` | The largest missing N64 feature. 2P has been live-tested (M&K + pad, respawn, 16:9 HUD). Before merge: Rule-2 review of `PROP__PORT_SIGNED` (D417) and a golden gate run on the maintainer's console. Open on the branch: D420 (stats screen shows a large number), D421 (gadget cycling off; needs a Rule-2 game-thread hook), D422 (no centred aim for P2+), D423 (black arrowhead in P2 view), and D428's sway accumulator is player-0-only. Untested: 3P/4P, pads-only mode, pause/watch/F10 during a match, MP audio, most stages, and the full N64 MP setup screens (scenarios, handicaps, teams). Needs per-pad seats (§5). An outside restore exists for reference: birdturtle `feature/local-mp-input`. |
| **PAL and JP ROM support** | decision | D258, D75 (pal/jpn sidecar regen), #85 | Conversion is broken at the source-data level (filelist naming). A repair path has been verified but not applied. Needs PAL/JP ROMs to test. #85 was closed as "retired for now", not won't-do, so public users currently have no tracker. Reopen it or file a fresh one when work starts. |
| **PAL and JP ROM support** | open | D258, D75 (pal/jpn sidecar regen), #85 | **Scheduled for Stage B (decided 2026-09-30):** done just before 1.0 so that region-specific issues can be unwound in isolation. Conversion is broken at the source-data level (filelist naming). A repair path has been verified but not applied. Needs PAL/JP ROMs to test. #85 was closed as "retired for now", not won't-do, so public users currently have no tracker. Reopen it or file a fresh one when work starts. |
| Drop-in ROM converter, remaining parts | decision | #6, [`dev/release-dropin-rom-plan.md`](dev/release-dropin-rom-plan.md) | NTSC-U drop-in shipped. Left: per-region output names and re-exec guard (with PAL/JP), native C emitters to replace the PyInstaller converter. |
| Cut content (TCRF weapons/items) | decision | [`dev/CUT-CONTENT-BACKLOG.md`](dev/CUT-CONTENT-BACKLOG.md) | Not part of the shipped game. Tier 0 already works via the debug/cheat menus. Tier 1 (config-toggle grants) is small and port-only. Tier 2/3 (placing props, re-enabling unused MP maps) need game data changes and are out of scope for this repo. |
@@ -109,7 +138,7 @@ below).
| Front-end: Rareware logo filtering, cast-roll models | partial | D75 | Cosmetic. The other front-end logos are fixed. The cast-roll character models have never been verified. |
| Authored mip chains not sampled for explicit-LOD textures | parked | D323 | Distant textures use driver mips instead of Rare's hand-authored chain (heaviest on Statue/Aztec). Parked as a fidelity enhancement. |
| Approximations in the software RSP | open | `gfx_pc.cpp` `G_CCMUX_LOD_FRACTION`; `Video.WrapFix` (§F row `RC3 · D167`, D74 family) | The LOD-fraction CC input is an eyeballed approximation. The non-power-of-two wrap fix is still opt-in (default off), with unknown visible impact. Both need a comparison against 1964. |
| Latent 64-bit ABI hazards | open | #107, #108; ~396 `-Wpointer-to-int-cast` sites (count from a local review note; re-count with a `-Wpointer-to-int-cast` build); AUDIT-M6 `struct player`/`struct hand` raw offsets (porting-notes §A1) | Harmless today, because the arena sits low in the address space. They would bite on macOS/ARM/ASLR. The fixes fall under the AGENTS.md ABI/layout exception. **#108 concern:** widening the `sndPlaySfx` bound disables the guard on every platform; prefer a range check. Check #107 against D430/D431/D434, which touch the same `lightFindVertexBaseForTri` region. A read-only provenance pass over the 396 sites is a good delegate task. |
| Latent 64-bit ABI hazards | partial | D441, #107, #108; ~396 `-Wpointer-to-int-cast` sites at the start of the sweep (185 remain in the 17 touched files; many are intentional `(s32)&ANIM_DATA_*` offsets, D34); AUDIT-M6 `struct player`/`struct hand` raw offsets (porting-notes §A1) | Part 1 landed (D441): 17 files widened to `uintptr_t` under `#ifdef PORT`, build and Bunker/Silo runs verified. **Still owed:** the full census with per-site provenance (H/R/I/?, table not yet written) plus a **store-to-u32 vs arithmetic-only** tag per site (a `uintptr_t` widening is enough only for arithmetic; sites that store back into a u32 field need `portHostToN64` under the macOS port_addr model), an env-gated high-arena test mode, and a check under an arena above 4 GB. Harmless today because the arena sits low. They would bite on macOS/ARM/ASLR. **#108 concern:** widening the `sndPlaySfx` bound disables the guard on every platform; prefer a range check. **#107 will now conflict** with D441 in `bg.c` (`bgRoomCalcBB`), so rebase or close it during the outside-PR review. A read-only provenance pass over the remaining sites is a good local-Qwen delegate task. |
| Controller preset parity (Xbox "Jinx" 1.1, 1.2/1.3 selector) | partial | D394 | The gadget category and pad-initiated binding capture are still open. |
## 3. Verification debt (fixed; a live check is owed)
@@ -118,6 +147,9 @@ A single campaign pass with these on the checklist would clear most of them.
| Item | Refs | Check |
|---|---|---|
| Presets: Original N64 / Port defaults | D440 | Press both rows in F10 and in the front-end PC Options with a mouse and a pad; draw distance applies from the next level load. `GE_OPTIONTREEPROBE` already fails on the base commit (separate look). |
| Probe strip (2026-09-30) | D245, D302, D318 | Frigate or Dam water/sky A/B against the previous build (the s16 sky path was removed); one Facility playthrough to confirm the D318 watchdog stays silent. |
| Infra fixes | D179, build-pc.sh, crash.c, CI | The 2026-09-30 re-exec no-op (PATHEXT=.CPL in the inherited env) is fixed and verified from the agent shell (see AGENTS.md). `./build-pc.sh` from a genuine MSYS2 login shell; the next CI run (pinned actions, workflow input check). `GE_CRASHTEST=6` shows resolved frames. The romdata `VirtualAlloc` failure path now fails boot instead of falling back to the heap copy: confirm you want that. |
| Light-fixture hit-type reads (impact sound + sparks) | D434, #119 | Shoot fixtures on Bunker and Caverns. |
| v0.4.1 batch (unreleased; fixes live on `release/v0.4.1`) | D424–D432; #114 fire rate, #115 tank-crush audio loop, #116 tank movement, #117 sway, #118 GL recoil, #119 light fixtures | All user-verified against GEPD/1964 on 2026-09-29. Close the issues when it ships. |
| FOV-scale edge culling | D222 | Max FOV, pan across NPCs at the screen edge. |
@@ -132,10 +164,10 @@ A single campaign pass with these on the checklist would clear most of them.
| Item | Status | Refs | Notes |
|---|---|---|---|
| macOS (Intel + Apple Silicon) / ARM64 Linux | open | PRs #88, #95; #101 | Outside drafts. The maintainer tests firsthand before merging. Needs the ABI-hazard sweep in §2. |
| Linux as a co-equal platform | decision | — | Does Linux carry the same sign-off gate as Windows (a full sweep of all 21 levels)? The last known Linux crash (D190) was closed as not-a-bug. |
| macOS (Intel + Apple Silicon) / ARM64 Linux | open | PRs #88, #95; #101 | Outside drafts, parked for `main` until after 1.0. **Address model decided (2026-09-30): port_addr (#95)**, i.e. a single `portN64ToHost`/`PORT_N64PTR` chokepoint with a `PORT_ADDR_STRICT` validator, over the dram-scoped alternative. The `macos` integration branch **exists on origin** (2026-09-30). It was cut from `origin/main` `0e8c2ce2`, and it takes #88 (merged as `c4153157`, using #95's conflict-resolved copy `ae1884cc`), #132 (Linux build fix, `39d4f8d0`: #88's `romdata.c` included `<errno.h>` before `ultra64.h`, and glibc's `errno` macro broke the `errno` fields in `PR/os.h`; #95 needs it on rebase) and #95 (retargeted to it, still open). italoarruda's work also goes there. Nothing merges from it to `main` before the post-1.0 pass. **Open against `macos`:** #131, the fork-friendly self-hosted sweep. It adds `platform` and `runner_labels` inputs, a macOS job running `level_sweep_mac.sh` with a JSON verdict, seed-dir-only ROM staging, and `docs/selfhosted-sweep.md`. It is a scaffold that has not been run on a Mac, and it needs #95 in `macos` first. **When `macos` is next rebased onto `main`** (after the v0.4.1 push), `selfhosted.yml` will conflict with the v0.4.1 ROM-cache fix. Take #131's version, which already includes that fix. The crouched-rifle table fix (`chr.c`), found three times (#95, italoarruda, #120), landed on `release/v0.4.1` via #120 (D445) on 2026-09-30. #120's shape is the correct one (it restores the N64 table length of 2; #95's terminator-only version leaves length 1), so **drop #95's `crouched_rifle` hunk** when `macos` is rebased. #95's finding labels (D29x, then D405–D421) clash with ours, so relabel on merge. The maintainer tests firsthand before merging. Needs the ABI-hazard sweep in §2 (store-to-u32 tagging). |
| Platform parity gate (Linux co-equal with Windows) | open | — | **Decided 2026-09-30:** yes. Every released platform carries the same gate: a 21-level sweep plus a campaign pass. Needs the golden baselines on both platforms first (§7). The last known Linux crash (D190) was closed as not-a-bug. |
| Low-end GPUs (~30 fps on Celeron/HD 400) | open | D339, #92 | Needs `GE_PERFSTAT` data from that class of hardware. The D372 low-end preset is the mitigation. |
| Steam Deck preset skipped when first launched from Desktop Mode | open | D283 | Workaround is documented in the README. Make the fix not depend on the env var. |
| Steam Deck preset skipped when first launched from Desktop Mode | verify | D283 | Fixed on `release/v0.4.1`: the Deck is detected by DMI hardware id (Valve Jupiter/Galileo; `STEAMOS` kept as a second signal) and the preset applies once per ini, only while Fullscreen/Maximized are untouched. Only the Windows build and a Linux `-fsyntax-only` check ran. **Owed: one run on a real Deck** (Desktop-Mode-created ini, then Game Mode; expect fullscreen 1280x800 and the log line `video: Steam Deck (DMI Valve ...)`). The Known-issues row above goes when this ships. |
| Windows 7 | decision | #98 | Recommend won't-fix (toolchain). |
## 5. Modern-remaster layer (port features beyond the N64)
@@ -147,7 +179,7 @@ ultrawide, FOV, MSAA/anisotropic/texture-filter options (including N64
keyboard/mouse rebinding with a GEPD-style layout, pad deadzone,
sensitivity, smoothing, southpaw and triggers, rumble, crosshair and HUD
customisation, HUD scale, music/FX volume, no-hit-flash, SkipIntro
(experimental), All unlocked (unsafe, see D387), and the F10 overlay.
(experimental), All unlocked (experimental, see D387), and the F10 overlay.
The modern defaults (native widescreen, overscan crop, 250% draw/LOD
distance, bilinear filtering) are **deliberate**. They are not a fidelity
@@ -161,16 +193,19 @@ Background research lives in the maintainer's local notes (gitignored):
| Item | Status | Refs | Notes |
|---|---|---|---|
| **"Original N64" preset toggle** | open | — | One switch that snaps every value the port has changed back to N64 values: draw/LOD distance, culling, texture filter (3-point), 4:3, no overscan crop, stock FOV, N64 aim. It's cheap, because each is already a config key. This is the port's answer to the XBLA/Nightdive "original vs enhanced" switch. |
| `All unlocked` writes fake unlocks into the real save | open | D387 | **The only data-integrity bug.** Fix it or gate the option harder. Until then, the README warns users to back up the EEPROM. |
| Settings units and values cleanup | decision | D357 ([plan](dev/D357-SETTINGS-VALUES-PLAN.md)) | Awaiting sign-off. Its "Now" column is partly stale: the deadzone is already split L/R, and draw/LOD defaults are 250, not 150. Covers FOV in degrees, sensitivity shown as ×, MSAA 16×, filter naming, volume step and the FPS preset grid. |
| "Original N64" preset: pillarbox at the original aspect | verify | D440, D447 | Landed 2026-09-30 (D447): `Video.AspectMode` (Window/Original), set by the Original N64 preset. Exact 4:3, or 16:9 while the game's watch Ratio is 16:9. Letterboxes in taller windows. `Crop overscan` no longer eats the game's Wide/Cinema letterbox. Measured: 160 px bars at 1280x720. Window-mode frame diff is within the run-to-run noise floor (dmean 0.93 vs base-vs-base 0.85). **Owed:** compare 4:3 and 16:9 against 1964; Wide/Cinema with crop on; menu mouse alignment in Original mode; MSAA 4/8/16, fullscreen toggle, Linux build. F10 full-window is deferred to an F10 polish pass. Known: with crop off, the D246 one-unit edge line shows at the rect edges. |
| `All unlocked` rework: RAM override instead of save patching | open | D387, D259 | **Approved 2026-09-30.** Levels are already RAM-only (the game's `debug_enable_all_levels_flag`/`debug_007_unlock_flag`, `port/src/main.c`). Replace the cheat half, a block-4 EEPROM read patch plus the D387 write merge in `port/src/libultra.c`, with one opt-in `#ifdef PORT` hook at the cheat-unlocked check (`src/game/file2.c` `fileGetIsCheatUnlocked`, and confirm every caller including `front.c` `frontCheckIfCheatIsUnlocked`). The save is then never touched: earned cheats and times persist normally, OFF restores real progress, and the D259 silent-volume quirk goes away. This is a `src/game` edit for an opt-in port feature, so document it as such (maintainer-approved). Then remove the D387 merge machinery. Already-persisted fakes from older builds stay unrepaired. |
| Settings units and values cleanup | open | D357 ([plan](dev/D357-SETTINGS-VALUES-PLAN.md)) | **Approved 2026-09-30 with changes** (recorded in the plan's §0): FOV in degrees; FPS cap presets 30/60/90/120/144/240/Uncapped **plus** a free custom value; MSAA adds 16×; mouse invert stays a separate toggle; the texture filter keeps "N64 3-point" (the plan's rename rests on a wrong premise). Refresh the plan's stale "Now" column first (the deadzone is split L/R; draw/LOD defaults are 250). |
| Restart button in the settings menus | open | — | QoL (2026-09-30): next to Quit to desktop, a "Restart game" action so that restart-required changes (MSAA, etc.) apply in one click. Relaunch the same exe with the same args after an orderly quit (the D344 path, since hard exits have BSOD'd). Offer it wherever a changed row is flagged "restart". |
| Front-end PC Options: Previous/Next paging for mouse users | open | — | QoL (2026-09-30): add clickable Previous/Next (page) controls to the main-menu settings screen, so mouse users don't have to rely on wheel scrolling. Today the wheel scroll competes with mouse hover: any small mouse nudge re-selects a row and snaps the scroll back. Fix that interaction too. Hover must not reset the scroll position, and it should only change the selection once the pointer moves onto a different row. Owner: the D357/D443 track (same file, `port/src/frontoptions.c`). |
| Outside PRs #107–#109, #122–#124 | open | #122 hot-plugged pads never reopen, #123 persistent crosshair (`gunfire.c`), #124 compact health/armour bars (`bondview2.c`); #107/#108/#109 (italoarruda) | **#120 and #121 merged locally into `release/v0.4.1` on 2026-09-30** (dolent's original commits kept for credit; relabelled D445/D446; the review is in `docs/dev/notes/PR-REVIEW-120-121.md`). Close both on GitHub when the branch is pushed. **Owed live:** kneeling rifle guards (#120), and the 2P watch-menu text (#121, on the MP branch). **Remaining order (proposed; approval owed):** #122 (then rebase split-screen on it; #122/#109/MP all touch `input.c`) → #107 (fold remaining sites into the D441 census, then close) → #108 (ask for a range check; belongs with `macos`) → #109 (after split-screen settles `input.c`) → #123/#124. **Policy:** any PR that adds a *feature* (#109, #123, #124, …) gets a **design review before any work**: compare against other PC ports and remasters (PD port, Nightdive, GE+/XBLA), then approve, or send suggestions back and defer until the submitter revises. **Their finding labels clash with ours:** relabel on merge. `src/game` touches (#120/#123/#124) each need a Rule-2 or ABI-exception check. |
| Gamepad button rebinding UI | open | PR #109, D394 | Keyboard/mouse rebinding has shipped. Review #109 under the outside-PR policy. |
| Per-pad tuning (device index, hot-plug seats) | open | — | Needed for split-screen anyway. |
| Full-gamepad menu navigation audit, Xbox/PS button glyphs | open | — | Menus are only partly audited for pad-only use. There are no glyphs yet. |
| Mute and auto-pause on focus loss; PNG screenshots to `screenshots/` | parked | D231, PR #56 (closed unmerged), `docs/dev/notes/parked/0001-QoL-*.patch` (local) | Deprioritised by the maintainer on 2026-09-25. F12 today is the dev PPM dump. |
| F10 overlay polish | open | D335b, #90 | Pillarbox the overlay in widescreen; colour experimental rows red; warn on unknown `ge007.ini` keys (promised in #90; today they are only logged). |
| Master volume, audio output device select | open | — | |
| Frame rate above 60 (120/144/uncapped with interpolation) | decision | [`dev/UNLOCKED-FPS-PLAN.md`](dev/UNLOCKED-FPS-PLAN.md) | The menu cap is {30, 60} (the ini accepts 0 = uncapped, but the sim ticks at the VI rate). The plan's premise predates the D248 real-60 fix, so re-measure first. Must stay compatible with netplay determinism. |
| Frame rate above 60 (120/144/uncapped with interpolation) | decision | [`dev/UNLOCKED-FPS-PLAN.md`](dev/UNLOCKED-FPS-PLAN.md) | The menu cap is {30, 60} (the ini accepts 0 = uncapped, but the sim ticks at the VI rate). The plan's Phase 0 is resolved by D248 (the default was a port bug; it is a real 60 fps) and its Phase 1 audit and Phase 2 slice scope are written (landed 2026-09-30), but the header still reads parked. Next step is a standing sim/render-rate measurement, not the old 30 fps investigation. Must stay compatible with netplay determinism. |
### 5b. GE+ / Dab's-mod gameplay options (opt-in; touch game code)
@@ -218,17 +253,14 @@ and a stage loader.
| Item | Status | Refs | Notes |
|---|---|---|---|
| Security review follow-ups | open | maintainer's local security review | Four open hardening items: one Medium (self-hosted CI workflow input handling), the rest Low (local-input only). Details are kept out of this public file. Also: pin Actions by SHA, pin `pyinstaller`, take the Dependabot action bumps (#1/#2). |
| Security review follow-ups | open | maintainer's local security review | Three Low hardening items remain (local-input only; details are kept out of this public file). Done on `release/v0.4.1`: the Medium item (self-hosted workflow input handling), actions pinned by SHA, `pyinstaller` pinned to 6.22.3, and the ROM no longer stored in `actions/cache` by `selfhosted.yml` (fork PR workflows can restore base-branch caches; no cache entry ever existed, checked 2026-09-30; it is now staged from the runner's `_rom-seed` dir only). The Dependabot action bumps (#1/#2) would now need to land as SHA pins, not tag bumps. |
| Deterministic 1964-vs-port comparison harness | open | — | Would replace by-eye/by-ear fidelity checks. Highest-leverage tooling item. |
| Scripted-playthrough harness | open | `GE_INPUTSCRIPT` | Input script + `GE_PCDUMP` + crash-log check, for unattended regression runs. The tool exists; the harness doesn't. |
| Golden baselines | open | `tools_pc/verify.sh`, D117 | bunker1 has been noisy since M-84, so the per-patch gate is unreliable. Re-base it, extend to all 21 levels × both platforms, and finish `GE_DETERM`. |
| Self-hosted CI runner link failure | open | D244 | Needs a `pacman -Syu` on the runner (maintainer-only access). Release tag builds use GitHub runners, so it doesn't block a release. |
| `build-pc.sh` TMP self-heal fails in-script | open | AGENTS.md | A standalone `.ps1` workaround is documented. |
| Remove temporary scaffolding before a release | open | [`dev/GE-ENV-PROBES.md`](dev/GE-ENV-PROBES.md), D302 | Probes: D207S, D157, D252POOL, D245V, D236 (RAW/RM/ORDER/ALPHA/ZFIX/BT), D288, D309, D318B/D320 repro, `GE_OBJT`. Tools: `tools_pc/scan_op12.py`, `scan_op13.py`. Also demote the D318 watchdog to detect-and-log (D329 removed the cause), remove the M-183/M-185 clamps after a full cutscene re-check, and grep hot paths for `getenv` (D302 repeat offender). |
| `crash.c` backtraces | open | — | The EBP walk dies at frame 1. Switch to `CaptureStackBackTrace`/`StackWalk64`. |
| `romdata.c` VirtualAlloc fallback hardening | open | D179 tail | Minor. |
| Findings log and doc size | open | — | `findings.md` is about 2 MB and growing. It needs the deferred porting-notes TOC and docs-budget script (context cost for every session). |
| Branch and PR housekeeping | open | — | Push local `main` (5cccbcef, post-v0.4.0 bookkeeping). Prune the ~20 squash-merged branches: keep `feat/mp-splitscreen-99`, `land/modloader-*` and `research/cut-content-backlog`, and first confirm the single unique commits on `feat/unlocked-fps-phase1-audit` and `docs/m138-security-disclosure` landed. PR #62 (mouse dt-decouple) stays held until mouse feel is fundamentally better. |
| Remove temporary scaffolding before a release | partial | [`dev/GE-ENV-PROBES.md`](dev/GE-ENV-PROBES.md), D302 | The 2026-09-30 pass removed D207S, D157, D252POOL, D245V, D288, D309, the D318B suite, the D320 repro harness and `scan_op12/13.py`, demoted the D318 watchdog to detect-and-log, and cached the per-frame `getenv` sites. **Left:** dead registry rows (D116, D51, D56, D60-D63, D69*, D71LOG, D85DUMP, D86-D88, D90, D96, D104, D154, D178); closed-finding TEMPs (`GE_D306C`, `GE_DYNTEXHASH_OFF`, `GE_FORCEALARM`, `GE_D204_OLD`, `GE_D252`, `GE_D243X2/X4`, `GE_D318T`, `GE_RSEED_LV`, `GE_D235_*` once D235 closes); the now-inert sky tile-capture/shift machinery in `sky.c` (`skyPortCaptureTile`, `skyPortPickShift`, `GE_D245_FIXEDSHIFT`); and the M-183/M-185 clamps, which need a full cutscene re-check first. |
| Findings log and doc size | partial | `tools_pc/docs_budget.py` | The budget checker exists (per-file and tier-1 token budgets; `--toc FILE`), and everything is within budget. `findings.md` is still about 2 MB (~500k tokens), so agents must keep using the index. Remaining: nothing blocks; trim only if the budget check starts failing. |
| Branch and PR housekeeping | open | — | 30 landed local branches pruned 2026-09-30 (tips kept in the notes archive, `prune-branches-2026-09-30.txt`). `docs/m138-security-disclosure` landed on `release/v0.4.1` with a stale-fact pass (SECURITY.md, the security-and-fidelity page, README roadmap bullet). `feat/unlocked-fps-phase1-audit`'s commit (Phase 0 resolved by D248, Phase 1 audit) landed as `docs/unlocked-fps-audit`. **On hold by the maintainer (2026-09-30, do not prune yet):** probe-only leftovers `d243-verify-local`, `investigate/d243-cutscene-race`, `park/d236-probes-m139`. Local `main` and `release/v0.4.1` are deliberately not pushed. **Pushed on 2026-09-30:** the `macos` integration branch; `ci/selfhosted-fork-runners` (PR #131 into `macos`, open); `fix/macos-errno-include` (PR #132, merged into `macos`). Pruned after merging, 2026-09-30: `fix/selfhosted-rom-no-cache`, `docs/macos-ci-tracking` and `fix/macos-errno-include` (local and remote), plus the `review/pr88` and `review/pr95` read-only refs. PR #62 (mouse dt-decouple) stays held until mouse feel is fundamentally better. |
| Docs site: Web 1.0 phase 2 | decision | — | Phase 1 shipped. Phase 2 (centred serif page) is plan-only. |
## Community forks (not ours; reference only)
@@ -242,16 +274,17 @@ and a stage loader.
## Decisions owed (maintainer)
1. **PAL/JP:** pursue it (needs those ROMs; also unblocks drop-in Part B), or stay NTSC-only?
2. **Split-screen:** approve the D417 Rule-2 change, then merge `feat/mp-splitscreen-99`.
3. **D387:** fix `All unlocked`'s save leak, or restrict the option?
4. **D357:** sign off the settings units/values plan (after refreshing its stale table).
5. **GE+ gameplay options (§5b)** and a port-native third-person camera: in or out?
6. **Enhanced visuals (§5c):** build our own, adopt parts of thepont's stack, or skip?
7. **Beyond-N64 multiplayer (§6):** netplay, bots, co-op, cheats. In scope after feature-complete, or not?
8. **Linux co-equal status** and **release cadence / tag policy**.
9. **Outside PRs:** #107 and #108 (with the guard concern), #109, #88/#95. Review order.
10. **D433 gunshot timbre, D323 authored mips:** worth chasing, or accept?
Decided 2026-09-30 (moved into the rows above): PAL/JP goes in Stage B;
`All unlocked` becomes a RAM override; D357 approved with changes;
platform parity gate; feature PRs get a design review first; the
#88/#95 address model is port_addr. Still owed:
1. **Split-screen:** approve the D417 Rule-2 change (`PROP__PORT_SIGNED`) when the branch is ready to merge.
2. **Outside PR review order:** final approval of the order proposed in §5a.
3. **GE+ gameplay options (§5b)** and a port-native third-person camera: in or out?
4. **Enhanced visuals (§5c):** build our own, adopt parts of thepont's stack, or skip?
5. **Beyond-N64 multiplayer (§6):** netplay, bots, co-op, cheats. In scope after feature-complete, or not?
6. **D433 gunshot timbre, D323 authored mips:** decided per gap during the Stage A fidelity triage.
## Not planned
+8
View File
@@ -93,6 +93,14 @@ cmake -S . -B build-pc -DROMID=ntsc-final
cmake --build build-pc -j
```
On Windows, `build-pc.sh` also locates the MSYS2 MinGW64 toolchain itself
(so it works from Git Bash or a non-login shell too, even with another
`cmake` earlier on PATH) and, if native child processes would get an
unwritable `TMP`, re-runs the configure+build under PowerShell with a
writable one. If MSYS2 is not installed at `C:\msys64`, set
`GE_MSYS2_ROOT` (e.g. `GE_MSYS2_ROOT='D:\M\msys64'`). Re-exec logs:
`build-pc/ge007-native-reexec-*.log`.
For PAL/JP you must first generate that region's ROM-asset symbol file
(the US one is committed):
+35 -1
View File
@@ -1,6 +1,6 @@
# 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
Status: **IMPLEMENTED 2026-09-30 (findings D443) for mouse sens ×, deadzone whole-%, volume step, FOV degrees, MSAA 16×, Restart game; texture filter unchanged by decision; frame-rate cap deferred (§0 presets + custom = separate project). Approved with changes; see §0.** Proposal dated 2026-09-27. Baselines: the PD port
checkout (`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
@@ -14,6 +14,23 @@ front top row reads **Profile**, the per-file row tag reads
**"(per profile)"** and the F10 unavailable value reads **"Select a
profile"** (§4).
## 0. Sign-off (2026-09-30) — these override §3 where they differ
- **FOV:** display in **degrees** (the PC standard). Storage stays unchanged.
- **Frame rate cap:** presets **30 / 60 / 90 / 120 / 144 / 240 / Uncapped**,
**plus** an arbitrary custom value. This keeps the ini's free range and
adds a custom entry in the menu, instead of the plan's presets-only grid.
- **MSAA:** add **16×**.
- **Mouse invert:** stays a separate On/Off toggle.
- **Texture filter: do NOT rename.** The §3 premise ("3-point *is*
trilinear") is wrong. 3-point is the N64's own 3-sample bilinear
approximation, and "Linear" and "Bilinear" name the same thing. Keep
"N64 3-point" and check the current enum strings before touching them.
- **Refresh §3's "Now" column before implementing.** The stick deadzone is
already split L/R, and the draw/LOD defaults are 250, not 150.
- Related QoL, tracked separately in ROADMAP §5a: a **Restart game** action
next to Quit to desktop, for restart-flagged rows such as MSAA.
## 1. Problem
The D356 regroup fixed the *layout*; the *values* still mix three unit
@@ -65,6 +82,23 @@ volume trio (master still M3-backlog).
Current state from `optionsoverlay.c` `rows[]` + the `configRegister*`
ranges (2026-09-27). "Display" = what the value column shows today.
### Implemented 2026-09-30 (D443) -- refreshed "Now" vs. result
The per-row tables below keep the 2026-09-27 audit text for history. Actual
pre-change state and what shipped (all port-only; no default or storage change,
so `kResetDefaults` is untouched and `GE_WSPROBE_RESET` stays failures=0):
| Row | Actual "Now" (pre-D443) | Shipped |
|---|---|---|
| Mouse horizontal sensitivity | 1-500 · step 5 · shown as calibrated `N/100` (50/100 at raw 100) | UI range 10-300, step 10, shown `%.1fx` (raw/100), linear bar; left the calibrated mapping |
| Stick deadzone L / R | already split `Input.PadDeadzoneL/R` 0-30000 · step 500 · calibrated `N/100` | step 300 + `dispDiv=300` `%` (whole-% grid, no key migration); left the calibrated mapping |
| Music / FX volume | step 128, `/328` | step 328 (1 display-%) |
| FOV | `Video.FovScale` 50-150 · step 5 · `%` | row "Field of view", horizontal degrees (see D443 formula); key/storage unchanged, step walks the displayed degree |
| Anti-aliasing | 1/2/4/8 · default 2 (C initialiser) | + 16x; range 1-16 (GL clamps to `GL_MAX_SAMPLES`) |
| Texture filter | Nearest / Bilinear / 3-Point | unchanged (decision §0) |
| Frame-rate cap | 30/60 toggle (sim ticks at 60 Hz) | unchanged this round |
| Restart game | -- | new action row after Quit to desktop (both UIs, shared table) |
### INPUT
| Row | Now (default · range · step · display) | PD port | Turok (notes) | Proposal |
+15 -12
View File
@@ -3,7 +3,7 @@
Every `getenv("GE_…")` in the tree. The table prose is hand-curated;
**`tools_pc/gen_env_probes.py`** re-greps the live sites and reports drift
(NEW / GONE vars + a fresh file:line map) — run it before trusting the
File:line cells. Last reconciled 2026-09-28 (v0.4.0 release sweep: rows added for probes introduced since 09-21 — D383 bind-capture smoke, D388 option-tree walk, D406 front-options diagnostics, D354/D344 watch-settings & quit-frame harnesses, D318B boundary suite, z-fight grid; the drift checker now also sees the cached env-flag wrapper reads from `port/include/envflag.h`). All are **env-gated**: unset = zero behavior
File:line cells. Last reconciled 2026-09-30 (ROADMAP §7 strip pass: D157/D157T/D157I/D157TB, D207S, D227V, D245_OLDVTX/OLDTEX, D252POOL, D288, D309, D318B suite, D320R removed and marked as tombstones; D318W demoted to detect-only). Previously 2026-09-28 (v0.4.0 release sweep: rows added for probes introduced since 09-21 — D383 bind-capture smoke, D388 option-tree walk, D406 front-options diagnostics, D354/D344 watch-settings & quit-frame harnesses, D318B boundary suite, z-fight grid; the drift checker now also sees the cached env-flag wrapper reads from `port/include/envflag.h`). All are **env-gated**: unset = zero behavior
change, so they are safe to leave in a release build (they only cost a
`getenv` on the guarded path). "Dead" below means *the finding it was built
for is closed* — the probe is strip-candidate scaffolding, not that it does
@@ -26,6 +26,7 @@ Two classes:
| `GE_UNLOCK_ALL` | `src/game/debugmenu_handler.c:1098` (`get_debug_enable_all_levels_flag`, `#ifdef PORT`) | Mission-select shows every solo level (playtest jump-to-any-level aid). Cached in a `static int c`. | **live** |
| `GE_DEBUG_UNLOCKALL` | `src/game/file2.c:404` (`#ifdef PORT`) | Forces the "all levels unlocked" flag TRUE regardless of the shipped save — same effect as `GE_UNLOCK_ALL` but at the file2 unlock-query site. Cached in a `static int`. | **live** (playtest aid) |
| `GE_DEBUGMENU` | `src/boss.c:566` (`#ifdef PORT`) | Makes the L+R debug menu respond to live controller input (C-up+C-down open/close, stick + buttons read from pad 0) instead of the `#ifndef DEBUGMENU` path; unset reproduces that path exactly. Cached in a `static int`. | **live** (playtest aid) |
| `GE_FAKE_DECK` | `port/src/video.c:490` (`videoDetectSteamDeck`) | D283: `=1` forces the Steam Deck path on any host (Windows/Linux desktop) so the once-per-ini Deck preset logic (`Video.DeckPresetApplied`, applied only while `Video.Fullscreen=0` and `Window.Maximized=0`) can be tested; `=0` forces it off on a real Deck (skips DMI + `STEAMOS`). Unset = real detection (DMI `Valve` + `Jupiter`/`Galileo`, then `STEAMOS`). **Writes the ini** (preset + flag) — use a throwaway data dir. | **live** (test hook) |
| `GE_OPTIONSOVERLAY` | `port/src/optionsoverlay.c:231` | Force-enable the F10 port-layer options overlay (D184 / PR #10) regardless of ini. `atoi != 0`. | **live** |
| `GE_D204` | `port/src/audio.c` (`audioSetNextBuffer`) | **Audio-health monitor (D204).** One line per 5 s of wall clock: `rt=` produced-audio/wall ratio (1.000 healthy; below 1 = the 30 Hz audio retrace is starving and the device is padding with silence), `q=` SDL queue depth / `queueLimit` (pinning at the limit with `drop=` climbing = overproduction, the "eventually no audio" state), `drop=` blocks discarded, `max=` largest block vs its `g_MaxFrameSize*4` allocation. **Deliberately cheap (~30 clock reads/s) — safe to leave on for a full playtest, unlike `GE_MIXERTRACE`.** The instrument of record for D202. | **live** |
| `GE_D204_OLD` | `port/src/audio.c` (`d204OldMode`) | Restores pre-D204 behaviour (`osAiGetLength()` reports the whole SDL queue depth; `queueLimit` back to 8192) so before/after can be A/B'd in **one binary**, without build-to-build variance. Temporary — remove when D204 closes. | **live** (D204 A/B) |
@@ -58,19 +59,19 @@ 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`). **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_D157` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_vertex` + `import_texture`; `#ifdef PORT`) | D219/D252's "rainbow particle" family — bullet-impact spark on Bunker1 door confirmed by screenshot to cycle magenta/green/cyan (M-157). Three sub-probes: original exact-color-match log (`g_BulletSparkColors[]` byte match → geometry_mode/LIGHTING + shaded RGB); **`D157T`** texture-identity-keyed vertex full-path log (tile 0 holds a loaded texture ≤16 KiB in frame window [90,240] → raw `cn[]`, post-shade RGBA, LIGHTING, geometry_mode, combine_mode, tile-0 tmem/fmt/siz/addr/size/line, uv; cap 400 + summary/200); **`D157I`** import census (small textures imported in the window, deduped per source address → addr/fmt/siz/size/tile/tmem). M-158: bug did NOT reproduce in 6 identical dev-box runs — probes left live to capture the full RGBA path on the next occurrence (user Steam Deck run with `GE_D157=1`). **M-201:** F12 opens a ~90-frame full-detail `D157TB` burst; continuous logging cut to a 2000-line baseline + 500 colour-match hits (the uncapped white match ran live play at 4–6 fps). | **dead** (D252 fixed) — removed 2026-09-30 (ROADMAP §7 strip pass, branch `chore/strip-temp-probes`); tombstone — the generator flags it GONE |
| `GE_DVM` | `src/game/gunfire.c` (`gunUpdateAndFire` gate dump + `gunRenderFirstPersonGunModels` render dump; `#ifdef PORT`) | 1P weapon viewmodel (D215). `[DVM]` line: the `field_87F` show/hide gate terms + `Gun_hand_without_item` sub-terms + `copy_of_body_obj_header` RootNode/Switches/numRecords. `[DVMr]` line: `87F` + `cmds_emitted` = gfx commands the weapon `subdraw()` produced (healthy ≈ 150; the D215 bug showed **1**). Rate-limited ~1/s per hand. **Found + verified D215** (`renderdata.flags` read 0 → all weapon DL nodes gated off). Probe kept inert. | **live** (D215 FIXED, probe kept) |
| `GE_DTEX` | `port/fast3d/gfx_pc.cpp:1001` | Per-`import_texture` param log (dims / line / size / lod / gen_mipmaps). Used to root-cause RC2 and D159. Kept as an inert diagnostic. | **live** (RC2/RC3 not finished) |
| `GE_TEXDUMP` | `port/fast3d/gfx_pc.cpp:1020`, `port/fast3d/gfx_opengl.cpp:700` | PPM dump of every uploaded texture + a `fmt/siz/palfmt/palidx/pal[0..3]` line. Root-caused D161 (Depot ceiling). Kept as an inert diagnostic. | **live** (RC3 not finished) |
| `GE_D116` | `port/fast3d/gfx_pc.cpp:1710,2362`, `src/game/textrelated.c:265,541` (cached `ge_d116`) | HUD/text "X-mirror" (D114/D116) vbo/uv trace. **D114/D116 CLOSED — NOT A BUG (M-33/D168): the captures were upside-down, not mirrored.** Probe is now dead scaffolding — strip on the next cleanup pass. | dead (D114/D116 closed) |
| `GE_D288` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_tri1`, right after the D233 trivial-reject guard; cached `ge_d288`) | M-152 diag for the Silo intro stray triangle: 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_D288` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_tri1`, right after the D233 trivial-reject guard; cached `ge_d288`) | M-152 diag for the Silo intro stray triangle: frame-gated to `num_dls` 550–755 (M-155), now **skips `any_behind_camera==1` triangles** (M-155 follow-up — the x/w divide is meaningless for w≤0; that subset was pure false-lead noise) and logs the render-mode word (`oml`) so a live capture can ID the culprit asset against level tables. Logs any triangle whose clip-space NDC bbox covers >50% of the screen or goes non-finite, with each vertex's `w`. Bare `-level_20` re-run: 1965 sane hits in the window, still zero for the target color RGB(83,62,44) — consistent with M-155's working theory that the bare boot's scripted camera never reaches the bug state; **headless probing is at its limit, live capture is next**. | **dead** (D288 closed) — removed 2026-09-30 (ROADMAP §7 strip pass, branch `chore/strip-temp-probes`); tombstone — the generator flags it GONE |
| `GE_D245V` | `src/game/sky.c:962` (`skyPortRenderPoly`-adjacent, `#ifdef PORT`) | D245 water-seam diag: per-call min/max of the recomputed S/T texture-coordinate ranges for the water fan vertices (rate-limited), to check whether the seam tracks a tc-shift boundary. Inert unset. | **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_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). | **inert** (D245 fixed) — since the s16 sky path was removed 2026-09-30 nothing consumes the forced shift; strip candidate |
| `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_D252POOL` | `src/game/image.c` (`texLoad`), `src/game/initmttex.c`, `port/src/gimgfixup.c` | **TEMP** (M-201): one line per new texture load (pool main/other, free bytes) + `POOL-FULL` events + stage pool range; `D252SYNC: N slots re-synced, M changed` per stage load (the D252 fix at work: M>0 from the 2nd stage on). Load-time only, no fps cost. | **dead** (D252 fixed) — removed 2026-09-30 (ROADMAP §7 strip pass, branch `chore/strip-temp-probes`); tombstone — the generator flags it GONE |
| `GE_D245_OLDVTX=1` / `GE_D245_OLDTEX=1` | `src/game/sky.c` (`skyPortRenderPoly`) / `port/fast3d/gfx_pc.cpp` (CI8 import + tri-path dims) | **A/B** (M-201): restore the pre-fix s16 sky/water vertex path / the pre-fix 32x43, 16-wide water texture geometry. | **dead** (D245 fixed) — removed 2026-09-30 (ROADMAP §7 strip pass, branch `chore/strip-temp-probes`); tombstone — the generator flags it GONE. The pre-fix s16 sky/water vertex path (`skyPortRenderPoly` tail + `skyPortEmitTileShift`) went with it; the float path is now unconditional |
| `GE_DYNTEXHASH_OFF=1` | `port/fast3d/gfx_pc.cpp` (`import_texture` cache key) | **A/B** (M-201, D341): disable the content-hashed cache key for dyn-pool textures (reverts to the stale address-only key). | **TEMP** (A/B switch) |
| `GE_D207S` | `src/game/gunfire.c` (player shot-sound block, `#ifdef PORT`) | **TEMP** (M-201): one line per player shot-sound decision: `D207S t=<g_GlobalTimer> hand item rate -> H1|H2|SKIP` (SKIP = both per-hand sound handles still held). Read-only, uncapped, cheap; use instead of `GE_AUDIOTRACE` for live captures (that probe does file I/O on the audio path). | **TEMP** (D207/D241) |
| `GE_D207S` | `src/game/gunfire.c` (player shot-sound block, `#ifdef PORT`) | **TEMP** (M-201): one line per player shot-sound decision: `D207S t=<g_GlobalTimer> hand item rate -> H1|H2|SKIP` (SKIP = both per-hand sound handles still held). Read-only, uncapped, cheap; use instead of `GE_AUDIOTRACE` for live captures (that probe does file I/O on the audio path). | **dead** (D207/D241 closed) — removed 2026-09-30 (ROADMAP §7 strip pass, branch `chore/strip-temp-probes`); tombstone — the generator flags it GONE |
| `GE_D306C="f0 f1 [wmax]"` | `port/fast3d/gfx_pc.cpp` (after `zf_note_emit`) | **TEMP** (M-201): depth-state census of every emitted triangle in frames f0..f1 (GL depth mode, geometry_mode, other_mode_l, tile-0 texture, NDC centroid, z/w range, min w); optional `wmax` keeps near geometry only. Cap 60000 lines. | **TEMP** (D306 closed; remove post-release) |
| `GE_INTROCAM=<n>` | `src/game/bondview_r.c` (`#ifdef PORT`, intro camera pick) | **TEMP/test-only**: force the level intro's first camera index (Dam truck shot = 5). The PRNG draw is still consumed. Logs `D306: intro camera index` when set (or with `GE_D306C`). | **TEMP** (useful repro tool) |
| `GE_STARTWEAPON=<ITEM_IDS>` | `src/game/bondview_r.c` (`#ifdef PORT`, after the start-inventory setup) | **TEMP/test-only** (M-201, D303): adds the item + 400 rounds of its ammo and makes it the starting right-hand weapon (M16 = 13). Pair with `GE_INPUTSCRIPT` `ZHOLD`. | **TEMP** (useful repro tool) |
@@ -78,7 +79,7 @@ Two classes:
| `GE_D235_OFF` | `src/boss.c` (same block) | **TEMP** A/B switch: skips the per-stage `videoResetTextureCache()`, i.e. reverts the D235 fix for one run. For comparing a restart with and without the fix. | **TEMP** (D235; remove when D235 closes) |
| `GE_D235_RELOAD=<frames>` | `src/boss.c` (same block + `bossMainloop` retrace branch) | **TEMP** reload harness: once the title stage loads after the first level (e.g. after a scripted watch Abort), wait `<frames>` retraces, then re-enter that level via `bossSetLoadedStage()`, the front end's own mission-start call. A headless death/restart without the pointer-menu walk. Pair with `GE_INPUTSCRIPT="800:START;1200:A;1300:RIGHT;1400:A"` (abort) on `-level_XX`. | **TEMP** (D235; remove when D235 closes) |
| `GE_D235_NEXT=<stage id>` | `src/boss.c` (with `GE_D235_RELOAD`) | **TEMP**: re-enter a *different* stage after the abort (level A → title → level B), used for D336 (Runway → Frigate). `GE_D85TEX=1` now also logs each stage's texture-pool size and address range (`src/game/initmttex.c`). | **TEMP** |
| `GE_D320R="tick:action;..."` | `port/src/d320repro.c` (hooked in `src/game/lv.c` per-tick PORT block) | **TEMP** forced-repro harness keyed on `g_GlobalTimer`. Actions: `roster`, `jump=<chr>,list=<hexid>,off=<n>`, `bit=<hex>`, `trace=<chr>`, `rename=<chr>,<num>`, `seed=<hi>,<lo>` (D320); `explode=<type>,<dist>` (explosionCreate ahead of Bond) and `cheat=gl|rl|inv|<id>` (game cheat activation) for D252 headless explosion/debris captures. | **TEMP** (D320/D252) |
| `GE_D320R="tick:action;..."` | `port/src/d320repro.c` (hooked in `src/game/lv.c` per-tick PORT block) | **TEMP** forced-repro harness keyed on `g_GlobalTimer`. Actions: `roster`, `jump=<chr>,list=<hexid>,off=<n>`, `bit=<hex>`, `trace=<chr>`, `rename=<chr>,<num>`, `seed=<hi>,<lo>` (D320); `explode=<type>,<dist>` (explosionCreate ahead of Bond) and `cheat=gl|rl|inv|<id>` (game cheat activation) for D252 headless explosion/debris captures. | **dead** (D320 fixed via D329) — removed 2026-09-30 (ROADMAP §7 strip pass, branch `chore/strip-temp-probes`); tombstone — the generator flags it GONE; `port/src/d320repro.c` + its `lv.c` hook deleted |
| `GE_AIMLOG=1` (+ `GE_INPUTSCRIPT` tokens `MDX<n>` `MDY<n>` `RHOLD` `LHOLD` `RREL`) | `port/src/input.c` | D337 aim-model harness: scripted sustained mouse deltas (treated as a grabbed mouse) and aim holds, plus a per-poll line with aim state, active device, the game's drawn crosshair x, `crosshair_x/y_pos` and camera angles. Measures range, edge-scroll and jitter headlessly. | **live** |
| `GE_PERFSTAT=1` | `port/fast3d/gfx_pc.cpp` (`gfx_run`/`gfx_end_frame`), `port/fast3d/gfx_opengl.cpp` (start/end_frame), `src/boss.c` (main loop) | #92/D339 perf probe: per-300-frame averages of render phases (pre / display-list / post / swap+pace), tris and draw batches per frame, game-thread busy time per scheduler message, and a GL_TIME_ELAPSED GPU timer (meaningful only when GPU-bound). | **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) |
@@ -86,12 +87,14 @@ Two classes:
| `GE_PDMOUSEAIM` | `port/src/input.c:487` (`pdMouseAimEnabled`) | Force on/off the PD-style mouse-aim model (D194 lineage); unset = default (on in N64 aim mode unless legacy GEPD aim is selected). | **live** |
| `GE_QUITFRAME` | `port/src/video.c:966` | D344 harness clean self-quit: exits after frame `<n>` completes and swaps (replaces `timeout` kills; the host never exits under an in-flight frame). Cached. | **live** (harness) |
| `GE_CRASHTEST` | `port/src/crash.c` (`crashTestMaybe`, called at the end of `crashInit`) | Deliberate-crash hook for verifying the crash handler: `GE_CRASHTEST=<N>` recurses N frames deep (capped at 64) right after the handler is installed and faults with a null write. `ge007.crash.log` should then show `#00`..`#N-1` in `crashTestRecurse`, then `SDL_main`, CRT and system frames; each frame line carries `va=0x…` for `addr2line -f -p -e build-pc/ge007.x86_64.exe <va>`. Unset/0 = no effect. | **live** (harness) |
| `GE_ROOMPOOL` | `port/src/video.c:371` (`portRoomPoolScale`) | Scales the port's room pool by a float factor — before/after captures, and a user opt-out if the larger pool ever causes trouble on a level. | **live** |
| `GE_BINDPROBE` | `port/src/input.c:885` | One-shot dump of the resolved PC bindings after setup (D383 bind-capture verification). | **live** (diagnostic) |
| `GE_BINDCAPTUREPROBE` (+ `GE_BINDCAPTUREACTION`) | `port/src/optionsoverlay.c:1000,1005` | D383 isolated host→menu smoke probe: when the overlay force-opens, injects a scripted bind-capture keypress (ESC/DEL/F2). Test data dir must be private — capture persists the key. | **live** (diagnostic) |
| `GE_OPTIONSOVERLAY_SECTION` | `port/src/optionsoverlay.c:989` | Isolated screenshot/probe of a nested F10 page by row key (opens that header; never a menu mode). | **live** (diagnostic) |
| `GE_OPTIONSOVERLAY_SELECTBACK` | `port/src/optionsoverlay.c:995` | Screenshot-only: pins the selection to the Back row when the overlay auto-opens, to check it against a normally selected setting on the same page. | **live** (diagnostic) |
| `GE_OPTIONTREEPROBE` | `port/src/optionsoverlay.c:888` | D388: opt-in in-memory walk of the real F10 page tree; writes no settings, binds or profile bytes; restores UI state afterward. | **live** (diagnostic) |
| `GE_PRESETPROBE` | `port/src/optionsoverlay.c:957` (`presetProbe`) | D440: one-shot at overlay init — drives the "Original N64" and "Port defaults" preset rows through the real arm -> confirm activation and checks the "Active" state; logs `GE_PRESETPROBE: PASS/FAIL`. **Writes the live config** (persisted on exit) — use a throwaway data dir. | **live** (diagnostic) |
| `GE_OVPOINTERMAP` | `port/src/optionsoverlay.c:1726` | D391 pointer-map diagnostic: on LMB-down (and every 120 samples) logs SDL mouse coords vs GL drawable pixel coords + the hover row, to separate hit-band/scroll errors from coordinate-space mismatch. | **live** (diagnostic) |
| `GE_FOOTERPROBE` | `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) |
@@ -108,7 +111,7 @@ Two classes:
| `GE_D51` | `port/src/libultra.c:496`, `src/game/model.c:125,244,544` | msgQ 32-slot overflow watch / ModelSlot layout | dead (D51 closed) |
| `GE_D54` | — (all blocks stripped M-32, commit `49ce620a`) | music seq-table ABI / endianness (`ALMidiHdr`) | dead (D54 closed) — fully removed (kept here as a tombstone; the generator flags it GONE) |
| `GE_D154` | `src/game/bg.c:3418,3447,3632` (capped 64 calls) | bg room-GDL call trace (room / gdlidx / vtxoff / op / raw hdr words) | dead (D154 closed) |
| `GE_D227V` | `src/game/sky.c` (`skyPortRenderPoly`, `#ifdef PORT`) | Sky/water per-vertex dump: `S`/`T`/`w`/`1/w`, screen xy, the chosen tc shift `k`, the baked `tc`, and whether the tile capture succeeded. **Found D227**: one sky quad spans ~30,000 texels in S and T but `Vtx.tc` is S10.5 in an `s16` (+/-1024 texels), so 7 of its 8 texture coordinates overflowed and wrapped -- the starburst/"two sky processes" seam. Root cause was a unit error (`unk20`/`unk24` are already S10.5, not texels; M-82's `*32` was spurious). FIXED M-100b. Probe kept inert. | **live** (D227 FIXED, probe kept; useful for D229 water) |
| `GE_D227V` | `src/game/sky.c` (`skyPortRenderPoly`, `#ifdef PORT`) | Sky/water per-vertex dump: `S`/`T`/`w`/`1/w`, screen xy, the chosen tc shift `k`, the baked `tc`, and whether the tile capture succeeded. **Found D227**: one sky quad spans ~30,000 texels in S and T but `Vtx.tc` is S10.5 in an `s16` (+/-1024 texels), so 7 of its 8 texture coordinates overflowed and wrapped -- the starburst/"two sky processes" seam. Root cause was a unit error (`unk20`/`unk24` are already S10.5, not texels; M-82's `*32` was spurious). FIXED M-100b. Probe kept inert. | **dead** (D227 fixed) — the probe lived in the pre-D245 s16 sky path, unreachable since D245 M-201; removed 2026-09-30 (ROADMAP §7 strip pass, branch `chore/strip-temp-probes`); tombstone — the generator flags it GONE |
| `GE_D176` | `src/game/bgfog.c:459,471`, `src/game/sky.c:325,381` (all `#ifdef PORT`) | D176 sky/fog env-match + sky-vert trace (Path B, PR #18) | **live** (D176(a)/(b) OPEN) |
| `GE_D178` | `src/game/front.c:6586,6594` (`#ifdef PORT`) | briefing-data u16 decode trace (blank-objectives, D143/D178) | dead (D178 closed) |
| `GE_D56` | `src/game/model.c:227,830` | watch `Model` raw-offset reads | dead (D56 closed) |
@@ -132,19 +135,19 @@ Two classes:
| `GE_D71LOG` | `port/fast3d/gfx_pc.cpp:645` | D71 fast3d trace | dead (D71 closed) |
| `GE_D75` | `src/game/title.c` `sub_GAME_7F007F30` (capped 8) | D75 Bug 2 gun-barrel model probe: logs `chrModelInstance`/`gunModelInstance` ptr+obj+numMatrices, `render_pos`, `renderData.mtxlist`, `g_GfxMemPos`, `osVirtualToPhysical(render_pos)` | **LIVE** (D75 Bug 2 OPEN — see §F "D75 Bug 2 — RUNTIME PROBE") |
| `GE_D305` | `src/snd.c:420` (`sndHandleEvent` desync branch, `#ifdef PORT`) | D305: logs the sound-preemption desync state when it is detected (after the NULL-guard fix made the crash itself impossible) — confirms whether `Game.AllUnlocked` correlates or was coincidental, and whether it recurs. Inert unset. | **live** (D305 monitoring) |
| `GE_D309` | `src/game/chrai.c:810,3424`, `src/game/chraction.c:10139` (all `#ifdef PORT`) | D309 freeze probe: logs AI label-not-found restarts (`chrai.c`) and the run-to-bond persistent-list step-on path (`chraction.c`, capped 400 lines) — the diagnostics used to root-cause the elusive complete freeze (D309/D310). Inert unset. | **live** (D309/D310 fixed — kept for recurrence) |
| `GE_D309` | `src/game/chrai.c:810,3424`, `src/game/chraction.c:10139` (all `#ifdef PORT`) | D309 freeze probe: logs AI label-not-found restarts (`chrai.c`) and the run-to-bond persistent-list step-on path (`chraction.c`, capped 400 lines) — the diagnostics used to root-cause the elusive complete freeze (D309/D310). Inert unset. | **dead** (D309/D310 fixed) — removed 2026-09-30 (ROADMAP §7 strip pass, branch `chore/strip-temp-probes`); tombstone — the generator flags it GONE |
| `GE_D314` | `port/src/optionsoverlay.c:681` (cached `s_d314Enabled`) | D314 F10-flicker probe: logs the overlay's viGetY / visible-row count / scroll / selection each poll. Inert unset; strip candidate once D314 is triaged. | **live** (D314 pending triage) |
| `GE_D318R` | `port/src/random.c:69,147` (`#ifdef PORT`) | PRNG golden probe (D284/D318): logs the first 4000 gameplay-PRNG draws with tick + caller return address, and every `randomSeed` set — verifies the bit-exact port of `random.s` and which call sites consume the stream. Inert unset. | **live** (D284 verification) |
| `GE_RSEED` | `port/src/random.c:134` (`#ifdef PORT`) | Test-only boot-seed override: `GE_RSEED=<hex64>` pins `g_randomSeed` at first seed instead of the wall-clock-derived value — for determinism and N64-parity tests. Inert unset. | **live, test-only** |
| `GE_RSEED_LV` | `port/src/d318watchdog.c:232` (`#ifdef PORT`) | One-shot level-start seed log: logs `g_randomSeed` at the first tick after level start (optionally armed for a specific tick via `<hex>@<tick>`) — ties a saved/loaded game's PRNG state to a level for D318-class repro. Inert unset. | **live** (D318 verification) |
| `GE_D318T` | `port/src/d318watchdog.c:270` (`#ifdef PORT`) | D318 watchdog timeline probe: per-tick log of the Facility ai_22 softlock signature window (c78 list/offset/attack state, c67 health) while the watchdog watches. Inert unset. | **live** (D318 verification) |
| `GE_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_D318W` | `port/src/d318watchdog.c` (`d318WatchdogTick`, `#ifdef PORT`) | **The D318/D320 pin detector, ON by default, DETECT-AND-LOG ONLY since 2026-09-30.** Watches the Facility ai_22 / ai_19 and Control ai_9 hold-pose pin signatures (findings D318/D320) and logs `D318W:` lines (first detection, confirmation after 600 ticks, periodic reminders while held). It used to re-seed the attack via `sub_GAME_7F025560`; that recovery was removed because D329 fixed the cause, so a hit now means a D329 regression. `GE_D318W=0` silences it. | **live** (D329 regression detector) |
| `GE_D294` | `src/boss.c:498` (`#ifdef PORT`) | One line per stage reset: stage number + current MA malloc value (D294 pool investigation). | **live** (D294) |
| `GE_D294BANK` | `src/game/bg.c:605` | D294: restarts the bank-window counter on stage change (pairs with the `GE_D104` windowing). | **live** (D294) |
| `GE_D303` | `port/fast3d/gfx_pc.cpp:1717` | D303: logs up to 200 tri emits with per-vertex bbox + w-sum (degenerate-triangle hunt). | **live** (D303) |
| `GE_D307` | `src/game/prop.c:709` | D307: load-time dump of every CCTV record's inputs + derived orientation, for word-for-word PC-vs-ROM comparison. | **live** (D307) |
| `GE_D318B` (+ `GE_D318B_ALL`, `GE_D318B_CLK`) | `port/src/d318bprobe.c:91,97,210` | D318/D320 boundary probe suite (M-199): per-tick ground truth of the aim-hold/derail/anim-pin state machine for env-selected chrs (`"5,9"`, max 4); `_ALL` logs every ACT_ATTACK tick; `_CLK=N` locksteps N sim ticks per rendered frame. Read-only. **TEMP — remove when D318/D320 conclude.** | **TEMP** (D318/D320) |
| `GE_D318B` (+ `GE_D318B_ALL`, `GE_D318B_CLK`) | `port/src/d318bprobe.c:91,97,210` | D318/D320 boundary probe suite (M-199): per-tick ground truth of the aim-hold/derail/anim-pin state machine for env-selected chrs (`"5,9"`, max 4); `_ALL` logs every ACT_ATTACK tick; `_CLK=N` locksteps N sim ticks per rendered frame. Read-only. **TEMP — remove when D318/D320 conclude.** | **dead** (D318/D320 fixed via D329) — removed 2026-09-30 (ROADMAP §7 strip pass, branch `chore/strip-temp-probes`); tombstone — the generator flags it GONE; `port/src/d318bprobe.c` + `port/include/d318bprobe.h` deleted. NB `GE_D318B_CLK` (sim-ticks-per-frame lockstep, used for the D329 pin repro) went with the suite |
| `GE_D329` (+ `GE_D329LOG`) | `src/game/model.c:3516,3519` (`#ifdef PORT`) | **The D329 anim-pin fix gate — ON by default** (unlike the other probes here): re-derives a stale pinned frameb when the endframe no longer blocks it. `GE_D329=0` A/B-disables; `GE_D329LOG` logs each trigger (cap 2000). Cached. | **live** (D329 fix) |
## Notes
+112 -28
View File
@@ -83,40 +83,124 @@ Two useful existing properties:
### Phase 0 — Baseline & ground truth (port-only)
Goal: know exactly what the default preset does before touching anything.
**RESOLVED (2026-09-12, commit 8f2f9e1d, D248).** The ~30 fps default was
confirmed a **port artifact**, not original N64/GE behavior: `sched.c`'s
`__scHandleRetrace` read a per-client "every retrace vs. every other
retrace" flag via a hardcoded `*((s32*)client + 2)` byte offset (`sched.c`
~line 350) that only lands on the right field when `OSScClient` is 8 bytes
(32-bit pointers, true on N64). On this 64-bit port the struct is 16 bytes,
so the read landed inside the gfx client's own `msgQ` pointer (always
non-NULL), silently collapsing the gfx client onto the audio client's 30Hz
path even though the VI-retrace pacemaker itself always ticked at a correct
60Hz. Fix (`#ifdef PORT`, `sched.c` ~line 348): index the struct
(`client[1].next`) instead of the hand-rolled offset, so it scales with
pointer width. Verified live via the `Video.DisplayFPS` overlay: rock-stable
30 FPS pre-fix, 60 FPS post-fix, same build/scene. Same bug class as
D122/D126/D132/D209 (pointer-width struct growth), just in the scheduler.
- [ ] Measure actual sim rate and present rate of the current default build
(speedgraph + a `video.c` counter; add a periodic log line
`sim=..Hz render=..Hz`). Confirm NTSC vs PAL build.
- [ ] Find why the default is ~30 rather than the console's 60 Hz. Candidates:
- tick-thread 4 ms chunked-sleep drift under host load;
- `__scTaskReady` double-buffer check (`sched.c`) stalling a task every
other retrace;
- `__scHandleRetrace`'s client flag read `*((s32*)client + 2)` is
**out-of-bounds of `OSScClient`** (struct is only `next`+`msgQ`; the
decomp even says "im wrong size"). For `gfxClient[0]` it happens to
read `gfxClient[1].next` (zero) — but this is layout luck, worth a
documented finding either way.
- [ ] Record a Dxxx finding with the measured rates and root cause.
- [ ] **Decision gate:** if 30 fps turns out to be a port artifact rather than
original behavior, document it and decide with data whether the default
preset keeps the current pace (test continuity) or is fixed. Default
assumption: keep current behavior as the "original" preset either way;
a fix becomes its own item.
**Decision-gate answer:** the default preset is now the console's real
60Hz(NTSC)/50Hz(PAL) rate — decision #1 above ("default preset = the
original pace") is satisfied by this fix, not by preserving the halved
30fps. All prior playtest/determinism baselines predate this fix and should
be treated as pre-D248.
- [x] Find why the default was ~30 rather than the console's 60 Hz — root
cause above; §F **D248**.
- [x] Record a Dxxx finding with the measured rates and root cause — D248.
- [ ] Measure actual sim rate and present rate with a dedicated periodic log
line (`sim=..Hz render=..Hz`) rather than the one-off `DisplayFPS`
overlay reading used to verify D248. Not yet built — still open if a
standing measurement (vs. a spot-check) is wanted before Phase 1/2
work lands.
- [x] **Decision gate** — resolved above.
### Phase 1 — PD-style pacing hardening (port-only)
Makes the default preset stable and drift-free; no game-code changes.
- [ ] Replace the free-running tick grid in `portTickThread` with a
wall-clock accumulator + remainder carry (PD `frametimeCalculate` math):
post ticks when real time says they're due, carry the remainder, bounded
catch-up after hitches (D155 philosophy). No drift under load, no burst
of back-to-back ticks.
- [ ] `video.c`: proper frame limiter for fractional caps (spin+sleep hybrid),
triple-buffered swap chain, and the PD safety rule (vsync off + no cap →
force a max cap so "unlocked" can't spin).
- [ ] Config: keep `Video.VSync` / `Video.FpsCap`; add whatever Phase 2 needs.
**Audit (2026-09-12, post-D248):** more of this than the plan assumed
already exists. Verified by reading the code directly (not just the
delegate's first pass, which mis-read one of these — see below):
- [x] **Accumulator + remainder carry, tick thread.** `portTickThread`
(`port/src/libultra.c` ~294-313) already schedules off `g_nextTickUs +=
g_tickIntervalUs` — the *scheduled* time, not `now` — so it doesn't
drift under load. It resyncs to `now + interval` only when a tick is
already overdue (`g_nextTickUs <= now`), which is a bounded-catch-up
behavior, not a naive reset. This already matches the PD
`frametimeCalculate` accumulator pattern.
- [x] **Accumulator + remainder carry, present-side limiter.**
`sync_framerate_with_timer` (`port/fast3d/gfx_sdl2.cpp` ~403-428) uses
the same shape: `next = previous_time + interval` (schedule-based, not
wall-clock-based), a sleep-then-busy-wait hybrid to hit the deadline
precisely, and only snaps `previous_time` to the real wake time `t`
when the overshoot exceeds ~1ms (`t - next < 10000` in its 100ns units)
— i.e. it already carries the remainder in the normal case and only
gives it up after a real hitch. (An earlier audit pass mis-read this as
"no remainder carry, resets every frame" — it does not; re-verified by
reading `gfx_sdl2.cpp:403-428` directly.)
- [ ] **Triple-buffered swap chain.** Not present — presentation uses a
standard SDL/GL double-buffer swap (`SDL_GL_SwapWindow`,
`gfx_sdl_swap_buffers_begin`, `gfx_sdl2.cpp` ~436-444). Not started.
- [ ] **PD safety rule (vsync off + no cap → force a max cap).** Not present.
The only existing floor/ceiling logic is the D186 guard in
`gfx_sdl_set_target_fps` / `video.c` (~289-292, ~464-467), which goes
the *other* direction — it refuses caps below 30 (because pacing still
runs inline on the sim thread, see Phase 2 note below) — there is no
"vsync off AND cap==0" case that forces a ceiling. Not started.
- [x] Config: `Video.VSync` / `Video.FpsCap` already registered
(`video.c:106-107`) and live-reapplied (`video.c` ~340-349).
**Net:** the hard part of Phase 1 (drift-free accumulator scheduling) is
already done, on both the sim-tick and present-side clocks. What's left is
narrow: a safety-cap rule and, if wanted, triple buffering — plus whatever
Phase 2 needs on top.
**Phase 2 first slice, scoped (2026-09-12).** The real gap for a 120fps
*presentation* preset isn't pacing math — it's that `gfx_run()`
(`gfx_pc.cpp:3179-3232`) composites straight into the default framebuffer
(fb 0) once per sim tick and then calls `gfx_wapi->swap_buffers_begin()`
once; there's no mechanism to show that same completed frame again on an
extra vsync between sim ticks. The existing `GfxRenderingAPI` already has
everything needed to do this **without any new GL primitives or a second
thread/context**:
- `copy_framebuffer(fb_dst, fb_src, left, top, flip_y, use_back)`
(`gfx_opengl.cpp:1270`) already blits the real window backbuffer
(`fb_src == 0` + `use_back = true` reads `GL_BACK`, confirmed
`gfx_opengl.cpp:1309-1311`) into an arbitrary allocated FBO/texture, and
can blit back the other way (`fb_dst == 0` writes straight into the real
default framebuffer, `gfx_opengl.cpp:1214`'s `fb_id == 0 -> bind 0`
convention).
- `gfx_pc.cpp` already keeps a couple of always-allocated internal
framebuffers this same way (`game_framebuffer`,
`game_framebuffer_msaa_resolved`, created at `gfx_pc.cpp:3069-3070`) — a
new "last presented frame" capture FBO is the same pattern, one more
entry.
- The tick clock (`portTickThread`) runs on its own pthread, independent of
the render/sim thread that calls `gfx_run`/`swap_buffers_begin` — so
spending extra wall-clock time *inside* `swap_buffers_begin` presenting a
duplicate frame does not touch the tick schedule at all; it only changes
how much idle time the render thread burns before the next tick message
is waiting for it. Sim rate stays exactly invariant by construction, per
decision #3.
**Smallest concrete slice:** capture the just-finished frame into a new
internal FBO right before the real `swap_buffers_begin()` call
(`gfx_pc.cpp:3230-3231`, via `gfx_rapi->copy_framebuffer(captureFb, 0, -1,
-1, false, true)`); when an experimental `Video.PresentRate` config knob
requests double-rate (2x — e.g. 120 from a 60Hz NTSC sim, 100 from 50Hz
PAL), have `gfx_sdl_swap_buffers_begin` (`gfx_sdl2.cpp`) do the normal swap,
then — using the same schedule-accumulator shape as
`sync_framerate_with_timer` — sleep to the halfway point of the tick
interval, blit the capture FBO back into fb 0
(`gfx_rapi->copy_framebuffer(0, captureFb, -1, -1, false, false)`), and
`SDL_GL_SwapWindow` again. Default (`Video.PresentRate = 0`) must take none
of these code paths, so the default preset stays byte-identical to today.
Restrict the first slice to exactly {0 = off, 2 = double}; don't generalize
to arbitrary/uncapped multipliers yet — that needs the vsync-off safety cap
from the Phase 1 list above first. No options-menu UI in this slice
(config-file-only); that's a fast follow once the mechanism is playtest-
verified not to tear/stutter.
### Phase 2 — Experimental presentation rates (port-only)
+10 -2
View File
@@ -99,7 +99,7 @@ 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."
D283,FIXED,"Steam Deck first-run preset (D-unnumbered in port/src/video.c/main.c, videoApplySteamOSDefaults()) did not apply for a user who added the game via Steam… —","FIXED (2026-09-30, port-only) —"
D280,CLOSED,"D236 wall, ninth pass —",CLOSED (2026-09-26 bookkeeping:
D275,CLOSED,"D236 wall, eighth pass —",CLOSED (2026-09-26 bookkeeping:
D274,CLOSED,"D236 wall, seventh pass —",CLOSED (2026-09-26 bookkeeping:
@@ -263,7 +263,7 @@ D383,CLOSED,PD-style keyboard rebinding in both options UIs (2026-09-27).,CLOSED
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):
D387,FIXED,M7 AllUnlocked's read-time fake unlocks leak into saved EEPROM (2026-09-27).,"FIXED 2026-09-30 (port-only per-slot raw/seen merge on EEPROM write, libultra.c geD387MergeSlotWrite; isolated ON-vs-OFF byte-identity matrix passed)."
D388,CLOSED,Nest functional binding/HUD menus and remove redundant M5 toggle (2026-09-27).,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
D389,CLOSED,F10 footer clipping and ambiguous submenu links (2026-09-27).,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
D390,CLOSED,F10 pinned Back selection did not visibly change (2026-09-27).,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
@@ -297,3 +297,11 @@ D431,FIXED,Light fixtures never reached lightFixtureBreak on PC:,FIXED in tree (
D432,FIXED,"Gunshot sounds ""off"" after D427:",FIXED in tree (gate offset rounded to odd at 1 tick/frame); measured shot/sound cadence identical to N64 rules; residual audible difference vs 1964 tracked as D433.
D433,OPEN,"Slight audible difference in automatic-gun shot sound vs GEPD/1964 after cadence was matched (user by-ear, Caverns).",OPEN (not a timing issue; audio layer suspected).
D434,FIXED,Raw ((u8*)&g_Textures[n])[0] & 0xf hit-type reads are wrong on PC; latent OOB once D431 returned real texture ids (found by sub-agent review).,"FIXED (.hitTexture at 3 sites, port-guarded); live check owed."
D440,FIXED,"""Original N64"" / ""Port defaults"" one-press presets in the F10 overlay and front-end PC Options (+ -n64preset/-portpreset).",FIXED (port-only; headless-verified; user live accept owed).
D441,PARTIAL,64-bit pointer-width sweep (part 1):,"PARTIAL (mechanical widening landed, build/run verified on Bunker and Silo; census/provenance table for the remaining sites still owed)."
D442,FIXED,Game.AllUnlocked reworked into a pure RAM/query-time override:,FIXED (build and eep-diff verified; level-select/cheat-menu live check owed).
D443,FIXED,"D357 settings-display cleanup (mouse ×, whole-% deadzone and volume steps, FOV in horizontal degrees, MSAA 16×) plus a ""Restart game"" action (orderly D344 quit…",FIXED (port-only; reset probes pass; both-UI visual check and one Restart click owed; Linux relaunch path not compiled).
D444,FIXED,Front-end PC Options:,FIXED (build-verified; human mouse test owed)
D447,FIXED,Pillarbox output mode (Video.AspectMode = Original) for the Original N64 preset; crop overscan no longer cancels Wide/Cinema.,"FIXED (build + 3 captures verified; Wide/Cinema, 16:9 Ratio, mouse and 1964 comparison owed to a human)"
D445,FIXED,Crouched-rifle firing animation (crouched_rifle_firing_animation_groupA) never resolved on PC -> SIGSEGV in modelConstrainOrWrapAnimFrame when an MP player cro…,"FIXED — layout only (§D5 linker adjacency, D164 class):"
D446,FIXED,Couch 2P watch-menu text (the MP pause page) differs by +-1-2 per channel between builds whose only difference is data layout (2026-09-29).,FIXED — port-only (fast3d):
1 # GENERATED by tools_pc/gen_findings_index.py -- do not hand-edit.
99 D279
100 D281
101 D282
102 D283
103 D280
104 D275
105 D274
263 D384
264 D385
265 D386
266 D387
267 D388
268 D389
269 D390
297 D432
298 D433
299 D434
300 D440
301 D441
302 D442
303 D443
304 D444
305 D447
306 D445
307 D446
+260 -12
View File
@@ -448,7 +448,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
| 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 | PARTIAL (2026-09-29 bookkeeping: mostly fixed as a side effect of D243, user-confirmed; a small ~1-in-10 offset residual remains, tracked in docs/ROADMAP.md §2). Earlier: **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 |
| 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. **Fallback tail hardened (2026-09-30, `chore/romdata-valloc`):** on Windows a failed fixed-address `VirtualAlloc` now logs `GetLastError` + a `VirtualQuery` of whatever occupies `0x10000000` and aborts boot cleanly instead of limping into the heap copy and a later AV; success path unchanged; POSIX `mmap` fallback untouched (still degrades) |
| 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.… |
@@ -498,7 +498,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
| 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. |
| 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 | FIXED (2026-09-30, port-only) — hardware (DMI) detection + `STEAMOS`, applied after `configLoad()` once per ini (`Video.DeckPresetApplied`) while the display keys are untouched; `GE_FAKE_DECK`-verified on Windows, real-Deck check owed. (Was OPEN: first-run-only preset skipped by a Desktop Mode launch.) |
| 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 |
@@ -671,7 +671,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
| 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.** |
| D387 | **M7 AllUnlocked's read-time fake unlocks leak into saved EEPROM (2026-09-27).** full `## D387` entry (fix section `### D387 Fix` at file tail) | FIXED 2026-09-30 (port-only per-slot raw/seen merge on EEPROM write, `libultra.c` `geD387MergeSlotWrite`; isolated ON-vs-OFF byte-identity matrix passed). Not repaired: fakes already persisted by older builds. Limitation: cheats earned while ON are not persisted. Live accept owed. |
| 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. |
@@ -705,6 +705,14 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
| D432 | **Gunshot sounds "off" after D427: sound-trigger gate (`SoundTriggerRate`) is per-tick on PC but frame-granular on the N64 (user: Cavern, Mac-10 run).** — full `## D432` entry at file tail | FIXED in tree (gate offset rounded to odd at 1 tick/frame); measured shot/sound cadence identical to N64 rules; residual audible difference vs 1964 tracked as D433. |
| D433 | **Slight audible difference in automatic-gun shot sound vs GEPD/1964 after cadence was matched (user by-ear, Caverns).** — full `## D433` entry at file tail | OPEN (not a timing issue; audio layer suspected). |
| D434 | **Raw `((u8*)&g_Textures[n])[0] & 0xf` hit-type reads are wrong on PC; latent OOB once D431 returned real texture ids (found by sub-agent review).** — full `## D434` entry at file tail | FIXED (`.hitTexture` at 3 sites, port-guarded); live check owed. |
| D440 | **"Original N64" / "Port defaults" one-press presets in the F10 overlay and front-end PC Options (+ `-n64preset`/`-portpreset`).** — full `## D440` entry at file tail | FIXED (port-only; headless-verified; user live accept owed). Follow-up: preset stretches 4:3 in non-4:3 windows (no pillarbox mode). |
| D441 | **64-bit pointer-width sweep (part 1): 17 files widened `(u32)`/`(s32)` pointer casts to `uintptr_t` under `#ifdef PORT` (ABI/layout exception; ROADMAP §2 "Latent 64-bit ABI hazards").** — full `## D441` entry at file tail | PARTIAL (mechanical widening landed, build/run verified on Bunker and Silo; census/provenance table for the remaining sites still owed). |
| D442 | **`Game.AllUnlocked` reworked into a pure RAM/query-time override: #ifdef PORT hooks in `fileGetIsCheatUnlocked` and `fileIsStageUnlockedAtDifficulty` (every stage reports COMPLETED); the EEPROM patch and the D387 merge are removed (RULE-2-SIGNOFF, maintainer opt-in feature).** Full `## D442` entry at file tail | FIXED (build and eep-diff verified; level-select/cheat-menu live check owed). |
| D443 | **D357 settings-display cleanup (mouse ×, whole-% deadzone and volume steps, FOV in horizontal degrees, MSAA 16×) plus a "Restart game" action (orderly D344 quit, then relaunch).** Full `## D443` entry at file tail | FIXED (port-only; reset probes pass; both-UI visual check and one Restart click owed; Linux relaunch path not compiled). |
| D444 | **Front-end PC Options: clickable Previous/Next page controls; hover no longer overrides the wheel/page selection; stale All-unlocked comments refreshed.** — full `## D444` entry at file tail | FIXED (build-verified; human mouse test owed) |
| D447 | **Pillarbox output mode (`Video.AspectMode` = Original) for the Original N64 preset; crop overscan no longer cancels Wide/Cinema.** — full `## D447` entry at file tail | FIXED (build + 3 captures verified; Wide/Cinema, 16:9 Ratio, mouse and 1964 comparison owed to a human) |
| D445 | **Crouched-rifle firing animation (`crouched_rifle_firing_animation_groupA`) never resolved on PC -> SIGSEGV in modelConstrainOrWrapAnimFrame when an MP player crouch-fires a rifle (2026-09-28).** — full `## D445` entry at file tail | FIXED — layout only (§D5 linker adjacency, D164 class): `crouched_rifle_firing_animation_group1` has no zero terminator and on N64 falls through into `...groupA` (source-order .data); GCC places groupA first, so groupA[0] kept the raw anim offset 0xB84. `#ifdef PORT` one array {right, left, terminator} + `groupA` alias (chr.c/chr.h); N64 resolution restored. |
| D446 | **Couch 2P watch-menu text (the MP pause page) differs by +-1-2 per channel between builds whose only difference is data layout (2026-09-29).** — full `## D446` entry at file tail | FIXED — port-only (fast3d): `GL_TEXTURE_MAX_ANISOTROPY` is per-texture-object state that `gfx_opengl_set_sampler_parameters` set only when mipmapped and never reset; a GL texture id freed by `gfx_texture_cache_delete_range` (unordered_map order = texture address order) and reused for a non-mipmapped glyph kept 4x. Now always written (level or 1). Render-only; sim never affected. |
Phase 2 replaced the Phase-1 demo loop with the real `mainproc()` on real OS
@@ -10622,7 +10630,7 @@ D243 (cutscene shake/duplication, its own CONSOLIDATED NEXT STEPS block appears
**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).** The first `goldeneye-pc-port-*-win64.zip` from CI (bundle-win.sh) shows the Rare/Nintendo logos (compiled-in `assets/rarewarelogo.c`) then AVs at `0x6b157a88`/`0x70157a88` — symbolised: `modelPromoteNodeOffsetsToPointers` (`model.c`) ← `load_object_fill_header` (`objecthandler_2.c`) ← first prop/item model load. Root cause is **not** a code regression: the port loads PC-layout model geometry from `data/pcmodels-<region>/{pcmodels.bin,manifest.csv}` and stage bg/stan from `data/pccg-<region>/{pccg.bin,manifest.csv}`, both produced offline by `tools_pc/d43_emit.py` / `d69_emit.py` from the ROM (see `port/src/pcmodels.c` / `pccg.c`; `pcmodelsReserveSize` logs *"pcmodels.bin not found — model loads will fail"* and returns 0). CI has no ROM so it never runs the emit scripts, and the two `data/` dirs are (correctly) gitignored ROM-derived game data — **cannot be committed or shipped** (converted Nintendo/Rare geometry, DLs, collision, stan nav; = distributing assets). A local build with those dirs present runs clean (verified M-34: 1741 frames, `romdataInit ... mapped at 0x10000000`, `pcmodels: 512 sidecars`, `pccg: 73 sidecars`, no crash — so `0x10000000` is *not* the problem here). The `docs/building.md` sidecar-gen step was also missing entirely. **Fix = release-side, backlogged** (`docs/BACKLOG.md` → "Alpha release"): bundle the emit scripts + their committed inputs as a pure-stdlib-Python asset-prep tool the user runs once against their own ROM (~5 MB output, no MIPS toolchain). Secondary/latent: the fixed-address `VirtualAlloc((LPVOID)0x10000000)` in `romdata.c` has no working fallback (`"using heap copy — direct ROM reads will fail"` then limps into the same crash) — didn't bite M-34 but will on a machine where something occupies `0x10000000`; harden separately (reserve earliest in `main`, or retry low bases and derive all segment math from the base obtained).
**Status (from §F table):** Diagnosed, not fixed — release packaging gap + latent `romdata.c` fallback
**Status (from §F table):** Diagnosed, not fixed — release packaging gap + latent `romdata.c` fallback. **Fallback tail hardened (2026-09-30, `chore/romdata-valloc`):** on Windows a failed fixed-address `VirtualAlloc` now logs `GetLastError` + a `VirtualQuery` of whatever occupies `0x10000000` and aborts boot cleanly instead of limping into the heap copy and a later AV; success path unchanged; POSIX `mmap` fallback untouched (still degrades)
## D180 — Native-PC input pass (M-XX QoL run, `qol/native-pc-input-menu`, port-only).
@@ -11133,7 +11141,7 @@ User played live with `GE_D236ORDER=1` set, walked toward Surface1's tree line,
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}`.
**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`. (Probes stripped `0503d335`; the two scan tools removed 2026-09-30, strip pass.) 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.**
@@ -11709,7 +11717,7 @@ User vs 1964: PC water jittered with player movement, "reset" even standing stil
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<<maskt)` base level, and in the tri path use CI8 geometry (width = line bytes, height = mask period). Headless still-camera A/B (300 frames): old 71 jump spikes, new 0 and a steady per-frame change.
2. **s16 vertex quantisation (the jitter).** The N64 draws sky/water as CPU-built RDP triangles with 32-bit S/T; the port routed them through `Vtx` (s16 `tc` needing a 2^k shift, k=4 for water = half-texel steps; s16 `ob` scaled by a fan-wide `wScale` ≈ 7, ~8 px position steps near the camera). New port-only `G_FLOATVTX_EXT` (0x47, `port/include/floatvtx.h`, loader `gfx_sp_vertex_float` in `gfx_pc.cpp`) passes clip-space positions and folded S/T as floats; `skyPortRenderPoly` (`sky.c`, `#ifdef PORT`) uses it for every sky/water fan, so no tile shift or wScale.
Ruled out on the way: the animation phases advance by `g_ClockTimer` (rate-independent); `PRIM_LOD_FRAC` and per-tile `uls/ult` offsets are handled by fast3d; `WrapFix` pre-wrap is off by default. A/B switches kept: `GE_D245_OLDVTX=1`, `GE_D245_OLDTEX=1`. Golden gate 3/3; Windows + Linux builds clean.
Ruled out on the way: the animation phases advance by `g_ClockTimer` (rate-independent); `PRIM_LOD_FRAC` and per-tile `uls/ult` offsets are handled by fast3d; `WrapFix` pre-wrap is off by default. A/B switches kept: `GE_D245_OLDVTX=1`, `GE_D245_OLDTEX=1`. Golden gate 3/3; Windows + Linux builds clean. **2026-09-30: removed** (ROADMAP §7 strip pass, branch `chore/strip-temp-probes`). Both A/B switches and the pre-fix s16 sky vertex path they selected (plus its `GE_D227V` probe) are gone; the float path is unconditional.
**Follow-up (2026-09-27): coloured dashes.** User playtest found rows of yellow/green/magenta dashes on Frigate's water, looking down near the horizon. The base-level crop in fix 1 had never run: its test used `loaded_texture.line_size_bytes`, which for a LoadBlock is the whole 1400-byte block, so the upload stayed 32x43 while the tri path normalised V by 32. The mip-chain rows (32-42) were sampled as garbage texels. Crop test now uses the tile's line size (the importer's row width): the dumped import is a clean 32x32 tile (indices 0-23). User-verified, golden gate 3/3.
@@ -11847,7 +11855,18 @@ Superseded history below (the original OPEN investigation, root cause not found
**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 in Desktop Mode and switched to Game Mode afterward — resolution stayed at some non-native value instead of the Deck's native 1280×800 (v0.2.1 user testing, 2026-09-15).** Root mechanism, confirmed by code inspection: the preset only applies when `getenv("STEAMOS")` is truthy AND no `ge007.ini` exists yet (`main.c:126-129`, before `configLoad()`); once any ini exists, it always wins over the preset on every future launch (by design, so user F10 changes persist). **Leading hypothesis (not verified on real hardware in this environment):** SteamOS's Desktop Mode (a normal Plasma/X11 session) likely does not export the same `STEAMOS` env var that the Game Mode/gamescope session does, so a Desktop Mode test-launch (a natural step when adding a non-Steam game via its file browser) creates `ge007.ini` with ordinary Linux defaults *before* the user ever reaches Game Mode — permanently skipping the preset for that install, since the ini already exists on the first real Game Mode launch. This exact caveat was already known internally (`docs/HANDOFF.md`: "a Deck that already has an ini needs `-fresh` or F10 to pick the preset up") but was never surfaced in user-facing docs (README/release notes) — same class of gap as the stale D230 release-notes reference. **Workaround (lighter than `-fresh`, which also wipes the save):** F10 → Resolution → 1280×800 manually; takes seconds, no data lost.
**Status (from §F table):** 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.
**Status (from §F table, before the fix):** 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.
**2026-09-30 fix (port-only; `port/src/video.c`, `port/src/main.c`, `port/include/video.h`):** the preset no longer depends on `STEAMOS` or on the ini being absent.
- **Detection by hardware:** `videoDetectSteamDeck()` reads `/sys/class/dmi/id/{board_vendor,sys_vendor,product_name}` (`#if defined(__linux__)`): vendor `Valve` + product `Jupiter` (LCD) or `Galileo` (OLED). `STEAMOS` stays as an extra signal. `GE_FAKE_DECK=1` forces the Deck path on any host, `=0` forces it off (docs/dev/GE-ENV-PROBES.md).
- **Runs after `configLoad()`** (`videoApplySteamDeckPreset()`, replacing the pre-load `videoApplySteamOSDefaults()`), so an ini created by an earlier Desktop Mode launch is seen and handled.
- **Once per ini, without clobbering choices:** new key `Video.DeckPresetApplied` (0/1, default 0). On a detected Deck with the flag 0: if `Video.Fullscreen=0` and `Window.Maximized=0` (the generic windowed state — `Window.Width/Height` can't be used as an "untouched" test, `videoSaveWindowState` rewrites them from the live window on every clean exit) it sets fullscreen + 1280x800; otherwise it leaves the display alone. Either way the flag becomes 1 and the ini is saved, so later F10 choices always win. A Desktop Mode user who deliberately ran windowed gets fullscreen once and can switch back in F10 (it then sticks).
- **Preset narrowed to display keys.** The old preset also wrote VSync 1 / MSAA 2 / draw+LOD 250; those are the generic defaults now, so writing them again on an existing ini could only clobber a user's choice. Dropped.
- Non-Deck hosts: behaviour unchanged except the new key is added to the ini once (the normal "newly-registered keys" rewrite).
**Verified (Windows, `GE_FAKE_DECK`, throwaway data dir, `GE_QUITFRAME=60`):** (1) windowed 960x540 ini, no env → no Deck log, `DeckPresetApplied = 0`; (2) same ini + `GE_FAKE_DECK=1` → `applying Deck preset (fullscreen, 1280x800)`, ini `Fullscreen = 1`, `Width = 1280`, `Height = 800`, `DeckPresetApplied = 1`; (3) then `Fullscreen = 0` hand-set + `GE_FAKE_DECK=1` → `preset already considered`, stays windowed; (4) ini with `Fullscreen = 1`, 960x540, no flag + `GE_FAKE_DECK=1` → `display already user-set ... preset skipped`, size kept, flag set. Windows build clean; the `__linux__` DMI path syntax-checked with WSL Ubuntu-24.04 gcc (`-fsyntax-only`, `video.c` + `main.c`, no new warnings). **Not verified:** a real Linux build/run and real Deck hardware (DMI strings, Desktop→Game Mode flow). Human check on a Deck: delete `Video.DeckPresetApplied` from an existing windowed ini (or start from a Desktop-Mode-created one), launch in Game Mode, expect fullscreen and the `video: Steam Deck (DMI Valve Jupiter/Galileo)` log line. README "Steam Deck" section still describes the old first-run-only behaviour (lead to update).
**Status:** FIXED (port-only; `GE_FAKE_DECK`-verified on Windows; real-Deck check owed).
## 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…
@@ -11886,7 +11905,7 @@ Superseded history below (the original OPEN investigation, root cause not found
**Status (from §F table):** OPEN — visually confirmed and precisely bounded (frames ~562-741 of a bare `-level_20` boot); M-152 narrowed to two live candidates (fast3d backface-cull NaN leak, or the D106/D271 portal-BFS family) via static review; two M-155 probe runs (ungated, then frame-gated) were both inconclusive — the `any_behind_camera=1` case's bbox math is a false lead, and the `any_behind_camera=0` subset found no match for the target color. Root cause still not identified; a live human repro is now the more promising next step over further headless probing. Promoted from the long-parked `GRAPHICS-BACKLOG.md` D171 entry with real evidence.
**v0.4.0 re-check (2026-09-23, native Windows, current `release/v0.4.0` build): does NOT reproduce.** Bare `-level_20` at 640x480 with a defaults ini, `GE_PCDUMP` 400–1200 every 25 frames, then 500–950 every 5 (91 frames covering the whole rocket pan and the cut to first person). A pixel scan for the reported flat `RGB(83,62,44)` ±6 finds only frames 825–840, and those are Bond's own head fading out as the camera pushes into first person (normal GE behaviour, eyeballed). No stray triangle anywhere in the window. **D271 A/B:** temporarily restoring the pre-D271 portal-bounds limit (1e5 instead of 1e38, `bg.c`) gives the identical result, so the D271 portal-culling fix is not what removed it. The M-155 probe runs (2026-09-16) had already missed the colour, so it has not reproduced headlessly since it was written up. Either it was fixed incidentally before 09-16 (not identified), or it is intermittent, which fits the user's "randomly". **Next:** the playtest brief item (play Silo normally, F12 if seen). If it isn't seen, close as not reproducible.
**v0.4.0 re-check (2026-09-23, native Windows, current `release/v0.4.0` build): does NOT reproduce.** Bare `-level_20` at 640x480 with a defaults ini, `GE_PCDUMP` 400–1200 every 25 frames, then 500–950 every 5 (91 frames covering the whole rocket pan and the cut to first person). A pixel scan for the reported flat `RGB(83,62,44)` ±6 finds only frames 825–840, and those are Bond's own head fading out as the camera pushes into first person (normal GE behaviour, eyeballed). No stray triangle anywhere in the window. **D271 A/B:** temporarily restoring the pre-D271 portal-bounds limit (1e5 instead of 1e38, `bg.c`) gives the identical result, so the D271 portal-culling fix is not what removed it. The M-155 probe runs (2026-09-16) had already missed the colour, so it has not reproduced headlessly since it was written up. Either it was fixed incidentally before 09-16 (not identified), or it is intermittent, which fits the user's "randomly". **Next:** the playtest brief item (play Silo normally, F12 if seen). If it isn't seen, close as not reproducible. The `GE_D288` probe was**removed 2026-09-30** (ROADMAP §7 strip pass, branch `chore/strip-temp-probes`).
**2026-09-25 playtest (user, Windows, one ~46-min session Silo → Caverns, `GE_D322=1 GE_D204=1`, build `7baf4900`):** user reports the Silo intro clean. Closed; the specific fixing commit was never isolated.
@@ -12078,7 +12097,7 @@ This is a **perfect, smooth white-hot → yellow → orange → deep-red → fad
- **Fix:** learn which compiled `G_SETTIMG` slots are IMAGESEG references on the first sync (marker still present), store them in `s_imgslot[][]`, and re-copy those slots on every call. Semantics now match N64 (fresh pointers per stage). Port-only; no game code touched.
- **Verified:** user playtest, boot → title → Facility → past the lockers, grenades/mines/tanks all correct ("yeah that fixed it"). `D252SYNC` (TEMP, `GE_D252POOL`) logged `32 texture slots re-synced, 32 changed` at the Facility load, i.e. all 32 were stale before the fix. Golden gate 3/3.
- **Scope:** covers every D252-family report that goes through these 17 global DLs (explosion parts, smoke, debris; `globalDL_0x000` too). The M-124/M-157 bullet-impact spark sightings are likely the same stale-pointer effect but were not separately re-tested.
- **TEMP probes to remove when the release settles:** `GE_D252POOL` (`image.c`, `initmttex.c`, `D252SYNC` in `gimgfixup.c`); `GE_D157` F12 burst (`gfx_pc.cpp`, `video.c`); `GE_D320R explode=/cheat=`.
- **TEMP probes to remove when the release settles:** `GE_D252POOL` (`image.c`, `initmttex.c`, `D252SYNC` in `gimgfixup.c`); `GE_D157` F12 burst (`gfx_pc.cpp`, `video.c`); `GE_D320R explode=/cheat=`. **2026-09-30: removed** (ROADMAP §7 strip pass, branch `chore/strip-temp-probes`). All three, incl. the whole D157/D157T/D157I/D157TB family.
**Status: FIXED (M-201, user-verified 2026-09-26).**
@@ -12604,7 +12623,7 @@ This confirms the M-160/M-164/M-165 hypothesis (`## D173`'s own consolidated-nex
**Scope note (§7 drift check):** v1's sign-off scope ("exactly one AI list affected") was wrong in one respect — PRINT records also exist in level-local lists, but with a different shape (embedded string), where the original NUL-scan is already correct. v2's *behavioral* change is still confined to global-list PRINTs: `m_RunToBondPersistent`'s two records. Local-list behavior is byte-identical to the original decomp — Trevelyan reverts to his pre-fix (working) pathing. Drift disclosed to user in-thread when the regression was reported; same sign-off intent (fix global PRINT sizing, no other behavior change).
**Cross-refs:** D309 (this fix neutralizes its spin mechanism — the teardown-ordering open question in D309's "why PC and not N64" remains, but reaching that state is now harmless); GE_D309 probes (`chrai.c` `ai()`/`chraiGoToLabel`, `chraction.c` `chrGoToBond`) remain as diagnostic tools.
**Cross-refs:** D309 (this fix neutralizes its spin mechanism — the teardown-ordering open question in D309's "why PC and not N64" remains, but reaching that state is now harmless); GE_D309 probes (`chrai.c` `ai()`/`chraiGoToLabel`, `chraction.c` `chrGoToBond`) remain as diagnostic tools. **2026-09-30: removed** (ROADMAP §7 strip pass, branch `chore/strip-temp-probes`).
**Perf gauge (2026-09-19, release review — M-192 item 6):** v2's up-front `chraiGetAIListID()` call in `chraiGoToLabel` (added so the sizer knows global-vs-local) runs on EVERY label jump; originally that lookup ran only in the rare EndList-not-found branch. Cost per call: ≤ (level-local lists + ~20-entry `g_GlobalAILists`) pointer comparisons ≈ tens of ns. Worst case for a heavy level (Train/Archives, ~50 active guards each executing 1–2 goto-family records per tick): ~1000 calls/tick × ~40 comparisons ≈ 40 µs/tick — under 0.3% of a 16.7 ms frame, and two orders of magnitude below the D250 `getenv` anti-pattern (which was per-TRIANGLE, tens of thousands of calls per frame). **No change warranted; re-measure only if an AI-heavy level shows a regression.**
@@ -13488,7 +13507,7 @@ Logs: `scratch/d318b_{fac,ctrl}_clk{1,2}.log`. Summariser: `scratch/d318b_summ.p
**Fix applied (2026-09-23, RULE-2-SIGNOFF: user approved option (c)).** `src/game/model.c` `modelSetAnimFrame2WithChrStuff`, `#ifdef PORT`: when the per-frame loop did not run, progress was requested, and `vb == va`, re-derive `frameb = constrain(va±1)` against the CURRENT endframe (as `modelSetAnimFrame` does); applied only if that next frame is unclamped. Genuine holds, anim ends and looping wraps are untouched. Env: `GE_D329=0` disables (A/B), `GE_D329LOG=1` logs triggers (cached getenv). **Verified (WSL, lockstep):** clk=1 with watchdog off (`GE_D318W=0`): Facility ai_19 and Control ai_9 no longer pin (frame 19→19.5→20…, 0.5/tick = the N64 clk=2 trajectory; aim→fire→re-aim cycle ≈192 ticks vs ≈195 at clk=2/3); authored holds intact. Trigger census: clk=1 Facility, 9 triggers, all on the repro guard, one per attack update; **clk=2: 0 triggers** over ~5300 ticks (Facility) and ~3600 (Control), so the fix is inert in the N64 regime. **Windows-native validation (2026-09-23):** Facility forced repro, native exe, `GE_D318B_CLK=1`, watchdog off: 48 post-update fire episodes, **0 pinned**, 23 D329 triggers. Bunker 1 golden (640x480 pinned, direct launch): REGRESSION with the fix ON (worst dmean 113.6) and equally with it OFF (`GE_D329=0`, 126.4), so it predates this fix (stale golden; re-baseline already owed). **Interactive playtest (2026-09-23, user, native Windows, shipping config + GE_D329LOG):** Ourumov reacts with N64-matching timing (user-confirmed on screen). Log: 4 D329 triggers (four guards, same tick, stale-pin state, released at 0.5/tick), 3 watchdog detections that cleared on their own, 0 watchdog rescues, no crash (`scratch/d329_playtest.log`).
**Status:** FIXED + playtest-verified (2026-09-23). Next: demote the D318 watchdog to detect-and-log post-release (M-192 item 7), then remove the TEMP probes (d318bprobe, d320repro). Watchdog stays shipped on `release/v0.4.0`; plan to demote it to detect-and-log after playtests. Cross-ref: D318, D320, D321, M-199, D155, D186/D193.
**Status:** FIXED + playtest-verified (2026-09-23). Next: demote the D318 watchdog to detect-and-log post-release (M-192 item 7), then remove the TEMP probes (d318bprobe, d320repro). **2026-09-30: done** (strip pass, `chore/strip-temp-probes`): `d318watchdog.c` is now detect-and-log only (no `sub_GAME_7F025560` re-seed); `d318bprobe.c` (incl. `GE_D318B_CLK`) and `d320repro.c` + the `lv.c` hook deleted. Watchdog stays shipped on `release/v0.4.0`; plan to demote it to detect-and-log after playtests. Cross-ref: D318, D320, D321, M-199, D155, D186/D193.
## D330 — #103 (v0.3.0, Win 11): Auto-Aim / Look Ahead "won't stay on" in the in-game watch options (2026-09-23)
@@ -15082,7 +15101,8 @@ not edited for the probe. Live RMB/MMB test and release gate remain open.
## D387 — M7 AllUnlocked read-time patches leak into persistent saves (2026-09-27)
**Status: OPEN; M7 cannot be signed off as a preservation-only playtest.**
**Superseded by D442 (2026-09-30): the save patch and this merge machinery were removed entirely; AllUnlocked is now a pure RAM/query-time override.**
**Status: FIXED 2026-09-30 for saves made from now on (see "D387 Fix" at the end of this file); fakes persisted by earlier builds are not repaired. Original diagnosis follows unchanged.**
User offered `data/` for investigation. Original `data/ge007.ini` and
`data/ge007.eep` were **read only**, checksums unchanged after all runs.
Private isolated test folders live under
@@ -15663,3 +15683,231 @@ Probe (`GE_D428`) showed the hook firing with the input clamped at 0.7 on most f
Under PORT `struct image_entry` puts `dataoffset:24` in bits 0-23 (D67), so byte 0 is the LOW byte of the data offset. On the N64 (big-endian) byte 0 is the MSB of word 0, the hit type. `chrprop.c` (impact_sounds ~1154, the explosion skip test ~1187) and `propobj.c` (~9685) used the raw byte, so on PC they indexed `g_HitTypeSounds[]` with an arbitrary nibble 0..15 (table has 13 entries; 13-15 read past it) and mis-tested the 5/6 spark skip. Harmless while `bghit.texturenum` was always -1 (D154); D431 now returns real ids for light fixtures, activating the path. (The Caverns playtest passed, so the light images' nibbles were presumably in range: luck, not design.) The comment in `image.h` claiming the N64 does the same was wrong and is corrected.
**Fix (`#ifdef PORT`, layout-driven, semantics-preserving):** use the `.hitTexture` bitfield at the three sites, as `chr.c` already does. **General rule (for porting-notes):** un-stubbing a `-1` sentinel (D154/D135/D431) re-activates every dormant consumer of the value -- grep all readers and check each for raw-layout reads and table-bounds assumptions first.
**Status:** FIXED; built. Live check owed (shoot light fixtures across levels; impact sound/sparks on lights).
## D440 — "Original N64" preset + "Port defaults" inverse (ROADMAP §5a, 2026-09-30)
**What:** two one-press action rows, "Original N64 preset" and "Port defaults preset", at the end of the GRAPHICS section (before its Reset row). They are rows in the shared `optionsoverlay.c` row table, so they appear in BOTH the F10 overlay and the front-end PC Options screen (`frontoptions.c` renders the same table; GRAPHICS now paginates to a second page there, the D406 paging). Activation reuses the D356 reset-row contract (edge-triggered arm -> "Confirm" -> apply within 3 s; held-repeat never fires it). The value column shows "Active" while every preset key already holds that preset's value. **An action, not a mode:** it writes the existing config keys (live-applied through `videoRequestLiveConfigForKey`, persisted on overlay close / clean exit like any F10 edit); no new ini key, so existing inis behave exactly as before unless a preset is pressed. D372's `Video.LowEndMode` flag no longer exists in tree (only the tree probe's "no M5" check references it), so there was no preset plumbing to reuse; the new generic hook is `configSetValue`/`configGetValue` (`port/src/config.c`, clamped like a file load).
**CLI route:** `-n64preset` / `-portpreset` apply the preset once at the top of `videoInit` (after `configLoad`, before any value reaches fast3d) and persist via the atexit save. Not added to `--help` (the help text lives outside this change's file partition). An "ini route" is just the keys below.
**Key audit** (every registered key; table lives in `port/src/video.c` `kVideoPresets`):
| Key | Port default | N64 value | In preset |
|---|---|---|---|
| Video.TextureFilter | 1 bilinear | 2 (3-point) | yes |
| Video.Anisotropy | 4 | 1 (RDP has none) | yes |
| Video.NativeWidescreen | 1 | 0 (4:3 projection) | yes |
| Video.WidescreenAuto | 1 | 0 (stock vertical FOV) | yes |
| Video.SafeAreaCrop | 1 | 0 (see below) | yes |
| Video.DrawDistance / LodDistance | 250 / 250 | 100 / 100 | yes |
| Video.DrawDistanceAutoFov / LodDistanceAutoFov | 0 / 0 | 0 / 0 | yes (identity guard) |
| Video.FovScale, Game.HudScale | 100, 100 | 100, 100 | yes (identity) |
| Game.ScreenShakeIntensity, Game.NoHitFlash | 1.0, 0 | 1.0, 0 | yes (identity) |
| Video.CrosshairHide/Color/Size/Style | 0/0/100/0 | same | yes (identity) |
| Input.AimMode | 0 (N64) | 0 | yes (identity) |
| Input.AimRange | 0 (PC) | 1 (N64 limits, D338) | yes |
| Culling | — | — | no key: frustum planes / fog-LOD scale (D222) and the room pool (D294 `portRoomPoolScale`) derive from FOV, widescreen, draw and LOD distance and are identity at the values above |
| Video.MSAA, VSync, FpsCap | 2, 1, 60 | n/a | **no** — performance/pacing; FpsCap 30 throttles the sim (D186), not faithful |
| Video.Fullscreen, Window.* | — | n/a | **no** — display choice |
| Video.FixMipTextures, DetailBaseTile, WrapFix | 1, 1, 0 | — | **no** — accuracy fixes that move output toward N64 |
| Video.CrosshairRed/Green/Blue, DisplayFPS | — | — | no (only used with Custom colour / overlay) |
| Game.SkipIntro, Game.AllUnlocked | 0, 0 | 0, 0 | **no** — progression/convenience, not presentation (AllUnlocked has save implications, D387) |
| Input.* feel (sensitivities, deadzones, smoothing, mouse model AimAbsolute/MouseDirectLook/NaturalPitch, CrouchMode), Input.Bind.*, Bond.* (per-save), Audio.*, Debug.* | — | — | **no** — spec: bindings/volumes untouched; mouse modes have no N64 counterpart |
**Overscan — which is faithful:** `SafeAreaCrop=0`. The N64 VI scans out the whole 640x480 framebuffer including GE's black safe-area border; a CRT's overscan hid part of it but by a set-dependent amount, and emulators/1964 show the full frame. The crop (`gfx_pc.cpp` `gfx_adjust_viewport_or_scissor`) is a port-side remap that stretches the inset gameplay viewport to the window (and trims the D246 1-unit side margin), so 0 = the signal the console output.
**4:3 limitation:** the preset gives a 4:3 *projection*; in a non-4:3 window it is stretched to the window (as the N64 image is on a 16:9 TV set to stretch). A pillarboxed gameplay mode does not exist in fast3d today — follow-up for the ROADMAP (lead).
**Verification (Windows, NTSC, 960x540 window, `-level_09`):**
- Port-default ini, `GE_PCDUMP=880-880` vs same with `-n64preset`: log `video: applied Original N64 preset (8 value(s) changed)`; the N64 frame shows the black safe-area bars + D246 side margin, the 4:3 projection stretched (narrower horizontal FOV than the Hor+ port frame) and 3-point-filtered textures.
- ini round-trip: after the `-n64preset` run the ini differs from the port-default ini in exactly the 8 keys (TextureFilter 2, WidescreenAuto 0, NativeWidescreen 0, DrawDistance 100, LodDistance 100, Anisotropy 1, SafeAreaCrop 0, AimRange 1); a following `-portpreset` run loaded those values (8 changed) and wrote an ini byte-identical to the original.
- UI dispatch: `GE_PRESETPROBE=1 GE_OPTIONSOVERLAY=4` drives both rows through the real arm -> confirm path: PASS (value "Confirm" when armed, "Active" after, other preset inactive).
- `GE_OPTIONTREEPROBE` reports FAIL both with and without this change (baseline build from the same commit) — pre-existing, not caused by the new rows (GRAPHICS is not in its page list).
**Not verified:** live mouse/pad press in either UI, draw-distance effect on an open level (DrawDistance is read at fog load, so it applies from the next level load), PAL/JP, Linux build.
**Status:** FIXED (port-only; headless-verified; user live accept owed).
## D441 — 64-bit pointer-width sweep, part 1 (ABI/layout exception, 2026-09-30)
**Why:** harmless today because the arena sits low (`0x10000000`) and the PE image base is `0x140000000` with a 4 GiB-aligned `g_pc_animdata_base` (D34), but each `(u32)ptr`/`(s32)ptr` cast truncates a host pointer and would fault once the heap or arena is above 4 GB (macOS/ARM64 or ASLR; #88/#95/#101). Same class as porting-notes §A1 / D255; permitted by the AGENTS.md ABI/layout exception.
**What (recovered from an agent worktree after a rate-limit crash, then reviewed hunk by hunk):** 17 files, +337 lines, every hunk `#ifdef PORT ... (uintptr_t) ... #else <original, byte-identical> #endif`: `bg.c` (`bgBuildRoomVtxBounds`, `bgTestRayIntersectionInRoom` x2, `bgRoomCalcBB` x2), `bondhead.c`, `bondview2.c` (anim table bases, `mp_respawn_handler` intro-record walk, `hudmsgBottomRender` blackbox call), `bondview_r.c` (intro-record walk), `chraction.c` (animation table bases), `explosion.c`, `front.c` (`load_walletbond`), `image.c`, `image_bank.c`, `initBondDATAdefaults.c`, `initactorpropstuff.c`, `prop.c` (`proplvreset2` setup-file rebase, 20 sites), `ramromreplay.c`, `stan.c` (`stanDetermineEOF` with `intptr_t delta`), `title.c`, `libultra/audio/cseq.c`, `music.c`.
**Review notes (checked, not just trusted):**
- `(s32)&ANIM_DATA_x + (uintptr_t)&ptr_animation_table->data`: the `(s32)` is intentional. D34 defines `ANIM_DATA_x` as an lvalue at a 4 GiB-aligned base + offset, so its low 32 bits ARE the segment offset. Only the base was widened; the offset is non-negative so the sign extension is harmless.
- `bondview2.c` `draw_blackbox_to_screen(arg0, &view_left, ...)`: the prototype takes `s32 *` and the four locals are `s32`, so passing the real pointers replaces a truncating int-to-pointer conversion.
- `explosion.c`: `g_FlyingParticlesBuffer + i` on a `struct FlyingParticles *` is the same scaled pointer arithmetic as the original (`(struct FlyingParticles *)(u32)ptr + i`).
- `front.c`: keeps the original low-24-bit trick on `srecord->Primary`; semantics unchanged (not independently verified beyond that).
- `prop.c`, `stan.c`, `bg.c`: rebase arithmetic on the RAM copy (`local_stage`, `newBase`, `ptr_bgdata_room_fileposition_list`) now done in `uintptr_t`. The `bg.c` sites `bgRoomCalcBB` and `lightFindVertexBaseForTri`-adjacent code overlap outside PR #107; this landed version is ours, so #107 will conflict and needs a rebase or closure.
**Verified:** clean build; pointer-to-int-cast warning sites in the 17 files 279 -> 185 (94 removed); `-level_09` and `-level_20` short runs (`GE_QUITFRAME=420`, `GE_PCDUMP` frame 400) exit 0, no crash lines, frames render correctly (Bunker intro camera; Silo guards + intro caption). No frame diff was run (the intro camera is non-deterministic, D117).
**NOT done (owed):** the full ~396-site census with per-site provenance classification (H real hazard / R ROM-address 32-bit / I integer-only / ?), the audit table `docs/dev/ABI-PTRCAST-AUDIT.md` (the agent that started it died before writing it), the `GE_HIGHARENA` high-address test mode, and review of #107/#108 (still the #108 range-check concern in ROADMAP §2). 185 pointer-cast warning sites remain in the touched files; many are intentional (`(s32)&ANIM_DATA_*` offsets, D34). Nothing was tested with an arena above 4 GB.
**Status:** PARTIAL (part 1 landed; census owed).
### D387 Fix (2026-09-30)
**The mechanism stays in the game, not in the file.** `geEepromRW` (port/src/libultra.c) still hands the game the AllUnlocked view at the block-4 read, but now remembers per slot the RAW bytes (`s_d387Raw`, what is on disk) and the SEEN bytes (`s_d387Seen`, what the game got). On every whole-slot write (`block = 4 + 12*slot`, 96 bytes) `geD387MergeSlotWrite` persists, for each cheat byte and each 10-bit completion time, the RAW value when the game left the field equal to what it saw, and the game's value when it changed it (a new record, a folder reset or delete). All other bytes come from the write. The game CRC is regenerated over the merged slot (the same `fileGenerateCRC` call the patch uses). The merge keys off "a patched read happened this process" (`s_d387Valid`), not the live flag, because the game keeps the patched view in RAM after the option is toggled off; a read with the option off clears it.
**Slot relocation (found by the test, fixed):** the game moves saves between slots (a folder's first completion relocates its data to a lower slot; copy-folder duplicates one). Comparing a write to the shadow of its own slot index dropped an earned cheat when slot 4's data was written to slot 0. The merge now picks the source shadow the write differs from least (own slot wins ties) and logs `eeprom: D387 slot N write descends from slot M's view`. A delete or reset differs from every view about equally, and every changed field is persisted as written anyway, so the choice does not matter there.
**Verification** (isolated dirs under the scratchpad, never the real `data/`; seed = a copy of `data/ge007 copy.eep`, where slot 4 is folder 0 with two real times and one earned cheat; a scratch-only probe, never committed, drove the game's own save APIs from the front-end tick):
| Scenario | Result |
|---|---|
| settings write (`GE_WSPROBE_FRONT`), ON vs OFF | EEPROM byte-identical (before the fix, ON persisted `ffff0f` cheats and 0x3FF fills) |
| genuine record via `fileUnlockStageInFolderAtDifficulty` (level 3, diff 0, 300 s) with the slot 4 to 0 relocation, ON vs OFF | byte-identical after the relocation fix (before it, ON lost the earned cheat `040000`) |
| delete folder 0, ON vs OFF | byte-identical |
| copy folder 0, ON vs OFF | not comparable: with ON every folder reads as completed (synthetic 0x3FF), so `fileCopyFolderToFirstFree` finds no empty folder and writes nothing. This is AllUnlocked's own behaviour, not a save leak. |
| ON result, then OFF restart | all five slots validate `OK`, the EEPROM is unchanged by the restart, no 0x3FF fakes |
| fresh (no EEPROM) settings write, ON vs OFF | no fakes either way. ON leaves the four untouched slots zero-filled, the normal state of a new file; an OFF boot then normalises them to the same bytes as a plain OFF run. |
**Known limitations:** (1) a cheat earned while ON is not persisted: its bit is already set in the synthetic view, so a write cannot show it changed. It is re-earned while OFF. (2) A first completion at or above 1023 s (the synthetic 0x3FF value) is not seen as a change. (3) If the game ever writes the destination slot after the source slot of a relocation, the source view would already be gone (the observed order is destination first). (4) Copy-folder is a no-op while ON. (5) Toggling the option takes effect at the next launch either way (the save is patched at read time), so the row now reads "ON (restart)". (6) Fakes persisted by earlier builds have no provenance and are not repaired: back up `data/ge007.eep` before first use; if a save already contains them, the only clean reset is the in-game file delete for that folder.
**Prior art:** the D387 agent that died in the 2026-09-30 rate-limit crash left a 302-line staged design in its worktree (`d387-agent-staged-WIP.patch` in the notes archive) that additionally replays the game's completion and cheat-award rules to persist cheats earned while ON, and normalises invalid slots. It was never built or tested. If limitation (1) matters, start there; this fix deliberately stays smaller and testable.
**Status:** FIXED (port-only; matrix passed); user live accept owed (play a mission ON, then OFF).
## D442 — Game.AllUnlocked reworked to a pure RAM override; save patch + D387 merge removed (2026-09-30)
**Status: RULE-2-SIGNOFF** (maintainer-granted opt-in port feature, edit confined to `#ifdef PORT` in `src/game/file2.c`). Maintainer, 2026-09-30: "All unlock - want that to allow for all levels/cheats to be unlocked, don't really care how we do it if debug/overrides are better than what we currently have" and, on the proposed hook design, "new plan sounds great". Runtime-verified (falsifier passed, first hook); second hook build-verified only, maintainer to check level select by eye.
**Change.** `fileGetIsCheatUnlocked` (`src/game/file2.c`, the D225 `#ifdef PORT` block) now also returns TRUE when `portAllUnlocked` is nonzero. Mission-select cheat gating (file.c) and the cheat menu (front.c `frontCheckIfCheatIsUnlocked`) both route through it, so no save byte is read or written for the option. Levels and 007 mode stay on the game's own debug flags seeded in `port/src/main.c`. In `port/src/libultra.c` removed `geEepromPatchAllCheats`, `geSaveGet/SetTime`, the `ge_save_slot` mirror, and the D387 `s_d387Raw/Seen/Valid` + `geD387MergeSlotWrite` machinery; `geEepromRW` now passes raw bytes both ways (D297 legacy-CRC migration kept). Fixes D387 (leak), the cheats-earned-while-ON limitation, and D259/D281 (the fresh-save zero-slot handling lived only in the patch; the game's own BLANKSAVEDATA reset now runs as on a stock build).
**Verification.** AllUnlocked=1, `GE_WSPROBE_FRONT=600 GE_QUITFRAME=780`, slot 0 with real data: the eep diff is only the slot checksum (bytes 32-39) and music_vol (byte 42, the probed change); cheat bytes and all completion-time bytes identical. Log shows `all-unlocked: RAM unlock flags seeded`. Not verified by eye: cheat menu population.
**Second hook (added same day, maintainer-approved).** Maintainer, 2026-09-30: "Decision - yes this is a requirement for the feature to be feature complete. Imagine the normal cheat menu / all unlocked which lets you select any level obviously." `fileIsStageUnlockedAtDifficulty` (`file2.c`, top of function, `#ifdef PORT`) returns STAGESTATUS_COMPLETED for every in-range level/difficulty when `portAllUnlocked` is set (query only; save untouched). Reason: the removed patch's 0x3FF times made every stage COMPLETED, and the debug flags only give UNLOCKED, while several gates compare against COMPLETED: `front.c:3182-3200` (level select with an SP cheat active), `front.c:5015` (extra MP characters), `cheat.c:1415` (level-unlock cheat counting), and via `file2.c:1003-1028` the Magnum / Laser / Golden Gun cheat rows (`front.c:1134-1140`). Callers checked: front.c 3164, 3189, 4226, 5015; cheat.c 1415; file2.c `fileGetHighestStageUnlockedForFolder`, the Cradle/Aztec/Egypt completed helpers and their AnyFolder variants. No gate bypasses the hook for these purposes; the direct `fileGetSaveStageCompletedForDifficulty` users (`fileGetHighestStageAndDifficultyCompleted`, `fileIs007ModeUnlocked` file2.c ~1486) are not needed (007 mode comes from `get_debug_007_unlock_flag`, front.c:3182).
Side effects beyond unlocks (read from code): `fileGetHighestStageUnlockedForFolder` now returns Egypt; the AnyFolder helpers (`front.c:8056,8074,8197,8203`) now report Cradle/Egypt completed, so the intro cinematic may pick the Cougar Magnum, Oddjob and Baron Samedi as if the campaign were finished. The mission best-time text is unaffected (it reads the raw time getter, which shows blank for 0). No save write.
**Stale references** (not touched, outside allowed files): `port/src/optionsoverlay.c:97-103` comment, `port/src/watchsettings.c:216,546` comments still cite the removed patch. Fakes already persisted by earlier builds are not repaired.
**Confidence:** high on the removal and the eep-diff result; medium on the UI outcome until eyeballed.
## D443 — D357 settings-display cleanup + "Restart game" action (2026-09-30)
**Scope (port-only, no `src/game` change, no config storage/default change):** the shared row table in `port/src/optionsoverlay.c` drives BOTH the F10 overlay and the front-end PC Options screen (`frontoptions.c` reads the same `rows[]`), so every change below lands in both UIs once.
- **Mouse sensitivity:** `Input.MouseSensitivity` shown as `%.1fx` (raw/100, 100 = 1.0x), UI range 10-300 step 10. Removed from `calibratedDefault()` (the Wave A "50/100" piecewise display) so the bar is linear and the number is a real unit. Ini values above 300 still load and display (e.g. 5.0x); the UI clamps on the next edit.
- **Stick deadzone:** already split L/R (`Input.PadDeadzoneL/R`, raw 0-30000). Done by display/step only: step 300 raw + `dispDiv=300` `%` = exactly 1 display-% per step; NO new key / migration (principle 3 not triggered). Default 7000 shows 23% (off the 300-grid by 100 raw, invisible). Also removed from `calibratedDefault()`. `Input.PadTriggerPct`, `MouseYScale`, `PadLookSens*` keep the calibrated display (out of scope).
- **Volumes:** `Bond.Music`/`Bond.FX` step 128 -> 328 (1 display-%).
- **FOV in degrees (`portFovHorizDegrees`, `port/src/video.c`):** vfov = 60 (`FOV_Y_F`) x [sqrt(aspect/(4/3)) only when WidescreenAuto && !NativeWidescreen] x FovScale/100, clamped 20..160 (mirrors `portScaleFovY`, the `src/fr.c` guPerspectiveF chokepoint). Projection aspect = window aspect under native widescreen (`portNativeAspect`, matches bondview2.c `faspect` for a full-screen viewport), else 4:3. `hfov = 2*atan(tan(vfov/2)*aspect)`, shown as `N deg`, i.e. HORIZONTAL FOV at the CURRENT window aspect (100% = 75 deg at 4:3, ~91.5 deg at 16:9). Convention/ambiguity: the value is aspect-dependent (it changes when the window is resized while the stored % does not); with native widescreen OFF and auto FOV ON the game projects at 4:3 with a boosted vfov, and we show that projection's hfov, not the stretched on-screen angle. Label changed "FOV scale" -> "Field of view". Storage steps are 1% (0.7-1.3 deg), so a keyboard step walks the stored % until the rounded degree changes (1-2 deg per press; not every whole degree is reachable); drag rounds to 1%. Vertical/aim-zoom rewrites of fovy are not reflected (menu shows the hip-fire value).
- **MSAA 16x:** `kMsaaSeq` + `Video.MSAA` range 1-16 + `gfx_msaa_level` snap in `video.c`. `gfx_opengl.cpp` already clamps to `GL_MAX_SAMPLES`; no SDL multisample attribute is set (FBO-based MSAA), so nothing else clamped at 8. Default remains 2 (C initialiser).
- **Texture filter / frame-rate cap:** no change (sign-off §0; 30/60 toggle stays, sim is 60 Hz).
- **Restart game:** new `__RestartGame` action row after `__QuitToDesktop` (label "Restart game (for restart options)"). It saves config, calls `videoRequestRestart()` -> the same D344 orderly quit (render thread parks, host `exit(0)`); `main.c`'s `portAtExit` (after `videoSaveWindowState()` + `configSave()`) then calls `sysRelaunchSelf()` (`port/src/system.c`): Windows `CreateProcessA` with `GetModuleFileNameA` + the original `GetCommandLineA()` (cwd inherited); POSIX `fork`+`execv` of `/proc/self/exe` with the saved argv (`_POSIX_C_SOURCE` raised to 200112L for fork/readlink). Relaunching only after the config write avoids the new instance reading a half-written ini. Not click-tested (would spawn a second window).
**Verified:** build green (ninja); `GE_WSPROBE_RESET=1` front boot: 8 sections all `failures=0`; `-level_33`: 8 stage sections all `failures=0`, saved bytes OK. `GE_OPTIONTREEPROBE` reports FAIL both as flagged pre-existing on the base commit; not compared to a base run here (launch budget) -- the Gameplay page stays under its 14-row cap (Restart adds one row). Linux compile of the POSIX branch of `sysRelaunchSelf` NOT done (no Linux toolchain in this shell).
**Owed (human):** eyeball both UIs (values: `1.0x`, `23%`, `75 deg`, `16x`; Restart row fits its width); click Restart once (Windows) and confirm the window relaunches with the saved settings and no BSOD/hang; confirm 16x on the target GPU.
**Confidence:** medium-high for display/step/MSAA/reset-table (probe-verified); medium for FOV convention (formula derived from code, not measured on screen) and for Restart (code-reviewed only).
## D444 — Front PC Options paging controls + hover/scroll fight (2026-09-30)
**Scope:** port-only, `port/src/frontoptions.c` (the front-end screen has its own page model: `ROWS_PER_PAGE`=11, wheel/Up/Down step the highlight and turn the page at the ends). F10 (`optionsoverlay.c`) is NOT affected: it already has "no hover-to-highlight" (its hover code only hit-tests clicks) and scrolls its own window.
- **Bug:** every non-A-held frame the highlight `s_hl` was recomputed from the pointer position, so after the wheel/keys moved the selection any small mouse nudge re-selected the row under the pointer and discarded it.
- **Hover rule now:** `s_hl` is persistent. The pointer (cursor_h/v_pos differing from the previous frame, excluding our own keyboard/wheel snap in `cursorToItemRaw`) re-selects ONLY when it lands on a different row than the last hovered row (`s_lastHover`; reset on level changes). Hover never changes `s_pageno`. A click (A press) acts on the row actually under the pointer (`s_hoverHit`), not a stale wheel selection.
- **Paging controls:** on the bottom hint row of a multi-page section, "Previous page" (left, hidden on page 1) and "Next page" (right, hidden on the last page), highlight box on hover, click via `pageCtlAt()`. Up/Down still page at the ends (the old text hint "Up: previous page / Down: next page" is replaced by the buttons). Controller: unchanged nav (D-pad down/up crosses pages); the buttons are mouse-only. Named "Previous page" to avoid confusion with the game's bottom-left Previous (back) tab.
- **Comments:** stale D257/D259/D281/D387 text in `optionsoverlay.c` (kAllUnlocked) and `watchsettings.c` (two places) updated to D442 (pure query-time override; the boot-time BLANKSAVEDATA patch is gone, so a fresh eeprom now reaches the blank-file-1 fallback).
**Verified:** build green. No probe exercises mouse hover/clicks, so no game launch; a human must test with a mouse.
**Owed (human):** in a >11-row section (Input, Graphics): click Next/Previous; wheel down then nudge the mouse slightly (selection and page must stay); move onto another row (selects it); click a row after wheeling (acts on the clicked row); confirm the new hint row layout and that controls hide at first/last page.
**Confidence:** medium (logic reviewed, not exercised live; the pointer-motion detection assumes cursor_h/v_pos is delta-driven and only changes on real motion or our snap).
## D447 - Pillarbox output mode for the Original N64 preset (port-only)
Design: `docs/dev/notes/PILLARBOX-DESIGN.md` (local). Maintainer decisions 2026-09-30: exact 4:3 (full 320x240 frame); 16:9 while the game's watch-menu Ratio is 16:9; Crop overscan must never remove the player's Wide/Cinema bars; F10 full-window deferred.
**Done (steps 1-5, 7 partly).**
- `port/fast3d/gfx_pc.cpp`: `gfx_set_output_aspect()` / `gfx_get_output_rect()` (declared in `gfx_api.h`). `gfx_start_frame` computes a centred, even-sized rect for the target aspect; `gfx_current_dimensions` and `gfx_current_game_window_viewport` both become the rect, so `different_size` stays false and the inherited PD offset path in `gfx_adjust_viewport_or_scissor` places every viewport/scissor/rect inside it (also per-player viewports: nothing is hard-coded to one viewport). Bars are the frame clear. Aspect 0 = bit-identical path. Also: mode-0 `aspect_scale` uses `gfx_current_dimensions.aspect_ratio`; autoresize fullscreen FBs and `gfx_copy_framebuffer` use window size / rect offsets. MSAA FB stays window-sized (same-size branch).
- `port/src/video.c`: `Video.AspectMode` (0 Window, 1 Original), read every frame in `videoStartFrame`; target = `get_screen_ratio()==SCREEN_RATIO_16_9 ? 16:9 : 4:3` (read only, no `src/game` edit). Preset row (N64 = 1, port = 0). GE_PCDUMP / screenshots now capture the whole window (bars included).
- `port/src/optionsoverlay.c`: "Aspect" row (Window/Original) and `kResetDefaults` entry (0). Front PC Options reuses the overlay row table, so it gets the row and pages automatically.
- Crop gating: the safe-area cache only adopts SP viewports >= 90% of the canvas height. Wide/Cinema (and split-screen half viewports) no longer define the crop band, so their letterbox survives `SafeAreaCrop=1`. If no full viewport has been seen yet, a centred 220-line band is seeded. Port-only.
- Mouse: `input.c` front-end pointer maps across the output rect (`videoGetOutputRectFrac`); the F10 overlay mouse already inverts through `gfx_get_ui_screen_rect`, which includes the rect offset. F10 stays drawn inside the rect (step 6 deferred).
**Measured (1280x720 window, `-level_09`, AspectMode=1, SafeAreaCrop=0, NativeWidescreen=0, MSAA 2):** columns 160..1119, i.e. 160/160 px side bars (expected 160). Top/bottom 30/30 px = the game's own 10/240 overscan fill at 720 px. 800x800 window, MSAA 1: full width, 125/125 px top/bottom = 100 letterbox + 25 game fill. Base smoke (AspectMode=0, frame 300, self-quit) OK.
**Not done / owed:** runtime check of Wide/Cinema crop gating and of Ratio 16:9 (code path only); mouse mapping not exercised; golden bunker1 at Window mode not run (identical by construction); MSAA 4/8/16, fullscreen toggle, PAL, Linux CI and the MP branch not run; step 6 (F10 full window) deferred by decision. Known cosmetic: with crop off, D246's 1-unit dark-blue edge line is visible at the rect's left/right edges. Human: compare against 1964.
Confidence: high for the rect and letterbox geometry (measured), medium for crop gating and Ratio 16:9 (unverified at runtime).
## D445 — crouched-rifle firing animation never resolved on PC (linker adjacency) -> MP crash (2026-09-28)
**Symptom.** SIGSEGV in `modelConstrainOrWrapAnimFrame` from `playerTick`
(bondview2.c ~11021) with `anim = 0xb84`, when a multiplayer player
crouch-fires a rifle (found by the 5PLUS bot runs: Complex, 4 seats, M16,
frame 5030; present in the 0.9.x friend bundles). Found on
`exp/5plus-multiplayer` as "D341" (renamed D420 in PR #120, then D445 on merge: D341 and D420 are already used here).
**Root cause (layout, porting-notes §D5 "linker adjacency", D164 class).**
`firing_animation_groups[1][5]` points at `crouched_rifle_firing_animation_groupA`
(chr.c). The tables hold anim *offsets*; `initWeaponAnimGroups` ->
`initResolveAnimGroupTable` rebases each table up to its zero terminator.
`crouched_rifle_firing_animation_group1` has ONE entry and **no terminator**:
on N64 .data is laid out in source order, so resolving group1 walks on into
groupA (resolving it too, len 2). GCC placed groupA *below* group1, so
groupA[0] kept the raw offset `PTR_ANIM_fire_kneel_left_leg` (0xB84) and
`modelSetAnimation` received it as a `ModelAnimation*`. Side effect before
the fix: crouching rifle guards saw len 1 instead of the N64's 2.
**Not covered upstream:** upstream D392 (chraction.c firing-table fields)
and D336 (bondhead.c) are different sites; chr.c/chr.h were unchanged on
09278407.
**Fix (8595a06e, `#ifdef PORT`, `#else` = decomp):** chr.c one array
`{kneel_right_leg, kneel_left_leg, terminator}` (exactly the N64 layout);
chr.h `#define crouched_rifle_firing_animation_groupA
(&crouched_rifle_firing_animation_group1[1])`. Resolution and len are the
N64's; no logic change. Upstream candidate (a real port bug).
**Verified (programmer, 5plus + main):** repro (4 seats, bots, Complex,
pinned seed, 18 078 frames x3) no crash, SH identical across runs; 8 seats
remote mines no crash; every firing/struck anim table in chr.c scanned --
this was the only unterminated one; gdb scan of all 24
`firing_animation_groups` entries at run time: 1 bad pointer before, 0 after
(also 0 on the main-line cherry-pick); P1 gate (1 324 recordings, frame
dumps) unchanged; netplay 2P/4P identical.
## D446 — couch 2P watch-menu text differs between builds that only differ in data layout: stale per-texture anisotropy (2026-09-29)
**Status:** FIXED, port-only (`port/fast3d/gfx_opengl.cpp`, `#ifdef PORT`). Render-only; the sim was never involved (state hashes identical throughout).
**Symptom.** The couch 2P script (MULTIPLAYER-DEV §6), frame 2400 (the MP pause/watch menu, "PLAY / RANK: 1ST / SCORES") is not byte-identical between two builds that differ only in data layout:
- 8b080caa vs 8b080caa + an unused 3 KB array in `expinput.c`;
- the controller-pages merge;
- R1's front.c/front.h array widening (programmer 1's bisect).
About 55-922 pixels differ, a few glyphs only, each by +-1-2 (mostly green). Every build is deterministic against itself. A 4 KB array at the end of `expinput.c` did not reproduce it, and neither did the pre-sync base with R1: only some layouts trigger it.
**Method (probes in fast3d, removed).** A (6c8915d7) vs B (A + the 3 KB array), frames 2398-2401:
1. Every texture import (format, size, content hash): **identical** sequences in A and B.
2. Every `gfx_flush` batch (VBO hash, depth mode, blend, viewport, scissor, combine mode, other modes, and for both texture slots the GL texture id, filter, clamp, content hash): **identical except the GL texture ids.** The same content is bound under different ids in A and B.
3. So the difference lives in GL texture-object state.
**Root cause.**
- `gfx_opengl_set_sampler_parameters` (`gfx_opengl.cpp`, the `if (mipmaps) glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY, current_anisotropy_level);` line) sets anisotropy only for mipmapped samplers and never resets it. Anisotropy is per-texture-object state; `Video.Anisotropy` defaults to 4.
- GL ids are recycled: `gfx_texture_cache_delete_range` (`gfx_pc.cpp`) pushes freed ids onto `free_texture_ids` while walking the `std::unordered_map` in hash-bucket order. The key hash is the texture **address**, so the recycling order depends on where textures live.
- A glyph texture (bilinear, no mipmaps) that inherits an id last used by a mipmapped world texture keeps `MAX_ANISOTROPY = 4`. llvmpipe then filters it slightly differently: the +-1-2 values.
- A layout shift changes which glyph inherits which id, hence which frames differ.
- Nothing in `src/game` is involved: no uninitialised read, no out-of-bounds read, no stale texture content (all ruled out by the probes and the earlier UBSan run).
**Fix.** Always write the anisotropy when sampler parameters are set: the level when mipmapped, 1.0 (off) otherwise, so a recycled id carries nothing over. This is identity for any texture whose id never had a mipmapped owner, which is why the base frames are unchanged.
**Verified (headless, silent, llvmpipe, scratch HOME, mp-sandbox data; 6c8915d7 base):**
| Check | Result |
|---|---|
| Couch 2P frames 2300-2500 (step 10), fix vs fix + 3 KB array (the reproducing variant) | **0 of 21 frames differ** (before the fix: frames 2300-2420 differed) |
| Couch 2P frames 2300-2500, base vs fix | 0 of 21 differ |
| Solo `-level_09` frame 1800, base vs fix | byte-identical |
| Couch 4P frame 2400, base vs fix | byte-identical |
| Netplay 2P (local relay, 60 s) | SH identical over 2 671 ticks |
**Residual risk (not fixed, noted).** Other per-object state can still follow a recycled id:
- mip levels 1..n of a previous owner, if a texture uploaded without mipmaps is later sampled with a mipmapped min filter;
- the free-id order is still address-dependent.
The general fix would reset the whole object on reuse, or sort the ids that `delete_range` frees. Neither is needed for this symptom. **Confidence: [H]** for the cause and the fix (A/B reproduced, fixed, bisected to the one GL parameter).
+21
View File
@@ -290,6 +290,8 @@ The offline model converter sizes each record's trailing arrays from its count f
When a PC-larger asset gets a bigger reservation inside a shared scratch buffer (D45: the wallet model 0xA000 -> 0x17000 in `ptr_logo_and_walletbond_DL`), grep for every other user of that buffer that computes a fixed offset (`base + 4096*10`). Those still point into the old boundary and silently overwrite the enlarged asset; the corruption only shows when the asset is not reloaded.
**A1 cross-tag (D441, 2026-09-30):** the 32→64 widening of `(u32)`/`(s32)` *pointer casts* (rebase arithmetic on RAM copies, segment/anim-table bases) is the same class. Pattern: `#ifdef PORT` `(uintptr_t)` arm + untouched `#else`. Exception: `(s32)&ANIM_DATA_x` is an intentional offset (D34: 4 GiB-aligned base, low 32 bits = segment offset) — widen only the *base* it is added to, never the offset. Census of the remaining ~185 sites in the touched files is still owed (D441).
## B. 16-byte PC `Gfx` / `Vtx` vs 8-byte N64
Any buffer reservation, `memcpy` size, slot stride, or pool budget
@@ -633,6 +635,7 @@ through a converter or a runtime bswap fixup reads scrambled.
- **A texture re-declared in another format must be normalised with the geometry it was IMPORTED with (D245).** GE's sky water loads a CI8 image and draws it through an RGBA16 tile; fast3d imports it as CI8 (D229) but its triangle path computed the UV divisor from the RGBA16 tile (half the width) and the whole mip-chain height (43 rows, not the 32-row mask period). Anything that changes the importer's format/extent must change the tri-path `tex_width/tex_height` with it, and wrap must equal the N64 mask period.
- **CPU-built RDP triangles need float vertices, not `Vtx` (D245).** Where GE builds RDP edge/texture coefficients on the CPU (sky/water), 32-bit S/T and positions are normal; squeezing them through s16 `tc`/`ob` costs visible precision on horizon-scale geometry. Use `G_FLOATVTX_EXT` (`port/include/floatvtx.h`).
- **A first-run-only preset is skipped forever by any earlier launch that created the ini, and window keys are not an "untouched" signal (D283).** Seeding values *before* `configLoad()` only works when no `ge007.ini` exists; one launch in a different environment (SteamOS Desktop Mode has no `STEAMOS`) writes a plain ini and the preset never runs. Apply presets *after* the load, record a one-shot flag key, and gate on keys the user must change deliberately (`Video.Fullscreen`, `Window.Maximized`) — `Window.Width/Height` are rewritten from the live window on every clean exit (`videoSaveWindowState`), so "still at default" never holds for them.
## D. N64 hardware idioms fast3d does not emulate
@@ -1747,3 +1750,21 @@ A port stub that forces a value to `-1` (D154 `texnum`, D135) can hide latent la
`propobj.c:~2393`) — those 4 rely on div-by-zero yielding NaN/inf without
trapping on both MIPS and x86-64 SSE, matching retail behavior; do not
"fix" them.
- **x64 Windows backtraces: unwind with the PE's .pdata, never walk RBP.**
`-fno-omit-frame-pointer` on MinGW-w64 GCC does not give an RBP chain:
most prologues set `RBP = RSP + N` (Win64 allows an offset frame
pointer), so `[RBP]` is not the saved RBP and a manual walk dies at
frame 1. `RtlLookupFunctionEntry` + `RtlVirtualUnwind` from the
exception CONTEXT is reliable and needs no dbghelp/symbols (crash.c,
2026-09-30). Print `va = preferred ImageBase + RVA` per frame so
`addr2line -e ge007.x86_64.exe` resolves it even if the image was
relocated. Test with `GE_CRASHTEST=<N>`.
- **Crossing msys -> native: pass files, not quoted strings; get paths from
bash's own `pwd -W`.** The msys argv conversion re-escapes embedded `"`
as `\"`, which `cmd.exe` does not understand, so an inline
`cmd //c "... \"C:\path\" ..."` silently never runs (build-pc.sh's TMP
probe was dead this way). And with Git Bash plus MSYS2 `usr/bin` on one
PATH, `/tmp`, `mktemp` and `cygpath` can belong to different runtimes
(different Windows dirs). Never trust `command -v cmake` for the MSYS2
toolchain either: a pip-installed cmake can shadow it (2026-09-30).
+15 -9
View File
@@ -10,7 +10,11 @@ This page answers two questions plainly: *what does installing this put on
your machine*, and *how faithfully does the port actually reproduce the
original N64 game's logic*. Both were reviewed end-to-end on 2026-09-15 and
re-verified for the v0.3.0 release bundles on 2026-09-18 (bundle contents,
no-networking source check, and the asset converter). The full engineering
no-networking source check, and the asset converter). The no-networking source
check was repeated against the `release/v0.4.1` tree on 2026-09-30 (no socket,
HTTP or name-resolution calls in `src/` or `port/`; neither the engine nor the
bundled `SDL2.dll` imports a networking DLL; no registry calls in the port's
own code); the bundle contents were not re-checked then. The full engineering
record behind each item is in the project's finding log
([`docs/dev/findings.md`](dev/findings.md)).
@@ -41,7 +45,10 @@ reads your ROM and writes new files — has no network access at all,
verifies your ROM against a fixed table of known retail GoldenEye 007
hashes before touching it (an unrecognized file is rejected outright), and
writes only inside the bundle folder. Its full source ships right next to
the compiled version in every release if you'd like to read it yourself.
the compiled version in every release if you'd like to read it yourself. The
engine starts it automatically on the first run, when the two derived asset
folders (`data/pcmodels-*`, `data/pccg-*`) are missing, passing it the ROM it
found and the output folder; after that first run it is not launched again.
See [`SECURITY.md`](../.github/SECURITY.md) for why the binaries are
unsigned and what that means in practice (a SmartScreen prompt on first
@@ -52,13 +59,12 @@ run, and why some antivirus engines flag the converter specifically).
Cloning and building pulls in only standard, well-known packages: MSYS2
`pacman` packages on Windows, `apt` packages on Linux — no `npm`, no
`cargo`, nothing downloaded at CMake configure time, no git submodules, and
no prebuilt binaries checked into the repository. **One real gap**: the CI
build installs `pyinstaller` from PyPI without pinning a version, which is
used to freeze the release's asset-converter tool. It's a widely-used
packaging tool, and what it packages is entirely in-repo, stdlib-only
Python — but an unpinned dependency means a future PyInstaller release
could change what ships without a corresponding diff in this repository.
Pinning it is a tracked follow-up.
no prebuilt binaries checked into the repository. The CI build freezes the release's asset-converter tool with `pyinstaller`,
which is pinned to an exact version, and every third-party GitHub Action the
workflows use is pinned to a full commit SHA (both from the release after
v0.4.0; earlier releases installed an unpinned `pyinstaller`). What
PyInstaller packages is entirely in-repo, stdlib-only Python, so a release is
reproducible from a diff of this repository.
One development-only backdoor exists in source builds: the `GE_DEBUG_UNLOCKALL`
environment variable (`src/game/file2.c`) pre-unlocks all cheat options at boot.