mirror of
https://github.com/jkdansereau/goldeneye-pc-port.git
synced 2026-10-23 15:52:42 +02:00
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:
commit
ad3856892a
72 files changed
+2228
-2080
No files matched your search
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
+260
-12
@@ -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).
|
||||
Reference in new issue
Block a user