diff --git a/docs/dev/HANDOFF-ARCHIVE.md b/docs/dev/HANDOFF-ARCHIVE.md index aa0ae997..382d557b 100644 --- a/docs/dev/HANDOFF-ARCHIVE.md +++ b/docs/dev/HANDOFF-ARCHIVE.md @@ -6,7 +6,9 @@ > [`findings-index.csv`](findings-index.csv), then read the specific `## Dxx` > entry. This file > is the frozen accumulation of -> prior sessions' handoff briefs (through session M-11), kept so the +> prior sessions' handoff briefs (M-2/M-3, plus M-140 → ~M-12 appended +> 2026-09-15; the M-4–M-11 briefs were never archived — pre-existing gap), +> kept so the > reasoning behind a fix is recoverable. Everything below is as-written > at the time and may be superseded. @@ -521,3 +523,5643 @@ locations attract mode happens to pick). ROM-asset formats (D69/D80-82 pattern) — likely the right call for D88 too, though a careful runtime fixup is also plausible; decide after the byte-level spec work. + +--- + +# Appended 2026-09-15 — moved verbatim from `docs/HANDOFF.md` (M-140 → ~M-12) + +> **Ordering note:** the material above this divider ends at session M-3. +> The M-4–M-11 briefs were never archived (pre-existing gap, not lost in +> this move). The block below runs **newest-first** (M-140 → ~M-12) and +> mixes section types (`### ARCHIVE — M-x`, `## ARCHIVE — D236…` pass logs, +> dated `## session` headings, `## Done this session (M-x)`). +> Normalize to monotonic order only if this file accumulates again. + +### ARCHIVE — M-140 current-task summary (superseded by M-141 above) + +*(This whole block, down to the next ARCHIVE heading, is M-140's original +CURRENT TASK write-up — release branch cut, D255 root-caused for real, +D285/D286 first surfaced. Kept for history; M-141 above is authoritative.)* + +### ARCHIVE — M-139 current-task summary (superseded by M-140) + +**STANDING DIRECTIVE (M-139b): do NOT push anything to GitHub right now; `main` is forbidden** (no direct pushes, ever — PRs only). All Phase R work lands on local branches for user review. The security/accuracy drafts + release-review design live in gitignored `docs/dev/notes/` — never push them; keep untracked `docs/security-and-fidelity-status.md` out of commits until the Phase F docs batch. + +**Read `docs/dev/notes/ROADMAP-1.0.md`'s M-139 addendum (top of file) first — it is now the plan of record, with all four planning decisions RESOLVED (M-139b):** working-tree extraction APPROVED (branch + sweep + land D266/D271 fixes), PRNG fix APPROVED (`port/src/random.c` → true MIPS64 shift semantics, verify vs `random.s`, log a `Dxx`), cutscene parallel dispatch APPROVED but needs a Claude/local-LLM session (start from the unmerged `investigate/d243-cutscene-race` branch — its `GE_D243` trace is not in main), PAL/JP DEFERRED to late pre-1.0 or 1.1. Also in M-139b: the owned PR-hygiene list for when pushing resumes (rebase+merge #56 D231 QoL — its code is NOT in main; diff-review #86; delete stale squash-merged branches like `fix/d230-raw16-bass-swap`/`feat/dropin-rom-part-a`). The user set the path to 1.0 explicitly: **Phase R** fix small/annoying N64-parity issues, **Phase Q** QoL wave, **Phase F** final-release gate; post-1.0 = macOS/ARM + LAN multiplayer (already public in the README Roadmap, which was checked and matches — no README change owed yet). Phase R order: **(1) D255 crash** (deterministic repro: Facility door-opening computer terminals, Linux-wide — top pickup, it's a crash on a level-progression step in the public "Latest" release); **(2) working-tree decision** — extract the uncommitted D266 IA4-decode + D271 portal-culling fixes from the D236 probe state onto a branch, run the owed game-wide regression sweep, land or shelve (D271 may overlap D249 distant-geometry drop-out — check first); **(3) cheap high-visibility parity items** — D252 rainbow particles (root-cause candidate known: `import_texture_rgba16` fire-tile width/height), D283 Deck preset skip, D251 F10 bottom-row duplicate, D75 logos; **(4) mid-size parity** — D245 water seam, D246 edge strips, D249 (after the D271 check), D240/D241 gunshot SFX, D230 music bass instrument; **(5) PRNG decision** — M-138's `port/src/random.c` not-bit-exact finding: fix-first (recommended) vs disclose. **Cutscenes (D243/D160/D173/D148, bar #4) run in parallel with Phase R as dispatched subagent work**, not after it. Four decisions are owed from the user at the bottom of M-139 (working-tree sweep, PRNG fix-vs-disclose, cutscene parallel dispatch, PAL/JP in 1.0 vs 1.1). + +### ARCHIVE — original RESUME BRIEF from M-139 (superseded, see the M-141 one near the top) + +Steps 1–4 of the original M-139 brief are DONE (see M-140 above: release +branch cut, R2 extraction landed, PRNG fixed, D255 root-caused and fixed). +Pick up at step 5: + +5. **Cheap parity batch** (one commit per the cadence preference): D252 + rainbow particles (confirm the `import_texture_rgba16` fire-tile + width/height suspect with one tile dump), D283 Deck preset skip, D251 F10 + bottom-row duplicate, D75 logos. +6. **Rebase PR #56** (D231 mute-on-focus-loss + screenshot hotkey — NOT in + main; stale base) onto the release branch and merge — **in scope for + v0.3.0** (QoL is on board by decision). Also scope **D282** (gamepad + front-end right-stick nav) into this release per M-139c. +7. **Mid-size parity as capacity allows:** D245 water seam, D246 edge + strips, D249 (only after step 2's D271 check — already confirmed no + overlap, see M-140's D271 finding), D240/D241 gunshot SFX, D230 music + bass instrument. +8. **Dispatchable (local-LLM/Claude sessions): cutscene thread** — start + from the unmerged `investigate/d243-cutscene-race` branch (`GE_D243` + trace, not in main); D243 first, D160/D173 data accrues from it. +9. **Optional pickup, not blocking:** D285 (new, M-140) — audio-thread + SIGSEGV, case identified via a live Deck crash + code read, not yet + gdb-confirmed or fixed. Unrelated to D255; fine to leave open into the + release if time runs short. +10. **Sign-off gate:** full 21-level sweep + campaign playtest (Windows; + Linux/Deck spot-check — the WSL `Ubuntu-24.04` environment set up this + session + the user's SSH access make this much cheaper now) + audio + pass → tag on the release branch → release→main PR (the one big merge) + → PR hygiene (#86 diff-review, delete stale squash-merged branches). + +**D236 tree investigation: STILL PARKED on `park/d236-probes-m139`, off `release/v0.3.0`, not merged.** Ninth pass (D280) found no quick win — remaining work is CPU-side room-streaming/instantiation, not a port fix. Do not resume without a specific reason; the D266/D271 fixes worth having were already extracted and landed in M-140. + +**Commit-cadence note (user feedback, M-139): batch related fixes into one commit/push instead of pushing per individual fix** — see `docs/dev/notes/COMMIT-CADENCE-PREFERENCE.md`. Also: deleting/recreating a release tag repeatedly resets its draft/prerelease flags on GitHub each time — avoid tag churn; get everything ready locally before the first tag push. + +--- + +## ARCHIVE — D236 Surface 1 tree "wall", ninth pass (D280, M-135): fog math VERIFIED N64-faithful, noise class visually confirmed as the painter, depth-race read inverted — **no quick win for trees**; remaining work is CPU-side instantiation/room-streaming, not a port fix (2026-09-15) + +**USER ORDER IN FORCE (superseded by v0.2.1 shipping, kept for history): offline only — do NOT push anything. All port changes stay uncommitted for user review.** + +### TL;DR for a fresh session — read D280 in findings.md §F first + +User asked: *any quick win to easily implement the trees now?* **Answer: no**, and this pass established why: + +1. **Fog is not a bug (D276 owed step #1, closed).** Expected per-vertex fog from the game's own constants (RSP ramp 0..154 → fmul=32000/foff=-31744; the noise class's z/w∈[0.992,1.0] → ≈26–28) vs the port's actual values for **all 2505 vertices** of `0xc81049d8`: ±2 (integer truncation only). D276's "low fog" is correct N64 behaviour — on hardware this quad also reaches the screen largely unfogged. +2. **The noise class is definitively the weave's painter (D276 owed step #3, closed).** `GE_D278C=1` collapse + `GE_PCDUMP=1500-1500:1`: band HF energy 14.8→2.9, mean 108→121; the band becomes a smooth bright haze. **No trees appeared without it** — the first unconfounded test (every prior A/B, D268/D273 included, was behind the opaque noise quad). +3. **Depth-race correction — D276's "occluder" framing was inverted.** N64 z/w: smaller = closer. At both confirmed weave points, ~278–310 of 356 `0x0c184b50` tris are CLOSER than the nearest noise tri. The open question is "why don't the closer textured cards show?", not "does the noise win the depth race?". +4. **Census at the weave points:** only 3 real classes reach `(0.05,0.10)`/`(0.25,0.15)`: `0xc81049d8` (IA8 noise painter), `0x0c184b50` (CI8, CPU-baked bimodal fog 25/255, gfog=0 — the tree candidate), `0xc8102078` (CI8). `0x00552048` (z/w=0) and `0x00504340` (z/w=-1) are probe artifacts. **`0xc8102078` ruled out as trees**: KEEPOML run draws 1.4M tris filling the lower half of the screen — terrain. +5. **The crux: `0x0c184b50` barely instantiates under deterministic boot.** `-level_36` + KEEPOML=0c184b50 → ~3–4 tris/frame, zero visible pixels, empty `texdump/`; the non-`-level_36` fog run had 720 covering tris. The room-streaming state feeding this class is not pinned by `-level_36`. + +**Do NOT re-chase:** fog computation for the noise class (verified faithful), `0xc8102078` as a tree layer (terrain), any "trees hidden behind the noise" theory (they're in front). + +### NEXT STEPS (if the tree thread resumes — multi-step, not a quick win) + +1. Single instrumented run: `-level_36` + `GE_D270PIX="0.050;0.100;0.250;0.150"` + `GE_TEXRAW=1` + `GE_PCDUMP=1400-1600:1`; correlate per tick — does a fog=25 `0x0c184b50` tri cover the point AND is the pixel dark? +2. Log `curRoom`/`roomsDrawn` (D271 infra) alongside `GE_D270PIX` to correlate `0x0c184b50` presence with room-streaming state. +3. Decode `0x0c184b50`'s CI8 texture offline — needs a run where it actually uploads (TEXRAW hasn't captured it yet). + +### Housekeeping / process gaps flagged this pass + +- **D279 was consumed by an undocumented partial session** (Sep 14 ~23:00; `scratch/d279_run.log`, `d279_imgmap_run.log`, `d279_g110/g220*.png`, `d279_gray_1500.png`) with no written conclusion — backfilled as a note-only row in findings §F. Next label after it: D280 (used this pass). +- **D277/D278 probe code** (`GE_D277`/`GE_D278C` in `port/fast3d/gfx_pc.cpp`) and the `scratch/d276_fog_run*.log` runs were added by an earlier session and never written up; their conclusions are now subsumed by D280 items 1–2, but the probes remain undocumented in findings.md. +- Artifacts this pass: `scratch/d279_collapse_1500.png` (noise collapsed — note: mislabelled d279 by the run script), `scratch/d279_treeonly_1500.png` (KEEPOML tree class, black frame); PPMs in `ppm/`; fog-run logs `scratch/d276_fog_run*.log`. +- Regenerated `docs/dev/findings-index.csv` after the D280/D279 edits. All changes uncommitted per user order. + +--- + +## ARCHIVE — D236 eighth pass (D275/D276), superseded by D280 above + +(Header originally mislabelled this "NINTH pass"; findings.md counts it as the eighth.) + +### TL;DR for a fresh session (you are the reviewer) — read D276 first, then D275, then D274 + +**A second external review of the D274 write-up flagged that pass seven's whole "which class is the wall" framing was never actually verified against the visible pixels** — every pass since D269 had reasoned about the CI8 OPA/XLU pair without first confirming those are the classes reaching the pixels the user is actually looking at. Full review preserved in this session's transcript; findings §F **D275** (probe work) / **D276** (M-133/M-134, what it found). + +**Method (D275):** picked 4 pixel coordinates directly off the reproduced weave screenshot (`scratch/d273_base_1500.png`, offline, before touching the game), extended `GE_D270PIX` from one point to a list, added a "kept N tris" counter to `GE_D270KEEPOML` (was silent — the reason pass seven's isolation attempt was a diagnostic dead end), then ran a 35 s tick-aligned capture (`GE_PCDUMP=1300-1700:25`) logging every triangle covering each of the 4 points with class, depth, and per-vertex fog. + +**Finding (D276): the D269-named CI8 classes (`0xc4112078` OPA / `0xc41049d8` XLU) don't appear at all at 2 of the 4 confirmed weave pixels.** The class that's actually there, front-most, with low fog (so its raw texel shows through largely unwashed): `oml=0xc81049d8`, **`fmt=3` (IA), not CI8** — a false-friend that shares the XLU class's low-24-bit oml bits (D272 already had to filter this exact class out of its own budget in an earlier pass without registering what it actually was). Decoded within-run (D274's address-matching method): a **fully-opaque, high-frequency blocky gray static/noise texture** — nothing like D274's gradient wedge, nothing like a tree. **Every instance of this class uses the byte-identical UV window `u:0..64, v:0..16`** (a 2× wrap of its 32-wide tile) — this is **D265's very first "all cards sample the same u:0..64/v:0..16 window" observation, verified to the exact texel.** D269's second pass reframed away from that lead onto the CI8 classes without ever confirming the CI8 classes were what D265 had actually been describing. + +**A third class, `oml=0x0c184b50` (CI8), is present at every sampled point with a wildly erratic `z/w` (`-0.0381` to `0.9940`)** — when it does land in the normal distant range its fog is high (up to 255, plausible "fading into haze" tree behavior). Flagged as the likely real owner of the N64 reference's discrete tree silhouettes, with its own probable occlusion/billboard-orientation bug (not reliably reaching these pixels). **D236 may have two separate root causes, not one**: (a) the noise texture not being washed toward invisibility the way it should be, and (b) a genuine tree-sprite class failing to draw/occlude where it should. + +**Do NOT re-chase**: the D269 CI8 OPA/XLU pair as "the wall" (D276 — they aren't reaching 2 of 4 confirmed weave pixels; still worth keeping in the picture, just not the primary suspect), the mip-blend-quality-of-a-gradient-wedge theory (D274's leading candidate, now secondary — the newly-identified content is noise, not a smooth wedge, so blend quality is a weaker explanation than it seemed), the ARGB1555 palette swap (D273/D274), seq-based cross-run identity (D274), portal culling (D271). + +### NEXT STEPS for a fresh session (priority order) + +1. **Fog-correctness check for the `oml=0xc81049d8`/`fmt=3` noise class.** D276 measured average fog 39.5–63.3/255 at the two confirmed weave pixels — is that the *correct* N64 value for this vertex's world position (work out what `fog_mul`/`fog_offset`/z *should* give), or a computation bug in this specific draw path? This is the cheapest, most direct next test — the data is already logged (`scratch/d275_pix_run.log`), no new probe needed, just the offline math. +2. **Attribute and understand `0x0c184b50`'s erratic `z/w`.** Extend `GE_D270PIX` (already list-capable) to a couple more points squarely on the N64 reference's visible tree trunks/canopies and see whether this class is the one meant to be there; if so, root-cause why its `z/w` swings from `-0.04` to `0.99` (billboard basis computed from a bad/momentarily-invalid camera vector is the leading guess, unconfirmed). +3. **Re-run the class-isolation A/B** (owed since D274, now with D275's kept-counter and D276's corrected target classes: `0xc81049d8`/fmt=3 vs. `0x0c184b50`, not the D269 CI8 pair) at the tick-aligned window established this pass (`GE_PCDUMP=1300-1700:25` — all 17 frames confirmed non-black/weave-consistent, so use any tick in that range, not just 1500). +4. Mip-blend-quality experiment (D274's original next-step) — still worth doing eventually, now lower priority given #1 is cheaper and the content is noise, not a smooth wedge. +5. Keep the D271 fix + all probes (D266–D276) until the wall closes. `GE_D273` stays default-off. `d267_slot_addr` (D274) is always-on, keep permanently. + +### Housekeeping + +- Regenerated `docs/dev/findings-index.csv` after the D274 (previously missing its own index row — added this pass), D275, D276 findings.md edits, and the D270 backfill row. +- `docs/dev/GE-ENV-PROBES.md` updated: new rows for `GE_D270PIX` (now multi-point), `GE_D270KEEPOML`/`SKIPOML`/`SKIP`/`KILLALL` (kept-counter), `GE_D270FOG`, and the `GE_TEXRAW` cap raise — the whole D270 family was undocumented until this pass. +- All changes this pass uncommitted: `port/fast3d/gfx_pc.cpp` (`GE_D270PIX` multi-point + `addr=`, `GE_D270KEEPOML` kept-counter), `docs/dev/findings.md`, `docs/dev/GE-ENV-PROBES.md`, `docs/dev/findings-index.csv`, this file. Scratch: `scratch/d275_*` (pixel-attribution run log + 17 tick-aligned frame PNGs, all confirmed non-black at the 4 sample points), `scratch/d275_r106_ia8.png` (the decoded noise texture — this is the one to look at first). +- Kill any running `ge007*` before linking/rebuilding. + +--- + +## ARCHIVE — D236 seventh pass (D274), superseded by D275/D276 above + +### TL;DR for a fresh session (you are the reviewer) + +**An external review of pass six's D273 write-up caught a real methodology hole and steered this pass** (findings §F **D274**, M-132): all prior passes' "seq" identifiers (D267's `d267_slot_img`, used by D269/D272/a D270 comment) are stale on a texture-cache hit — the stamp is written *after* the cache-hit early return in `import_texture()`, so it can report the identity of some earlier, unrelated upload into that TMEM slot. Confirmed with hard evidence: **the same `seq` value was logged against two different ROM/arena addresses within one 15 s run.** + +**Fix:** added `d267_slot_addr[128]` (`port/fast3d/gfx_pc.cpp`), stamped *unconditionally* before the cache-lookup early return, threaded as `addr=%p` into every `D272`-family log line. This surfaced a **second** identity hazard: the runtime address itself isn't stable **across separate process runs** either. **Practical rule: identify and decode a texture within one run**, never across runs. + +**Using that corrected method, this pass closed pass six's open question** — D269's "soft gradient blobs" vs. the on-screen herringbone weave turned out to be **this session's own offline-script bug** (big-endian palette read instead of native little-endian), not a port finding. Re-decoded correctly, the port's existing palette decode produces a plausible gradient wedge matching D269's description. **Mip/LOD state confirmed nominal** for that tile (`genmip=1`, `tex_lod=1`, correct `GL_REPEAT`). + +**Superseded by D276: this pass's whole premise (that the D269 CI8 pair is "the wall") turned out to be wrong at 2 of 4 confirmed weave pixels.** The gradient-wedge analysis above is still correct as far as it goes — it's just very likely describing a secondary/different layer, not the dominant visible artifact. See D276 above. + +### D272 (M-130) — still holds + +### TL;DR for a fresh session (you are the reviewer) + +**D273 (M-131):** the user supplied two screenshots — +`docs/img/screenshots/gepd-surface.PNG` (1964 N64 emulator, GEPD-edition ROM, +Surface 1 corridor) and `docs/img/screenshots/port-surface.PNG` (our PC port, +same spot). **N64: discrete conifer silhouettes (trunk+canopy visible), +sparse, blending into the sky. PC: a uniform, fine diagonal +herringbone/houndstooth weave, flat-topped, no tree shapes at all** — +reproduced headless at `-level_36` frame 1500 +(`scratch/d273_base_1500.png`). + +While chasing a live reference this session also found an **existing, +uncommitted `GE_D273` probe already sitting in `palette_to_rgba32`** +(`port/fast3d/gfx_pc.cpp`) — added by some prior pass, never mentioned in +this file or findings.md before now. It A/Bs the CI4/CI8 palette-entry +decode: standard N64 ARGB1555 vs. the inherited PD-shifted layout, with a +comment theorizing the shift flips an alpha-test outcome for the wall's +palette. **Tested it against the reference this session: RULED OUT.** Same +frame, `GE_D273=1` vs. unset — the wall's herringbone pattern is pixel-eye +identical either way (`scratch/d273_fixed_1500.png`), while unrelated CI8 +content broke (snow path → cyan/magenta moiré, viewmodel hand → teal +skin). Do not enable `GE_D273`; do not pursue this decode swap further for +D236. Full writeup + the process-gap note (an investigative lead sat +undocumented across a handoff boundary — `git diff` suspect files, don't +trust the day's HANDOFF narrative as a complete uncommitted-work inventory) +in findings §F **D273**. + +**Open discrepancy this reference exposed — RESOLVED by D274, see above.** +D269's offline CI8 decode of these wall cards described "soft vertical +gradients / sparse blobs." What's actually on screen is a sharp, regular +herringbone weave. This was flagged as a possible decode/content bug; D274 +traced it to a palette-byte-order mistake in this session's own offline +analysis script, not a port bug — reconciled, no discrepancy. + +### D272 (M-130) — still holds + +This session reviewed the D271 negative outcome and executed HANDOFF's own +next-step #1 (the per-card S/T audit) instead of guessing further. Result, +in full in findings §F **D272** (M-130): + +- Extended the tri-probe family with a new `GE_D272` (env-gated, budget 200, + filtered to D269's exact two wall-card `oml` classes) that dumps raw + per-vertex UV (texel units) + tile fmt/siz/wh/mask/shift for every matching + band tri. Built + ran against `-level_36` (boot camera), 3 iterations to + get the filter right (first pass had no oml filter and was swamped by + unrelated sky/floor geometry in the same screen band; that unfiltered data + is what likely produced D265's "cards all sample the same u:0..64/v:0..16" + read — several *unrelated* degenerate-UV draws in that band really do + share one (0,0) UV, but they are not the wall cards). +- **Both classes confirmed CI8** (`fmt=2 siz=1`), matching D269. +- **UV windows are NOT identical across card instances** — small (~0.3–1.6 + texel) but real per-vertex jitter between cards sharing the same bound + texture. This kills the "converter/tile-setup collapses every card onto + one window" theory outright. +- **The XLU class's U range is ~143–145 texels — ~4.5× its own tile's + 32-texel wrap period.** Read `tools_pc/d43_emit.py`'s `emit_main_vtx`: the + vertex S/T fields go through a plain BE→LE 16-bit byteswap, no scale/ + offset/reinterpretation (same pattern as every other `Vtx` field) — **the + wide wrap cannot be a converter bug; it must already be authored in the + ROM.** This also rules out the D108–D112 `put_f32`-byte-reversal bug class + for this specific case (verified by reading the code, not by inference). +- **Conclusion: the wide horizontal texture wrap is Rare's own design** + (tiling one CI8 texture ~4.5× across a long background quad), so the wall + is not a UV-windowing or converter bug. Hypothesis reframed back to + **TEXTLOD/mip handling of a heavily-wrapped, heavily-minified tile** + (D269's suspect #2) — how OpenGL's `GL_REPEAT` + auto-generated mips render + that wrap vs. how the RDP's own per-scanline TMEM wrap + LOD blend would — + now ranked ahead of fog range/application and palette. + +**Do NOT re-chase**: "identical UV window across cards" (D272 disproves it +for the real wall classes), a `d43_emit.py` S/T conversion bug (code-read +verified clean), portal culling (D271, fixed & kept), IA4/IA8 decode, the +blend pipeline, fog combine (all D265–D269, ruled out with evidence). + +### NEXT STEPS as of pass six (superseded by the SEVENTH-pass NEXT STEPS above; kept for narrative continuity) + +1. ~~Reconcile texture content vs. what D269 decoded~~ — done, see D274 above (resolved: no discrepancy, was an offline-script bug). +2. ~~Minification measurement~~ — done, see D272's `scr0/scr1/scr2` + `zw` fields, already in the live probe. +3. ~~Confirm mip generation actually runs~~ — done, see D274 above (`genmip=1`, `tex_lod=1` confirmed). +4. A second N64 source (`mupen64/`) — still nice-to-have, not blocking. +5. Keep the D271 fix + all probes until the wall closes. + +### Housekeeping (pass six) + +- Regenerated `docs/dev/findings-index.csv` via `tools_pc/gen_findings_index.py` + after the D272 and D273 findings.md edits. +- `docs/dev/GE-ENV-PROBES.md` updated with the `GE_D272` and `GE_D273` rows. +- All changes this pass are uncommitted (`port/fast3d/gfx_pc.cpp` D272 probe + addition — `GE_D273` in the same file predates this session and was found, + not written, here; `docs/dev/findings.md`, `docs/dev/GE-ENV-PROBES.md`, + `docs/dev/findings-index.csv`, this file). Scratch artifacts: + `scratch/d272_run*.log` (disposable iteration history), + `scratch/d273_base_1500.png` / `scratch/d273_fixed_1500.png` (the A/B + comparison frames — `d273_base` matches the user's `port-surface.PNG`), + `docs/img/screenshots/gepd-surface.PNG` / `port-surface.PNG` (the user's + reference pair, kept — not disposable). +- Kill any running `ge007*` before linking/rebuilding. + +--- + +## ARCHIVE — D236 fourth/fifth pass (D271 negative re-test, D272), superseded by D274 above + +### TL;DR for a fresh session (you are the reviewer) + +The prior pass (same day) implemented and verified **D271**: at Surface 1's +boot camera, 11 portals around room 13 straddle the near plane every frame; +their z==0 clip points project to ±1e20–1e26-scale *finite* screen coords, +and the old D106 guard (`lim=1e5`) mapped all of them to full-screen boxes, +force-drawing rooms N64 culls (roomsDrawn 17–18 vs 10–13 post-fix). Fix: +guard `lim` → `1e38f` (non-finite-only); finite garbage now clamps/culls in +`bgRectIntersect` exactly as on N64. Verified: `d106Fallback` → 0, room +drops traced per-portal to N64-matching empty clamps, no crashes. Full +write-up: findings §F **D271** (M-129). + +**USER RE-TEST RESULT: NEGATIVE — "it's still not right, trees are a big +wall rather than the texture being mapped correctly."** So culling was a real +N64-divergent over-draw (the fix is kept — it's a fidelity fix, do NOT +revert) but it is **not** the wall's root cause. The user's phrasing points +at texture mapping. + +### State of the hypothesis space (evidence trail D265 → D269 → D271) + +- **What the wall is** (D269): CI8 32×32 billboard cards, two draw classes + per room stream (OPA oml=0xc4112078 + XLU oml=0xc41049d8), both BILERP + + TEXTLOD-on + perspective-corrected + 2-cycle, omh FOG bit set. Card + contents decoded from ROM: soft vertical gradients / sparse blobs (index + histogram concentrated at low indices 6–14). NCC of every dumped card vs + the captured frame < 0.35 — the cards are not visible as rigid copies. +- **Ruled out**: IA4/IA8 decode (zero IA4 draws from this camera; the three + IA8 images are noise/mast sprites), the blend pipeline (force-alpha A/B, + D268, proves texel-alpha compositing works end-to-end), fog combine (no + blend cycle in play references `CLR_FOG`/`A_FOG`, so the omh FOG bit is + inert), and now portal-culling over-draw (D271 — fixed, wall unchanged). +- **TOP SUSPECT: per-card S/T / UV windowing.** D265 noted *"all captured + cards sampled the same u:0..64/v:0..16 window — confirm against N64"*. + If N64 gives each card its own S/T window into a shared tree-silhouette + texture and the PC samples one region for every card, the result is + exactly a repeating sheet = the reported wall. Mechanism candidates in + order: + 1. **Model DL vertex S/T data** — what `tools_pc/d43_emit.py` emits for + these cards' S/T (s16 fixed-point; format depends on tile line length). + A conversion bug (byte-swap/scale/offset) collapses all windows onto + one region. The converter is the usual suspect class here (cf. + D108–D112 `put_f32` byte-reversal). + 2. **Tile setup in the DL** (`gsSPSetTile` → image size, foldS/foldT, + tcShift) — wrong tile dimensions for this CI8 surface would map every + card's S/T into the same window. + 3. **fast3d UV handling** in `port/fast3d/gfx_pc.cpp` (tex_lod path, + GL_REPEAT period, fold math). +- **N64 ground truth**: `docs/img/bugs/d227-surface-emulator-ref.png` — a + smooth bright band with *sparse organic tree clusters* centred x≈0.35–0.75, + NOT a continuous sheet. `mupen64/` (local emulator, Rice plugin) is + available for more reference captures if needed. +- **Side suspect (palette)**: the archived third-pass noted the port's + `palette_to_rgba32` reads a=bit0, r=bits15:11, g=bits10:6, b=bits5:1 vs + the standard N64 A=15, B=14:10, G=9:5, R=4:0 — verify against the RDP + spec; if wrong it affects every CI texture and would turn a tree image + into an unrecognizable gradient blob (consistent with D269's decode). + +### NEXT STEPS for a fresh session (priority order) + +1. **Per-card S/T audit.** Extend `GE_D266W` (band-gated tri probe in + `gfx_sp_tri1`) to log per-vertex S/T + tile index + image size/line for + the wall card classes (oml 0x0c184b50 / 0xc81049d8 family). The deciding + question: do distinct cards carry **distinct** S/T windows, or identical + ones? Identical on PC → compare against what the ROM model data says + (decompiled DL words) — converter bug if they differ there. Distinct on + PC but all landing in the same texture region → tile-setup/fold bug. +2. **Decode the CI8 card + palette to an RGB PNG and LOOK at it** + (`scratch/d269_decode.py` exists; `GE_TEXDUMP=1 GE_TEXRAW=1` now also + writes `.pal` files). If it's a tree silhouette → S/T windowing owns the + bug. If it's a gradient blob → texture data or palette decode is wrong. +3. **Cross-check `d43_emit.py`'s S/T emission** for these cards against the + raw ROM model bytes (byte order, fixed-point format). +4. If PC S/T windows are distinct and sane: fall back to TEXTLOD/mip + sampling (does the port generate mips for these tiles? which min filter?) + and the palette item above. +5. Keep the D271 fix + all probes until the wall closes; then remove the + probe sets together (D266–D269 + D271). + +**Do not re-chase**: AC_DITHER/alpha-compare math (D266), IA4/IA8 decode, +blend pipeline, fog combine, portal culling (all ruled out with evidence — +see findings D265–D271). + +**Separate, still-open, NOT the same bug**: D176(a) — the Surface sky +rendering dark instead of the expected bright horizon band. It has its own +NEXT STEPS list (non-standard combine encoding `m1=8`, IA16 cloud texture) +in the "third pass" archive below. Do not conflate the two threads. + +### D271 evidence (pre-fix; reproducible, 3/3 runs) + +Probe: `src/game/bg.c`, small `#ifdef PORT` additions, all inert unless +`GE_D271`/`GE_D104` are set (uncommitted — `git diff src/game/bg.c` to see them): + +1. A `hasPortals` field added to the existing `GE_D104` room-visibility probe + (~line 656) — confirms Surface 1 **does** use portal occlusion (not the + "no portals, skip culling" branch). +2. Two file-scope counters near line 269 (`g_d271_ordinary_degenerate_hits`, + `g_d271_d106_fallback_hits`), incremented at the two branch points in the + portal-bounds degenerate check (~line 1724-1749): + - `ordinary_degenerate` = the legitimate min>=max case (a portal genuinely + edge-on to the camera — expected, matches N64). **Always 0** across + every run. + - `d106_fallback` = the D106 non-finite/out-of-range guard firing. **Never + zero, climbs every tick, every run** (e.g. run1: 396 hits by tick 601, + 781 by tick 1201; run3: 55 by tick 601, 109 by tick 1201). +3. A raw-bounds dump (first 12 hits) on the `d106_fallback` path. **Every + single hit across the whole capture logs identical numbers**: + ``` + min=(2103.126953, -67984331585523982488567808.000000) + max=(83783986331400287157223424.000000, 2070298263518753597685760.000000) + onscreencount=8 + ``` + Identical numbers on every hit = the **same portal**, every frame, from + this static boot-camera viewpoint — not a rare edge case, the dominant + condition for this viewpoint. `min.x=2103` is sane; the other three are + ~10^24–10^25 magnitude and wildly asymmetric (not "bracketing the view + symmetrically" the way the original D106 comment assumed N64 does) — this + is the `inv_z = -1e20` overflow the D106 comment already described, + `transform3Dto2DWithZScaling` being fed a z==0 (or z-straddling) portal + corner point. + +Repro (headless, ~15s, no input needed): +```sh +cd /path/to/gh-fullhist +GE_D104=1 GE_D271=1 timeout 15 ./build-pc/ge007.x86_64.exe -level_36 > scratch/d271_verbose.log 2>&1 +grep "D104\|D271 d106" scratch/d271_verbose.log +``` +Also noted: `roomsDrawn` (11–19) and the starting `curRoom` vary run-to-run +with no input given — almost certainly intro-camera/room-streaming timing +jitter (`g_RoomLoadBudget`), not the culling bug itself. Don't treat +`roomsDrawn` alone as a metric; the `d106Fallback` counter is the decisive +signal, not room count. + +### Housekeeping + +- **Commit/land decision** for the uncommitted D271 fix + probes (user order + is offline-only — nothing pushed). The fix is verified N64-faithful; it can + land independently of the wall outcome. +- If regenerating the findings CSV: `python tools_pc/gen_findings_index.py` + (CI's validate job requires `docs/dev/findings-index.csv` to be current). +- **An instance may still be running** from the user's re-test — kill any + `ge007*` before linking. + +### Probe env vars (all inert when unset) + +| Var | Purpose | +|---|---| +| `GE_D104` | Existing room-render-pass visibility probe (`bg.c` ~line 656); now also prints `hasPortals` + the two D271 counters. | +| `GE_D271` | Gates the two degenerate-bounds counters and the 12-hit raw-bounds dump (`bg.c` ~line 1724-1749). | + +### Files modified this pass (uncommitted, verify with `git diff`) + +- `src/game/bg.c`: **the fix** — D106 guard `lim` 1e5→1e38 in + `sub_GAME_7F0B5864` (~line 1770) + revised comment; probes: `hasPortals` + field on the `GE_D104` print (~line 663), + `g_d271_ordinary_degenerate_hits` / `g_d271_d106_fallback_hits` globals + (~line 272), per-point z/projection capture in the bounds builder + (~line 1652+), counter increments + raw-bounds/points dump in the + degenerate check, and a finite out-of-range (`D271 oor-finite`) logger. +- `docs/dev/findings.md`: D271 index row + M-129 detail section. +- `docs/dev/GE-ENV-PROBES.md`: `GE_D104`/`GE_D271` rows updated to post-fix state. +- Scratch artifacts: `scratch/d271_*.{ppm,log,png}` (A/B captures + heat + maps; disposable), `scratch/d271_1500_sbs.png` = the rough visual reference. + +--- + +## ARCHIVE — D236/D265–D269: Surface tree "wall" (third pass, superseded by D271 above) + +**USER ORDER IN FORCE: offline only — do NOT push anything. All port changes stay uncommitted for user review.** + +### CRITICAL REFRAME (this session's conclusion) + +The N64's bright horizon band (~231 luminance) **cannot come from the dark CI8 cards we identified**. The ROM data is genuinely dark (palette lum mean 77–172, top indices map to near-black). No bright fog color exists anywhere in a 1300-frame run. Our quads render dark. **The bright band IS the sky** (`skyRenderFull`/`skyRenderTri` → `skyPortRenderPoly` in our PC port), which renders dark in our build due to a texture/S-T/combine issue. + +### Key findings this session (all env-gated probes, inert when unset) + +| Finding | Evidence | +|---|---| +| KILLALL: band → pure black | Band IS tri1 geometry (not clear-color) | +| No single CI8 class explains the dark band | A/B skip meandiffs only 4–11 levels | +| ROM card data is genuinely dark | Both port + standard N64 palette decode give lum 77–172, top indices → near-black (0–42) | +| No bright fog color exists | 71,912 G_SETFOGCOLOR calls over ~1300 frames: all dark blue-gray (96,96,128), (0,0,0), etc. | +| Quad A (oml=0x00552048) IS the sky quad | `skyPortRenderPoly` sets clip.z=0 by construction; IA16 64×64 cloud tile; re-imported every frame | +| KEEP-A test: band = fog color exactly | pixel(0.15,0.06) = (98,97,128) ≈ fog (96,96,128). Sky renders as pure fog — texture not contributing | +| Quad B (oml=0x00504340, z/w=-1) renders NOTHING | COMPLETELY BLACK in KEEP-B test. N64 RDP clip range is [-w,+w] so z/w=-1 may still rasterize there; our port's near-clip discards it | +| D176(b) = the exact user-visible defect | "tree textures scrambled noise" — geometry exists, silhouette correct, all tree textures wrong | +| m1=8 in sky quad oml is non-standard | No G_CC_* define in gbi.h uses numeric 8. Must be GE-extended encoding. **Answer is NOT in gbi.h — it's in gfx_pc.cpp runtime parsing** | + +### Frame 730 draw order at pixel (0.15, 0.06) + +``` +[A sky quad] z/w=0 ZMODE_NONE fog=(25,25,25) IA16 64×64 cloud tile +[B1 card] z/w≈0.996 ZMODE_DEC fog=(128,146,148) CI8 16×32 +[B2 card] z/w≈0.992 ZMODE_DEC fog=(87,5,15) CI8 16×32 +[C1 card] z/w=0.908 ZMODE_DEC fog=(25,25,25) CI8 16×32 ← WINS (nearest) +[C2 card] z/w=0.908 ZMODE_DEC fog=(25,25,25) CI8 16×32 +``` +Winner: C card (CI8 0x0c184b50). But the sky quad is drawn FIRST with ZMODE_NONE — it's the background layer. The cards overlay it with depth test. + +### Why our sky renders dark (the actual bug) + +In KEEP-A test (sky quad only), the band shows (98,97,128) = fog color. This means: +- The IA16 cloud texture is contributing **zero** to the output +- Possible causes: (a) wrong/stale texture import, (b) S/T fold sampling a dark region, (c) combine mode m1=8 producing dark output in our port's GL shader, (d) vFog interpretation issue + +### NEXT STEPS (priority order) + +1. **Read `gfx_pc.cpp` combine-mode extraction code** — find where the port parses oml bits to determine pixel combine operation. The m1 field position/width may differ from standard fast3d. Search: `grep -n "combine\|COMBINE\|oml\|OML\|setCombine\|dp_set_combine" port/fast3d/gfx_pc.cpp | head -50`. **DO NOT grep gbi.h — it's been exhaustively searched; the answer is in runtime code.** + +2. **Decode the sky's IA16 cloud texture** — find which texdump rNNN corresponds to quad A's 64×64 IA16 import (line=64B, fmt=3). Check if it's bright (haze gradient) or dark. If dark → texture import bug. If bright → combine/S-T issue. + +3. **Check S/T fold correctness in `skyPortRenderPoly`** — the cloud tile is 64×64 with GL_REPEAT period=64. Verify foldS/foldT + tcShift produce correct texel sampling. A wrong fold could sample a dark region of an otherwise-bright tile. + +4. **Investigate quad B (z/w=-1) rendering failure** — N64 RDP clip range is [-w,+w], so z/w=-1 may still rasterize on N64. If this is a second sky layer, our port's near-clip or projection handling discards it. + +5. **If mupen64 path is pursued**: use `mupencheat.txt` to write rmon token at fixed RAM address (find where `rmonGetToken` reads from in decompiled code), run with level_36, capture via Rice plugin screenshot. High effort but definitive. + +6. **Check `palette_to_rgba32` 1-bit shift** — port reads a=bit0, r=bits15:11, g=bits10:6, b=bits5:1 vs standard N64 A=15, B=14:10, G=9:5, R=4:0. Doesn't change the wall-card conclusion (both dark) but may affect other CI textures. + +### Environment & build + +- Build: `export PATH="/c/msys64/mingw64/bin:$PATH" && cmake --build build-pc -j 8` +- Python: `"C:\Users\james\AppData\Local\Programs\Python\Python313\python.exe"` (has numpy+PIL) +- Run: from repo root, ROM in `./data/`. Level: `-level_36` +- Frame capture: F12 → `ppm/frame_NNN.ppm`; or `GE_PCDUMP=lo-hi:step` +- Input script: `GE_INPUTSCRIPT="120:START;..."` (M-99: CANNOT reach mouselook) +- **Do NOT run D266W with skip rules** — heavy logging + skip causes heartbeat hang +- Probe runs ≤ ~15–20 s (user preference) + +### Modified files (uncommitted, verify with `git diff`) + +- `port/fast3d/gfx_pc.cpp`: D270PIX probe (~line 1683), skip hooks + KILLALL + SKIPOML + KEEPOML (~line 1655-1760), D266W bbox probe v6 (~line 1932), `num_dls` extern + frame counter, fog logging (GE_D270FOG at ~line 2960), tdc cap raised to 600 (line 1181), `palette_to_rgba32` at line 905 +- `port/fast3d/gfx_opengl.cpp`: AC-aware edge handling (D266) +- `scratch/d269_decode.py`: offline CI8/IA16 decode script + +### Evidence on disk + +- `ppm/frame_000730.ppm` — baseline frame 730 (1957×1468) +- `scratch/base_730.ppm` — same frame, copy for analysis +- `scratch/run_skipoml.log`, `run_keepA.log`, `run_keepB.log` — A/B test logs +- `scratch/run_dump600.log` — 1229 texdump files, 436 D267 lines +- `scratch/run_fog.log` — 71,912 fog color entries +- `scratch/run_d270map.log` — D270P full run with dl= counter +- `texdump/r*.bin` + `.pal` files (cap now 600) +- `docs/img/bugs/d227-surface-emulator-ref.png` — N64 reference (sky-facing, NOT same camera as frame 730) +- `docs/img/bugs/d227-surface-starburst-ours.png` — our broken version (same scene as ref, NCC 0.33–0.41) + +### Key code locations + +- `port/fast3d/gfx_pc.cpp`: D270PIX probe ~line 1683; skip/kill hooks ~1655-1760; D266W bbox ~1932; `num_dls` defined line 3689; `gfx_run()` line 3697; fog color ~2960; `palette_to_rgba32` line 905; tdc cap line 1181 +- `src/game/sky.c` (3050 lines): `skyRender` line 275; `skyRenderTri` calls `skyPortRenderPoly` at line 2166 under #ifdef PORT; `skyRenderFull` calls it at line 2680; `skyPortRenderPoly` at line 1822 +- `src/game/sky.h`: declares sky functions +- `include/PR/gbi.h`: G_CC_* defines (lines 497-583) — all symbolic, no numeric 8 +- `mupen64/`: local N64 emulator (Rice video plugin, ROM, Glide64mk2) + +### Probe env vars (all inert when unset) + +| Var | Purpose | +|---|---| +| `GE_D266W` | Band-gated bbox probe (v6) — logs all tri1 draws covering band region | +| `GE_D270SKIP=oml24,oml24,...` | Drop draws matching lower-24-bit oml | +| `GE_D270KILLALL` | Drop ALL tri1 draws | +| `GE_D270PIX=x,y` | Pixel attribution: barycentric point-in-tri at normalized (x,y) | +| `GE_D270SKIPOML=oml24,...` | Skip specific oml classes (lower 24 bits) | +| `GE_D270KEEPOML=oml24,...` | Keep-list: drop everything NOT in list | +| `GE_D270FOG` | Log every G_SETFOGCOLOR call | +| `GE_TEXDUMP=1` + `GE_TEXRAW=1` | Dump texture imports to texdump/ (cap 600) | + +### Oml class frequency (all frames, ~1300 frame run) + +| oml24 | fmt | count | identity | +|---|---|---|---| +| 0x00000000 | 3 (IA) | 33 | Unknown (UI/fill?) | +| 0x00504340 | 3 (IA) | 167 | Quad B (z/w=-1, renders nothing) | +| 0x00552048 | 3 (IA) | 790 | **Quad A = SKY** (z/w=0, 64×64 cloud tile) | +| 0x0c184b50 | 2 (CI8) | 356 | C-cards (nearest, win depth) | +| 0xc8102078 | ? | 34 | D-class | +| 0xc81049d8 | ? | 251 | B-cards | + + +--- + +## 2026-09-18 session — **v0.2.1 hotfix batch: 6 of 7 items done, user-tested (D263 resolved as not-a-bug)** + +(All v0.2.1 changes remain uncommitted for user review; nothing pushed.) + +### Batch status (user test pass completed this session) + +| Item | Finding | State | +|---|---|---| +| Mouse wheel direction swap | D260 (`port/src/input.c` `inputPostWheel`) | ✅ user-confirmed | +| Watch inventory 3D models missing | D264 (`src/game/gunfire.c` — cross-global `ModelRenderData` template read zeroed on PC; ABI/layout class, `#ifdef PORT` explicit template) | ✅ user-confirmed | +| Watch menu WASD/stick sensitivity | D261 (`src/game/options.c` `geWatchStickFastStep()` bounded auto-repeat: delay 15 / rate 6 frames) | ✅ works; **follow-up logged**: W/S still too sensitive — want tactile one-press-one-item, no whole-list scroll on over-held key. Candidate tweaks (NOT done): longer delay, slower rate, or gate repeat to analogue stick only. | +| AllUnlocked sound on fresh save | D259 (`port/src/libultra.c` `geEepromPatchAllCheats` seeds music/sfx vol 0xFF for all-zero slots pre-CRC) | ✅ **by-ear confirmed this session** (fresh-eep launch, jingle plays) | +| Facility Ourumov not shooting Trevelyan | D263 | ✅ **RESOLVED: not a port bug.** Beat is proximity/LOS-triggered as on N64 (Bond must be walked into Trevelyan's room, lower west wing); full chain incl. execution shot verified firing on PC via temporary teleport probe + **user playtest confirmed the sequence plays normally**. All probes removed (game files byte-identical to HEAD). Kept: `port/src/input.c` GE_INPUTSCRIPT stick-token ordering fix (test harness only). Full map in findings D263. | +| Trees/billboards "wall" | D236/D265 | 🅿️ **PARKED, reverted.** IA4 I3:A1→I2:A2 decode fix was applied and user re-tested: still a wall → `git checkout port/fast3d/gfx_pc.cpp` (file is back at committed state). Full evidence trail + ranked suspect list in D265 (top suspect now: fast3d's `texture_edge`/`SHADER_OPT_TEXTURE_EDGE` path quantizes alpha to opaque/discard, defeating even a correct decode; then per-card UV windowing; then fog). Also verify what N64 actually shows there before more port-side changes. | +| Bing SEO | — (`docs/_config.yml` site.description shortened; `docs/index.md` frontmatter trimmed + duplicate h1 removed) | ✅ done, takes effect on next docs deploy (Jekyll not available locally — verified by reasoning over fetched live HTML) | + +**Uncommitted (user review pending):** `port/src/input.c`, `port/src/libultra.c`, `src/game/gunfire.c`, `src/game/options.c`, `docs/_config.yml`, `docs/index.md`, `docs/dev/findings.md`, `docs/dev/GRAPHICS-BACKLOG.md`. Untracked `scratch/*.png` = D265 evidence images (disposable). Next finding label: **D266**. + +### D263 prep (SUPERSEDED — D263 resolved this session; kept for the record) + +**Post-D263 options (user to pick):** (1) D261 follow-up — Watch-menu W/S still too sensitive, want tactile one-press-one-item (candidates documented in findings: longer delay / slower rate / gate repeat to analogue stick only); (2) commit/land decision for the uncommitted v0.2.1 batch (user order was offline-only, nothing pushed); (3) D255 sign-off — NULL-model guard in vtxstore.c (rule #2, diagnosis pinned); (4) rebase + land PR #56 (D231 mute-on-focus-loss + F12 productization); (5) parked cosmetics: D236/D265 tree wall (top suspect now fast3d `texture_edge` alpha quantization — also verify what N64 shows there), D243 Dam level-end race, D245 water seam, D246 edge strips, D75 logos, D251 F10 overlay dup; (6) parked proposals: UNLOCKED-FPS-PLAN, WIDESCREEN-FOV-PLAN, QOL-INVENTORY. + +Facility = `-level_34` (stageId 34 = `LEVELID_FACILITY`; `boss.c` decodes `-level_XX` as d0*10+d1−0x210; table in `port/src/main.c`). The scripted opening beat (Ourumov + squad; on N64 Ourumov shoots Trevelyan) never triggers. "ouromov" is ROM asset data, not a source symbol — static chase is out; this is a runtime probe session. + +**Map already located (saves the next session the search):** +- Stage setup blob for Facility = **`UsetuparkZ`** (`setup_text_pointers[34]`, `src/game/chraidata.c:817`; names are NOT level-name-derived — don't look for "fac"). Loaded at `src/game/prop.c:1271` via `_fileNameLoadToBank("UsetuparkZ", …)` into `g_ptrStageSetupFile`; internal byte-offsets rebased onto the RAM copy into `g_CurrentSetup` at prop.c:1280+. +- The beat itself = `g_CurrentSetup.intro` — a tagged record array (`stagesetup.intro`, `src/bondtypes.h:4144`). Record types: `SetupIntroEmpty/Spawn/Item/Ammo/Swirl/Anim/Cuff/Camera` at **bondtypes.h:3849–3965** (tagged union-ish; `SetupIntroCamera` carries a linked `prev`). Consumed in `src/game/bondview_r.c` (init, ~line 115) and `src/game/bondview2.c` (per-frame/respawn, ~8749). +- Suspects per D263 triage: (1) **D193 family** — NPC beat timing/trigger volumes under port tick pacing; (2) beat start condition needing Bond still / specific camera mode (intro-handling changes); (3) silent anim-name lookup failure (low priority — user says *never triggers*, not *triggers invisibly*). Known-good precondition: D253 (crouch-tile SEGV) fixed, so Bond reaches the beat. + +**First moves (next session):** +1. Preflight per the block above; read D263 + D193 + D203/D253 in findings via the index; skim `docs/dev/GE-ENV-PROBES.md` for probe conventions. +2. Repro: `-level_34`, wait out the intro, stand at the opening crouch tile, watch ~60 s. Headless is fine for the first pass (`GE_INPUTSCRIPT`; note ~2 controller reads per render frame — time scripts empirically, see D261's pulse-train notes). +3. Instrument: env-gated probe logging the intro-record state machine (which record index is active, its tag, and the beat's trigger/sequence counters) each tick for the first ~60 s; compare against what `UsetuparkZ`'s intro array actually contains (dump the rebase'd records once at load — offsets are relative to the blob start). +4. Diff behaviour vs a level whose opening beat DOES work (e.g. Dam `-level_33`) to isolate "Facility data" vs "general beat machinery". +5. Any fix: port-only unless it's the documented ABI/layout class; new finding = D266+; append to `docs/porting-notes.md` if a new bug class emerges. + +**Environment (unchanged from prior sessions):** build `export PATH="/c/msys64/mingw64/bin:$PATH" && cmake --build build-pc -j 8` (or `./build-pc.sh ntsc-final`); kill any running `ge007*` before linking. Exe `build-pc/ge007.x86_64.exe`, run from repo root (ROM in `./data/`). Headless: `GE_INPUTSCRIPT` (tokens A/B/Z/START/UP/DOWN/LEFT/RIGHT/CUP/CDOWN/CLEFT/CRIGHT/SDOWN/SNONE, `tick:TOKEN;…`, pulse = 6 reads), `GE_PCDUMP=lo-hi:step`, `GE_INPUTLOG=1`, F12 → `ppm/shot_NNN.ppm`. Python (PIL/numpy) at `/c/Users/james/AppData/Local/Programs/Python/Python313/python.exe`; **cast uint8 arrays to int32 before bit shifts**. + +--- + +## 2026-09-17 session — **v0.2.0 PUBLISHED** (tag `e1cb96ef`, live 2026-09-14 04:32 UTC; user flipped draft→public themselves) + +Final pre-publish commit `e1cb96ef` (on top of the docs pass `436484a9`): +AllUnlocked default **OFF** (fresh-save audio quirk, new finding **D259**; F10 +toggle unchanged), draw/LOD distance defaults 100→**150%**, MSAA default off→**4×**. +v0.2.0-pre draft release deleted; v0.1.0 kept. User re-tested and published. + +**Next (post-release backlog, in order):** +1. **D255 sign-off** — NULL-model guard in vtxstore.c (rule #2, needs user + sign-off; diagnosis pinned). A fresh Deck crash log arrived during the + publish window (old v0.2.0-pre bundle; user then said "user error or + something" — unconfirmed, but consistent with D255's combat repro). +2. **D259 fix** (small, port-only): seed `music_vol`/`sfx_vol = 0xFF` for + all-zero slots in `geEepromPatchAllCheats` before CRC recompute (~6 lines, + libultra.c) so AllUnlocked-on with a fresh save doesn't load volume 0. +3. **Rebase + land PR #56** (D231 mute-on-focus-loss + F12 productization; + branch orphaned by the history rewrite — rebase onto new main). +4. D243/D245/D246/D75/D251 per findings index → issue #85 (PAL/JP) when picked up. +5. **`.github/release-notes.md` still holds the v0.2.0 body** — rewrite for the + next cycle before the next tag (CI only re-stamps ``). + +## 2026-09-16 session — D230 bass fix (PR #83) + drop-in ROM Part A (PR #84); BOTH MERGED, user playtested; only the v0.2.0 tag+publish remains + +**State:** `main` = `b2d5551e` (#84 merge; #83 = `99bc2a3a`). **ALL pre-tag work +is done.** User built + playtested the merged main locally after the merges — +test passed, no further defects reported. The tag flow in "Next steps" below is +the only thing left for v0.2.0. + +**What landed:** + +- **D230 fix (PR #83, merged)**: the bass "organ" timbre on Control/Cavern/ + Runway was a byte-order bug — RAW16 wavetable samples bswap'd per-sample in + `romdataFinishCartMap` (port/src/romdata.c, D55 in-place-fixup idiom). +- **Drop-in ROM Part A (PR #84, merged)**: issue #6 Part A + interim — + drop-in NTSC-U ROM, no Python. Engine-side spawner `port/src/romconvert.c` + (`romConvertEnsureSidecars`, called from `romdataInit` after ROM validation): + missing sidecars → spawn `/prepare-assets/ge007-convert[.exe]` with + `--rom … --out …`, wait, re-check; failure aborts boot with an actionable + message (no more D179-class crash). `prepare-assets.py` runs its three emit + scripts via `runpy` when frozen (two real bugs fixed: `run_path` does NOT set + `sys.path[0]` — d88's `import d88_propdefs` died; tuple-assignment syntax + error); NTSC-U-only gate after SHA-1 with clear PAL/JP message. CI freezes the + converter (PyInstaller `--onefile`) in both linux + windows jobs before + bundling; bundle scripts copy it into `prepare-assets/` (hard-fail if missing) + and raise the size guard 60→120 MB. Release notes updated: US-only drop-in, + no Python. Verified locally: frozen exe output **byte-identical** to unfrozen + path and to shipped sidecars; `./build-pc.sh ntsc-final` clean. First CI run + failed twice, both fixed before merge: (a) windows-build — MSYS2's MinGW + Python has **ensurepip disabled**, so the pip bootstrap died → install + `mingw-w64-x86_64-python-pip` via pacman; (b) validate — D258 row made + `docs/dev/findings-index.csv` stale → regenerated with + `python tools_pc/gen_findings_index.py`. Final CI run all green. +- **Issue #85 filed** = Part B follow-up: PAL/JP filelist CSV repair (31 + truncated model rows in `filelist.e.csv` + 1 mislabeled JP font row — D258, + repair path verified but NOT applied), per-region exes + re-exec guard, + multi-region CI, optional C port of the emitters. +- **D258** added to findings (PAL/JP conversion broken at source-data level; + no PAL/JP PC build has ever been producible from this tree). + +**Next steps for a fresh session, in order:** +1. v0.2.0 tag flow (the ONLY remaining pre-publish work): user is ready → + `git tag v0.2.0 && git push origin v0.2.0` from `b2d5551e` → CI cuts both + bundles (with the frozen converter) + re-stamps `.github/release-notes.md` + (``→`0.2.0`) → verify: CI green, assets + `goldeneye-pc-port-0.2.0-{win64.zip,linux-x86_64.tar.gz}` + sha256s, notes + body final + pinned GIF URL (raw/`v0.2.0`/…) resolves → **explicit user + approval** → publish → delete the `v0.2.0-pre` draft (keep v0.1.0). + HARD RULE: never publish without explicit approval; CI only updates the + DRAFT. Optional pre-tag smoke: `./build-pc/ge007.x86_64.exe --version` + should show codename `larkspur` + origin line (#82 feature; the user's + post-merge playtest already covered the game itself). +2. Post-release backlog: **D255 sign-off** (NULL-model guard in vtxstore.c — + rule #2, needs user sign-off; diagnosis pinned, see D255) → **rebase + land + PR #56** (D231 mute-on-focus-loss + F12 productization; branch orphaned by + the history rewrite — rebase onto new main) → D243/D245/D246/D75/D251 per + findings index → issue #85 (Part B: PAL/JP + C emitters) when picked up. +3. Housekeeping: strike this section's done state after publish. + +**Environment gotchas found this session (MSYS2 MinGW Python 3.14):** +- `python -m pip` absent AND `ensurepip` disabled → `pacman -S --noconfirm + mingw-w64-x86_64-python-pip`, then `python -m pip install --break-system-packages …` + (PEP 668 externally-managed). Same on the CI windows runner. +- PyInstaller freeze: `python -m PyInstaller --onefile --noconfirm --clean + --name ge007-convert --distpath tools_pc/dist/prepare-assets --workpath + build-pyinst --specpath build-pyinst tools_pc/dist/prepare-assets/prepare-assets.py` + — needs `HOME` + `USERPROFILE` set (MSYS2 shells often lack them). Output + ~9.6 MB exe; both are gitignored (`tools_pc/dist/prepare-assets/ge007-convert*`, + `build-pyinst/`). +- `runpy.run_path` does NOT prepend the script dir to `sys.path` (only + `python script.py` does) — sibling imports need an explicit insert; MSYS2 + paths carry Windows backslashes, normalize for sys.path entries. +- **User WIP in working tree — do NOT commit these:** `README.md`, + `docs/index.md`, `tools_pc/dist/README.md.in` (style pass, still uncommitted + at session end). + +--- + +## 2026-09-15 session — v0.2.0: everything merged, tag + publish are the only steps left + +**State:** `main` = `2c95fcd1` (post-rewrite hash — see history note below). ALL +pre-tag work is done and merged: #77/#78/#79, then #80 (docs/media pass), +#81 (release notes final), #82 (build provenance: `--version` prints codename +**larkspur** + origin URL; fork policy in CONTRIBUTING). User completed a full +campaign playtest — **all 21 missions, Agent difficulty, no crashes** — so the +README/Pages now carry the end-to-end-completable claim (playtested), D254 is +field-verified FIXED, and D255 got its register capture: `Rdi=nil` → the +diagnosis is pinned to a **NULL `ObjectRecord.model`** on an active record +(destroy-path ordering; repro Bunker + Frigate during combat). Truncation was +already ruled out. Media finalized: the mp4 montage is gone from the tree AND +from history (filter-repo purge); the 28 MB GIF is the single montage asset in +README hero + Pages + release notes; old Sept-1 attract PNGs replaced with the +Sept-13 playtest gallery shots (shot-06 Bunker, shot-01 Dam); three orphan bug +screenshots deleted. Release notes had the "mute-on-focus-loss" claim DROPPED +(D231 is NOT on main — it ships post-release via #56; F12 screenshot IS in +main and stays claimed). + +**History note (2026-09-15):** `git filter-repo` purged the mp4 from history. +~289 of 666 commits got new hashes (deeper than expected — the repo had been +rewritten before, and filter-repo's message cleanup touched older messages too, +e.g. stripped one `(#NN)` suffix). Tip tree verified byte-identical; tags +`v0.1.0` + `v0.2.0-pre` re-pushed at new targets; releases/assets untouched. +**Old SHAs cited elsewhere in this file are pre-rewrite** — GitHub redirects +them, but use `git log` for current ones. Undo is still possible (old objects +unpruned locally: main `3b9e704d`, v0.2.0-pre tag obj `70702aeb`, v0.1.0 +`7aba3637`) but the user decided to keep the lean history. + +**Remaining to ship (in order; HARD RULE: never publish without explicit user +approval — CI only ever creates/updates the DRAFT):** +1. Local smoke: build + launch, confirm `--version` shows `larkspur` + origin + line and the startup log carries both (#82 feature, unverified this session). +2. `git tag v0.2.0 && git push origin v0.2.0` → CI builds both bundles, + creates/updates the draft release, stamps ``→`0.2.0` in the notes. +3. Verify: CI green; assets `goldeneye-pc-port-0.2.0-{win64.zip, + linux-x86_64.tar.gz}` + sha256s; body shows final notes and the pinned + GIF URL (raw/`v0.2.0`/…) now resolves. +4. User reviews the draft → explicit approval → publish. +5. Delete the `v0.2.0-pre` draft release after the final is live (keep v0.1.0). +6. Housekeeping: strike this section's done state, then post-release backlog: + **D255 sign-off** (fix = NULL-model guard in `vtxstore.c` + `vtxstore_fix_refs` — rule #2 game-code change, needs user sign-off; + diagnosis is pinned, see D255) → **rebase + land PR #56** (D231 + mute-on-focus-loss + F12 productization; its branch was orphaned by the + history rewrite — rebase onto new main, don't expect a clean merge-base) + → D243/D230/D245/D246/D75/D251 per the findings index. + +--- + +## 2026-09-14 session — v0.2.0 finalization (SUPERSEDED by the section above; PRs #80/#81/#82 merged, tag still pending) + +**State:** `main` = `442afa58` (#77/#78/#79 all merged; #79's conflict was the +optionsoverlay row table — resolved by keeping all three new rows: NoHitFlash, +SkipIntro, AllUnlocked). Three PRs now await user merge, **all must land +before the `v0.2.0` tag**: + +- **PR #80** (`docs/preview-montage`): the two montage commits (32 s mp4 in + README hero + Pages site, preview-video runbook; demo-gif pruned) + + finalization pass: "v0.2.0 (pre-release)" → v0.2.0 everywhere; stale + "not yet verified on real Deck hardware" line replaced with the playtest + fact (D253 fixed / D255 open); known-issues lists aligned 1:1 with release + notes incl. finding labels; AllUnlocked in the Working list; first-public- + release note (hedged "to our knowledge") in README hero + site; new + `social-preview.png` (1280×640 link card, composed from a montage frame via + local `scratchpad/make-social-preview.py`); `manifest.csv` committed, + `CONTACT-SHEET.jpg` gitignored, `scratch-gdb-cmds.txt` deleted. **User must + eyeball the social-preview card** — this environment's model can't view + images. +- **PR #81** (`docs/release-notes-020`): `.github/release-notes.md` finalized + for final (title, blockquote rewritten honestly — "load and run crash-free", + NO end-to-end-completable claim yet; new bullets: Everything-unlocked-by- + default D257 + Deck first-run preset/NoHitFlash/SkipIntro; known issues + unchanged — all eight confirmed still open). Preview GIF at top of body, + raw URL **pinned to the `v0.2.0` tag** → 404s until #80 merges AND the tag + exists. If the user does a full 21-mission campaign run before tagging, say + so and upgrade the blockquote to the completable claim. +- **PR #82** (`feat/build-attribution`): `GE007_VERSION_CODENAME` ("larkspur") + + `GE007_ORIGIN_URL` in versioninfo → `--version` prints `build : + (larkspur)` + `origin : https://github.com/jkdansereau/goldeneye-pc-port`, + startup log carries both. CONTRIBUTING gains a "Forks and derivative works" + section (rebrand/localize/extend welcome; attribution + preserve-or-link + `docs/dev/` are the ask; stripping attribution disqualifies). Build + verified ntsc-final. + +**Fork intel — PRIVATE, never in tracked files or PR bodies (user order):** +`MukaSanches/goldeneye-ascension` (+ site `mukasanches.github.io/ +goldeneye-ascension`) is a fork of THIS repo created 2026-09-12 (pre- +v0.2.0-pre), rebranded PT-BR "Ascension": own logo/roadmap/changelog + ~20 +AI-generated "learning series" course PDFs linked from its README; presents +itself as an independent line while preserving our credits for now. User's +read: low-effort bad-faith AI spam to deter. Strategy shipped in #82 (nothing +hostile): every build from our tree self-identifies (origin URL + codename +**larkspur**); when/iff they sync to pick up v0.2.0 (first cut with 60 fps + +audio + crash fixes — the only sane base) they inherit it. Detection if they +later claim independent authorship: search their tree for `larkspur` / +`GE007_ORIGIN_URL` / the origin URL. Do not engage publicly unless the user +asks. + +**Remaining to ship (order):** user merges #80 → #81 → #82 → cut tag +`v0.2.0` (CI builds both bundles + re-stamps release body from the notes +file) → verify CI green, asset names `goldeneye-pc-port-0.2.0-*`, sha256s, +body shows final notes + GIF resolves → **user publishes/flips to final** +(HARD RULE: never publish without explicit approval). Also user's call: +what happens to the `v0.2.0-pre` draft (suggest: delete after final is +published, keep v0.1.0 as history). Then Task C = D236 billboard diagnosis +(half-day budget, stop-and-report; next finding label **D258**). + +--- + +## 2026-09-13/14 session — post-gate feature queue (Tasks 0–2 done as PRs; Task 3 gated on publish state; Task 4 next) + +Per `docs/dev/notes/NEXT-SESSION-PROMPT.md`'s 5-task queue. Own branch + PR per item; user merges. + +- **Task 0 — DONE, PR #77 open** (`feat/deck-first-run-preset`): Steam Deck first-run preset (the section further down describes it). Caveats: existing-ini Decks need `-fresh`/F10; 16:10 stretch. Real-Deck test owed from the CI artifact. +- **Task 1 — DONE, PR #78 open** (`feat/f10-game-toggles`): `Game.NoHitFlash` (D232) + `Game.SkipIntro` (D216) as ROW_TOGGLE rows after Screen shake. Deliberately excluded `Video.FixMipTextures`/`Video.WrapFix` (opt-in fidelity bug-fixes, not preferences). User-verified live: rows render/toggle, persist to ini, SkipIntro=1 boots to file-select. +- **Task 2 — DONE, PR #79 merged** (`feat/all-unlocked` @ `f49331c1`): `Game.AllUnlocked` option (default flipped OFF for the v0.2.0 final: with it ON a fresh save loads audio at volume 0, D259; F10 toggle unchanged). All levels at all difficulties + 007 mode via the two live `LEFTOVERDEBUG` RAM flags seeded in `main.c`; eep-shim read-time patch sets all 20 cheat-unlock bits **and** fills empty completion times (0x3FF) with per-slot `fileGenerateCRC` recompute — the completion half is required or enabling any cheat locks every level in mission select (`g_AppendCheatSinglePlayer` → COMPLETED-only; user repro "could only do Silo"). User playtest passed (time-modifying cheats untested). Full mechanism + the `times[80]`-vs-76 mirror pitfall: findings D257. +- **Session end (release plan agreed with user):** all eight known issues in `.github/release-notes.md` walked one by one — **all confirmed still open** (wording-only changes for final; nothing removed). Plan for next session, in order: (1) user merges #77/#78/#79; (2) Task A README/pages + media pass (docs-only PR; commit referenced media, delete `scratch-gdb-cmds.txt`; user says pull new images from the contact-sheet montage) — **must land before the tag** because bundles ship their own README.md; (3) Task B finalize `.github/release-notes.md` for v0.2.0 final (``→`0.2.0`, drop pre-release, rewrite blockquote, new What's-new bullets incl. explicit AllUnlocked-default-ON bullet; "all 21 missions completable" becomes a claim only if the user does a full campaign run); (4) tag `v0.2.0` → CI re-cuts assets + release body → **user publishes/flips to final** (hard rule: never publish without explicit approval); (5) Task C = D236 billboard diagnosis (half-day budget, stop-and-report). Full detail: `docs/dev/notes/NEXT-SESSION-PROMPT.md` (rewritten for the fresh session). + +D255 stays open with its decision procedure in the section below. Next finding label: D258. + +--- + +## v0.2.0-pre: at the publish gate (2026-09-13 evening) — D253/D254/D256 fixed, D255 narrowed; user publishes the draft when ready + +**HARD RULE (user-stated): do NOT publish/deploy the release without explicit user approval.** CI only ever creates/updates the *draft*; publishing is manual on the GitHub Releases page (or `gh release publish` only if the user explicitly asks). + +**State:** tag `v0.2.0-pre` = `1e88fea0` (notes-only bump over `8d4e7443`, the code state). Draft release live with 4 assets + sha256s; CI re-cut of `1e88fea0` was in progress at session end (two push runs, ~10 min) — **verify both green and that the draft body shows the final notes** (D256 bullet, updated D255 line) before telling the user it's ready. The code in the assets is byte-identical to what the user already playtested on Windows + Deck. + +**Session work (all committed/pushed on main):** +- `c1b8560c` **D253 FIXED** (ABI-class): `stanCheckLinkedSpecialTile` prototype float/s32 mismatch — SysV x86-64 ABI divergence, N64 codegen unaffected. Deck-verified by user (got past the crouch tile). +- `ce59e856` **D254 FIXED**: Linux crash handler re-entered on SIGABRT and truncated its own log. Now: re-entrancy guard, `_exit(128+sig)`, FAULT ADDR (`si_addr`) + full register dump (Rax/Rcx/Rdx/Rsi/Rdi/R8–R11/Rsp/Rbp) from ucontext in `ge007.crash.log`. Note the log is written to **CWD**, not exe dir — a launch from an unwritable CWD loses it. +- `8d4e7443` **D256 FIXED + Deck-verified by user** ("it fixed the save on steam deck"): POSIX `$S` in `port/src/system.c sysResolvePath()` now mirrors the Windows branch — CWD `data/` else exe-dir `data/` via `readlink(/proc/self/exe)`, best-effort mkdir. This also explains the Deck "draw distance is low" report: config never persisted, every launch started at defaults (DrawDistance 100% = faithful N64 far-clip). +- `1e88fea0` release notes: D256 bullet added, D255 line updated, "no known crashes" claim dropped from the blurb. + +**D255 (OPEN, intermittent) — current understanding:** SIGSEGV in `sub_GAME_7F09BAC4` (`vtxstore.c`, PD name `vtxstore_fix_refs`) during heavy firefights on Deck. Called from `vtxstore_tick` (7F09BBBC) whenever vertex batches merge; walks active PropRecords with type==1 (PROP_TYPE_OBJ), reads `ObjectRecord.model` (offset 0x14) via a **32-bit zero-extended load** (decompiler mislabels the union member as `ChrRecord.chrflags`), then derefs `model->obj`. **Truncation is RULED OUT:** all game memory (mempool banks incl. MEMPOOL_STAGE where Models/records live) sits in the emulated N64 DRAM window 0x70000000–0x70800000 (< 4 GB; `tlbmanageGetTlbAllocatedBlock` → 0x70700000, n64stubs.c). Remaining mechanism: **stale/NULL model pointer on an active OBJ record** (destroy-path ordering — crate/effect dies, record lingers or its memory is reused when the merge tick lands in the window). `objInit` only sets model when non-NULL; the NULL-model path found (`chr.c:1890`) is CHR-only (type 3, skipped by the walk). Next Deck crash's `ge007.crash.log` decides: FAULT ADDR == 0x10 → NULL model; other small/garbage → reused record memory; Rax = the ObjectRecord. Fallback if ambiguous: env-gated probe in `vtxstore_tick` dumping each active OBJ's model pointer per merge (GE_D51/GE_D250 precedent). User has since played Facility on Deck without crashing — it's intermittent; tell them to grab the log before the next crash overwrites it. Any level with object churn works, not just Facility. + +**Deck testing facts:** local WSL builds are NOT Deck-compatible (glibc 2.43 vs SteamOS ~2.38) — only CI artifacts (ubuntu-24.04) ship to Deck; WSL is for debugging only. User's Deck flow: SFTP the tarball from the draft release, unpack, `data/` with ROM + generated sidecars (`prepare-assets`), non-Steam game shortcut. **Sidecar gotcha found while testing:** a `data/` without `pccg-*/`+`pcmodels-*/` starts fine but faults deterministically in `load_bg_file` (bg.c:865) on level load — expected degraded mode (reserve fns warn "stage loads will fault"), not a bug; don't chase it. + +**Tree hygiene:** untracked in repo root/docs: `docs/img/shots/CONTACT-SHEET.jpg`, `docs/img/shots/manifest.csv`, `docs/media/goldeneye-*.gif/.mp4` (playtest media from the gallery pass) — decide keep-vs-gitignore next session. `scratch-gdb-cmds.txt` at repo root is throwaway. + +**After publish:** D255 register capture (above); then post-release backlog per findings index (D252 rainbow particles — user said document-only for this release; F10 bottom-row duplicate; D243/D230/D245/D246/D75). + +**Post-gate local branch `feat/deck-first-run-preset` (unpushed, "final v0.2.0 touches"):** Steam Deck first-run preset + better default view distance, per user request. `getenv("STEAMOS")` in `main.c` → `videoApplySteamOSDefaults()` (`port/src/video.c`) before `configLoad()`: Fullscreen=1, Window 1280×800 (Deck native), VSync=1, MSAA=4, DrawDistance=150, LodDistance=150. First-launch-only by construction — no ini → configLoad's first-run path saves the preset; existing ini always wins (verified: user values 220/MSAA 2 survive a STEAMOS relaunch; note `Video.DrawDistance` clamps to min 100, pre-existing). Verified on Windows with `STEAMOS=1` from an isolated CWD: first-run ini seeded correctly, window opened 1280×800, repo's live `data/ge007.ini` untouched. Caveats logged in code + README Deck section: 16:10 panel stretches the 4:3 render (letterbox = parked WIDESCREEN-FOV-PLAN); a Deck that already has an ini (user's playtest one) needs `-fresh` or F10 to pick the preset up. Draw-distance context for item 2: default 100% is N64-faithful authored fog/fade (the Dam lock fade-in is console behaviour); `Video.DrawDistance` scales far fog + prop/char fade cutoffs (`bgfog.c`, `propobj.c`), `Video.LodDistance` scales LOD-swap distance (`model.c`) — both F10-settable and persistent since D256. + +--- + +## v0.2.0 pre-release cut (superseded by the section above, 2026-09-13) — docs + release artifacts; Steam Deck was the final blocker + +**Task:** cut the v0.2.0 **pre-release** (Windows + Linux/Steam Deck). User +will do a full manual playtest in lieu of the `verify.sh` sweep. + +**Done this session (all committed on main):** +- `f3882040` D194 closure recorded (M-123); `436e7c82` D251 F10 overlay QoL pass (ships WITH the intermittent 4K-fullscreen ghost-row bug — user decision, logged as known issue). +- `4f6092dd` legacy always-grab mouse mode removed entirely (D192 + D239 closed): `Input.MouseCaptureMode` gone from `input.c`/`input.h`/`optionsoverlay.c`; click-to-lock is the only capture mode. +- Docs: ROADMAP-1.0.md got a "Task 1 status addendum (v0.2.0 pre-release cut)"; README Status rewritten + Download section now leads with the two v0.2.0-pre bundles (asset names follow `bundle-*.sh`: leading `v` is stripped, so `goldeneye-pc-port-0.2.0-pre-{win64.zip,linux-x86_64.tar.gz}`) + new Steam Deck subsection. + +**Remaining before tag:** +1. `tools_pc/bundle-linux.sh`: bundle SDL2 `.so*` into the tarball with an `$ORIGIN` rpath (mirror of `bundle-win.sh`'s DLL allowlist) so the Deck sideload is turnkey — README already claims this; make it true. +2. Build both bundles from the tag commit: `tools_pc/bundle-win.sh v0.2.0-pre`, `tools_pc/bundle-linux.sh v0.2.0-pre` (Linux bundle: cross-check on a real Linux box or CI artifact). +3. **Steam Deck test session (~1 hr, needs the hardware — who has the Deck?):** install tarball via USB, Facility walk+fire (D203 repro profile), gamepad check, `Video.DisplayFPS` on Dam + Streets, grab `ge007.crash.log` if anything faults. +4. Tag `v0.2.0-pre`; publish pre-release with known issues: D243 (Dam end race), D230 (music quality), D245 (water seam), D246 (edge strips), D197 (face wrap on Silo), D75 (front-end models), the D251 4K ghost row, and "not yet verified on real Deck hardware". Flip to final after the user's playtest passes. + +**Environment:** build as before (`/c/msys64/usr/bin/bash.exe -lc 'export PATH=/c/msys64/mingw64/bin:$PATH; cd /c/Users/james/Source/Repos/gh-fullhist && ./build-pc.sh ntsc-final'`). CI (`.github/workflows/ci.yml`) builds Linux per push — the `linux-build` job's artifact is the bundle source for the tarball if local Linux testing isn't available. + +## F10 overlay QoL session (2026-09-13, shipped in v0.2.0-pre) — D251; known bug: intermittent 4K-fullscreen "repeat of top entry" at panel bottom (MSAA×fullscreen driver suspect, unverified) + +**Task:** QoL pass on the F10 in-game options overlay (`port/src/optionsoverlay.c`, port-layer only). User is playtesting live; an instance I launched was PID 11508 (may be dead by now — just relaunch from repo root, see commands at bottom of this file). + +**Done + verified building (D251 in findings.md):** +- Scrollable panel: `maxVisibleRows()` / `s_visIdx[]` / `s_scroll`; 17 rows no longer overflow the 240-tall in-game space. `s_sel` is a **visible-list index**, not a row index. +- MSAA cycle wraps both directions: `idx = (idx + dir + 4) % 4`. +- Auto-coupled % rows (Draw distance %, LOD distance %) hidden while their auto toggle is ON (`hiddenIfOn` → resolved to `hidePtr` at init). +- Nav (wheel + arrows) **clamps** at both ends (no wrap). Sliders clamp; only discrete cyclic rows (MSAA/enum/res/toggles) wrap. +- **Hover-to-highlight removed entirely** — mouse movement never moves selection or scroll; only click / wheel / arrows do. (The old hover→select→scroll-shift feedback loop near list edges was a likely source of the "duplicate row" twitch.) +- Sensitivity link: `Input.SensLink` config key (default ON) in `port/src/input.c`; `rowSet()` scales AimModeSens=38 : MouseTurnSpeed=50 bidirectionally with an `s_linkDepth` re-entrancy guard. Overlay-side only (direct ini edits bypass it — by design). +- "Mouse aim speed" row repointed from inert `Input.MouseAimSpeed` to the active GEPD knob `Input.AimModeSens` (uiMin 1, uiMax 80). New "Link aim/turn sens" toggle row added. + +**OPEN BUG — REASSESSED (wrap theory dead, local repro clean):** launching at 4K (user's monitor 3072×1728; live ini `Fullscreen = 1`, `MSAA = 4`), opening F10 shows a **dark, unselectable repeat of the top entry ("Fullscreen") at the bottom** of/under the panel. Only in that mode. + +**Reassessment (fresh trace, this session):** +- `gfxFramebuffer` has **zero readers** — the textured-quad present (`G_SETTIMG_FB_EXT`/`select_texture_fb`) is never used by the game. Presentation is a full-frame `glBlitFramebuffer` from the same-size MSAA FBO to the default framebuffer (fb index 0 is reserved for the window; `different_size` is structurally always false since both dims come from `gfx_current_window_dimensions`). No UVs in a blit → **UV-wrap is impossible; that theory is dead.** Do not apply the CLAMP_TO_EDGE fix — it would change nothing. +- The overlay DL draws each visible row exactly once, in-bounds, own scissor (0,0,W,H). `aspect_mode` is never set by GE (no `G_ASPECT*` anywhere in src/), so 16:9 just stretches RATIO_X≠RATIO_Y uniformly through viewport+scissor — no letterbox offset, mouse mapping stays correct. +- **Local repro attempted and CLEAN:** windowed-maximized 16:9 (2987×1705) + MSAA=4 + `GE_OPTIONSOVERLAY=1` + `GE_PCDUMP` on `-level_09` → no ghost row. The FBO/MSAA path alone is not the trigger. +- Remaining variable: **true SDL fullscreen on the 3072×1728 display** (driver/display-level suspect — that resolution is non-standard; check Windows DPI scaling vs actual drawable size if it persists with MSAA off). Also removed: the `GE_OV_FORCE_SEL` temp debug hook (done this session). + +**User update (logged, NOT fixed yet):** the ghost is *intermittent* across launches; user suspects the Draw/LOD distance % rows appearing/vanishing when their auto toggles flip (`hiddenIfOn` visible-list resize). Unverified — emit and input both rebuild `s_visIdx`+scroll together each frame, so if it's this, the window is the transition frame itself. User: so far it appears on *first open* of F10 (not only after an auto-toggle flip). **Next (needs the user, ~1 min):** bisection on their machine — F10 → set **MSAA = OFF** (tagged restart) → restart, still fullscreen 4K → open F10: ghost gone ⇒ MSAA×fullscreen driver interaction (park as display-specific, maybe try a standard 2560×1440 fullscreen); ghost persists ⇒ pure-fullscreen path bug, then instrument `gfx_current_window_dimensions` vs `SDL_GL_GetDrawableSize` at frame start in fullscreen. **Also logged (QoL follow-up):** make the auto-linked % rows visually subordinate to their toggle (indent / dimmer colour) so the pairing reads obviously. + +**Environment / state notes:** +- User's live `data/ge007.ini` already has `Fullscreen = 1`, `MSAA = 4` (backup at `/tmp/ge007.ini.bak` — I did not modify their ini). Monitor: single display 3072×1728, window fullscreen at 0,0. +- `ppm/user4k.png` exists from a PowerShell `CopyFromScreen` capture but was taken with the overlay **closed** — low value; user asked to stop analyzing captures. +- Build: `/c/msys64/usr/bin/bash.exe -lc 'export PATH=/c/msys64/mingw64/bin:$PATH; cd /c/Users/james/Source/Repos/gh-fullhist && ./build-pc.sh ntsc-final'`. Launch from repo root: `./build-pc/ge007.x86_64.exe &` (needs `./data/` ROM). +- Design doc for the overlay: `docs/dev/OPTIONS-MENU-PLAN.md` (approach C: port-layer overlay, zero src/ menu edits). + +## M-123 (2026-09-14) — D194 ① log mined + wrap-spike fixed; spazz NOT in our writes (they're rock-stable); next: live capture with the new residue/autoaim logging, then user A/B playtest + +**Worked ① (crosshair/arm spazz) per M-122's plan.** + +**Log mining (`d194_gepd.err`, 1964 gepdaim lines) — key result: the port's own writes are rock-stable.** Zero crosshair/camera jumps while aim is held (scan: consecutive gepdaim lines with btn&0x2000, Δcam>3° or Δcross>0.5 → 0 events). Every large camera delta in the log turned out to be user hipfire stick swings between polls (verified against the interleaved `cont0` stick lines — e.g. the ±20–38° "jumps" at gepdaim indices 1183/1348 follow stick ramps to (80,26)). Exactly **ONE true anomaly**: a +359° one-tick spike in raw `vv_theta` (cam 0.5 → 359.7 at log line 534) — our own `[0,360)` wrap of the edge-scroll accumulator firing when a leftward scroll crossed zero. **Fixed: the two wrap lines are deleted** (`port/src/input.c aimGepdCompute`) — GEPD does bare `camx += ...`, and the game never wraps `vv_theta` in on-foot play either (`bondviewApplyVertaTheta` only sin/cos's it; tank code is the sole wraper). **Correction to M-122's suspect (a) mechanism:** the theory that the spike feeds the crosshair via `speedtheta` does not hold — `speedtheta` comes from stick input (`bondview2.c:6056`, `analogTurn/70` or the aim-turn easing), never a `vv_theta` delta; all solo-play consumers of `vv_theta` are sin/cos or `(360−θ)` rotations (raw-delta consumers at bondview2.c:1361 swirl / :9397 backstab check are MP-only). So the wrap fix is correct-vs-GEPD hygiene, but likely NOT the visible spazz. + +**Also resolved from static reading:** suspect (b) tick ordering — our write runs BEFORE `bondviewProcessInput` each tick (`osContStartReadData → inputComputePad`, libultra.c:1094), vs GEPD writing after game code; but the displayed fixed point is `X·damp+turn` either way in steady state, so ordering alone can't cause spazz. Suspect (c) 2px deadband — does not apply to the GEPD path (deadband lives only in the legacy fallback). Suspect (d) centre-spring nudge — ~0.1–0.3° over its 2 ticks, negligible; kept (clears `docentreupdown`, which must not fight aim). + +**Remaining live suspect for ① (now instrumented):** the game-side turn terms added on top of our overwrite every tick — `turn_x = autoaimx | speedtheta*0.3`, `turn_y = autoaimy | −speedverta*0.1` (HONEY block bondview2.c ~6140-6170 → `caclulate_gun_crosshair_position_rotation` gunfire.c:4819, which does `pos = pos*damp + turn`). Prime suspect: **auto-aim pull oscillating as lock-on jitters** (displayed crosshair = our value·damp + autoaim term; matches "worst near enemies"). The gepdaim log line now also prints `game=(resX,resY)` (pre-overwrite residue = last tick's `pos·damp+turn` — the gap vs our write IS the turn term), `aa=(autoaimx,autoaimy)`, `st=`/`sv=` (speedtheta/speedverta). **A live capture while aiming at guards will confirm or kill it in one look.** + +**Item ② (FPS drop):** checked `sysLogPrintf` — plain buffered `fprintf(stderr)` (port/src/system.c:87), cheap; the ≤10fps-while-aiming session's correlation with GE_INPUTLOG is probably coincidence, but the control retest (logging OFF) still stands as first step. Frame-time lines in the old log are too sparse to use. + +**Repo state:** `fix/d194-mouse-dt-decouple` @ ef49d9e6 + UNCOMMITTED: `port/src/input.c` (wrap removal + extended gepdaim log), `docs/dev/findings.md` D194 status (M-123 note). Build clean, `-level_09` headless sanity run crash-free (gepdaim lines only fire while aiming — needs live mouse). `d194_gepd.err` still in repo root (mined; can be deleted once the new capture supersedes it). + +**M-123 update (user playtest of `a7059c46`+`bd6caef5`, sens 25):** user verdict — **"looks good... other than sensitivity we're good, 95%."** ① is ACCEPTED as-is (residual arm/gun/crosshair glitching deemed playable; not worth another round). The `d194_gepd2.err` capture came out EMPTY (PowerShell 5.1 `2>` redirect on a native exe didn't capture stderr — use the `Start-Process -RedirectStandardError` form from M-122 if a capture is ever needed again). Remaining work = **③ sensitivity co-tune, live**: ini now at `GepdSens = 30` (was 25; code default also 25 — resync the default once the user lands on a number they like). Tune loop: close game → edit `data/ge007.ini` `GepdSens` (1–80) → relaunch; no rebuild needed. If 30 is too fast, bisect 27/28. Once settled: update code default + this note, then re-test ② (aim-mode FPS with logging OFF — never properly controlled yet) before considering the D194 branch merge-ready. + +**M-123 CLOSED (this session):** sensitivity co-tuned live and **merged to main as `f239adf5`** (`fix/d194-mouse-dt-decouple`, branch deleted). Final state: hipfire gain recalibrated (`MouseTurnSpeed` default 100→50; the old gain saturated the game's quadratic natural-turn curve at ~13 px/poll = always full 315°/s, the "too fast when I leave aim mode" complaint); `MouseSensitivity` master now scales aim mode too so both modes move in lockstep (D238 step-1); defaults set ~25% above the user-approved match point (`AimModeSens = 38`, `MouseTurnSpeed = 50`, master 100 = calibrated baseline). User-facing key renamed `Input.GepdSens` → `Input.AimModeSens` (community name; old key kept as deprecated alias, internal `gepd*` identifiers kept for GEPD provenance). **D194 is now fully closed:** ② (aim-mode FPS) resolved by user report — "no performance [issue] is good now" during the calibrated playtest sessions; the original ≤10fps complaint did not recur, so it's closed on user evidence (the formal logging-off control was never run; revisit only if an FPS regression appears later). Playtest logs `d194_gepd*.err` deleted. Findings/HANDOFF closure notes are recorded but **uncommitted by user request** — commit them with the next change. + +--- + +## M-122 (2026-09-13) — D194 GEPD-mirror aim IMPLEMENTED + user-confirmed "mechanically works"; three items left: ① crosshair/arm spazz glitch, ② aim-mode FPS drop (perf.ps1), ③ sensitivity tuning. Next session starts at ①. + +**The aim rewrite landed.** After four failed stick-synthesis models (absolute → dial → relative-angle → velocity-mapped stick; see M-121 + commits `8a8d066b`/`650b7f63`/`7f65ed15`), the session found and read **GEPD's actual source** (`C:\Users\james\Games\Emulators\Nintendo 64\1964_GEPD_Edition\1964\source.tar.xz` → `MouseInjectorPlugin/games/goldeneye.c`, extracted copy at `/c/Users/james/AppData/Local/Temp/gepd-src/`) and mirrored its model exactly. **User verdict: "mechanically it works... the aim mode works how we'd want."** Committed as `ef49d9e6` on `fix/d194-mouse-dt-decouple` (branch is up to date with main; PR #75/#76 already merged in). + +**The model (all in `port/src/input.c`, `aimGepdCompute()` ~line 1377):** mouse delta moves a crosshair POSITION accumulator (`s_gepdCrossX/Y`, clamped ±`GEPD_CROSSHAIR_LIMIT`=5.16); each tick it writes `crosshair_x/y_pos` + `gun_azimuth_angle/turning` (GEPD formulas, failsafe weapon offsets 0.15/0, RATIOFACTOR=1 for our 4:3, `GEPD_BASE_FOV`=90) and edge-scrolls the camera only past 72% toward the screen edge (`(ratio-0.72)*475*dt*(fov/90)`). **No look stick is emitted while aiming.** On aim release we stop writing and the game's own damping eases crosshair+arm back to centre; every non-aim tick adopts `crosshair_x/y_pos` so re-entry starts where the game left it (GEPD does the same). + +**The key correction this session (cost a broken build):** GEPD patches **nothing** in bondview2's damped crosshair update (`caclulate_gun_crosshair_position_rotation`, gunfire.c:4819) — its per-frame overwrite simply *wins* over the damping. The first implementation guarded that function off via `portAimOverrideActive()` → crosshair froze at centre, because the visible HUD crosshair + aim ray are drawn from `crosshair_angle` (gunDrawSight gunfire.c:6395; get_bullet_angle :4932) and the arm offset from `field_FFC` (gunUpdateAndFire ~gunfire.c:540-547, scaled by weapon stats PlayX/PlayY) — **both derived only inside that function**. Guard removed entirely; `bondview2.c` now carries only an explanatory comment. **There is NO permanent game-code change** — the previously-approved bondview2 exception was never needed. (findings.md §F D194 still describes the old attempts — update it this session.) + +**Field mapping verified:** GEPD's N64 RAM offsets (crosshair 0x9F0/0x9F4, gun 0xA04/0xA08, cam 0x148/0x158, aimflag 0x124) differ from our 64-bit struct by a **constant 0xBE0** = pointer-width expansion between vv_theta (0x148, matches exactly) and the crosshair block. Field *names* are correct; do not "fix" them toward GEPD's numeric offsets. + +### Remaining work, in user-specified priority order + +**① Crosshair/arm "spazz" glitch (TOP PRIORITY).** User: crosshair "glitches or spazzes out and moves around a bunch, so does the positioning and Bond's hand/gun." Ranked suspects: + - **(a) `vv_theta` [0,360) wrap in `aimGepdCompute`** — if the game holds a *negative* `vv_theta` (turning left), the first edge-scroll tick snaps it +360. sin/cos consumers see nothing, but anything doing raw deltas (e.g. `speedtheta`, "computed from vv_theta") gets a one-tick 360° spike → HONEY feeds `turn_x = speedtheta*0.3` into the crosshair (bondview2.c ~6157) → visible slam. GEPD just does `camx += ...` with no wrap. **Fix: delete the two wrap lines.** + - **(b) Tick ordering vs auto-aim/view-speed turn terms.** HONEY's caclulate_ input is NOT the stick — it's `autoaimx/autoaimy` (target lock) or `speedtheta*0.3`/`-speedverta*0.1`. Those get *added* to `crosshair_x_pos` every tick on top of our overwrite; if `inputComputePad` runs BEFORE `bondview2ProcessInput` in a tick, the derived `crosshair_angle` shown that frame includes damp-shrunk value + turn term → one-frame tugs, worst near enemies (auto-aim snap) and while edge-scrolling. Check call order; GEPD's plugin writes *after* game code each frame — if ours is before, consider deferring the write to a post-update hook. + - **(c) 2px/poll deadband stick-slip** (`AIM_ABS_DEAD_PX`) at low mouse speeds — whole-poll drop makes the crosshair crawl in steps; GEPD has no deadband (emulator deltas are clean). Consider subtractive deadband or dropping to 1px. + - **(d) `s_centreClearTicks` spring-clear nudge** (2-tick ±61 pitch stick on aim entry, docentreupdown) — with caclulate_ running again it feeds `turn_y` → small crosshair jump at aim entry. GEPD doesn't do this; candidate for removal. + - **Diagnosis first:** repro with `GE_INPUTLOG=1`, capture the log (stderr → file, see below), look for 360° jumps in `cam=(...)` and oscillation in `cross=(...)` in the `gepdaim` lines. Repro into a level without menu navigation: `./build-pc/ge007.x86_64.exe -level_09` (BUNKER1) or `-level_33` (Dam); drive input with `build-pc/aiminject.exe [dx dy steps holdms]` (RMB + relative moves, finds the "GoldenEye" window). + +**② Aim-mode FPS drop.** User saw ≤10 FPS (DisplayFPS overlay) in the last playtest session; hip-fire/menu fine. **First control: that session had `GE_INPUTLOG=1` — retest with it OFF before anything else** (per-tick `sysLogPrintf` while aiming). Then run `tools_pc/perf.ps1` (ETW/xperf, needs elevated PowerShell): baseline `perf.ps1 -level_33 -Seconds 30` without aiming, then a second capture while holding RMB + moving the mouse (user or aiminject), diff `cswitch_thread.csv`/`profile_detail.csv`. Caveat from its header: our exe's DWARF symbols don't resolve in WPA — hot frames show as raw module; symbolicate sample addresses with `addr2line -e build-pc/ge007.x86_64.exe -f -C ` (image base 0x140000000). Suspects if CPU-bound: auto-aim scan while RMB held (verify it's actually NEW cost vs legacy aim, which also held RMB), viewmodel skinning, the edge-scroll camera writes. If NOT CPU-bound: render-path stall (crosshair/decals/draw calls) — instrument frame time in `port/fast3d/gfx_pc.cpp`. + +**③ Sensitivity tuning (LAST, user will co-tune live).** Knob: `Input.GepdSens` (default 20, range 1–80) → `sens = gepdSens/2920` per window-px. GEPD's own formula for reference: `crosshairpos += delta/10 * (SENS/292) * accel` with their basefov=60 override; ours is adapted to native 90° FOV + no /10 chunking, so the numbers are NOT directly transferable — tune by ear/eye against "normal aiming" feel (C-stick speed levels `(stick-60)/10`). Also tunable: `GEPD_SCROLL_SPEED` (475), `GEPD_EDGE_THRESHOLD` (0.72) if edge-scroll feel is off; per-weapon `offsetpos[32]` table from goldeneye.c if the arm pose looks wrong for specific weapons (we use failsafe 0.15/0). + +### Environment / procedure notes (this session's hard-won ones) + +- **Build:** `export PATH="/c/msys64/mingw64/bin:$PATH" && ./build-pc.sh ntsc-final` from repo root (~5s cached). The manual `build-pc/link.bat` path also works but needs its `set PATH=C:\msys64\mingw64\bin;%PATH%` line (cc1.exe dies 0xC0000135 without it) and Windows-style TMPDIR. **Kill any running `ge007*` process before linking** (exe lock). +- **Run for playtest:** from repo root, `powershell -NoProfile -Command "$env:GE_INPUTLOG='1'; Start-Process -FilePath '.\build-pc\ge007.x86_64.exe' -WorkingDirectory (Get-Location) -RedirectStandardOutput 'd194_gepd.log' -RedirectStandardError 'd194_gepd.err'"` — **GE_INPUTLOG lines go to stderr** (`d194_gepd.err`), stdout stays empty. ROM at `./data/ge007.ntsc-final.z64`, ini at `./data/ge007.ini` (user's live values: DisplayFPS=1, 1152x864). +- **clang strictness:** game files build with gcc's leniency; clang rejects implicit decls/int-conversions. `struct player *p` not `Player *p`. Forward-declare statics before use. +- GEPD mouse settings live in the emulator's `plugin/mouseinjector.ini` (numeric, 10 values × 4 players; couldn't fully decode — first value 87 doesn't fit the CONFIG enum). We use GEPD defaults + our own knob instead. + +**Repo state at session end:** `fix/d194-mouse-dt-decouple` @ `ef49d9e6`, clean tree, up to date with main. No running game process (killed before close-out). **Untracked `d194_gepd.err` in repo root is kept on purpose: it holds the `gepdaim` per-tick lines from the user's "mostly feels good but spazzes" playtest of the final build — mine first evidence for ① (look for 360° jumps in `cam=` and oscillation in `cross=`). Next session: preflight per usual, then start at ① above — suspect (a) is a two-line fix worth doing before the log-driven diagnosis. + +--- + +## M-121 (2026-09-13) — PR #75 (D250 getenv fix) + PR #76 (D222/D218/D249 FOV/culling/drawdistance) both MERGED, user-confirmed across resolutions; RMB aim-mode still broken, two more attempts failed, session ended on usage limit mid-investigation + +**PR #75 and PR #76 both merged to main this session, both user-confirmed working.** D250 (getenv caching fix) confirmed clean at every resolution tested, 640x480 through 4K — the initial "still bad at 1440p+" report was a stale-build false alarm, corrected in the docs. D222 (FOV-scale cull-plane fix) + D218 (Video.DrawDistance) + D249 (Video.LodDistance) landed together, then needed **two live retuning passes** before the user was satisfied: (1) a real regression found and fixed live — D222's `c_lodscalez` widening also fed `propobj.c`'s character/prop fog-visibility-fade cutoff uncompensated, making guards fade *sooner* at wide FOV; composed `portDrawDistanceMultiplier()` into those call sites to fix it. (2) The `DrawDistanceAutoFov` coupling needed doubling twice live (1x -> 2x -> 4x `FovScale`, clamp ceiling 2.5x -> 4.0x -> 6.0x) before the Dam tunnel's blue fog artifact and guard draw-distance were acceptable. **User's own final verdict: "good for now."** Repo is clean on `main` after both merges. + +**RMB aim-mode (D194 continuation): two more attempts this session, both did not land, session ended mid-plan on the weekly usage limit.** + +- **Attempt 1 (small, safe): raised `AIM_MOVE_THRESH` 0.3->2.5px and lowered `Input.MouseAimSpeed` default 16->14 (matching the user's live ini, which had drifted to 50).** User's verdict: **"didn't actually fix anything"** — confirms the core problem isn't a magnitude/gain issue, it's the shape of the response. +- **Real root cause found via code reading (still valid, not invalidated by what follows):** `bondviewProcessInput(s8 stick_x, s8 stick_y, ...)` in `src/game/bondview2.c` takes a **signed 8-bit stick** — the smallest representable nonzero aim-turn value is a hard jump to 10% speed (stick=61; `(stick-60)/10` is exactly 0 at stick<=60). No threshold/curve tuning on the port's synthesis side can produce a smooth ramp through an integer that can only be 60 or >=61. Confirmed via `pd_port` (`C:\Users\james\Source\Repos\pd_port`) reference: PD's own mouse-look (`src/game/player.c:3669`, `src/game/bondeyespy.c:945`) never synthesizes an N64 stick at all -- it's `theta += mdx * scale`, a continuous float angle accumulator, no floor, no int8 quantization anywhere. +- **User also reported a second, more specific symptom mid-investigation: aim resets to center the instant mouse movement pauses (even while still holding RMB), breaking precision tasks (shoot-the-lock, watch-laser-cuts-metal-in-train puzzles).** Traced `bondviewCurrentPlayerUpdateSpeedTheta`/`speedVertaUp`/`Down` (bondview2.c) in detail: confirmed this is a smoothed rate-based decel model (`g_CurrentPlayer->speedtheta` eases toward 0 over several ticks when input stops, does NOT hard-reset) -- so a literal snap-to-center was not found in that specific mechanism. Root cause of the reset symptom specifically was **not conclusively identified** before moving to the rewrite. +- **Attempt 2 (bigger, PD-style, approved by user before starting): bypassed the s8-stick synthesis entirely for aim-mode.** `port/src/input.c` computed a continuous -1..1 turn/vert speed (`inputGetContinuousAimSpeed()`, new function + header decl) instead of synthesizing `sx`/`sy`; `src/game/bondview2.c`'s aim-mode block (`#ifdef PORT`) read it directly, bypassing the `(stick-60)/10` conversion but leaving all downstream `speedtheta`/`speedverta` easing untouched. Removed the now-dead `Input.AimBand`/`Input.MouseAimCurve` knobs. **Compiled clean (verified both PORT and N64 `#ifdef` brace balance carefully), crash-free in a headless sanity check -- but broke live: "somethings broken... can't aim at all... it locks my input completely with the mouse."** Root cause of THAT breakage was not identified (no repro available headlessly; ran out of session before a careful live-debug pass). **Fully reverted** (`git checkout -- src/game/bondview2.c port/src/input.c port/include/input.h` back to the `db6eba2b` merge commit) — repo confirmed clean, PR #62's mouse work is back to its pre-session (working, if imperfectly-tuned) state. Live ini's `MouseAimSpeed` also manually restored 14->16 to match the reverted code default. + +**Best-guess candidates for what broke Attempt 2, NOT verified, next session should check these first:** (a) thread-safety -- `aimContinuousTurnSpeed`/`VertSpeed` were plain `static float`s in `input.c`, read from `bondview2.c` with no synchronization, and this port has separate scheduler/game/input threads (D24/D134 territory); (b) `bondviewProcessInput`/`MoveBond` may run multiple times per rendered frame during multi-tick catch-up (per D193's earlier finding that `g_ClockTimer` can exceed 1), so the continuous value could have been consumed repeatedly per single mouse sample in a compounding way neither of us anticipated. + +**Proposed next step (discussed with the user, not yet started -- session ended before implementation):** a safer, lower-risk redesign that stays **entirely inside `port/src/input.c`**, never touching `bondview2.c` at all. Instead of deriving the synthetic `sx`/`sy` fresh from only the current poll's raw delta, maintain a smoothed/persistent stick value (exponential filter on the *output*) that ramps toward a target implied by recent mouse speed and decays gradually on release, rather than snapping instantly in either direction. This softens both the floor-crossing (builds up over a few polls) and the release/reset behavior (decays instead of vanishing), while keeping the change confined to one already-well-understood port file -- easy to revert, easy to reason about, no `#ifdef PORT` game-logic surgery. **Add a temporary debug log (`GE_INPUTLOG`-gated) that prints the smoothed value every poll, so it can be sanity-checked from a log BEFORE handing back to the user for a live test** -- the process this session was explicitly asked for and didn't happen before Attempt 2. + +**Repo state at session end:** clean, on `fix/d194-mouse-dt-decouple` (still holds PR #62's D194(b)/D238/natural-pitch work, unmerged, mergeable, behind main by the two PRs merged this session -- rebase before resuming). No uncommitted changes. No running game process. + +--- + +## M-120 (2026-09-13) — D250 ROOT-CAUSED AND FIXED (uncached getenv() ~50% of render-thread CPU time); PR #75 open; PR #62 aim-mode still needs work; new tools_pc/perf.ps1 + +**D250 resolved this session — the fps drop was `getenv()`.** Set up a real profiler (`tools_pc/perf.ps1`, new — wraps `xperf` CPU-sampling + context-switch tracing, sister script to `debug.ps1`) since ad-hoc timers had ruled out `gfx_run`/`lvlRender` without finding the cost. A live WPR/xperf trace on Dam with Microsoft's public symbols resolved (`-Symbols`) found the dominant render thread spending only **5.2%** of its CPU time in the game's own code — **43% in `msvcrt.dll`**, and `getenv()`/`_getenv_helper_nolock` alone at **~50% of the thread's total CPU time**. Root cause: `port/fast3d/gfx_pc.cpp`'s `import_texture()` (every texture bind) and several per-triangle/per-`G_SETCOMBINE` debug-probe checks called `getenv()` **uncached** on every call, unlike the safe cached pattern used everywhere else in this codebase — tens of thousands of locked, locale-aware Windows CRT environment lookups per second on a texture-heavy scene. **Fixed:** cached every site in `gfx_pc.cpp`/`gfx_opengl.cpp`. **Verified:** the exact idle-Dam test that was pinned at 45-50fps now holds a rock-steady 60-61fps, zero dips — and the user confirmed live at 640×480 it "seems fine" now. This also retroactively explains the earlier session's MSAA/aniso/FOV/resolution/vsync A/B pass all showing "no change" — the getenv() cost was a large per-call constant swamping any real, smaller GPU-side signal at every setting tried. **PR #75 pushed** (`fix/d250-frame-pacing-sleep-units`, now titled around the getenv fix — also carries the earlier sysSleep unit-conversion fix and the `GE_D250` `lvlRender` probe from this same investigation), CI running at session-end, not yet merged. + +**"Still runs badly at 1440p+" turned out to be a stale-build false alarm** — user re-tested 1440p/4K on the actual getenv-fix build and confirmed it holds up fine there too. **D250 is fully resolved, all resolutions tested clean, no follow-up needed.** One gotcha hit setting up `perf.ps1`: its first version was missing `debug.ps1`'s mingw64-bin-on-PATH fix and failed with a DLL load error from a plain elevated shell — fixed, same PATH-prepend logic now in both scripts. + +**PR #62 (mouse feel) status unchanged from earlier this session** — hipfire/primary look confirmed good by the user; aim-mode (RMB/Shift) still too sensitive, `AIM_STICK_MIN=61` hard-floor hypothesis recorded in `findings.md` D194, not yet fixed/reverified. Deprioritized this session in favor of the performance investigation once it became clear perf was confounding the aim-mode feel-check anyway. + +--- + +## M-120 EARLIER (2026-09-13) — PR #62 rebased onto main (picks up D248); user playtest: hipfire good, aim-mode still too sensitive; performance diagnosis (D250) queued next + +**PR #73 merged, `fix/d194-mouse-dt-decouple` (PR #62) rebased.** #62 was 15 commits behind `main`, missing D248's 60fps fix among others — merged `main` in (two doc-only conflicts in `findings.md`/`findings-index.csv`, resolved by keeping the newer main-side text + regenerating the CSV), rebuilt clean (`ninja -j1`), pushed. A headless `verify.sh bunker1` sanity check afterward reported `NO-FRAMES (pixcount 0)`, but the script's own capture-directory (`mktemp`/`/tmp`) plumbing looks broken in this shell environment (its `run.log` write itself failed with a path error) — looks like tooling, not a real regression; not chased further, worth a look next session. + +**User played PR #62 live. Verdict: hipfire/primary look is GOOD, aim-mode (RMB/Shift) is still too sensitive and unusable for precision aiming.** Updated D194's status cell with the detail (`docs/dev/findings.md`). Static re-read of `port/src/input.c`'s `aimHeld` branch found a plausible concrete cause the M-107 curve fix didn't address: `AIM_STICK_MIN=61` is a hard floor the stick snaps to the instant `|aimEdx| >= AIM_MOVE_THRESH` (0.3 px/poll) — the `aimCurveGamma` response curve only shapes the range *above* that floor, so tiny mouse nudges still jump straight to 10% of max aim-turn speed with no near-zero ramp, unlike the hipfire path (fully proportional, no floor) the user says feels right. **Not build/runtime-verified yet** — a hypothesis from reading the code, not a confirmed fix. **PR #62 stays held, unmerged** — needs another aim-mode iteration + re-playtest before it can land. + +**User also reports the port is now performance-limited — ~45fps at 720p on a high-end PC — which makes it hard to judge feel further.** This is the user-facing surface of **D250** (filed M-119, tied to D248 exposing real per-frame cost that used to be invisible while the port silently ran at half rate). **Same session: diagnosed partially, real bottleneck not caught yet.** Ruled out: build flags (`-O2 RelWithDebInfo` confirmed the actual default) and idle-load — `GE_D193=1` on bunker1 and streets both idle at a healthy 55-60fps in this dev box, well under budget, not reproducing the user's 45fps. So the real cost is very likely gameplay-load-dependent (AI/combat/particles active, not an empty idle scene) or resolution/GPU-fill-rate-bound in a way idle testing can't exercise — this session had no way to drive real keyboard/mouse input into the game headlessly to generate a loaded scene. Docs-only PR **#74** (`docs/d250-perf-diagnosis`, off `main`) carries the write-up. **Same session continued, live with the user driving the game on Dam.** PR #74 merged (docs-only, uncontroversial). Confirmed live: `sim` stays healthy 59-67/s, `render` sags to 45-50/s, `frame rendered in Nus` (`gfx_run` cost) stays 6-10ms — comfortably under budget. One-at-a-time A/B via the F10 overlay **ruled out MSAA, anisotropy, FOV, and resolution** (1280×960 fullscreen vs 640×480 windowed — zero change) **and VSync** (render never exceeds ~60 even fully uncapped, and the bad numbers didn't move) — the cost is resolution/GPU-setting-independent. **Found and fixed a real bug in passing, but confirmed NOT the active cause here:** `sync_framerate_with_timer()` (`port/fast3d/gfx_sdl2.cpp`) passes a 100ns-unit value straight into `sysSleep()` (expects microseconds) — a genuine 10x oversleep, but only on the `target_fps`-gated path, which was inactive in the user's `FpsCap=0`/`VSync=0` test config. Fixed anyway (correctness). **Added `GE_D250`** (`src/boss.c`, wraps `lvlRender()`) to check DL-building cost separately from `gfx_run` — also cheap (~0.5-1.5ms), also not it. **So: two most likely CPU call sites both ruled out, ~10-15ms/frame still unaccounted for.** Leading remaining suspect: the emulated N64 thread-handoff/scheduler round-trip (`gesched.c`/`sched.c`) — real OS thread wake/context-switch latency standing in for what was free hardware handoff on N64. **Next: needs a real profiler** (Visual Studio's sampling profiler, or WPR) attached to `ge007.x86_64.exe` during a live bad-fps moment — ad-hoc printf-timers have now ruled out the obvious spots without finding the cost; more of the same probe-and-guess approach has diminishing returns. PR **#75** (`fix/d250-frame-pacing-sleep-units`, off `main`) carries the sysSleep fix + GE_D250 probe + full write-up. + +## M-119 (2026-09-12) — docs hygiene pass (findings-index regen + stale-status fixes); D249/D250 filed + +**Docs-only session, no code changes.** Did the roadmap's own overdue +housekeeping item (regenerate `findings-index.csv` + fix stale status cells) +and logged two new user-reported issues. + +- **`docs/dev/findings-index.csv` regenerated** via `tools_pc/gen_findings_index.py`. + Only one row actually changed (D194) — everything else the roadmap flagged + as potentially stale (D219, D228, D229, D233, D245-D248) turned out to + already be current on `main`; a naive python cell-split I used mid-session + falsely flagged several of these as stale before I re-checked with the + Read tool — don't trust a naive `" | ".split()` on this file, several rows + have pipe characters inside code spans/backticks that break it. +- **D194's status cell was genuinely stale** ("root cause and fix direction + known, not implemented") — corrected to reflect that part (b) (dt-decouple) + has been merged since M-107, and part (a) (aim curve) + D238 (master + sensitivity) + the SOLITAIRE natural-pitch scheme switch are all + implemented and build-verified on PR #62, held only on the required human + playtest. +- **`docs/dev/GRAPHICS-BACKLOG.md`'s D233/D234 rows were stale** ("Observed, + not investigated") despite both being FIXED + visually verified since + M-107/M-108 per `findings.md`/the index — corrected. Same doc-lag class the + roadmap has flagged repeatedly (D77/D217/D227 previously). +- **Two new findings filed** (user QA report): **D249** — LOD/culling breaks + down on large open levels (Streets, Egyptian) at *default* FOV, far + geometry specifically; distinct from D222 (high-FOV near-edge culling) and + D218 (fog tint boundary); also a QoL ask to make any LOD improvement + togglable so N64-accurate LOD stays selectable. **D250** — FPS is + inconsistent, dropping to 20-40fps intermittently on many levels since the + port started actually running at 60fps; very likely a direct consequence + of **D248** (M-118) exposing real per-frame cost that was invisible while + the port silently ran at half rate — D248's own write-up already flagged + this exact risk as untested. Neither investigated yet; both filed in + `findings.md` §F (regenerate the index whenever these are touched again) + and D249 additionally in `GRAPHICS-BACKLOG.md`. `docs/dev/notes/ROADMAP-1.0.md` + and `docs/dev/UNLOCKED-FPS-PLAN.md` got short addenda pointing at both — + the FPS plan's "default = ~30fps" premise is now stale post-D248 and + needs a Phase-0 re-measurement before it resumes, likely gated on D250. + +**Not done this session:** any actual investigation of D249/D250 (this was a +docs/triage-only pass, no build or runtime access exercised); the +`bunker1` golden re-baseline / full `verify.sh sweep` still owed since D217; +PR #62's human A/B playtest (still the top blocker on Track C). + +## M-118 (2026-09-12) — D248 FIXED: the port has been running at 30fps instead of 60fps game-wide; frame-rate bar B now unblocked + +**User asked to verify the game holds a stable 60fps post-timing-keystone (bar B). It did not — found and fixed a real, previously-undiscovered bug that had been halving presentation/tick rate to 30fps since the port's inception.** Root cause: `src/sched.c` `osScAddClient()` stashes a per-client "every retrace vs. every other retrace" flag by writing into the *next array slot's* `.next` field (`c[1].next = next`) instead of a real struct member — `gfxClient` gets `NULL` (60Hz), the audio client gets a nonzero sentinel (30Hz, its correct rate). `__scHandleRetrace` reads that flag back via a hardcoded `*((s32*)client + 2)` byte offset that only lands on the right memory when `sizeof(OSScClient)` is 8 bytes (32-bit pointers, true on N64). On this 64-bit port `sizeof(OSScClient)` is 16 bytes, so the same offset instead reads into the middle of `client->msgQ` (always non-NULL) — the gfx client's flag reads as permanently set, and it silently falls onto the audio client's 30Hz path. Same pointer-width-growth bug class as D122/D126/D132/D209, just in the scheduler. **Fix:** `#ifdef PORT` reads `client[1].next == 0` (indexes the struct, scales correctly with pointer width) instead of the hardcoded offset; N64 line kept verbatim under `#else`. **Verified live:** `Video.DisplayFPS=1` overlay read a rock-stable "30 FPS" pre-fix and "60 FPS" post-fix on `-level_09` (BUNKER1), same build/scene, `GE_PCDUMP` screenshots both ways. Rebuilt clean; `-level_09`/`-level_20` both ran crash-free post-fix. §F **D248**, `porting-notes.md` §A. Working tree has the one-line-plus-comment `src/sched.c` change, `docs/dev/findings.md` D248 row, `porting-notes.md` D248 entry, `findings-index.csv` regenerated — **uncommitted**, review before landing. + +**Environment note (recorded once already at M-108, re-hit this session):** `ninja` (parallel, default `build-pc.sh` invocation) fails every object with `Cannot create temporary file in C:\Windows\: Permission denied` on this box even with `TMP`/`TEMP` correctly set. Workaround: `cd build-pc && ninja -j1` (single-threaded) builds clean every time. Use `-j1` here until someone roots out the real cause. + +**Not done this session:** `verify.sh sweep` across all 21 levels (only `-level_09`/`-level_20` spot-checked); a live human playtest for feel now that the game actually renders/ticks twice as often per second (should feel smoother/more responsive, not different in speed — deltaFrames-based catch-up should have kept real-time game speed correct even while halved, but this wants eyes-on confirmation); re-measuring `docs/dev/UNLOCKED-FPS-PLAN.md` Phase 0 baseline now that the "default preset" is actually 60fps, before scoping whether a 120fps preset targets 1.0 or post-1.0 (this was explicitly asked as a scope call — still open, see below). + +**C track (mouse/input, bar #6) not started this session** — D248 took the full session once it turned into a real bug hunt instead of a quick measurement. Still queued next: D194 full pass (aim curve + SOLITAIRE controldef + D238 sensitivity, on held PR #62) → user A/B playtest → merge; then D239 click-to-lock parity; D192 grid pointer; playtest debt (D223 wheel-cycle, focus-loss mute). + +## M-117 (2026-09-12) — D195 + D228 FIXED (both user-verified live); D245 REOPENED; D247 closed NOT A BUG; D246 narrowed + +**Worked the M-116 punch list (D245 → D247/D246 → D195 → D228), committing each as it landed.** Two real, previously-undiscovered bugs found and fixed this session, both confirmed by the user in live play: + +- **D195 FIXED — `G_TEXTURE_GEN` envmap UV generation was missing entirely.** User reported gold/silver guns rendering solid black, which redirected the stale D195 (Control glass) investigation onto the real, much bigger bug: an earlier session (D72.1) had removed the PD-inherited envmap block wholesale to fix the Rareware logo, instead of gating it on the `G_TEXTURE_GEN` bit that actually distinguishes the logo (doesn't set it) from every shiny/reflective material in the game (does). Restored the envmap UV generation from the reference PD port, gated on `G_TEXTURE_GEN`. **User-verified:** console glass and gold/silver guns are reflective again, not flat black. The residual "not translucent" complaint on the glass is confirmed NOT a bug — user confirmed it was never see-through on the original N64 either (reflective by design). Full write-up + porting-notes lesson: findings.md D195. +- **D228 FIXED — IA16 palette intensity/alpha byte order was swapped.** `GE_TEXDUMP`+a throwaway correlation probe on Facility ruled out the original texpool/RC2-clip hypothesis (decoded size was always correct) and found `palette_to_rgba32`'s `G_TT_IA16` branch reading intensity/alpha backwards relative to the already-correct direct-IA16 decoder two functions above it in the same file. Fixed the swap. **User-verified:** AK47 and other metallic guns render correctly, no more white patches. +- **D247 (black bars top/bottom) CLOSED as NOT A BUG.** Traced to `bondviewGetCurrentPlayerViewportHeight()`'s unmodified `VIEWPORT_HEIGHT_DEFAULT` (220/240 NTSC scanlines) — genuine, faithful N64 TV-safe-area letterboxing, confirmed identical on a fresh EEPROM (rules out a stale Screen=Wide/Cinema setting). No code change. +- **D246 (left/right edge strips) narrowed, still OPEN.** `GE_PCDUMP` captures show clean edge columns in the native render, ruling out an RDP/game-DL bug — the defect must be introduced in the SDL window presentation path, which headless capture doesn't exercise. Needs the user's window size / `Video.MSAA` setting to chase further. +- **D245 (water seam) REOPENED.** The `allowShift=FALSE→TRUE` fix (mechanistically sound-looking, confirmed via a `GE_D227V` static trace) did **not** fix the live symptom — user reports the water still flips between two patterns and still tracks view angle, unlike the sky (same underlying mechanism, no longer broken there). The static single-frame/single-angle trace this session used was evidently insufficient evidence; needs a fresh investigation with a live or multi-frame-rotating capture, not a repeat of the same approach. Don't re-apply the same reasoning without new evidence. + +**Process note, worth repeating for future sessions:** `verify.sh` and any `taskkill //IM ge007.x86_64.exe` kill whatever game instance is currently running — including one the user has open themselves for manual testing. Hit this mid-session: ran `verify.sh` while the user was actively testing a fix, killing their window without warning. **Always ask before running verify.sh/launching/killing the exe if there's any chance the user has it open.** When the user is actively testing, do documentation-only work and wait for their report rather than re-launching to check yourself. + +**Not done this session:** a full `verify.sh sweep` after D228 (only a headless single-level check + user's own live playtest); D246's live-window follow-up. + +## M-116 (2026-09-12) — D229 (green/pulsating IsWater water) ROOT-CAUSED AND FIXED (port-layer); next: D195 + +**D229 closed out this session.** The earlier "one texunit resolves blue and the other green" framing was wrong on mechanism, right on symptom. The water image (`skywaterimages[2]` = texnum 1509 `IMAGE_WATER_BLUE`) is **ZLIB+CI8**: the texture pool holds raw 8-bit palette indices (dumped pool block: 1400 bytes, every byte ≤ 23 — the 24-colour blue palette's index range; full mipmap chain + row padding). `texSelect` loads it with `gDPSetTextureImage(CI, 16b)` + `gDPLoadBlock` (the 16b is only fast3d's 4KB-per-block convention: 700 × 2 = 1400 bytes of 8-bit data). On N64 GE's custom RSP ucode expands indices→RGBA16 in TMEM at load time, so `sub_GAME_7F09343C` can re-declare TMEM slot 0 as RGBA/16b and sample blue. fast3d had no expansion: `import_texture_rgba16` read index byte-pairs as 16-bit texels — g = 2·idx mod 32 dominates, r/b ≤ 2 → green mottle; the "pulsating" was just the pre-existing `sinf()`-driven PRIM_LOD_FRAC cross-fade between the two mis-decoded tiles. (The N64 side is provably expanded: no shade of a G-dominant texel can multiply into B>G output.) **Fix (port-layer only, `port/fast3d/gfx_pc.cpp`):** `texture_to_load.fmt` was previously dropped — now recorded into `LoadedTexture.src_fmt` by `gfx_dp_load_block`/`gfx_dp_load_tile`; in `import_texture`, an RGBA/16b tile whose slot's last load came from a CI source routes through `import_texture_ci8` (palette expansion via `rdp.palette`) — the port equivalent of the ucode's expand-at-load. Fires only on the genuine format disagreement; real RGBA16 textures untouched. **Verified:** Frigate `-level_26` live run, water renders blue, user confirms "looks better". A GE_D229-gated confirm log (one-shot) remains in import_texture; the ad-hoc SetTexImage/LoadBlock/LoadTile probes were removed. **Still owed before the row is fully closed:** standard verification ritual — `verify.sh sweep` + by-eye on Surface 2 (the other IsWater level). Docs updated: findings.md D229 row + §H resolution, index csv regenerated (D229 → FIXED), ROADMAP Task 1 item 1(a) struck through. + +**New user bug reports filed same session (documented only, not investigated): D245** IsWater water shows two competing patterns + a view-tracking seam — "looks almost exactly like skybox"; strong lead is the documented `allowShift=FALSE` s16-tc-overflow tolerance in `sky.c` `skyPortBeginFan` (D227's mechanism, made visible by the D229 fix); **D246** tiny raw-pixel strips at the left/right screen edges ("overscan") — suspect present/scale blit rounding; **D247** black bars top/bottom — possibly intended 4:3 letterbox pending the parked WIDESCREEN-FOV-PLAN, needs the user's window aspect to tell. All three are in findings.md §F + index. + +**Next (ROADMAP-1.0 Task 1):** item 1(b) **D195 Control transparency** — uninvestigated, needs a capture first. Then 1(c) D228 white NPC-weapon patches. Unchanged elsewhere: PR #56 still blocked on filing the fast-alt-tab audio-restore-race Dxx (index topped at D244); PR #62 held for the combined D194(a)+SOLITAIRE+D238 pass; bunker1 golden stale until re-baselined. + +## M-113 (2026-09-12) — D219 (purple explosions) FIXED: genuine byte-order bug in the runtime texture decompressor (next session starts here) + +**D219 root-caused and fixed this session.** A dispatched subagent (M-112) hit a dead end — its diagnostic probes never fired because they filtered on the GBI tile's declared width (16) instead of the bitstream's real decoded width (14), and it also hit an environment issue getting stdout from the game exe. Picked it back up interactively: fixed both (the real width is 14×14; the "no stdout" issue was `timeout` stripping the child's env in that shell — a plain background `&` + `taskkill //IM` gets it fine here) and traced the actual bug to `src/game/image.c`'s runtime texture decompressor (`texReadUncompressed`, `texChannelsToPixels`, `texBuildLookup`, `texInflateLookup`, `texInflateLookupFromBuffer` — all ground-truth decomp, never modified before now). These reconstruct each wide (>1 byte/pixel) pixel as a shifted-OR integer and commit it with a **native machine store** (`dst16[x] = value`) — correct on N64 (native store = big-endian, what `port/fast3d/gfx_pc.cpp import_texture_rgba16` expects), silently byte-reversed on this little-endian host. Only `IMAGE_FIRE_0..14` hit it because they're one of the only assets that are both genuinely 16-bit-per-pixel (RGBA16) *and* runtime-decompressed (static C-array textures are already normalized by D71's `gfx_tex_normalize_source`; every other runtime-decoded format in the game is single-byte/no-endian). Fix: `PORT_PIXEL16`/`PORT_PIXEL32` macros (`__builtin_bswap16`/`32` under `#ifdef PORT`, identity under `#else`) wrapped around every wide-pixel store in those five functions — narrow, mechanical, N64 build byte-for-byte unaffected. **Verified:** `GE_TEXDUMP` PPM captures of all 15 fire textures flipped from B-dominant (pre-fix, matches M-111's finding) to R-dominant/warm (post-fix); a 10x-upscaled render and a 6-frame animation strip show a clean fire-sprite look (bright core → orange → red/ash), no purple/blue. A 12-texture montage of unrelated Dam world textures confirmed no regression. `-level_09`/`-level_33` both boot and render crash-free post-fix. Full write-up: `docs/dev/findings.md` D219 row, "M-113" paragraph; new porting-notes.md entry under §C (native-store byte-order bugs in runtime-decoded multi-byte pixel formats). **Not yet done, next session:** a full `verify.sh sweep` (21 levels) and a live human playtest of a real (non-scripted) firefight/explosion to close the row for good — everything here is scripted-repro + texdump evidence, strong but not a human eyeball on the actual game. Working tree left **uncommitted** (`src/game/image.c`, `docs/dev/findings.md`, `docs/porting-notes.md`) for review before landing. + +## M-108 (2026-09-12) — D194(b) played, held (not merged); D160 finally live-traced, real root cause found and split to D243; D240/D241/D242 filed + +**Environment gotcha, hit hard this session, fix it first:** this box's Bash tool is Git Bash, not true MSYS2 — `MSYSTEM=MINGW64` is set but `/mingw64` doesn't resolve. Every build/run command this session needed `export PATH="/c/msys64/mingw64/bin:/c/msys64/usr/bin:$PATH"` prepended first. Also: **`ninja -j` (parallel) failed every time** with `Cannot create temporary file in C:\Windows\: Permission denied` from gcc, even though `TMP`/`TEMP` were correctly set and writable — looked like an env-propagation race under `-j`. **`ninja -j1` (single-threaded) built clean every time, no code changes needed.** Use `-j1` for this project on this box until someone roots out the real cause. Also: the exe resolves save/config paths as `data\` **relative to CWD** (`port/src/system.c:212,244`) — launch it with CWD at the repo root (or wherever `data/` is a direct child), never from inside `data/` itself, or saves/config silently fail into a nonexistent `data\data\` path with no crash, just silent write errors in the log. + +**D194(b) played (PR #62, `fix/d194-mouse-dt-decouple`): real improvement, not enough — PR stays unmerged.** User's verdict: "better by default but still feels bad. vertical input is different than horizontal, right mouse aiming is still way too sensitive." Both residual complaints match already-catalogued, not-yet-implemented parts of D194 exactly — nothing new to investigate: RMB over-sensitivity = **D194(a)** (the aim-mode response curve, separate `[61,80]` clamp system, unimplemented); vertical/horizontal asymmetry = the **`CONTROLLER_CONFIG_HONEY` vs `SOLITAIRE`/`GOODNIGHT` controldef** finding from M-87 (hipfire pitch is digital-only under HONEY, forced by the port; SOLITAIRE frees the whole analog stick for continuous pitch+yaw, unmodified GE control scheme). **User's explicit call: hold #62, do D194(a) + the controldef switch + D238 together as one pass, then re-playtest before merging anything.** PR #62's CI is green/CLEAN and mergeable whenever that full pass lands and feels right — it doesn't need rework itself, it's just incomplete on its own. + +**D148/D160/D173 cutscene investigation: finally got a live trace this session (`GE_D160=1`, Dam `-level_33`, two debug-loads + one fresh-save normal playthrough) — real progress, not just re-confirmed dead ends.** Big correction: **D160's old premise ("cutscene never plays, cuts straight to mission complete") is REFUTED on the current build** — the trace shows the AI trigger firing and all 3 scripted `camera_switch` calls executing every time. The cutscene runs; it renders corrupted. New finding **D243** filed with the real mechanism: a `D146` fast3d guard burst (freed/uninitialized-memory byte pattern `0xfafbfbfcfcfbfdfc`, same 4 addresses repeating) fires right in the camera-cut window — a stale display-list-memory read, likely a render-pipeline race (two debug-load runs showed the AI-list's camera_switch calls landing in a *different order* relative to `bossReturnTitleStage` between runs, and the corruption appeared in one run but not the other — nondeterministic, i.e. a real race, not a fixed logic bug). **User-confirmed the camera-glitch reproduces on a real fresh-save playthrough too**, not just the debug-load harness, so it's a genuine bug, not a `-level_N` testing artifact. A *second* symptom seen only on the debug-loaded runs (no report screen, straight to file-select, save not landing) did **not** reproduce on the fresh-save run — that one is most likely a `-level_N` debug-boot artifact (same class as D190) and was deliberately NOT filed as its own bug; don't re-investigate it as real without a fresh-save repro. **Next step for D243:** an env-gated probe on whatever frees/recycles the segment or double-buffer slot backing the outgoing camera shot's geometry during the `ai_17` window, cross-referenced against the 4 repeating fault addresses (session logs `data/playtest_d160.log` + `_run2.log`, not committed — regenerate by repeating this session's steps if needed). + +**D240/D241/D242 filed** (subagent, docs-only, `docs/dev/findings.md`): D240 gunshot-SFX-timing (LOW-MED confidence, could be a stale pre-D209 report or a real residual in the same timing-keystone family); D241 PP7/AK47-silent-not-Klobb (**HIGH confidence this is D207 reproduced** — fold into D207 rather than tracking separately, the repro D207's own M-77b session couldn't get); D242 death-sequence-needs-LMB-click (**HIGH confidence NOT a port bug** — the 3-angle auto-death-camera and its no-input auto-transition exist unmodified in the decomp; LMB is GE's own original Z_TRIG skip button; still needs a playtest confirming the *automatic* no-click path actually completes promptly on PC). + +**D233 visually confirmed fixed this session** — `-level_26` (Frigate; note the *correct* level id, `LEVELID_FRIGATE=26` per `bondconstants.h`, not the `-level_02` guess in the old finding text) doorway/corridor geometry no longer flickers/vanishes near room boundaries. Closed in `findings.md` (both the detailed row and the older duplicate stub row now point to FIXED+VERIFIED). + +**Still open from M-107, not reached this session:** D235 wrong-textures-on-death, D230 music quality, D236/D195/D229 visual backlog items. See M-107 below for the full list — it's still accurate for everything this session didn't touch (minus D233/D234/D219/D194/D160, all covered above). + +**Session wrap (end state):** docs-only PR **#64** open (`docs/m108-playtest-findings`, off `main`, CI running at session end — check status before merging) carries all the findings.md updates above (D160 correction, D243 new, D233/D234 verified-fixed, D219 reopened). PR **#62** (D194b) still open and unmerged, untouched this session beyond the playtest. No `src/`/`port/` changes this session — investigation and docs only. Game process closed; repo left on `main`, clean except one pre-existing untracked `.github/workflows/selfhosted.yml` (not this session's — leave alone). **Next session, in priority order:** (1) merge #64 once CI is green (docs-only, should be uncontroversial), (2) decide on #62 per the user's own call already recorded above, (3) pick up D219's live-repro capture (now the top-priority open item — game-wide per the user's impression, previously believed fixed) or D243's stale-DL-memory race, whichever the user wants to tackle live next. + +--- + +## M-107 (2026-09-12) — D233 culling fix + cutscene investigation MERGED; D194(b) mouse fix OPEN, held for a human playtest session (next session starts here) + +Three parallel tracks dispatched this session (portal/frustum-cull, cutscenes, +D194 mouse feel) all landed. Two are merged to `main`; **D194(b) deliberately +NOT merged** — it changes live mouse-feel code and needs your hands-on +confirmation first, per your own instruction this session. **Next session's +job is a live playtest pass** — here's the ordered checklist. + +### Playtest checklist, priority order + +1. **D194(b) — mouse-look dt-decoupling (PR #62, branch + `fix/d194-mouse-dt-decouple`, still OPEN).** Build that branch, play with + default `Input.MouseDtDecouple=1` and see if "sensitivity correlates with + movement/frame rate" feels better. Then set `Input.MouseDtDecouple=0` in + `ge007.ini` and A/B back to the old behaviour to confirm the escape hatch + still reproduces the old feel (proves the knob actually does something, + not just placebo). `GE_INPUTLOG=1` prints a `lookdt scale=... raw=(...)` + trace line if you want to see the correction factor live. **If it feels + better: merge #62.** Either way, D194 part (a) (the aim-mode response + curve) and the `CONTROLLER_CONFIG_SOLITARE` controldef switch are still + unimplemented — recommend doing those together with **D238** (mouse + sensitivity rework, filed this session) as one design pass once (b) is felt. + +2. **D233 — indoor geometry vanishing near doors (PR #63, MERGED to main, + `port/fast3d/gfx_pc.cpp`).** Fix is build-verified + crash-free but **never + visually confirmed** (the dispatched agent's sandbox had no ROM/display). + Play any doorway-heavy interior (Frigate `-level_02` matches the original + bug screenshots' ship-corridor geometry) and walk up close to a room + boundary/wall — confirm the flicker/vanish is gone. `GE_PCDUMP=` + before/after if you want a frame capture instead of eyeballing live. + +3. **D219 — purple explosions (Dam PPK firefight).** Believed already fixed + by D172 (M-90 runtime re-check proved coverage) — this is just a + by-eye confirm-and-close, cheapest item on this list. Blood/explosions + should be dark red/orange, not magenta/cyan. + +4. **D234 — Dam guard towers invisible except the first.** NOT root-caused + (this session's investigation ruled out matrix-stack depth, D217-family + cache-keying, and D233's own w<0 mechanism — see `findings.md` D234). + Needs a live look at Dam (`-level_10`) from a distance along the dam + structure — confirm the symptom still reproduces post-D233, then a + `GE_PCDUMP` capture is the next investigation's starting point. + +5. **D235 — wrong textures after dying/restarting a level.** Clarified this + session: trigger is death/restart, NOT Facility-specific, seen on other + levels too. **Needs from you:** does it repro on a **fresh** level boot + (rules out reload-state) or only after death/restart (confirms it)? Which + surfaces? A screenshot would help the next investigation a lot. + +6. **D148/D160/D173 — cutscenes (PR #61, MERGED, investigation-only, no + fix).** Two hypotheses ruled out this session (propDef-stride for D160; + two struct-aliasing candidates for D173) — root causes still unknown for + both. If you have time: `GE_D160=1` on Dam, run to the exit trigger, + watch for the rappel cutscene; and on Cradle (`-level_41`, NOT `_51`) + watch the Trevelyan-death sequence for the float/spin (D173) reported + there. Long pole for 1.0 bar #4 — a live capture here is worth more than + another static-analysis pass. + +7. **D230 — music quality (bass instrument sounds corrupted).** Repro is + solid already (Control elevator + regular track, Cavern elevator, Runway + ~5-7s in) — no new playtest needed unless you notice it on a track not + already listed. Next step is code-side (isolate the shared instrument/ + patch id), not a playtest item. + +8. **D236 (Surface 1 billboard trees), D195 (Control transparency), D229 + (Frigate/Surface2 green water)** — all still open, all need a screenshot/ + capture whenever you're passing through those levels anyway. Not worth a + dedicated pass on their own. + +**Do NOT do a full 21-mission re-playthrough yet** — that's the acceptance-bar +confirmation step, sequenced *after* the above land (see `ROADMAP-1.0.md` +M-10x+3). Doing it now would just re-surface everything on this list. + +### What NOT to re-investigate (already settled this session) + +- D160's propDef/`sizepropdef`-stride hypothesis is dead — `CutsceneRecord` + has zero pointer fields, the D122/D126/D132/D209 struct-growth bug class + cannot apply here structurally. Don't re-propose it. +- D173's two strongest D209-shaped candidates (`PadRecord.stan` zeroing, + the anim-root-motion Y chain) are both cleared — `stan` is + ground-truth-zero on N64 too, and the Y chain was already exhaustively + swept during D193/D209 (M-80/M-81). The bug is somewhere else. +- The D209-style "64-bit byte-alias" bug class does **not** turn out to be a + shared root cause across D148/D160/D173/D233/D234 — each needs its own + live capture, not one more aliasing sweep. + +## M-106 (2026-09-12) — user QA bug report filed: D233–D236 (graphics), D237–D239 (QoL). Nothing implemented yet. + +User submitted a batch of bug reports + QoL asks; all are **filed, not worked**: + +- **Graphics (→ `GRAPHICS-BACKLOG.md`, new rows):** D233 indoor geometry + invisible near doors (all levels, world objects still draw); D234 Dam guard + towers invisible except the first; D235 Facility wrong textures "now" + (regression — bisect vs pre-D217-fix build first); D236 Surface 1 billboard + trees render as a wall of tree texture (D233 has screenshots committed at + `docs/img/bugs/20260912-cullingbug-{bugged,notbugged}.png`; none yet for + D234–D236 — ask for them). +- **QoL (→ `QOL-INVENTORY.md` new rows + §F index):** D237 F10 overlay + wheel-scroll + more settings + category grouping; D238 mouse sensitivity + rework (separate RMB aim, one linked master default, up/down ≈ left/right, + GEPD Mouse Injector reference); D239 click-to-lock/always-grab — + click-to-lock is already the default (`mouseCaptureMode=1`), user recommends + dropping always-grab + removing `Input.MouseCaptureMode` (re-confirms the + M-82 removal item in `docs/BACKLOG.md`, closes D192 by deletion). +- All seven labels are also in the `findings.md` §F index. Checkboxes live in + `docs/BACKLOG.md` "M-106 user QA report". + +## M-105 (2026-09-11, continued 2026-09-12) — D223 fixed (PR #54), docs-hygiene PR #55 (+D190 closed), QoL batch PR #56, D232 no-hit-flash WIP; D230/D231 filed. + +- **D223 directional mouse-wheel weapon cycling: FIXED, PR #54** (branch + `fix/d223-wheel-cycle`). Wheel-up keeps the A-pulse forward cycle; + wheel-down synthesizes the N64 "hold A, tap Z" backward combo (A-only + poll, then A+Z edge → `weaponBackOffset`, `bondview2.c:5337`). Port-only, + local build green (241/241) + CI green both platforms. **Owes a human + play-test** (up = next, down = previous, no accidental shots) before merge. +- **Docs-hygiene PR #55** (branch `docs/stale-status-claims`): the four + stale claims the M-104 roadmap refresh flagged — README music (×2) + sky + lines, GRAPHICS-BACKLOG D77 row, findings D217 status cell — all scrubbed; + **D230 filed** for the in-level music *quality* residual (wrong instruments, + elevator track; occasional garbling; suspect seq/instrument-bank decode, + not the proven mixer/trigger layers). Also corrected D196's stale OPEN + status (already fixed/merged `6e758f1d`). +- **D190 closed as not-a-bug** (committed onto PR #55): level 45 (BASEMENT) + is MP-only — its solo setup name `UsetupimpZ` was never shipped in the ROM + (only `Ump_setupimpZ`), so a solo `-level_45` debug boot requests a + nonexistent file and walks stale pool memory. Reproduced on Windows + (`0xc0000005` in `modelLoad` via `domakedefaultobj`) — M-48's "Linux-only" + framing was wrong; N64 is equally broken for this boot. No fix possible in + the port layer (the walks need terminators no zeroed buffer provides). + Bar #2 now has no known crash on either platform. +- **Tier-3 QoL batch: PR #56** (branch `feat/qol-mute-screenshot`, off main; + D231 filed): (1) `Audio.MuteOnFocusLoss` default on — mixed blocks dropped + while the window is unfocused (`audioHandleFocus()` from the SDL focus + events in `video.c`; gate in the `osAiSetNextBuffer` queue path, `audio.c`). + Pause-on-focus-loss deliberately deferred (D204-family AI feedback-loop / + scheduler interaction). (2) Screenshot hotkey productized: `Video. + ScreenshotKey` (default F12, D214-style scancode name, parsed lazily, + unknown → F12 + warning), `Video.ScreenshotDir` (default `screenshots/`), + timestamped `screenshot_YYYYMMDD_HHMMSS.ppm` with `_N` collision suffix; + key check now scancode-based. Build green; smoke run confirmed the ini + migration adds all three keys to an existing `data/ge007.ini`. **Owes human + playtest** (alt-tab mute/unmute, screenshot lands in `screenshots/`, one + custom key+dir). **User playtest result (2026-09-12):** mute-on-focus-loss + works, but a FAST alt-tab-away-and-back sometimes fails to restore audio + (works reliably if you alt-tab slower / pause a beat before returning) — + a focus-event race in `audioHandleFocus()`/`video.c`'s SDL focus handling, + not yet root-caused. **User's call: park this for now, not a merge + blocker** — file as a new low-priority `Dxx` before merge, don't fix this + session. Screenshot-key part still being tested separately. Note: PR #54/#55/#56 all touch `findings.md` + the CSV — + expect small table/CSV conflicts on merge; resolve by re-applying the row + and regenerating with `tools_pc/gen_findings_index.py`. +- **D232 no-hit-flash: IMPLEMENTED, verification in progress** (branch + `feat/qol-no-hit-flash` off main, commit `3315de09`, unpushed). Design: + `Game.NoHitFlash` int config (declared + registered in `port/src/video.c` + next to the other route-(b) vars) + a route-(b) gate in + `currentPlayerSetFadeColour` (`src/game/bondview2.c` ~4236): when the flag + is set, zero any non-black RGB. Rationale (verified by exhaustive caller + grep): EVERY fade routes through this one function, and the only non-black + callers are the two damage paths — the per-frame `g_DamageTypes` envelope + in `bondviewPlayerTickDamageAndHealth` (all 8 rows are WHITE 0xFF,0xFF, + 0xFF with an alpha envelope scaled by remaining health) and the red + 150,0,0 death overlay (`bondview2.c:9121`) — every other caller passes + (0,0,0). So zeroing RGB kills exactly the hit flash; blackouts, the colour + screen and the death fade are untouched. Default 0 = original. + **Verification state (frame A/B via `GE_PCDUMP=1-900 -level_09`, BUNKER1, + 721 frames each):** (a) determinism CONFIRMED — two default runs are + near-identical (max Δ 0.41 mean-luminance units, most <0.01), so A/B + methodology is valid; (b) default vs gate-on: the gate-on run is uniformly + ~24 luminance units BRIGHTER at frames 28–45 (no colour cast: A=(57,53, + 52) vs B=(81,78,77)), reconverging by ~frame 460, plus a tiny +0.15 blip + in the default run at frames 473–494 (direction matches a small flash). + **Anomaly:** white→black zeroing should make gate-on DARKER, not brighter; + the simple overlay model out=S(1-a)+rgb·a is refuted (fitting requires + S>255). So either `port/fast3d`'s implementation of G_CC_PRIMITIVE + + G_RM_CLD_SURF for fill rectangles behaves differently than assumed, or a + non-black fade interacts with something else. **Next steps:** (1) add a + `GE_NOHITFLASH_TRACE` env-gated log inside the gate printing r/g/b/frac of + every gated call (an edit attempt landed on the wrong branch and was NOT + applied — re-apply on this branch; pattern: bondview2.c already uses + getenv, e.g. GE_D160 at line ~791; sysLogPrintf is `port/include/system.h`, + LogLevel enum in `platform.h`), rebuild, run with the flag on and + enumerate which non-black fades actually fire in a BUNKER1 boot; (2) read + how `port/fast3d/` composites G_CC_PRIMITIVE + G_RM_CLD_SURF. Fallback if + the anomaly resists: code-inspection stands (call sites enumerated) and the + human play-test with the toggle flipped is the merge gate anyway. +- **Test binaries baked for the user** (in gitignored `build-pc/`): + `ge007-test-d223-wheel.exe` = PR #54, `ge007-test-qol-out.exe` = PR #56. + The plain `ge007.x86_64.exe` on disk is a PR #56 build while the working + tree sits on `feat/qol-no-hit-flash` — rebuild before any D232 frame run. + **Untracked junk at repo root to clean:** `ppm_A/`, `ppm_B/` (frame dumps; + `ppm/` itself is gitignored), `cmp_000030_{A,B}.png`, + `cmp_000477_{A,B}.png`. `data/ge007.ini`: `NoHitFlash = 0` (restored after + testing); it also carries the three PR #56 keys from the smoke run — + unknown to pre-#56 binaries but harmlessly ignored. +- **Next for the next session:** (1) human play-test gates: PR #54 (wheel up + = next / down = previous, no accidental shots; inert while RMB aim held — + pre-existing), PR #56 (alt-tab mutes/unmutes, F12 → timestamped PPM in + `screenshots/`, custom key+dir via ini); (2) D230 repro details from the + user (which instruments wrong on the elevator track; deterministic vs + intermittent) — decode bug vs live voice-stealing; (3) finish D232 + verification above, then file the finding + regenerate CSV + push PR; + (4) merge order once tests pass: #54 → #55 → #56 (all three touch + `findings.md`/CSV — trivial conflicts); (5) remaining Tier-3: fog option, + HUD scale, rebinding UI, FXAA toggle, resolution/integer-scale, 120fps + preset, gamepad rumble, per-pad tuning. +- **Session close-out (2026-09-12): PR/branch state after full playtest pass.** + `feat/qol-no-hit-flash` **PR #58 opened, CI green** — D232 shipped with a + SECOND user-caught bug fixed: the gate zeroed RGB but not `frac` (the fade's + alpha), so the white flash became a same-opacity BLACK flash instead of + disappearing; fixed by zeroing `frac` too. User-verified twice (color gone, + then flash gone entirely). `docs/issue-6-dropin-rom-plan` **PR #57 opened, + CI green** — local-Qwen research + writeup (repo-mapper then + mechanical-coder) resolving 3 of the drop-in-ROM release plan's open + questions; had to be rebased off `main` after a first push accidentally + carried the D232 branch's history (branched from a commit instead of + `main` — check this before pushing a delegate's doc branch next time). + PR #54 (D223) fixed + user-verified, CI green. PR #56 (D231) partially + verified: screenshot hotkey confirmed working, mute-on-focus-loss works but + has a known fast-alt-tab-doesn't-always-restore-audio edge case — **user's + call: park it, not a blocker, file a low-pri Dxx before merge, don't fix + this session.** PR #55 unchanged, still green. **All 5 open PRs (#54-#58) + are CI-green and ready for merge** pending the user's own final review. +- **D223 follow-up bug FOUND + FIXED via live user playtest (2026-09-12, this + session).** User play-tested `ge007-test-d223-wheel.exe` (PR #54's original + build) and found the backward cycle unreliable: PP7->AK47->PP7->wheel-down + sometimes correctly reached melee, sometimes landed back on AK47; separately + AK47->wheel-down sometimes skipped straight past PP7 to melee. Root-caused + with an instrumented trace build (temp `git worktree` at `../gh-fullhist-d223`, + `GE_D223_TRACE` env-gated logs in `port/src/input.c` + `bondview2.c` + + `gun.c`, all reverted before landing): the wheel-down pulse staggered the + synthetic "hold A, tap Z" combo across 2 polls (A alone, then A+Z one poll + later). Whenever those land in 2 SEPARATE game ticks (a D117-class + poll/tick-rate race), the "A alone" tick is itself a fresh A edge with no Z + held -> `bondview2.c`'s `weaponForwardOffset` formula reads that as a + genuine cycle-FORWARD request and fires a REAL weapon change before the + correcting backward tick runs. Trace showed literally every wheel-down + after the first one in a session mis-firing forward. **Fix** (`port/src/ + input.c` only): present A+Z together from the first poll instead of + staggering — `weaponForwardOffset` requires Z NOT held, so simultaneous + A+Z is unambiguous (backward only), regardless of tick timing; + `moveData.triggerOn` (real fire) is separately gated off while A is held, + so no accidental-shot risk. **User-verified fixed** via a second + instrumented rebuild (every notch now produces exactly one correct, + symmetric step both directions). Committed `384c3e7e` directly onto + `fix/d223-wheel-cycle` (traces stripped, real fix only), pushed — **PR #54 + updated, CI re-running.** Temp worktree removed after push. +- **D232 anomaly RESOLVED (2026-09-12, this session): not a bug, it's D117.** + Resumed the verification per the "Next steps" above. (1) Added the + `GE_NOHITFLASH_TRACE` env-gated `osSyncPrintf` in the gate (pattern: + `getenv`-gated like `GE_D160`, `bondview2.c:4247`), rebuilt (241/241 + green). (2) Ran it headless (`GE_PCDUMP`, `-level_09`, ~3500 frames, + unmanned/no-input) — **zero trace fires**, confirming no damage ever + occurs in this kind of run, so the gate is provably inert here regardless + of setting — the earlier frame-28-45 "brighter" delta cannot be the + hit-flash gate touching a real fade. (3) Re-ran the actual A/B properly: + same binary, only `Game.NoHitFlash` toggled via `data/ge007.ini` (no env + override for it), A0/A1 = two runs with the flag OFF (repeat), B = ON, + each 100 `GE_PCDUMP` frames, diffed with `tools_pc/framediff.py` in both + `--exact` and the project's own default structural/tolerant mode. + **Result: A0 vs A1 (same build, same config, run twice) = 0/100 frames + match** (87% of pixels differ by frame ~73, tolerant mode too) — i.e. the + port is NOT deterministic even by frame ~73 in this scenario. **A0 vs B + (gate off vs on) = 99/100 frames match** (tolerant mode; differences are + ~0.1-0.3% of pixels, an order of magnitude smaller than the A0/A1 same- + build noise floor). **Conclusion: D232's gate causes no detectable visual + effect beyond ordinary run-to-run noise** — the prior session's "gate-on + uniformly ~24 luminance units brighter" read was a false signal from an + under-powered metric (mean luminance, coarse enough to miss/alias massive + per-pixel divergence) colliding with the **already-catalogued, still-OPEN + D117** frame-to-frame nondeterminism (`findings.md` D117: "root-caused; + `GE_DETERM` deferred... determinism NOT achieved"; `framediff.py`'s own + header already documents "two runs of the same build diverge 15-40% of + pixels by frame ~200" — this session's repro (87% by frame ~73) is the + same phenomenon, not a new one). **Lesson for future headless A/B verification + on this port: use `framediff.py`'s per-pixel/structural comparison, not a + single scalar like mean luminance — D117 noise is large enough that a + coarse metric can average it away and look like a false "clean" result OR + (as happened here) get misread as a real signal.** No code fix needed; + D232 stands as **code-verified correct** (exhaustive caller audit, now + also headless-trace-confirmed inert absent real damage) with the human + playtest as the actual merge gate, same as always intended. Test junk + from this pass (`ppm/`, `ppm_A0/`, `ppm_A1/`, `ppm_B/`) cleaned; `data/ + ge007.ini` restored to `NoHitFlash = 0`. **Still owed:** file this as a + `docs/dev/findings.md` §F row (next `Dxx` after whatever lands from + #55/#56) + regenerate the CSV index — deferred this session (out of + scope for a "confirm testing is ready" pass; low risk to leave for the + session that pushes the D232 PR, since the trace addition is a + behavior-preserving `#ifdef PORT` diagnostic, same class as `GE_D160`). +- **Environment gotcha #2:** `timeout`'s TERM does not reliably kill the + game on Windows (the SDL app survives it) — a zombie `ge007.x86_64.exe` + then holds `build-pc/ge007.x86_64.exe` open and the link fails with + "cannot open output file ... Permission denied". Check `tasklist | grep + ge007` before linking; kill leftovers of your own timed runs (confirm via + `Win32_Process.CommandLine` it isn't the user's session first). +- **Environment gotcha (cost a misdiagnosis this session):** when spawning + the toolchain from an agent shell, mingw `cc1.exe` can fail to launch + (gcc exits 1 with *no output*; direct cc1 run → 127; via PowerShell → + `0xC0000135` STATUS_DLL_NOT_FOUND) even though every imported DLL exists — + it's a DLL-search-PATH problem in the spawned child, not a broken + toolchain (`as`/`ld` work fine, which made it look like cc1 was blocked). + Fix: prepend the bin dir for the child, e.g. run the build as + `powershell -Command "$env:PATH='C:\msys64\mingw64\bin;'+$env:PATH; bash ./build-pc.sh ntsc-final"`. + If a future session sees silent gcc failures, check this before + concluding the toolchain is broken (and before blaming AV/sandbox). + **The same PATH prepend is needed to RUN the game exe from an agent shell** + (bare `./build-pc/ge007.x86_64.exe` → exit 127, no output) — and note a + failed build does NOT stop a `&&` chain when piped through `tail` (the + pipeline's exit status is tail's), so verify the link line actually passed + before copying/renaming the exe. + +## M-102c (2026-09-11) — user field corrections to the roadmap: in-level music actually plays (quality issue, not absence); D228 confirmed world-model-only; D194 got real external corroboration. + +Three corrections to `docs/dev/notes/ROADMAP-1.0.md` from the user's own +current testing, folded in: + +1. **In-level music is NOT absent** (contra `findings.md`/README/ + `GRAPHICS-BACKLOG.md`, all still saying D77 = silent past intro/menu — + stale, flagged in the roadmap's own footnote as a follow-up docs PR, not + fixed here). The real remaining issue: some instruments on some tracks + (elevator music named specifically) sound wrong, plus occasional + general-play audio garbling. Needs its own `Dxx` — not yet created. + Re-scoped in the roadmap to an audio-quality investigation (seq/ + instrument-bank decode, not mixer/trigger). +2. **D228 confirmed world-model-specific**: NPCs carrying guns show white + missing-texture patches on the world-model weapon geometry; the + player's already-fixed 1P viewmodel isn't implicated. Narrows the + search surface. +3. **D194 mouse feel — external corroboration, not a new mechanism.** + Investigated `C:\Users\james\Games\Emulators\Nintendo 64\1964_GEPD_Edition` + (the user's reference point for "feels better") — the 1964 emulator's + bundled `Mouse_Injector.dll` (closed-source, no source available; the + emulator's own `source.tar.xz` is just the stock 1964 core). Its exports + (`HookRDRAM`) and readme show it writes directly into the emulated + game's RAM rather than going through N64 controller-pad emulation, and + its own FAQ says responsiveness needs mouse polling decoupled from + frame pacing (recommends `NtSetTimerResolution` + disabling vsync). This + independently corroborates the port's own existing D194 diagnosis (part + b: sensitivity couples to frame/poll rate) as the dominant lever — raises + confidence in prioritizing that half of the fix, doesn't change the fix + direction or supply new constants (nothing else was recoverable from a + stripped, hardcoded-per-ROM-title binary). + +## M-102b (2026-09-11) — refreshed `docs/dev/notes/ROADMAP-1.0.md` (v1.0 triage), same style as the docs-triage pass below. + +Rewrote the plan of record: fresh "Where we are now" superseding the stale +M-49/M-87 sections, an acceptance-bar status table, and a full +impact×effort triage of every open `findings-index.csv` row into +Tier 1 (blocks the bar: D77 in-level music, cutscene reliability +D148/D160/D173/D219, D194 mouse feel, D190 Linux crash, D195/D229 visual +correctness) / Tier 2 (D228 AK47 textures, the D222/D218/D220 FOV +break-set, assorted papercuts) / Tier 3 (QoL: HUD scale, rebinding UI, +per-pad tuning) / explicitly-deferred (GE_DETERM test-infra, multiplayer, +region scope — decisions not bugs). Sequenced milestones + open questions +carried forward and updated. File is gitignored (`docs/dev/notes/`), no +PR needed. + +**One finding surfaced, not fixed:** `findings.md` row D217's status cell +still leads `OPEN — root cause characterised (M-86)` even though its own §H +shows the green-texture defect fixed and merged (PR #47); only the D228 +white-texture residual is actually open. Same class as the D194 +false-`FIXED` bug fixed on `docs/m102-triage-fixes` — worth a follow-up +edit + index regen next time that branch (or a new one) is open. + +## M-102 (2026-09-11) — docs triage pass. Fixed a live findings-index false-positive (D194 was silently reading FIXED), the release `` bug, and landed the parked docs/site-cleanup patch. + +Ran `docs/dev/notes/PROMPT-docs-triage-session.md`. Full triage table and the +scoped-not-started list are in `docs/dev/notes/FOLLOWUP-docs-triage-session.md` +(gitignored, local). Headline items: + +- **Real bug, same class as the D227/M-98 misfile that cost a session:** + `findings-index.csv`'s generator bucketed **D194 as FIXED** — it's an open + design item — because that row's table structure was broken (2 cells, no + distinct status column, so the generator's fallback keyword search matched + "fixed" somewhere inside 6,500 chars of narrative). Fixed by adding a real + status cell; also fixed 9 more rows stuck in the generator's "?" bucket + (some had unrecognized verdict words like `ANALYZED`/`GUARDED`/ + `DIAGNOSTIC SHIPPED`/`ROOT-CAUSED →`/`NOT A BUG`, now added to + `gen_findings_index.py`'s vocabulary). `--check` reruns clean; CSV diff + reviewed — only those 10 rows moved. +- **`.github/release-notes.md`'s `` placeholders were never + substituted** — every published release body (v0.1.0 included) shipped the + literal text. `ci.yml`'s release step now `sed`s it to the tag first. +- **Landed the parked `UNLANDED-docs-site-cleanup-steps3-5` patch**: deleted + the orphaned `docs/StructureGuide.md`, added frontmatter to 5 Pages docs, + rewrote `docs/index.md`'s stale "Phase 2 of 4 / no audio yet / 19-of-21" + status text to match the current README (the 2 files whose hunks no longer + applied via `git am` were redone by hand against current content, per the + note's own instructions). +- Also fixed a dead `PORT-LEARNINGS.md` reference in `docs/BACKLOG.md` + (renamed to `docs/porting-notes.md` long ago). +- **Not done, scoped in the follow-up note:** the findings.md monster-row + convention (>15 rows still 5,000+ chars, oldest-content-first), a full + status-accuracy sweep of all 119 rows against `§H`/`main`, a + duplicate-defect clustering pass, and the `docs/dev/*-PLAN.md` + live/historical/superseded triage. Each is a session of its own; the + follow-up note has the priority order. +- Changes are docs + `ci.yml` only, not yet committed to a branch/PR as of + this note — do that next if picking this up mid-session. + +## M-101 (2026-09-11) — green guns were a STALE BRANCH, not a bug. Green/pulsating water is real, pre-existing, filed as D229 (not root-caused). + +**Read `docs/dev/findings.md` §H "D229 — M-101".** + +**Green weapon textures on Frigate/Surface 2: not a bug.** This branch was 2 +commits behind `main`, and those 2 commits were exactly the D217 fixes +(`7278baad` #47 texpool alignment, `f15a12b9` #49 CI cache palette-content +key). Merged main in (`f8822805`); user confirms the gun is good. **Process +point: check branch-vs-main before investigating any D217-family symptom +reported against a feature branch.** + +**Green/pulsating water on IsWater levels: real, pre-existing, D229.** A/B +proven — plain `main` with none of the D227 work shows the same green. +Mechanism partly identified: `sub_GAME_7F09343C` binds tile 0 and tile 1 to the +same TMEM, offsets tile 1, and cross-fades TEXEL0/TEXEL1 by a `sinf()`-driven +`PRIM_LOD_FRAC` — that sine is the pulsing, so one texunit resolves blue and +the other green. Shade path ruled out by measurement. **Not root-caused.** + +**NEXT TASK if picking up D229:** the existing `GE_D172` multitex probe does +not fire here (it filters on `tile1.tmem != tile0.tmem`, equal in this draw) — +widen it. And capture a **sequence** across the sine period: a single frame is +useless, frame 340 reads green on all three builds tested because of where the +sine lands. Then check `skywaterimages[2]`'s real format against the RGBA/16b +that `sub_GAME_7F09343C` re-declares. + +**D227 is deliberately decoupled from D229** — `skyPortBeginFan` now takes +`allowShift` and the water quad passes FALSE, leaving that path byte-identical +to pre-D227. Do not "fix" the water by shifting both tiles: that was tried live +and flattens the cross-fade into constantly-green. + +**bunker1 golden is now badly stale.** `verify.sh bunker1` reports REGRESSION +at `worst_cell` 55.02 on this branch — but plain `main` is **worse at 73.24**. +main's D217 texture fixes moved the pixels; the re-baseline owed since +M-83/M-84 is now overdue and is making the per-patch gate useless. Worth doing +before the next rendering change. + +**User environment note:** their playtests run with **FOV turned up** +(`Video.FovScale`); all headless captures here ran at the ini default of 100. +Green reproduces at 100, so FOV is not required for D229 — but the black bands +at the capture edges are probably the FOV-coupled D218/D222 family, not D229. + +## M-100b (2026-09-11) — D227 FIXED (pending your play-test). Cause was a UNIT ERROR, and it also fixes the "sky scrolls too fast" complaint. + +**Read `docs/dev/findings.md` §H "D227 — M-100b".** M-82 ("D176(a) Defect 1") +read `unk20`/`unk24` as texel counts and baked `tc` as `(S - fold) * 32`. +They are not texels — `skyRender` sets them as `worldpos * 0.1f` and the N64 +path hands them to the RDP coefficient block unscaled, so they are already in +the RDP's native S10.5 1/32-texel units. That stray `* 32` caused **three +symptoms tracked as separate bugs**: D227's seam/starburst (overflow), the +over-tiling, and **M-96's "the N64 sky is nearly static, ours races"** +(`g_SkyCloudOffset` advances 1/32 texel per tick, not 1). Fix is +`tc = (S - fold)` 1:1 with the fold rebased to a whole tile period. + +**Committed:** `3f2b1b4d` (fix), `a0b1de40` + `f1711b59` (the investigation). +Branch `land/d176a-sky-perspective-scroll`, PR #43 still draft. + +**NEXT TASK — play-test.** Both prior D227 "fixes" (M-95, M-98) passed static +headless checks and then failed live, so this is not closed until played. +Check Cradle, Statue and Surface: seam/starburst gone, cloud scale sensible, +and the sky should now scroll ~32x slower (near-static, like the N64). Then +PR #43 can leave draft. + +**Also worth doing:** re-measure the scroll rate against M-98's ~7 px/tick +baseline — it should now be ~0.2 px/tick. Method is in §H "D227 — M-98" +(cross-correlate `GE_PCDUMP=345-355:1` consecutive frames, static camera). + +**New probe:** `GE_D227V=1` dumps per-vertex S/T/w, the chosen shift k, and +the baked tc from `skyPortRenderPoly`. This is what measured the whole thing. + +**Kept deliberately:** the tile-shift safety valve +(`skyPortPickShift`/`skyPortCaptureTile`/`skyPortEmitTileShift`) — the +observed span is 31,513 against an s16's 32,767, only ~4% headroom, so it +degrades gracefully rather than overflowing if a camera exceeds it. Uses the +RDP's own tile `shifts`/`shiftt`, which `port/fast3d` already implements. + +## M-100 (2026-09-11) — D227 ROOT CAUSE FOUND AND PROVEN: `s16` texture-coordinate overflow. Fix designed, not implemented — one decision needs you. + +**Read `docs/dev/findings.md` §H "D227 — M-100" for the full record.** Short +version: the seam is not a crack, not a quantization mismatch, and not +view-dependence. One sky quad spans **~30,000 texels in S and T**, but +`Vtx.tc` is S10.5 in an `s16` = **±1024 texels**, so **7 of its 8 texture +coordinates overflow and wrap to arbitrary values**. M-95 and M-98 each fixed +a real second-order bug; neither could touch this. + +**What unblocked it:** the user's emulator-vs-ours screenshot pair on Surface +(`docs/img/bugs/d227-surface-emulator-ref.png` vs +`d227-surface-starburst-ours.png`). The real symptom is a *radial starburst*, +not a crack — which is a completely different mechanism from what M-94→M-98 +were chasing. + +**M-99's "blocked, needs a human co-session" was wrong.** The defect +reproduces headlessly and statically; M-98 had already captured it (Cradle +frame 350, `d176a-cradle-tower-starburst.png`) but misfiled it as unrelated +"Defect 2". **D227 == D176(a) Defect 2 == the Cradle tower starburst.** +`GE_INPUTSCRIPT` camera-pan is still a dead end, but D227 never needed it. + +**New probe, kept:** `GE_D227V=1` dumps per-vertex S/T/w from +`skyPortRenderPoly` (env-gated, `GE_D176` style). This is what proved it. + +**The fix, and the one open decision.** Subdivide until each sub-primitive's +S/T span is under ~960 texels, with per-sub-primitive folds (folds differing +by multiples of 64 texels are seamless under `GL_REPEAT` at tile width 64). +Two parts are already worked out and written up in the finding: the exact +perspective-correct subdivision math, and where to place the cuts — **S is +linear in w, so cut uniformly in w**; uniform-in-screen is badly wrong (on +this quad's right edge, half the screen distance carries 807 texels and the +other half 26,943). What is NOT decided is the **vertex budget**: +`dynAllocateVertices` is an *unchecked* bump allocator over a 50 KB/frame +pool shared with the whole scene, and pure tessellation wants ~1,200-1,300 +verts (~40% of it) at this camera. Options A (pure tessellation, no fast3d +change, ~40% of the pool), B (widen the texcoord transport in `port/fast3d/`, +then ~4x4 subdivision suffices, ~100 verts, wider blast radius), C (hybrid). +and **D — RDP tile shift, which dominates the other three and is the +recommendation.** `shifts`/`shiftt` in the tile descriptor is the RDP's own +precision-vs-range knob, and `port/fast3d/` already implements it fully +including the left-shift case (`gfx_pc.cpp` ~1783). Emit tc at `32/2^k` units +and set `shifts = 16 - k`: 2^k more range, **no tessellation, no fast3d +change, no vertex-budget cost**, authentic RDP idiom. `k = 5` gives ±32,767 +texels, covering the measured 29,830-texel span outright — i.e. exactly the +diagnostic build that already removes the artifact, with the scale corrected +rather than left wrong. Care needed: emit the shift on the sky's tile only +(an extra `gDPSetTile` after `texSelect`), and pick `k` per draw from the +measured span. **Ask the user before implementing.** + +**Recheck after the fix:** whether the M-96 "sky scrolls extremely fast" +complaint and M-98's measured ~7 px/tick were the same bug all along +(wrapped tc would make the apparent pattern move at a rate unrelated to the +true 1 texel/tick scroll). Plausible, unconfirmed. + +**Environment notes** (all still apply, plus M-99's below): `GE_PCDUMP` +writes to `./ppm/` **relative to the cwd you launch from** — run from the +repo root, not `build-pc/`, or the frames land somewhere you won't find them. +Backgrounding the exe with a trailing `&` inside a tool call kills it before +it captures; use a proper background run instead. + +## M-99 (2026-09-11) — human playtest: D227 seam STILL PRESENT after M-98's fold fix, AND view-angle-dependent. Headless repro blocked; needs a human co-session. + +**Human playtest result (this session, after M-98's fold-sharing fix was +pushed):** the seam is **still there on Statue**, and — new, sharper data +point — **it visibly moves/changes as the mouse/player view moves.** Also: +**"still pretty bad" on Cradle**, and **Surface has the same issue.** This +means M-98's fold-sharing fix was real (verified via static-frame diff, +findings.md §H "D227 — M-98") but is NOT the dominant thing being seen live +— same pattern as M-95's wScale fix before it. Read that finding fully +before touching sky.c again; don't re-derive it. + +**What this session ruled out, cheaply, before getting stuck:** +- **Cross-quad water/cloud texture-scale mismatch.** Hypothesis: block1 + (the "below-horizon" quad, `sp274`/`sp43c`, `skyRender`'s first big + switch) and block2 (the cloud quad, `sp94`/`sp4b4`, second switch) are + drawn from independently-computed vertex data that share the horizon + boundary but never share a `wScale`/fold — texture-scale differs by 10x + between them (`unk0c = raw` in block1's IsWater case vs `unk0c = raw*0.1f` + in block2, both lines confirmed by direct read). **Checked via the + existing `GE_D176` env probe: both Statue AND Cradle report `IsWater=0`.** + For `IsWater==0`, block1 is a flat `gDPFillRectangle` (`sky.c` ~874-897) + — no texture, no scroll, can't produce a texture-pattern seam. This + theory doesn't apply to either level actually tested; **still untested on + an `IsWater==1` level** (candidates: Frigate, Runway, Depot, Surface1/2 — + check with `GE_D176=1` first) where block1 WOULD go through + `skyPortBeginFan`/`skyRenderTri` textured and could genuinely conflict + with block2's independent fan. Worth ruling in or out properly before + the next session assumes it's dead. +- **Headless camera-pan repro is NOT wired up.** Tried to reproduce + "changes with view" headlessly via `GE_INPUTSCRIPT` — both sustained + analog-stick tokens (`SRIGHT`) and repeated C-button pulses + (`CRIGHT`/`CUP` etc.) across a 90-tick window produced **zero visible + camera rotation** in captured frames (same look direction throughout, + confirmed by identical HUD/weapon-arm framing). The real in-game + mouselook almost certainly injects yaw/pitch through a path + `GE_INPUTSCRIPT` doesn't touch (it only emulates the N64 controller: + buttons + one fake analog stick) — worth checking `port/src/input.c`'s + actual mouse-delta injection point for a debug hook, or adding one + (env-gated, e.g. `GE_FORCEVIEW=yaw,pitch` set once per frame) if this + investigation needs more headless angle sweeps. **Until that exists, any + view-angle-dependent visual bug in this game needs a human playtest to + chase, full stop** — don't burn another session's budget re-trying + `GE_INPUTSCRIPT` stick/button tokens for camera turn, they don't do it. + +**What's genuinely still open, ranked by how promising it looked before +getting stuck on repro:** +1. **Cross-block (water-quad vs cloud-quad) independent fan state, on an + `IsWater==1` level.** Not ruled out yet (see above) — check first, it's + cheap (one `GE_D176` run per candidate level) and if true it's a clean, + scoped fix (extend `skyPortBeginFan`'s shared state to span both blocks + in one frame instead of resetting between them — needs restructuring + since block1 draws before block2's vertices even exist; the two calls + would need to become "begin/accumulate/begin-again-without-reset/end"). +2. **Frame-to-frame popping at the fold boundary, not a same-frame spatial + seam.** `skyPortRenderPoly`'s fold recomputes fresh every frame from the + current scrolling min-S; each time it crosses a 64-texel boundary the + chosen fold jumps by 64 — *should* be invisible (GL_REPEAT period 64) + but was never independently verified to actually be seamless in + fast3d's real texture-wrap implementation for this specific combiner + setup. A static-camera, long, `step:1` capture bracketing a known + fold-crossing tick (derivable from the ~7px/tick rate measured in M-98) + would show this directly if real, and — importantly — this one doesn't + need view rotation to reproduce, just enough elapsed ticks, so it's + still headlessly testable. **Good first move for next session if a + human isn't available yet.** +3. **The clip-shape switch's discrete vertex-count/topology changes as + screen corners cross the horizon** (`skyRender`'s 16-case switch reruns + every frame from scratch; adjacent cases can hand block2 a differently + *shaped* fan, not just moved vertices) — could read as "the pattern + changes as you look around" without being a same-frame crack at all. + Not investigated. + +**Concretely blocked on:** a screenshot or precise repro detail from a +human — where on screen, at roughly what look angle/pitch, which level. +Asked for this at end of M-99; if it's in hand for the next session, START +there instead of re-running the ranked list above blind. + +**Also outstanding, unchanged:** D228 (AK47 white missing-texture patches, +different mechanism from the already-fixed D217 green-palette bug) is +still fully unstarted — reasonable fallback task if the seam investigation +needs a human and one isn't available yet. + +**Environment notes for whoever picks this up:** +- Launching `ge007.x86_64.exe` directly (not via `verify.sh`) needs + `export PATH="/c/msys64/mingw64/bin:$PATH"` first or it exits instantly + (code 127, no output) — DLL resolution, not a real crash. +- `run_in_background: true` + `taskkill //F //IM ge007.x86_64.exe` once you + have the frames you need is the normal pattern for a capture — the + background task will report "failed" (nonzero exit from the forced kill) + even on a fully successful capture. That's expected, not a bug. +- `tools_pc/ppm2bmp.py in.ppm out.bmp ` needs an *integer* scale arg + or it silently no-ops and prints usage — easy to lose a few minutes to. + +## M-98 (2026-09-11) — D227 seam residual FIXED (fold-sharing) + scroll-rate quantitatively measured + +Picked up the exact "left for next time" from M-97: re-derived the intended +sky scroll rate instead of re-attempting the seam blind. + +**Ruled out:** the global tick rate. `skyTick()` (verbatim N64 logic) adds +`g_ClockTimer` (~1/tick, per D193/D204) to `g_SkyCloudOffset` once per logic +tick — not per-render-frame, not double-counted at `sub_GAME_7F097388`'s +call sites either. A global timing bug would speed up everything, not just +the sky, so it isn't this. + +**Found + fixed:** M-95 shared `wScale` across a whole sky fan but left +`skyPortRenderPoly`'s S/T fold (`foldS`/`foldT`) computed per-call — the +exact same bug class, one field over, never touched. Two triangles sharing +a fan vertex could floor its S/T to different multiples of 64 texels; +nominally invisible (`GL_REPEAT` period 64) but each triangle bakes +`(S-fold)*32` into its own `s16` buffer, so two folds 64 texels apart = tc +values 2048 apart — a real crack. This is why M-96's playtest still saw the +seam after M-95. Fixed the same way as `wScale`: `skyPortBeginFan()` now +also computes one shared fold, threaded into `skyPortRenderPoly`. +`#ifdef PORT` only, `src/game/sky.c`, no game-logic touch. + +**Verified:** build clean; `-level_22` frame 345 (same frame M-95 used) — +fresh capture shows no visible crease at the ~x=1000/1280 location that's +clearly cracked in `docs/img/bugs/d227-sky-seam-statue.png`; `verify.sh +bunker1` PASS, `worst_cell` unchanged (known stale-golden-baseline number, +not a regression). No new screenshot committed — didn't have an M-96-era +"still broken" capture to diff against. + +**Scroll-rate measurement (the actual ask):** 11 consecutive frames +(`GE_PCDUMP=345-355:1`, static camera) cross-correlated on a sky-band row → +consistent **~7 px/tick at 640px capture width**, i.e. a full screen-width +sweep roughly every 90 ticks (~1.5s @60Hz). Hard number confirming +"extremely fast," not just a subjective read. **Root cause of the magnitude +itself is NOT found** — the accumulator and vertex geometry feeding it are +unmodified N64 logic, so if it's genuinely wrong the bug has to be in how +many screen pixels the D176(a) Path B substitution maps one texel-tick to +(camera/FOV/projection-matrix scale), not in the tick-rate accumulator. +Next step: compare the recovered per-pixel S gradient within one frame +against what the N64 `#else` edge-setup math (`130.0f`/`arg4`-driven) computes +for the same triangle — full spec in findings.md §H "D227 — M-98". + +**Environment gotcha hit this session:** launching `ge007.x86_64.exe` +directly (not through `verify.sh`, which already has this) needs +`export PATH="/c/msys64/mingw64/bin:$PATH"` first, or the process exits +immediately with code 127 (DLL resolution) and prints nothing. + +**Cradle side of PR #43 checked this session (was outstanding since M-97):** +`verify.sh cradle` PASS, no crash/regression from the fold fix. But found a +much more visually dramatic **pre-existing** artifact while there: looking +up the suspension tower near spawn, ~half the sky renders as a sharp +radiating "starburst" instead of clouds — screenshot +`docs/img/bugs/d176a-cradle-tower-starburst.png`. Confirmed pre-existing +(pixel-identical with `sky.c` reverted to pre-M-98) and confirmed **static** +(pixel-identical 30 ticks apart, not the D227 "moves at different rates" +symptom) — this is the already-known, already-open **D176(a) Defect 2** +(horizon-band tessellation), just now with a concrete repro instead of +"inconclusive." Not fixed — real tessellation work, separate task. Full +write-up: findings.md §H "D176(a) — M-98". + +**Not done:** human eyeball still owed on the Statue seam-close, the +scroll-speed question, and this new Cradle starburst screenshot before PR +#43 comes off draft. Defect 2 itself (Cradle starburst) not attempted — +next session candidate if D227/scroll-speed get closed out first. D228 +(AK47 white texture patches) still unstarted. + +## M-97 (2026-09-11) — PR triage/merge pass + D227 human playtest (NEGATIVE result) + +**PR triage** (fastest→slowest, per the new `docs/dev-process.md` §6 policy): +merged **#48** (D219 doc), closed **#26** (stale — D191/D193/D149 all +superseded by since-fixed/verified findings, real conflicts), merged **#51** +(Pages cleanup, folded the new §6 policy doc in rather than a 9th PR), merged +**#52** (README cleanup — also corrected a real staleness bug found in +review: README/LEVEL-STATUS still said "19/21, two levels crash" when D191 +and D193 are both FIXED+verified — reworded to "no known crashes, full +21-mission re-confirmation still owed" instead of either the stale claim or +an unverified "21/21" overclaim), merged **#50** (D221 — human mouse-playtest +on SELECT FILE: confirmed pass, all slots + Copy/Erase, no crash). + +**PR #43 (D176a/D227 sky) — human playtest: FAILED, stays in draft.** Built +this branch, launched Statue (`-level_22`) directly. Result: **the D227 +seam/crack is still visible despite the M-95 `wScale`-sharing fix**, which +directly contradicts that commit's "closes the visible seam" claim — M-95 +itself had flagged the temporal "moves at different rates" theory as +never re-verified (only the static-frame quantization theory was actually +checked), and this playtest is that missing check, landing negative. +**New data point, not previously documented:** the sky's scroll rate should +be *near-static* on N64 and is currently *extremely fast* on this build — +stronger than M-93's "still reads too fast," and possibly the same +wrong-magnitude root cause behind both the speed complaint and the seam +(a wrong per-frame tc delta could produce both). **Next investigation:** +re-derive the intended N64 sky scroll rate from `sky.c`/ROM env data and +check what the port's actual per-frame tc delta computes to — do not +re-attempt the seam fix in isolation from the speed question. Full +write-up: findings.md §H "D227 — M-96". + +**Not done this session:** the Cradle side of PR #43 (only Statue was +checked), PR #47's weapon-texture-matrix playtest, PR #49 (blocked on #47). + +--- + +## M-91 (2026-09-15) — D217 defect ① ROOT CAUSE FOUND + FIXED (M-90). Texpool arena base misalignment on PC; one-line-class fix in `texInitPool`; FACILITY/BUNKER1/DEPOT verified clean. 21-level sweep: 21/21 PASS. + +### M-91 continued (same session, later) — D219 re-checked (PR #48), defect ② swept + PR'd (#49), **D221 root-caused + fixed** + +1. **D219 runtime re-check — DONE.** Scripted-fire repro (`GE_INPUTSCRIPT` Z-pulses on `-level_33`, `GE_D172=1` + PPM dump): 40/40 particle multitex tris sample the correct RGBA fire tile (tmem=392), zero BUG/unknown-GBI/D146-abort lines, 0 purple/magenta pixels across 28 frames. Mechanism covered by the D172 fix on current `main`; final close pending a human by-eye pass at a live Dam PPK firefight (folds into the PR #47 playtest). Docs-only: **PR #48** (`docs/d219-runtime-recheck`, non-draft). +2. **D217 defect ② (CI-cache content key) — swept + PR'd.** The stale `fix/d217-model-texture-palette` branch was abandoned; its one commit (`f6fc0399`) cherry-picked onto fresh `fix/d217-ci-cache-content-key`. Full sweep on that branch: **21/21 PASS**. **PR #49** (draft — rebase + re-sweep after #47 lands so the pair is tested together). +3. **D221 (SELECT FILE mouse hit-box) — ROOT CAUSE FOUND + FIXED, headless-verified, swept, PR'd.** It was NOT a coordinate/origin mismatch. `interface_menu05_fileselect` passes four separate f32 locals (`&xmin, &ymin`) to `projectRectCornersTo2D`, which takes `{min,max}` coord2d *pairs* — relying on the N64 compiler laying each max adjacent to its min. PC MinGW lays them out in declaration order, so `arg1->f[1]` read **ymax-as-X** and `arg2->f[1]` read an unrelated stack float (observed `2.25734`, later `nan`) → the vertical hit band collapsed to a ~14 px strip at each wallet's bottom edge (sometimes NaN = unselectable). Exactly "click below the file." Fix: explicit `{xmin,xmax}`/`{ymin,ymax}` pairs under `#ifdef PORT` in `front.c` (N64 call untouched; ABI/layout exception per rule 2). Controlled headless test: same cursor pos + A-press — pre-fix never selected, post-fix selects folder 0 and advances. Full write-up: findings.md §H "D221 — M-91". **PR #50** (`fix/d221-fileselect-hitbox`, draft; sweep on branch: **21/21 PASS**; probe stripped). +4. **D220 (muzzle-flash FovScale drift) — ANALYZED, no bug found.** The flash nodes share the gun DL and the (scaled) per-frame projection → attachment is geometrically preserved; the whole camera-anchored viewmodel migrates toward screen center as FOV widens (correct perspective). Disposition: human call — QoL viewmodel re-anchor if it bothers (VIEWMODEL-RESEARCH family), not a fidelity fix. Index row updated (commit 2 of PR #50). +5. **Open / human-gated:** PR #47 weapon-matrix playtest (+ Dam PPK by-eye closes D219); PR #43/D176a sky eyeball; PRs #49/#50 await review/merge (#49 rebase + re-sweep after #47). All three M-87 QA items (D217①, D220, D221) now have resolutions pending human eyeballs. **Next AI task when resumed:** nothing blocking on the v0.2.0 gate is left un-investigated — remaining gate items are all human-gated; after merges: rebase #49, re-sweep, then tag/release per ROADMAP-1.0. + +--- + +**The M-89 stale-arena-pointer theory is REFUTED and the real cause is found, fixed, probe-verified.** Full write-up: findings.md §H "D217 — M-90" (read that before touching D217 again). One paragraph version: `texAlignIndices` pads CI index rows to *absolute* 8-byte boundaries, and the model-DL `G_LOADTLUT` palette offset is a *formula* that only matches reality when the texpool base is 8-aligned. On N64 the memp bump allocations feeding these pools happened to be 8-multiples; PC pointer bloat (D36/D37-class struct growth) made some STAGE-bank sizes not, so on FACILITY both weapon-pool bases land %8==4 → every palette in those pools sits 4 bytes early → the grip's count=1 TLUT reads the next tenant's header slot (`texnum=26` → LE s16 → `0x1A00` = the green). The same mechanism explains M-86's "leading zero pad" tell on larger loads (palette 2 entries early ⇒ first two read entries are zero pad). BUNKER1 was clean only because its pool bases happened to be aligned. + +**Fix landed (working tree, uncommitted):** `src/game/image.c` `texInitPool`, `#ifdef PORT`: round the arena base up to 8 before publishing `start`; `end` unchanged (pool loses ≤7 bytes). No-op on N64. ABI/layout-class edit per AGENTS.md rule 2's narrow exception — no logic touched. + +**Verified so far:** +- `./build-pc.sh ntsc-final` clean; no new/duplicate symbols. +- FACILITY `-level_34`: 188/188 decodes %8==0 (was 30 at %8==4); draw probe reads `pal0=0001` from the correct slot (was `1a00`); **0** exact-green pixels in PPM frames 250–450 (pre-fix ~1300 in 300–410), 0 strong-green. +- BUNKER1 `-level_09`: still clean (183/183 aligned, 0 CHANGED draws, 0 green). +- DEPOT `-level_30`: clean (229/229 aligned, 0 green in window). +- **Full `verify.sh sweep`: 21/21 PASS** (log: `sweep_d217.log` at repo root, scratch). + +**Probes still in tree (strip before commit, or keep one session for the playtest follow-up):** `GE_D217TEX` decode probe extended with a `D217TEX-SIZE` line (`src/game/image.c` `texInflateZlib`); `D217TEX-DRAW` change-detection probe in `port/fast3d/gfx_pc.cpp` `gfx_dp_load_tlut`. Both env-gated, inert by default. + +**Next steps (in order):** +1. ~~Read `sweep_d217.log`~~ — done: 21/21 PASS, no CRASH/REGRESSION/STALLED. +2. Commit: the `texInitPool` fix + findings/HANDOFF updates (strip probes first unless keeping them for step 4). Suggested branch name `fix/d217-texpool-alignment`. +3. **Human-gated:** user playtest eyeball of the M-87 weapon matrix (PP7/PPK/AK47/M16 grips+bodies, watch face, NPC-held guns) — hand off with a short list of what to look at; this closes D217 defect ①. +4. Defect ② (`fix/d217-model-texture-palette` @ `f6fc0399`, CI-cache content hash) still parked, still owes its own sweep — separate PR when it gets one. +5. Rest of the v0.2.0 gate unchanged: PR #43 (D176a sky) draft + human eyeball on `-level_22`; D219 scripted-Dam-explosion runtime re-check; then tag/release per ROADMAP-1.0. + +**Unchanged:** Tier 2 cluster untouched. Environment notes from M-90 still hold (MSYS2 PATH, `tasklist` check before "build broken" if `ld.exe: Permission denied`). + +## M-90 (2026-09-11) — session cut short (usage limit). D217 probe extended + built; a local-Qwen triage's specific citations were caught WRONG on hand-verification (see below); the underlying mechanism was independently re-derived and looks solid. NOT yet run to completion — next session picks up at "read the run.log", not at square one. + +**Housekeeping done:** untracked `docs/dev/MODERN-PORT-FEATURES-BACKLOG.md` (from a concurrent session) moved to `docs/dev/notes/` (gitignored) per user instruction — do not treat this as new work, it's someone else's in-flight doc. + +**Local-Qwen dispatch result — DO NOT TRUST WITHOUT RE-VERIFYING.** Dispatched `triage` to investigate the D217 "stale-arena-pointer" lead (per M-89's exact spec). It came back HIGH confidence with specific citations: "`texWriteLoadToTmemAddr`'s only call site is `tex.c:918`" and "`texFindInPool` has only two callers repo-wide (`tex.c:405`, `image.c:2443`)". **Both are false** — direct grep shows `texFindInPool` is also called at `tex.c:856` and `tex.c:920` (same file it claimed to have fully searched), and `tex.c:918` is actually `texnum2 = (in->words.w1 >> 12) & 0xfff;` inside `texLoadFromGdl`'s `TEXTURETYPE_DETAIL` case — not a `texWriteLoadToTmemAddr` call at all. This is the exact "confidently fabricates" failure CLAUDE.md already warns about. Its overall shape (address baked once into a GDL at model/room-load time, reused pool) pointed in a directionally-useful place, but every specific line citation from it needs independent re-derivation, which is what the rest of this session did by hand. + +**What was independently verified by hand-reading the real code (trust this part):** +- `texWriteLoadToTmemAddr` (`tex.c:494`) emits `gDPSetTextureImage(gdl++, tex->gbiformat, depth, 1, tex->data)` — `tex->data` is a real `u8*` (`image.h:42`), and `gSetImage`'s packing macro (`include/PR/gbi.h:3106-3111`) stores it as `(uintptr_t)(i)` — a full 64-bit store, NOT a 32-bit-truncated N64 physical address. **This rules out a D3x-class pointer-truncation bug for this path** — a theory I formed and then killed myself before wasting a build cycle on it. Don't re-open this angle without new evidence. +- `texLoadFromGdl` (`tex.c:779`) — which bakes those addresses into a display list — runs only at object/room LOAD time: callers are `objecthandler_2.c:82` (model load) and `bg.c:2499/2601/2603` (room primary-GDL load, confirmed by the doc comment at `lightfixture.c:31` — "Room load ... texLoadFromGdl() walks the room's display list"). It does **not** run per-frame. +- `gfx_dp_load_tlut` (`port/fast3d/gfx_pc.cpp:2202`) — the draw-time consumer — does a **live re-read of memory at the baked address on every single draw call** (`base = (const uint16_t *)rdp.texture_to_load.addr + ...; *dst++ = PD_BE16(*src++);`, loop at line 2230-2234). It is NOT cached across frames; it always reflects whatever bytes currently live at that address. +- **Conclusion (HIGH confidence, own derivation):** the mechanism is real and matches the symptom shape exactly — a texture's address is baked into a model/room's DL once, at load time; every subsequent frame's draw re-reads that literal address fresh. If some OTHER later texture decode reuses the same `texpool` arena bytes (bump allocator, `image.c` `leftpos`/`rightpos`, no per-texture free/refcounting — confirmed at `image.c:2323-2328`, `2508-2525`) before the first model's DL stops being drawn, every frame after that point reads the new tenant's bytes — permanently, matching "solid green from a fixed point onward, never intermittent." This does not require N64/PC to differ in the C code (AGENTS.md ground truth holds) — the divergence must be in *pool sizing/reuse cadence* between platforms, not logic. + +**Probe extension shipped this session (built, NOT yet run to a read result):** +- `src/game/image.c` (`texInflateZlib`, `#ifdef PORT` `GE_D217TEX` block) — added `addr=%p` (the `dst` pointer, i.e. `tex->data`) to the existing decode-time log line. +- `port/fast3d/gfx_pc.cpp` (`gfx_dp_load_tlut`) — added a new `GE_D217TEX` log (`D217TEX-DRAW addr=%p pal0=%04x pal1=%04x count=%u`, via `sysLogPrintf`, NOT `osSyncPrintf` — that doesn't link into `port/fast3d`, cost one failed build to learn) logging a **live re-read** of the palette bytes at draw time, keyed by address (texnum isn't available at this layer — correlate the two logs by `addr=`, not by texnum). +- Both builds clean (`./build-pc.sh ntsc-final`, MSYS2 MINGW64 PATH). Neither is applied/committed yet — working tree only. +- **Hit one environment snag:** the build failed once with `ld.exe: cannot open output file ... Permission denied` — a leftover `ge007.x86_64.exe` (PID from an earlier/concurrent run) had the file locked. User approved killing it; by the time `taskkill` ran the process had already exited on its own. If this recurs, check `tasklist //FI "IMAGENAME eq ge007*"` before assuming the build is broken. + +**Exact next step (do this first, it's cheap):** `tools_pc/verify.sh` was run once (`GE_D217TEX=1 ./tools_pc/verify.sh facility`, PASS, 3 frames) but the session ended before locating/reading the resulting log. `verify.sh` writes the run's stdout+stderr to `"$capdir/run.log"` (see `tools_pc/verify.sh:184`, `CAPDIR="$capdir"` set at line 231) — **find that capdir** (likely under a `tools_pc/`-relative temp/capture dir printed by verify.sh itself, or grep `verify.sh` for how `capdir` is constructed if not obvious) **and grep it for `D217TEX` lines.** Correlate the decode-time `D217TEX texnum=... addr=...` lines against the draw-time `D217TEX-DRAW addr=...` lines by address: if any `addr` shows a *different* `pal0` between its most-recent matching decode-time line and a draw-time line that comes after it, that is direct proof of the stale-arena-pointer mechanism, and narrows the fix to whichever call site re-runs `texInitPool`/reuses that arena region while an earlier consumer (a still-drawn weapon or room model) is still alive. If the addresses never collide in this FACILITY run, extend the capture window or try the exact BUNKER1-vs-FACILITY pairing from the M-89 decode-only probe (frame ~300, post-vent-drop-cutscene) with both probes active together. + +**Not done this session, unchanged from M-89:** PR #43 (D176a sky) still draft, still owed a human eyeball on `-level_22`/Cradle. D219 still needs the scripted-Dam-explosion runtime re-check. Tier 2 cluster untouched. + +## M-88/M-89 (2026-09-10/11) — v0.2.0 fidelity push. PRs #39-#42, #44-#46 all merged. D77 CLOSED. PR #43 (D176a) draft, CI-green, owed a human eyeball. D217 still open — two theories refuted this session, new stale-arena-pointer lead for the next session. D219 triage note added (likely already fixed by D172, needs a runtime re-check). + +**D217 status for the next session, read this before doing anything else on +it:** two full theories are now refuted with hard evidence (not just +inspection) — (1) the offline sidecar (`tools_pc/d43_emit.py`) never touches +texture bytes (M-88); (2) the `gfx_dp_load_tlut` pitch/address math is +provably correct, AND (empirically, PR #45, merged) the decode itself +(`texInflateZlib`) produces byte-identical palette content for the affected +texture (`texnum=1608`, the PP7 grip) on both BUNKER1 (correct) and +FACILITY (wrong/green) — proving the bug is strictly downstream of a +correct decode. The already-written, previously-untested-against-this-repro +parked fix `fix/d217-model-texture-palette` (CI-cache content-hash, defect +②) was built and run directly against the FACILITY repro **and still shows +green** — so defect ① is NOT the same bug as ②, don't expect ②'s merge to +fix this. **Current best lead (untested): a stale/dangling `tex->data` +pointer into the `texpool` arena** — probe the draw-time address vs. the +most-recently-decoded content at that address (spec + probe extension +notes in findings.md D217, search "stale-arena-pointer"). A `GE_D217TEX` +env-gated probe is already in tree (`src/game/image.c`) logging +decode-time `texnum`/`palette[0..1]` — extend it with an address field +rather than starting over. + +**Goal for this arc:** ship v0.2.0 in the next few days, fidelity-only scope +(graphics + audio 1:1 with N64; hard QoL out of scope — see +`docs/dev/notes/ROADMAP-1.0.md` "M-87 triage" section, which IS the priority +order). Tier 1: D217 (weapon/NPC texture palette) > D77 (in-level music +missing, hard-gates UNLOCKED-FPS-PLAN/WIDESCREEN-FOV-PLAN) > D176(a) (sky). +Tier 2 (after Tier 1): D222+D218+D220 FOV-slider break-set, D148/D160/D173 +cutscene actors, D219 purple explosions. QoL items (D194, D223, D224, D225, +D226, D211-as-degrees, D218-as-slider) are explicitly OUT OF SCOPE this pass. + +**Usage note (mid-session):** user flagged we were at ~70% of the cloud +usage limit; killed one just-started cloud subagent (D217 follow-up) with +near-zero loss, paused further cloud subagent dispatches, and switched to +the local Qwen delegate (`delegate-local` MCP, cost-free) for further D217 +work once its backend (LiteLLM proxy + Unsloth Studio) was confirmed up. +**If picking this up in a new session, check the local backend first** +(`local_backend_status()`) — it was down for a stretch this session, up by +the end. + +**Landed this session:** +- **PR #39** merged (`e117c0d7`) — golden bunker1 re-baseline (D215) + + M-85 D217/D211 triage docs. Docs+PNG only. +- **PR #40** merged (`531e24e1`) — `tools_pc/verify.sh` fast-kill (aa199729 + picked up from a prior local branch): kills each level run as soon as its + last `GE_PCDUMP` frame lands instead of idling the full watchdog. Verified + locally: `bunker1` 19.5s (was ~45s). Makes every subsequent verify faster. +- **PR #41** merged (`5e111f0d`) — docs-only: proves D217 defect ① is NOT a + `tools_pc/d43_emit.py` sidecar bug (full texture-reference-chain trace + + `GE_TEXDUMP` reproduction). New lead: `port/fast3d/gfx_pc.cpp`'s + `gfx_dp_load_tlut` palette-address/pitch math × `src/game/tex.c`'s + `texWriteLoadToTmemAddr` literal `width=1` `SetTextureImage` call. +- **PR #42** merged (`114d1edd`) — **D77 CLOSED.** In-level music was silent + because `src/game/mp_music.c`'s `sub_GAME_7F0C0BF0()` had no `return` + statement (D6-class UB, same as D187) — every in-level music-start trigger + fed a garbage/zero volume into `musicTrack1ApplySeqpVol`/`Track3` right + before `Play()`, silencing the track while note-on processing continued + normally underneath (which is why it looked like a deeper pipeline bug). + Front-end/menu music bypasses that path entirely, hence unaffected. Fix: + one line, `#ifdef AVOID_UB`. Verified with a new `[AUDIOLVL]` peak/RMS + probe (`port/src/audio.c`, `GE_AUDIOTRACE`-gated): before fix, 29/29 + samples over a 28s BUNKER1 capture read `peak=0 rms=0.0` despite 173 note + events firing; after fix, every sample is sustained non-zero. **This was + the hard gate for `UNLOCKED-FPS-PLAN.md`/`WIDESCREEN-FOV-PLAN.md` — they + can be picked up again now** (still Tier-2-and-later work, not this pass). + **Owed:** a human by-ear BUNKER1 pass to confirm audible (headless capture + only proves non-zero PCM reaches the SDL queue). + +**PR #43 (draft)** `land/d176a-sky-perspective-scroll` — D176(a) sky +scroll/tiling too fast, and Cradle's sky was black at default FOV even +after Path B (PR #18). Root cause: `skyPortRenderPoly` placed sky-quad verts +under a plain `w=1` ortho projection, so fast3d interpolated `tc` *linearly +in screen space*; the source S/T is computed upstream without a perspective +divide (RDP `G_TRI_SHADE_TXTR`-equivalent), and a quad's near-vs-horizon +vertices can differ ~20-38x in camera-space `w` — linear interpolation +smears the whole S/T delta evenly across every pixel instead of weighting it +toward the near vertices. Fix: a custom projection matrix gives each vertex +its real camera-space `w` (already computed upstream, `unk0c`, previously +unused) while keeping screen-space x/y unchanged, so the GPU's normal +perspective divide does the right thing — same pipeline every other textured +draw already relies on. CI green, kept **draft** per project convention +(rendering fix, no display access to fully confirm) — **owed a human +eyeball on `-level_22` (Statue) or Cradle (`-level_41`)**: does the cloud +scroll read like a reasonable slow drift now, not a fast/aliased hatching? +Defect 2 (horizon-band tessellation) and the separate FOV-coupled +black-sky-past-frustum-edge symptom (D218/D222-family) are untouched. + +**Still parked, do NOT lose track of it:** branch +`fix/d217-model-texture-palette` (local only, commit `f6fc0399`) — a REAL +but INSUFFICIENT fix for D217 defect ② (CI-texture-cache palette-content +collision; FNV-1a hash added to the cache key in `port/fast3d/gfx_pc.{cpp,h}`). +Does not fix the visible symptom (that's defect ①, in progress below). Owes a +full 21-level `verify.sh sweep` before it can merge — **per user instruction, +reserve that sweep for right before cutting v0.2.0**, don't run it now. Land +it then, or fold its diff into whatever the D217 defect-① fix needs. + +**D217 defect ① — worked directly (not delegated) after two local-Qwen +dispatches (see PR #44/#45 above) supplied leads that turned out wrong on +inspection.** Summary of the chase, in order: (1) `d43_emit.py` sidecar — +ruled out (PR #41). (2) local-Qwen proposed hardcoding `gfx_dp_load_tlut`'s +`pitch=1` — hand-derived and refuted, it's a no-op given the GBI `width-1` +packing (PR #44). (3) built the `GE_D217TEX` decode-time probe myself, +ran it on BUNKER1 vs FACILITY — **decode is byte-identical across levels**, +ruling out a ROM/asset-table issue too (PR #45). (4) built + ran the parked +defect-② fix directly against the FACILITY repro to test whether ① and ② +are the same bug — **still green, they are NOT the same bug** (also PR #45). +Current best lead: a stale/dangling `tex->data` pointer into the shared +`texpool` arena, resolved once and not re-checked at draw time (full spec +in findings.md D217, search "stale-arena-pointer") — nobody has probed this +directly yet, that's the next session's starting point. + +**D219 (Tier 2, purple explosions)** — dispatched a local-Qwen triage ahead +of schedule since it was free and idle-time-shaped. Static analysis found +no separate/unfixed code path from the already-fixed D172 mechanism; +plausibly already fixed, needs a runtime re-check with a scripted Dam +explosion + `GE_D172=1` (not yet built). Docs-only note landed, PR #46. + +**Environment note confirmed this session:** this session CAN build +(`./build-pc.sh ntsc-final` from Bash with `/c/msys64/mingw64/bin` prepended +to PATH) and run `tools_pc/verify.sh ` for a single targeted level — +consistent with `env-no-runtime-verify.md` memory (interactive sessions can). +Did not run a full sweep (correctly deferred per user instruction). + +**Not yet started:** Tier 2 cluster (D222/D218/D220 FOV break-set, +D148/D160/D173 cutscenes) — pick up after Tier 1 lands, per ROADMAP-1.0.md +M-87 triage order. D219 got a head start (triage note above, still needs a +runtime re-check, not a real fix session yet). If Tier 1 alone doesn't fit +the time budget, ship v0.2.0 with whatever's landed/verified and hand off +the rest (explicit user instruction — don't stretch the release for Tier 2). + +**Session end state (M-89):** Tier 1 = 1 of 3 fully closed (D77), 1 of 3 +CI-green-draft awaiting a human eyeball (D176a, PR #43), 1 of 3 still open +with a concrete next probe (D217). Everything landed is on `main`; nothing +is stuck in a stale worktree or unpushed branch except the deliberately +parked `fix/d217-model-texture-palette` (sweep-gated, see above) and +`docs/v0.1.0-completion-count-fix`/`chore/agentic-speedups-s1-4-6`/ +`fix/linux-stan-linktile-ptr-trunc` (pre-existing local branches, unrelated +to this session, not investigated — check their status before assuming +they're still relevant). + +## M-83 (2026-09-09) — Agentic session. THREE PRs (all CI-green, all off `main`): #36 QoL wave, #37 D215 viewmodel fix, #38 D216 SkipIntro. + +**Session env:** could build (MSYS2 MINGW64 toolchain on PATH), run +`tools_pc/verify.sh` (win), and capture `GE_PCDUMP` frames — NOT display-blind +like M-82. Could NOT drive in-level gameplay (no scripted playthrough), so +feel-checks are still owed to a human. + +| PR | Branch | State | Contents | +|---|---|---|---| +| #36 | `feat/pc-qol-fov-aniso-fps` | ready, CI green | **D211** `Video.FovScale` (FOV slider, `WIDESCREEN-FOV-PLAN.md` Phase 4) · **D212** `Video.Anisotropy` · **D213** `Video.DisplayFPS` · **D214** `[Bind]` keyboard rebinding. All port-only, default = byte-identical. | +| #37 | `fix/d215-viewmodel-renderdata-flags` | **draft** (owed 1 eyeball), CI green | **D215** — the "no gun visible while playing" bug. | +| #38 | `feat/d216-skip-intro` | ready, CI green | **D216** `Game.SkipIntro` → boots to the SELECT FILE menu. | + +**D215 detail.** `gunRenderFirstPersonGunModels` seeded `ModelRenderData` via +`*(ModelRenderData *)&D_80035CC0` — a reinterpret across two separately-declared +adjacent globals (`u32 D_80035CC0=0` then `u32 D_80035CC4[]={1,3,0,...}`, gun.c) +that only lands right on the N64 linker + 4-byte pointers. On x86-64 +`renderdata.flags` read **0**, gating every weapon-model DL node off in +`modelRenderNodeGundl` (`flags & 1`) → weapon `subdraw` emitted 1 gfx cmd not +~150. All 3 `VIEWMODEL-RESEARCH.md` suspects ruled out first by a new `GE_DVM=1` +probe. Fix = `#ifdef PORT` explicit field init (`zbufferenabled=TRUE; flags=3`). +Headless-verified: flags 0→3, subdraw 1→153 cmds, `bunker1` framediff +worst_cell 12.2→28.5, 6-level sweep crash-free. `GE_DVM` probes left in +(env-gated). `VIEWMODEL-RESEARCH.md` retired. + +**Testing / merge notes for the next session:** +- Branches are independent off `main` and don't conflict in code. To test all + three together, make a scratch integration branch (`git merge` all three onto + a `test/m83` branch) — the only conflicts are 1-line `findings.md` / + `findings-index.csv` row-adjacency near `| D85 |` (keep all rows). +- **D215 owes a golden re-baseline:** the gun now renders in the `tools_pc/golden` + bunker1 frames, so `verify.sh bunker1` reports REGRESSION until the golden is + regenerated. Do that only after the gun is eyeball-confirmed correct. +- `#37` stays draft until someone boots BUNKER1 and sees the gun. +- The working `data/ge007.ini` has non-default values left by interrupted + verify.sh runs (`MSAA=8`, `TextureFilter=2`, `ScreenShakeIntensity=10`, + `MouseAimSpeed=16`, `PadTriggerPct=23`) — delete the file or reset in F10 if a + test looks off; not from the PRs. + +**Owed feel-checks (in-level, human):** +- D211 `Video.FovScale` ≠ 100 (F10 "FOV scale %"): world widens uniformly, HUD + unmoved, aim-zoom composes, no stretch in a 4:3 window. +- D212 `Video.Anisotropy` 1 vs 16 on a grazing surface (Dam walkway / Depot roof). +- D213 `Video.DisplayFPS`: reads ~30, no HUD overlap. +- D214: a non-default bind takes (`Input.Bind.Fire = Left Alt`); ESC still + cancels menus; a fresh ini shows the whole `[Bind]` block. +- D215: BUNKER1 — gun visible + correctly posed. +- D216: SELECT FILE menu is fully interactive after the skip (cursor, open a slot). + +Docs landed on the branches: `findings.md` D211–D216 rows + index regen; +`WIDESCREEN-FOV-PLAN.md` Phase 4, `QOL-INVENTORY.md`, `GRAPHICS-BACKLOG.md`, +`VIEWMODEL-RESEARCH.md`, `GE-ENV-PROBES.md` (GE_DVM) all updated. + +## M-84 (2026-09-09) — user test pass on the M-83 PRs. D211 refixed. D213/D214/D216 verified. D217/D218 logged. + +**Session env:** interactive — user built + ran + playtested in-level. Could +not drive gameplay from here; user did the eyeballing. + +**Test branch:** `test/m83-all` = `main` + #36 + #37 + #38 + M-84 commits +(throwaway; delete once the PRs merge). The M-84 FOV refix + findings were +then folded into **PR #36** and pushed (`01376660`); a PR comment flags the +new `#ifdef PORT` scope. + +### Results + +| Item | Verdict | +|---|---| +| **D216** SkipIntro | PASS — boots to SELECT FILE, menu interactive | +| **D213** DisplayFPS | PASS — top-right FPS counter, clear of HUD | +| **D214** rebinding | Code PASS (alt/ctrl). **Doc bug fixed:** section is `[Input.Bind]` not `[Bind]` (config.c groups by the *last* dot), or use a fully-qualified `Input.Bind.Fire =` line. Watch-pause menu still Enter-only for back (pre-existing, `options.c`, minor — small port-side add). | +| **D212** aniso | PASS (M-83 already) | +| **D211** FOV | **REFIXED.** Original fast3d `gfx_apply_fov_scale` only scaled *pure* perspective loads; the world uses `field_10E0` = CPU-combined proj×view (`bondview2.c:8224`) → guard-rejected → only viewmodel/sky changed. Moved to game-side fovy scale at `fr.c` `guPerspectiveF` chokepoint (`#ifdef PORT`, `frFovY *= portFovScale`, clamp 160°), gated `!= LEVELID_TITLE` (front end shares the path). fast3d hook removed; `video.c` sets `f32 portFovScale`. **User-verified: menu unaffected, in-level widens.** Nits owed: HUD-static + aim-zoom eyeball at ~120. | +| **D215** viewmodel | Gun draws ✓ but **textures garbled** → split to D217. #37 still draft; flip to ready (D215 itself is done). Golden re-baseline owed after merge. | + +### New findings + +- **D217 (OPEN)** — 1P viewmodel + 3rd-person Bond model textures render garbled (green/blue/yellow); Bond's watch face too. Stale-palette CI-texture bug, **D161 family**; surfaced by D215, not caused by it. Next: `GE_TEXDUMP` on a weapon texture in BUNKER1; check `gDPLoadTLUT` reaches `gfx_pc.cpp` for model DLs. +- **D218 (LOGGED, parked)** — wide FOV exposes the per-level `Visibility.FarFog` draw-distance/fog boundary ("blue artifacting" down the Dam tunnel at high FovScale). `bgfog.c:305` — `FarFog` is both far clip and fog end. Proposed fix (`Video.DrawDistance` multiplier + `Video.DrawDistanceAutoFov` toggle) written into the D218 finding; **not implemented** by user request. + +### Reconciliation state — DONE (M-84 cont.) + +**All three PRs merged to `main`:** #36 (04a6372b), #37 (bdd924c7), #38 +(7a7dfb94). Conflicts on merge were only `findings.md` / +`findings-index.csv` / `QOL-INVENTORY.md` row-adjacency + a trivial +`video.c` (portSkipIntro vs portFovScale coexist) — resolved keeping all. +`findings-index.csv` is generator-output (`tools_pc/gen_findings_index.py`, +enforced by the `validate` CI gate) — hand-edits fail CI; regenerate. +Throwaway `test/m83-all` + merged feature branches deleted. + +### NEXT SESSION + +1. **Owed on `main`:** regenerate the `tools_pc/golden` bunker1 capture — + D215 makes the gun render in-frame, so `verify.sh bunker1` reports + REGRESSION until re-baselined. (Interactive/console session required.) +2. **D217 triage** — viewmodel + 3rd-person Bond model textures garbled + (green/blue/yellow), watch face too. Stale-palette CI-texture bug, D161 + family. `GE_TEXDUMP` on a BUNKER1 weapon texture; check `gDPLoadTLUT` + reaches `gfx_pc.cpp` for model DLs. +3. **D218** (parked) — `Video.DrawDistance` + `DrawDistanceAutoFov`; spec in + the D218 finding. Only if asked. +4. D211 nits owed: eyeball HUD-static + aim-zoom-composes at FovScale ~120. +5. `build-pc/` holds a stale build — rebuild. + +## M-85 (2026-09-09) — golden re-baseline done; D217 did NOT repro; D211 HUD-static confirmed. + +Session env: interactive — build + `verify.sh` + `GE_PCDUMP` capture OK; no +scripted in-level input. + +1. **Golden bunker1 re-baselined** — branch + `chore/golden-bunker1-rebaseline-d215` (commit `1e86bd66`, off `main` + @ 7a7dfb94). Fresh default ini pinned 640x480, `GE_PCDUMP=200-440:120`. + Gun eyeball-confirmed visible + posed + clean. `verify.sh bunker1` now + **PASS worst_cell 12.3** (was REGRESSION ~28). **Needs push + PR + merge.** +2. **D217 — repro pinned to FACILITY.** Clean on `-level_09` BUNKER1 in + every config; **`-level_34` FACILITY reproduces at ~frame 300** (just + after the vent-drop intro cutscene): PP7 grip = solid green block, green + ring at the barrel joint, hand has a green cast, slide/barrel upper is + correct black. `GE_TEXI` confirms `gDPLoadTLUT` DOES reach `gfx_pc.cpp` + for model DLs (weapon tiles import `fmt=2 palfmt=0x8000 palidx=0`). + **User: it's widespread — guns on many levels, multiple weapons, wrong/ + missing textures.** Facility PP7 is just a clean headless repro. + `GE_D217` probe (reverted): weapon model DLs issue partial-count + `G_LOADTLUT` (count 1/28/125/172/193/…); **`GE_D217Z` — zeroing + `rdp.palette[count..255]` did NOT fix it**, so the bad texels index + within `[0..count)` ⇒ wrong palette *bound* to the grip tile. + Best theory: **`TextureCacheKey` collision / missing CI-cache + invalidation on palette change** (`gfx_pc.cpp:990`, no palette-content + hash) — Facility renders green guards right before the viewmodel, grip + takes a stale `cache=HIT` against the guard-fatigue palette. Fix needs + an all-21-levels world-CI regression sweep (hot path). Full next-steps + in the D217 finding. Screenshots: scratchpad `fac_gun_crop.png`, + `fac_gun_D217Z.png`. +3. **D211 nits** — headless FovScale 100-vs-120 diff: HUD ammo counter + screen-position identical, no 4:3 stretch, viewmodel composes. HUD-static + **confirmed**. aim-zoom-compose still owed (button-held, not scriptable + here). +4. D218 stays parked (untouched). + +## M-81 (2026-09-08) — **D193 SOLVED (= D209). AI locomotion fixed + verified. PR #32 MERGED to main.** + +Branch `diag/d193-timing-keystone-measured` — **merged to `main` 2026-09-08 +(merge commit `34ae9f54`, PR #32), branch deleted, local `main` fast-forwarded.** +CI went green after regenerating `docs/dev/findings-index.csv` for the new D209 +entry (`chore(D209): regen findings index` — the `validate` job's +`gen_findings_index.py --check` gate had caught it stale). No session trailers. + +**Root cause (D209, D3x pointer-width class).** `get_sound_at_range()` +(`chraction.c:3573` — misleading name; it is the *locomotion-animation +selector*) picks walking / running / sprinting from an `arg1` speed tier. Its +only caller (`chraction.c:3665`) passed that tier as +`self->act_ubytes.padding[45]` — a **raw byte alias into the ChrRecord action +union**, not the named field. On N64 offset 45 *is* `act_gopos.unk59`. On +x86-64 the three pointer members of `act_gopos` widen 4→8 B, moving `unk59` +to 81, so the literal 45 lands on **byte 5 of `waypoints[1]`** — and because +the arena is low-4GB (`0x00000000_70xxxxxx`) that byte is **always 0x00**. +Result: `arg1 = 0` forever → **every AI chr in the game bound to +`ANIM_DATA_walking` / `ANIM_DATA_walking_unarmed`**, on every level, since the +64-bit transition. GE travel is anim-root-motion driven, so they all moved at +walk pace no matter what the ailist commanded. Bond is unaffected (explicit +physics, no root motion) — exactly the reported "player fine / all AI slow" split. + +**Fix:** `chraction.c:3665`, `#ifdef PORT` → `self->act_gopos.unk59`; N64 line +verbatim under `#else`. ABI-only, no logic change. Both callers reach it with +`actiontype == ACT_GOPOS`, so it is the same byte N64 reads. +**User-verified on Cradle: Trevelyan and the guards run.** + +Also fixed the `// guess: room` comment on `unk59` (`bondtypes.h:2191`) — it is +the SPEED tier. + +**Owed / next (also tracked in local gitignored `docs/BACKLOG.md`):** +- Re-playtest the other D193 instances now that the fix is on `main`: Facility + Ourumov + squad beat, and Natalya escort pace on Bunker ii / Statue / + Control / Archives. Expect all to resolve; confirm **D170** (Ourumov/Trevelyan + "flee" looks wrong) is subsumed. +- **Cradle completability** — D193 was the reason the final level could not be + finished. Needs a full end-to-end Cradle run to confirm 21/21 completable — + this is the v0.2.0 gate. +- Probe scaffolding (`GE_D193`/`GE_D193A`/`GE_D193B`) is env-gated and inert; + strip once the re-playtest above passes. +- ~~Commit/PR this branch~~ — DONE (PR #32 merged). + +**Validation status:** the fix is **Cradle-verified only** (Trevelyan + guards +run). The other instances above and the full completion run are still owed; the +PR body flags this explicitly so it is not read as fully validated. + +**Traps worth remembering (cost real time this session):** +- `chrlvApplySpeed`'s `speedPtr` out-param is a **turn** rate, not a travel + rate (`chraction.c:8070` passes `&act_runpos.turnspeed`). `act_gopos.speed == 0` + means "running in a straight line", NOT "stalled". +- The exe is `WIN32_EXECUTABLE` (GUI subsystem), so PowerShell `2>` captures + **nothing**. Use a `.bat` wrapper (`build-pc/d193a_cradle.bat`) — cmd sets the + handle before process start. Also needs the MinGW runtime dir `C:\msys64\mingw64\bin` on PATH + (libwinpthread-1 / libgcc_s_seh-1 / zlib1 / SDL2). +- `PROP_TYPE_VIEWER = 6`, `PROP_TYPE_CHR = 3`. The viewer stops going through + `chrTick` after the intro, so Bond cannot be measured with `GE_D193A`. + +--- + +## M-82 (2026-09-08) — Lane C visual sweep + A1 sibling of D209. 4 PRs open. + +Agentic session, focus = Lane C visual bugs toward 1.0. No display this +session, so rendering fixes ship as "owed one screenshot" not "done". + +**Landed (all `#ifdef PORT` / port-only, verify.sh crash-free):** + +- **D196** — PR #33 (**ready**). F10 options overlay left the OS cursor + visible on close unless you'd already clicked-to-lock in a stage. Fix: + re-assert `applyCursorVisibility()` each poll after `reconcileGrab()` + (`port/src/input.c`). Owed: 5-sec eyeball (open F10 from a menu, close, cursor gone). +- **D210** — PR #35 (**ready**). *New A1-class bug, sibling of D209, found by + the post-D209 sweep.* `chrToPatrol` inits `act_patrol.lastvisible60` via the + raw union alias `act_init.padding[0x13]`; `act_patrol` widens (leading + `path` ptr) so on PC the write misses and `lastvisible60` stays garbage → + the patrol "haven't seen the player recently" gate reads junk on entry to + PATROL. Fix = named field under `#ifdef PORT` (`chraction.c:3872`). + verify PASS bunker1/facility/archives/dam. Owed: patrol-guard behaviour + eyeball. The rest of the `padding[N]` sweep (anim/dead ticks → pointer-free + arms) is layout-stable; `struct player`/`hand` raw offsets (AUDIT-M6 #1-4) + already fixed under D115. +- **D176(a) sky** — PR #18 (**draft**, rebased onto main + Defect 1 fixed). + Cloud tile = `s_skywaterimages[0]` 64×64 IA8; the `tc = S*32` scale was + right (matches GE `width<<5` idiom), the bug was `(s16)` overflow on the + ±20k-texel horizon S/T. Fix = per-primitive phase-fold to the 64-texel + period (`sky.c skyPortRenderPoly`). Defect 2 (horizon-straddling tris) + still open → adaptive tessellation. **Owed: one `-level_22` screenshot** — + see `docs/dev/D176a-SKY-NOTES.md` "Verification ask". + +**Investigated, not fixed:** + +- **D172** (magenta/cyan particles) — PR #34 (**draft**, diagnostics only). + Withdrew *two* dead root causes: 0xB9 is `G_SETOTHERMODE_L` (handled), and + the records *do* set `G_CYC_2CYCLE` (fast3d applies it — probe-confirmed). + Real suspect: the `G_CC_INTERFERENCE` two-tile combine / TEXEL1 bind + (IA8 smoke × RGBA16 fire @ tmem 0x188). Shipped a decisive `GE_D172=1` + probe — **one Silo capture** (close guard kill) dumps both texunits' + tmem/fmt/siz and settles it. `docs/dev/D172-PARTICLE-COLOR-RESEARCH.md` §5-6. + +**Not worked (Lane D / audio):** blocked on the user's by-ear playtest — +D207 monitoring, D202/D204/D206/D208 fixed & need ear-check only. + +**Playtest validation (M-82, user, off merged `main`):** +- **D210 + D209 — PASS** on Dam, Facility, Bunker. Guards react to the player + and move at running pace. (Natalya-escort levels not yet checked.) +- **D196** — not explicitly re-checked; user instead decided to **backlog + removing `MouseCaptureMode=0`** entirely (it's the D192-bugged path). See + `BACKLOG.md` "M-82 playtest feedback". +- **D176(a) sky — renders clouds but WRONG.** Too small / over-repeating, + scroll too fast. Path B's ortho emit has no perspective-correct texturing; + `unk20/unk24` are pre-persp-divide S. Needs grid tessellation or Path A + (RDPHALF decode). Detail in `BACKLOG.md`. PR #18 merged; revert candidate + if it reads worse in motion than black. +- **Audio** — SFX good; title/intro/menu music "mostly accurate". Not a + full N64 A/B but positive. +- **D172 probe** — not run yet. + +--- + +## M-80 (2026-09-08) — B1/D193: timing keystone MEASURED, hypothesis contradicted (superseded by M-81) + +Branch `diag/d193-timing-keystone-measured` (commit b457c014, not pushed). +Added `GE_D193=1` probe in `waitForNextFrame()` (per-wall-second: render fps / +sim ticks Σ / clamp hits / max raw delta). Idle runs of `-level_09/_20/_27/_34`: +**dead-steady render=30 fps, sim=60 ticks/s, clamped=0, maxraw=2** on every +level, VSync irrelevant. The wall-clock sim sum is *exactly* N64's ~60 — B1's +"g_GlobalTimerDelta / g_ClockTimer sum low" premise is **wrong for steady +state**. Findings D193 + GE-ENV-PROBES.md updated. + +**LIVE-LOAD CONFIRMED (M-80 cont.):** user ran Cradle `-level_41` with `GE_D193=1` +and drove the Trevelyan sequence while Trevelyan was visibly walking really slow — +probe held `render=30 sim=60 clamped=0 maxraw=2` dead steady, no `[D156]` fires. +**D193 is a locomotion/animation bug, not the frame clock.** The pivot is final. + +**D193 port side now instrumented (M-80 cont., commit 43d20538):** `GE_D193A` +(chr.c `chrTick` — per-sec travel speed + root-motion scalars) and `GE_D193B` +(model.c `sub_GAME_7F06D3F4` — raw anim-bitstream decode). Silo+Facility: +**every port-side input reads nominal** — `g_GlobalTimerDelta=2.0`, +`playspeed=1.0`, `scale=0.1`, `anim_translation_scale=1.0`; the C in this path is +identical to N64 (only non-firing D156 guards are `#ifdef PORT`). `animrate=0.0` +is the default. Patrol guards travel ~60–99 u/s. `GE_D193B`: `base=0`/`angle=0` +every call. + +**Next for D193 — BLOCKED on a reference oracle.** M-80 audited the anim path +end to end: frame clock (ruled out, measured), all model/chr scalars (nominal), +`ModelSkeleton` (ruled out — static C), anim-data endianness / D33 fixup (audited, +complete for this path — walk/run records are in `animation_table_ptrs1`, no +double-swap, no fixup errors, `sizeof` matches both platforms). Anim decode is +structurally sound on static review — a magnitude error, if any, needs the N64 +side to see. **Two paths (fresh session — no MIPS toolchain / emulator here):** +(A) set up mips-linux-gnu + IDO-recomp + an N64 emulator w/ osSyncPrintf capture, +build ROM with `#ifndef PORT` logging matching `GE_D193B`, diff same scene; +(B) user records N64 footage of one guard over a measured distance, frame-count +→ port-vs-N64 speed ratio (clean 2x = per-frame/per-tick doubling; fractional = +scale/data error). **Recommend (B) first.** + +Residual (unobserved) frame-clock risk: no remainder carry in the per-frame round +— a steady 45–55 fps scene would under-advance; only bites in the 20–30 fps band, +which the port never enters. Parked. + +## Milestone (2026-09-04, M-50) — v0.1.0 SHIPPED + +**v0.1.0 alpha is published** (GitHub release, tag `v0.1.0`, marked +pre-release by the user's choice — no green "Latest" badge is expected). +Both bundles + `.sha256` attached, smoke-tested. Cut off the M-49 state: +full-campaign playtest, **18/21 missions completable** — Bunker ii + Statue +crash (D191); Cradle can't be finished (D193 scripted-sequence stall, not a +crash). PR #25 (docs reframe + hygiene) merged. + +**Open / owed after the release:** +- **PR #26** — M-49/M-50 doc-accuracy corrections (18/21, Cradle=D193, + D149 flagged stale, CI `` substitution fix). Needs merge. The + published release body still shows `19 of 21` / literal `` — + edit it to match (corrected full text was prepped in this session's + scratchpad). +- **social-preview.png** — regenerate from `.github/social-preview.html` + (updated) + re-upload via Settings → Social preview. Script in + `.github/social-preview.md`. +- **README hero screenshots** — user has `boris` / `archives` shots (in + `docs/img/screenshots/`, gitignored); need resize/crop + move into the + tracked `docs/img/`, then wire into the README hero row + release body. + +**Now targeting 1.0.** Plan of record: `docs/dev/notes/ROADMAP-1.0.md` +(local-only; dependency order: **B1** timing keystone → **B2** D191 crash → +**B3** audio ∥ → **B4** GE_DETERM → **B5** cutscenes → **B6** visual wave → +**B7** input). **Next dev milestone M-50/51:** +`docs/dev/notes/AGENTIC-SPEEDUPS-PLAN.md` Steps 1/2/4/5 (verify.sh, +context-tax, crash→brief) + get the D191 gdb backtrace and fix it → 21/21 +crash-free → **v0.2.0**. Then B1 (instrument +`g_GlobalTimerDelta`/`g_ClockTimer`, root-cause D193). + +Local working notes live in `docs/dev/notes/` (gitignored) alongside this +file and `docs/BACKLOG.md`. + +## Direction + +**Post-v0.1.0: driving to a playable 1.0.** See the Milestone block above and +`docs/dev/notes/ROADMAP-1.0.md`. The earlier "breadth-first, sweep all 21 +levels for load+render+no-crash" pivot (M-11 → M-30) is **done** — all 21 +load and render, 18/21 completable end to end. + +## Diagnostic env probes + +All `GE_*` env-var probes in the tree are catalogued in +**`docs/GE-ENV-PROBES.md`** (var → file:line → what it does → live/dead). +Live tooling: `GE_PCDUMP`, `GE_INPUTSCRIPT`, `GE_STARTMENU`, `GE_UNLOCK_ALL`, +`GE_INPUTLOG`, `GE_SAVELOG`, `GE_D160`, `GE_DTEX`, `GE_TEXDUMP`. The rest are +closed-finding `GE_Dxx` scaffolding (strip candidates; all env-gated and inert, +except `bg.c`'s `GE_D63` lines which are not `#ifdef PORT`-guarded). + +## Playtest bug reports (M-49 cont., 2026-09-04) — logged for future triage, NOT v0.1.0 blockers + +User playtested the **v0.1.0 win64 bundle** (`C:\Users\james\Games\goldeneye-pc-port-0.1.0-win64`, +fresh save). Bugs filed in findings §F (D191–D197), all alpha-acceptable. +NB: Cradle does **not** crash — it's the D193 scripted-sequence stall (an +earlier note here wrongly said "Cradle also crashes"). + +- **D191 — SIGSEGV in `modelGetNodeRwData` (`model.c:478`), the `while (root->Parent)` + node-tree walk. TWO instances, identical fault PC `0x1400801b1`:** (1) killed the + first guard after escaping the cell in Bunker ii; (2) Statue, right after the + Trevelyan-meeting cutscene. Instance 1 `Rdx = 0x70267bc0<<32` (32-bit ptr in the + high half); instance 2 `Rdx = 0`, FAULT ADDR `0x0` (NULL). Both `R8 = 0x1d`. A + model's `Parent` chain has a truncated/NULL link → pointer-width / byte-swap + family (D119/D122/D3x), NOT weapon-fire-specific. Crash logs saved to + `scratchpad/crashlogs/`. Shipped exe has full symbols; `debug-crash.ps1` in the + bundle dir runs it under gdb + `GE_D51=1` (→ `d52rw.log`). **Awaiting a real `bt` + + `p *root`/`p *Objinst`** — the crash.c EBP walk broke (`#01: 0x1d`). No fix. +- **D192 — mission-select grid pointer can't reach outer cells, `MouseCaptureMode=0` only.** + D169's "FIXED" clamp routes through `getPlayer_c_screenwidth()` = + `g_CurrentPlayer->c_screenwidth`, which is the ~320×240 stage viewport in the + front end, not the real 440×330 (`front.c:8570`). Relative P-controller path + (`input.c:659-690`) winds up short of the grid's 317/235.5 outer split points. + Default `MouseCaptureMode=1` (absolute cursor, `input.c:629-657`) feels fine. + Fix is port-only + small: use the real front-end rect (`[20,420]×[20,310]`) in + the `menuMode` branch. Confirm with `GE_INPUTLOG=1` in mission-select. +- **D193 — NPC AI locomotion too slow. ⇦ user's "biggest overall flaw" after the + full playthrough.** NOT mainly an anim bug — scripted/AI characters (guards, + Trevelyan, Ourumov, Natalya) *travel* to their destinations slower than N64. + Leading hypothesis: the D117/D134/D155/D156 wall-clock timing family — + `g_GlobalTimerDelta`/`g_ClockTimer` (which scale every `pos += speed*dt` and + `while(iParent` node-tree walk — `root` arrives at `modelGetNodeRwData` +*already* garbage (`0x70267a9000000000` = the `u32` `0x70267a90` with its +halves transposed), passed straight in by frame #1 +`bondviewSelectCuff(model, header, switchindex=29)` <- `gunUpdateAndFire(GUNRIGHT)` +(runs on the frame the player fires). `bondviewSelectCuff` byte-indexes +`header->Switches` (a `ModelNode *` array — 8-byte stride on PC, widened per +D43/D45) with `offset = switchindex << 2` (N64 4-byte stride); every `base[N]` +deref then lands mid-slot. **Identical to D141** (`gunfire.c` had the same +`Switches` byte-index bug, fixed the same way; this call site was missed). +Fix: PORT-only `offset = switchindex * (s32) sizeof(ModelNode *)`; the +downstream `base = (u8*)switches + offset` math and the `#else` N64 line are +untouched. The Statue instance (`Rdx=0`, NULL) is the same site landing on a +zero-reading slot. + +**Verified:** build clean; `-level_09` (bunker1) golden framediff 3/3; +`-level_27` (bunker2) + a scripted-fire `GE_INPUTSCRIPT` run crash-free. +**PLAYTEST-VERIFIED (M-79):** user replayed Bunker ii (first-guard kill) and +Statue (post-Trevelyan cutscene) on the fix build — **neither crashes**. That's +**21/21 solo levels crash-free → v0.2.0 gate met** (Cradle's remaining problem +is D193 timing, not a crash). Fix build staged in the bundle dir +(`ge007.x86_64.exe.D191FIX` / `playtest-D191.bat`; `restore-v0.1.0.bat` reverts). +(A headless scripted-fire repro of the *pre-fix* crash was inconclusive — the +quick `-level_27` boot doesn't set up the fresh-save Bond model state the +crash needs — so the human replay is the real verification, same as D206/D208.) + +Findings §F D191 updated; porting-notes.md §B (D141 entry) extended; ROADMAP B2 +is now done pending playtest. + +## Done this session (M-78) — feat/phase3-audio MERGED to main (PR #28); D205 CLOSED; D207 probe re-armed for a real-level repro. + +**PR #28 merged** (16-commit Phase 3 audio branch: software mixer D198–D206, +D202 closed, D204 tempo, D206 bank-index). CI green, fast-forward, branch +deleted. + +**D205 CLOSED (M-78).** User re-tested the explosion→scream symptom on Bunker +post-merge: the explosion SFX is correct by ear now. All four D205 symptoms +were D206 (the +1 bank-slot alias); the earlier "scream" was a coincident +guard death-yelp (faithful). No presentation-layer bug — M-74's pan audit +already showed GE never spatially pans SFX. + +**D208 FOUND + FIXED (M-78c) — player fire sound goes permanently silent +mid-firefight.** This is the "player gunshots stop and stay muted after the +alarm ends" half of the user's D207 Bunker repro — a *distinct* bug from +D207's voice-pool question. `bondview.h:200 s32 field_A48` is used as a +second `ALSoundState *` handle slot for the player's double-buffered per-hand +fire sound (`gunfire.c:3186-3200`); `sndPlaySfx` stores an 8-byte pointer +through `&field_A48` (`pendingState->link.next = nextState`) which, at `s32` +width, tears into `field_A4C` (the per-frame auto-fire cadence timer). After +enough double-buffered cycles `field_A48` is a non-0 non-pointer → the +`else if (field_A48 == 0)` re-arm gate never fires again → no further player +gunshot. Proven in the user's `audiotrace.log`: silenced-PP7 idx-46 requests +stop at line 18930/25086 while everything else continues; slot `0x70076630` +caught holding torn `0x000001db00000000`. Fix: `#ifdef PORT` → `field_A48` is +`ALSoundState *` (runtime-only struct, layout-safe). Build clean. +**Runtime verification owed:** user replays the Bunker firefight, idx-46 +`sndPlaySfx` should continue the whole time. + +**D207 — still needs a real-level repro; probe re-armed.** +The user's Bunker capture showed the voice pool never above 7/8 and the SFX +event queue peaking ~12/64 — so D207's "(a) heavy-level 8/8 starvation" is +still unproven. Stays MONITORING pending a Facility/Silo/Statue capture. Re-added just the +`[D207-DROP]` counter (NOT the reverted last-resort preemption pass) at both +`sndDisposeSound` no-voice sites in `src/snd.c`, gated on `GE_AUDIOTRACE`, +logging `state`/`prio`/`flags`/`count=N/max`. Static re-check of `ALARM3` +slot 162: `sampleVolume=80` (quieter than gunfire's 90–110 — weakens the +"mixed too loud" theory); `keyMax=8` → NOT `RETRIGGER` → continuous looped by +design on N64 too. `build-pc/d207_run.ps1` = forced-alarm auto-repro on +level_09. **Blocked on the user:** which level + weapon/situation they hit +"alarm overrides everything / doesn't recover", and a `GE_AUDIOTRACE=1` +capture there. Then: `[D207-DROP]>0` + `count` pegged at 8 → (a) heavy-level +starvation → re-apply the reverted last-resort pass; alarm voice never freed +after alarm-off → (b) leak on a stop path; drops=0 but still wrong → (c) +perceptual, compare drone loudness/loop against N64. + +**Still to do after D207 closes:** prune the D202 probe set — `GE_AUDIOTRACE` +(`src/snd.c` ×many, `src/game/propobj.c`, `src/libultra/audio/*`, +`src/game/mp_music.c`), `[VOL]`, `[DISTVOL]`, `[D207-DROP]`, `GE_FORCEALARM`, +`#include "audiotrace.h"`. Keep the D206 `#ifndef PORT`/`#else` `-1`. + +## Done previous session (M-77) — D206 validated by playtest; D207 investigated (fix tried + reverted). — **then prepped feat/phase3-audio for merge (M-77c).** + +**MERGE PREP (M-77c):** user by-ear A/B vs N64 confirmed D206 (armour / melee / +silenced PP7 all correct) → **D202 CLOSED**, D205 narrowed to symptom (2), +D207 → MONITORING (acceptable in play). Commits `378386d1` (D206 fix), +`04a539ba` (GE_FORCEALARM probe), `c25285d8` (doc status), `d56c15d9` +(GE-ENV-PROBES row — fixes the `validate` CI job). All pushed. +**PR #28** retitled + body rewritten to cover the full Phase-3 audio scope +(D198–D206, D202 closed); `validate` now green; waiting on Win/Linux build +checks. `mergeStateStatus` BLOCKED only = CI in progress; no branch protection, +no required review → user can merge once green. Local verify: `-level_09` +framediff 3/3 + crash-free, `-level_20` crash-free. + +**After merge:** delete `feat/phase3-audio`; next audio work = D205 (explosion +→scream) and, if it recurs, D207 on a heavy level. Prune the removable D202 +probe set (`GE_AUDIOTRACE`/`[VOL]`/`[DISTVOL]`/`GE_VOICEDUMP`/…; keep `[EXPIRE]` +and `GE_D204`) + `GE_FORCEALARM` once D205/D207 close. + +--- + +## Original M-77 notes + +**D206 (every SFX one bank slot too high) — FIXED and PLAYTEST-CONFIRMED.** +`struct ALInstrumentAlt_s.soundArray` (`src/snd.h:165`) is at struct offset +**12** on N64 (3×s32, 4-byte ptrs) but **16** on PC (8-byte ptr + alignment). +N64's offset-12 array aliases the on-disk `bendRange`/`soundCount` words so +`soundArray[N]` == on-disk entry `[N-1]` — GE's `SFX_ID`s are 1-based by +design. Fix: `src/snd.c:1001`, `#ifndef PORT`/`#else` → `soundArray[soundIndex +- 1]`, N64 line verbatim (D3x ABI/layout class). Build clean; BUNKER1 +`GE_AUDIOTRACE` 60s crash-free, all ~90 distinct indices resolve to +`rom_sfx_decode.py` slot N-1; `-level_09` golden framediff 3/3 ×2. **User +playtested: "most audio is good now."** Root-causes D202's silenced-PPK "slap" +and D205 (3)/(4). NOT D205 (2). + +**D207 (alarm klaxon "overrides everything, doesn't recover") — INVESTIGATED, +NOT SOLVED.** D206 correctly turned `ALARM3_SFX` from a wrong one-shot into the +real infinite-loop klaxon (slot 162). Hypothesis was: it permanently holds 1 of +8 SFX voices → combat SFX starve. Built the designed fix (last-resort +LOOPED-voice preemption in the `snd.c` scan) + instrumented the real drop +sites. **Result: it doesn't hold up.** BUNKER1 `GE_FORCEALARM` + scripted-fire +soak: **0 true drops**, the new pass never fired, voice count recovered to 0 +after every alarm-off (no leak). The "~20% drop" from the earlier session was a +log-latency measurement artifact. **The fix was reverted** — `snd.c` carries +only the D206 `-1` now. BUNKER1 combat is too light to peg the 8-voice pool, so +either it starves only on a heavy level, or the symptom is a different +mechanism (leading guess: perceptual — the klaxon is a *gapless continuous* +loop where a real GE alarm pulses, and/or slot-162 is mixed louder than N64). +Full write-up: `findings.md` §F D207 + detail (M-77b section). + +**Temp probe left in tree (intentionally, for the D207 follow-up):** +`GE_FORCEALARM` in `handle_alarm_gas_timer_calldamage` (`src/game/propobj.c`, +`#ifdef PORT` / `getenv`, inert unless the env var is set). + +**Uncommitted:** `src/snd.c` (D206 `-1` fix only), `src/game/propobj.c` +(`GE_FORCEALARM` probe), `docs/dev/findings.md` (D205 M-73/M-74 + D206 + D207), +`docs/porting-notes.md` (§E), `docs/HANDOFF.md`. Nothing committed. + +### Next session, in order: + +1. **D206 sign-off** — user by-ear A/B vs N64 (armour pickup, unarmed melee + whiff, silenced PP7). If clean: D202 **closes outright**, D205 shrinks to + symptom (2) alone. +2. **Commit D206** + the pending D205/M-73/M-74 doc changes (independent of + D207, and playtest-confirmed). +3. **D207 needs a real repro from the user** before any code — ask which + **level + weapon/situation**, and get a `GE_AUDIOTRACE=1` capture (or clip) + of the failing alarm. Then decide between: (a) genuine 8/8 starvation on a + heavy level → re-apply the reverted last-resort pass (design is in + `findings.md` D207 detail); (b) a leak on a specific alarm-stop path; (c) + perceptual — compare slot-162 playback volume + whether it should pulse + (`SOUND_FLAG_RETRIGGER`) against N64. Soak Facility/Silo/Statue with + `GE_FORCEALARM` + sustained fire watching `[D207-DROP]` / `count=8` if no + user capture. +4. **Remove temp probes** once D207 is resolved: `GE_FORCEALARM` + (`src/game/propobj.c`); the D202 set — `GE_AUDIOTRACE` (`src/snd.c` + ×several), `[VOL]` (`src/snd.c:556`), `[DISTVOL]` + (`src/game/propobj.c:12780`) + `#include "audiotrace.h"`. Keep the D206 + `#ifndef PORT`/`#else` `-1`. +5. `docs/dev/notes/D206-IMPL-BRIEF.md` can be deleted (superseded). + + +## Prior session (M-74b/M-75) — D206 found + reframed. (Fix-direction claims below are SUPERSEDED by M-76 above; the evidence still stands.) + +The user supplied a verified sound rip (`~/Downloads/Nintendo 64 - GoldenEye 007 - Miscellaneous - Sounds`, +`B00I00S.wav`, 186 files — **filenames are HEX bank indices**) and +identified three sounds by ear. That broke the case open. + +**Proven:** rip index == bank index (186/186 exact PCM frame-count matches vs +`rom_sfx_decode.py`; 0/185 at ±1). Correct armour = `S50` = bank 80, what the +port plays = `S51` = bank 81 — and the game **requests 81**. The actual Klobb +sound = `S69` = bank **105** — the exact index the game requests for a punch +whiff (`PUNCHING_AIR_SFX`). Both off by **+1**. + +**Structural confirmation:** ROM bank `soundCount = 261` (valid 0..260), but +`SFX_ID` has **262** members starting with the `NOTHING_SFX = 0` sentinel that +`snd.c:992` short-circuits. 261 real IDs (1..261) fit 261 slots (0..260) only +with a `-1`. And `BIG_CLANK_SFX` = 261 currently reads **one past the end**. +`src/snd.c:1001` is unmodified decomp: `soundArray[soundIndex]`. + +**D202 corroboration:** `scratchpad/B00I00S2D.wav` — the reference clip that +drove D202 from M-60 to M-67, provenance "found online" — is a file from this +same rip. `S2D` = bank **45**, and `wppksil_stats.Sound` = 0x2E = **46**. +**M-61's rejected "index 45 matches at +0.999" was right all along**, and the +"slap" on every silenced PPK shot is `bank[46]` = `PUNCH1_SFX`. + +**Why eight sessions missed it — read `porting-notes.md` §E:** the verification +tooling shares the bug. `exact_match.py` compares a runtime voice to a ROM +decode *of the requested index* with the same missing `-1`; `diff_bank.py` +walks both layouts with identical indexing. M-68/M-70 proved self-consistency, +never correctness. + +**Explains:** D202 silenced-PPK slap ✔ · D205 (4) melee→Klobb ✔ user-confirmed · +D205 (3) armour ✔ user-confirmed · D205 (1) slap/glass over gunfire (consistent, +unconfirmed). **NOT explained: D205 (2) explosion→scream** — 169-183 maps to +168-182, all still explosions; stays with D205, M-73's blast-killed-guards +reading is the likely answer. + +### Next session, in order (M-75 — supersedes M-74b's list): +1. **Work the `docs/dev/notes/D206-IMPL-BRIEF.md` decision gate, candidates + A→C→B (cheapest first).** (A) is the rip keyed by `SFX_ID` not by 0-based + bank slot? — re-audit `rom_sfx_decode.py`'s `sound_offsets` walk vs the + runtime `soundArray`; if yes there is **no engine bug**. (C) does + `GE_AUDIOTRACE` log `soundIndex=81` (not 82) for an armour pickup? (B) + byte-compare PC runtime `soundArray[81]` (`GE_AUDIOTRACE` probe at + `snd.c:1002`) to `rom_sfx_decode.py` bank 80 vs 81 → if PC index 81 == ROM + bank 80, converter (`port/src/romdata.c`) dropped an entry. +2. **Only once a locus is proven with a byte-level artifact**, apply the fix + there. If it lands on (D) genuine data-side (+1 in ROM constants), **stop + and escalate to the user** — not a legal port edit. +3. **Re-verify.** Fix `exact_match.py` / `diff_bank.py` to the agreed + convention first (they pass either way otherwise). Re-run the M-72 + controlled captures: silenced-PPK → `bank[45]` (rip `S2D`), punch whiff → + `bank[104]`, armour → `bank[80]` (`S50`). +4. **User by-ear pass** on melee / armour / silenced PPK vs N64. +5. Then the D202 probe removal + close listed below. + +## Done this session (M-74) — D205: M-73's root cause WITHDRAWN. **Its closing conclusion ("the PC side is exhausted; the N64 A/B is gating") is SUPERSEDED by M-74b/D206 above — the bug was in the index mapping all along. The M-73 withdrawal and the pan audit below both still stand.** Re-counting the same capture shows the "idx 109 barrage" is normal in-spec guard combat; the one untested layer left is spatial presentation (pan/volume). **Next session starts here.** + +**M-74 (findings.md D205 + §F row + porting-notes §E).** M-73 concluded that +guards were stuck re-triggering fire sound idx 109 ("256x over 29.7 s, no +bullets") and pinned it on `stanTestLineUnobstructed` guard→Bond LOS over +converted collision geometry, then queued a full pccg-stan decode + ray-replay +investigation. **Re-counting the same file (`build-pc/audiotrace.log`) breaks +every load-bearing number:** + +- idx 109 is requested **128×**, not 256 — the `GE_AUDIOTRACE` double-log + artifact **D204 already corrected for M-63 on this exact sound index**. + Count `sndPlaySfx: t=` lines, never bare `soundIndex=` matches. +- 128 / 29.73 s = **4.31/s aggregate** across ~11 guards ≈ **0.4/s each** — + about **6× UNDER** the AK47's own `SoundTriggerRate` (`RATE_AK47` = 4 + `g_GlobalTimer` ticks ⇒ 15/s per guard ceiling). The `field_178` gate that + M-73 accused never binds. +- "No bullets/impacts" is false: the same window holds ~30 ricochet/wall-hit + requests (19–41), 69 ×2 flesh, 12 body-falls (123–132), 14 guard yelps + (134–147). Guards were firing *and* hitting. +- The striking monotone sweeps 134→147 and 123→132 are the ground-truth + round-robin `male_guard_yelp_counter` (`chraction.c:2454-2470`) — faithful + cycling in byte-matched code, not a broken `random() % n`. + +**Do NOT open the pccg-stan / LOS geometry investigation on this evidence.** +The only open observation left from that capture is that the firefight lasted +a while, which is a gameplay-fidelity question for an N64 A/B. + +**Where D205 stands.** Everything upstream of presentation is proven: bank +conversion 261/261 identical (M-71), sample+pitch exact (M-68/M-70), and every +requested index in the capture correct (182 explosion ×1, 81 armor ×1, +46/47/48/49/105). That leaves **one untested layer: spatial presentation — +per-voice volume, pan, concurrency.** It explains all four complaints in one +shape (a wrongly-placed sound is, by ear, a sound at your own position: "a gun +sound came out of my punch", "the explosion screamed", "a slap over my +gunshot"), and it is the youngest code in the stack — `sndCreatePostEvent` was +stubbed out entirely by D138 until M-65 un-stubbed it, and has never been A/B'd. + +**Ranked:** H-A pan/spatialisation wrong or absent in the mix (top) · H-B +distance law right but scale wrong (`[DISTVOL]` follows the documented curve +yet only reaches 0.60 of max at dist 1794; never compared to N64) · H-C +voice-pool masking (weak — M-70 measured alloc ≤ 5/8) · H-D PC guards +genuinely engage where N64 guards don't (possible, but now with *no* +supporting evidence; gated behind the N64 A/B). + +## Previous session (M-73) — D205: proposed a guard LOS/collision root cause. **WITHDRAWN by M-74 above — the counts it rests on do not survive re-derivation. Kept for history; do not act on its next-steps.** + +**M-73 (findings.md D205 + §H):** analysed the user's full-combat +`GE_AUDIOTRACE=1` capture (`build-pc/audiotrace.log`, 288 unique requests). +**idx 109 (`GUN_B4_BOLTACTION_SFX`, AK47/Spectre fire sound) is requested +256× over 29.7 s**, from ~11 distinct NPC state slots, each on its own fixed +period (median gaps 200/434/499/698/934/1299 ms — multiples of ~200 ms). +Only ~32 non-109 requests exist in the window (~0.6/s) → **guards re-trigger +weapon fire sounds WITHOUT firing bullets** (no impact SFX at 10×/s). Code: +`chrlvFireWeaponRelated` (`chraction.c:6530`) passes `phi_a2 = sp27C || +sp278` to `sub_GAME_7F02BFE4` (:6902) — non-bullet auto ticks (sp278) still +play the weapon sound, gated only by `SoundTriggerRate`/`field_178`; so +sustained ACT_ATTACK = sustained roar. Sustained attack needs +`seen_bond_time >= g_GlobalTimer - 100` (:6593), refreshed by +`setSeenBondTimeToNow` behind `stanTestLineUnobstructed(...OBJS|DOORS|CHRS| +PATHBLOCKER|AIOPAQUE...)` LOS queries (:3917/:3972, `chrCanSeeBond` :3945). +**Leading suspect: converted collision geometry makes guard→Bond LOS return +clear where N64 blocks → guards never lose their target.** Per symptom: +(1) "Klobb on melee" — 106 was NOT requested anywhere in this capture, and the user WAS punching in it (47/48/49 ×13, 105 ×19, all voiced); the only gun sound present during those punches was the 109 barrage. OPEN: is the heard "Klobb" a 109, or a real 106 from another session? (Klobb = specific SMG = Skorpion, idx 106 — user's N64 A/B identification stands; do not assert misidentification.) Decisive: ROM samples 106 vs 109 as WAVs for the user + targeted punch capture. +(2) "explosion→scream" — 182 requested once (correct); GET_HIT_MALE +(137–139) ±1 s around it, all under the barrage. (3) "armor wrong" — 81 +requested once (correct!), bracketed by 109 ticks. Same phenomenon as D202 +M-63's unexplained idx-109×94/60 s anomaly. + +## Done this session (M-72) — D205: fourth symptom added (unarmed melee → Klobb shot idx 106); controlled `GE_AUDIOTRACE` runs exonerate every request path; mechanism (b) extra-events now the leading suspect. + +**M-72 (findings.md D205 + §H):** environment fix first — the "flaky /GS +crash" was `SDL2.dll` missing from PATH (`C:\msys64\mingw64\bin`; exe is +`-mwindows`, so a DLL-load failure just dies 0xC0000105, no console). Launch +via `build-pc/d205_*.ps1` (gitignored) which prepends it. Controlled traces: +(1) PPK fire at `-level_09`: per shot exactly {46 GUN_SILPPK, one of the +20-entry `ricochet_sounds_small` table (gun.c:415 — `rnd1 % 20` is IN RANGE), +122 CART_SPENT} — no glass/slap layer. (2) Levels 01–08 all start unarmed; +fire-press requests ONLY 105 PUNCHING_AIR (whiff). (3) Static: punch hit = +constant {47,48,49} (gunfire.c:2338); idx 106 is data-only +(`skorpion_stats.Sound = 0x6A` via `bondwalkItemGetSound`); the player +fire-sound path (gunfire.c:3192–3200) needs `GUN_ANIM_STATE_FIRE`, which a +fist never enters — so Bond's own punch CANNOT request 106; an NPC guard +firing its Skorpion (`chraction.c:5588`) can. **No symptom reproduces in +micro-scenarios → the layering must come from extra/different EVENTS during +real gameplay (mechanism b).** + +### Previous session (M-70 + M-71) — D202 data-closed AND by-ear-closed: probe removal + close is all that remains. NEW ACTIVE TASK: **D205** — PC requests wrong/extra SFX indices vs N64 (explosion→scream, armor pickup wrong sample, slap/glass over gunfire); bank converter + playback chain exonerated. + +**M-70 (findings.md):** decisive clean re-capture, 209 scripted `-level_09` +requests → every voiced request plays the exact ROM sample at the exact ROM +pitch (200/209 auto CC ≥ 0.85; the 4 MID are idx=109 verified 0.946 by +manual ratio sweep; the 5 NO-WIRE = 1 matcher window artifact + 4 +never-voiced requests, NOT pool exhaustion — alloc ≤ 5/8 at each drop). +Zero wrong-sample playback. Matcher v3 fixes in `scratchpad/exact_match.py`: +death-clip both sides, slide() sparse-tail bug (step-1 over 0..2205, +step-4 across the span, ±8 refine), PLAY window 600 ms → 2 s. Output: +`scratchpad/exact_run6_match.out`. Trace files in `build-pc/` +(`audiotrace.log`, `audiotrace_wire.log`, `voicedump.raw`). + +**M-71 (findings.md):** user by-ear pass on M-66b = **PASS** ("the metal +door sound does not loop forever now") — Disposition C validated; D202's +only remaining step is probe removal + close. The user then reported a NEW +complaint class, N64-A/B confirmed non-faithful: (1) slap/glass layered over +general gunfire (audible on PC, silent on N64), (2) explosion plays a +soldier-scream sample, (3) armor pickup wrong sample. Split out as **D205**. +This session exonerated the two remaining static suspects: the D37 bank +converter offline (`scratchpad/banktest/`: real `romdataFixupAudioBank` +harness + `diff_bank.py` → 261/261 entries identical N64 vs PC, 0 +mismatches) and the D154/D135 GBI raycast parsers + volume law (static +re-audit, ABI-correct). So D205 = **PC requests different or extra sound +indices than N64** — converted level/setup data (sndID / explosion-type / +item-sound fields in pccg sidecars) or extra hit events from converted +collision geometry. Full ranked next-steps in findings.md D205. + +### Next session, in order (M-74 updated — M-73's list is withdrawn): +1. ~~D205 step 1, static audit of the presentation layer~~ — **DONE this + session. H-A FALSIFIED.** The port's stereo path is correct end to end + (`snd.c:461/530` → `alSynSetPan` → `env.c:400-402` eqpower L/R targets → + `port/src/mixer.c` `aEnvMixerImpl` independent `dry[0]`/`dry[1]` gains), + **but GE never uses it**: `AL_SNDP_PAN_EVT` has no poster anywhere in the + tree, and 252 of 261 bank sounds have `samplePan == 64` (`AL_PAN_CENTER`) — + every index in the user's capture included. **GE does not spatially pan SFX + on either platform.** Bank `panvol` re-diffed 261/261 clean (closing the one + field-coverage gap in M-71's prose). Volume formula has no PC-side overflow + divergence. +2. ~~D205 step 2, pan probe run~~ — **cancelled: pan cannot vary, by design.** +3. **The PC side of D205 is exhausted.** Bank, playback, requested indices, + sample+pitch, pan and per-sound volume are all verified faithful. GE's only + spatial cue is distance volume, and it is shallow by design (0.60 of max + still at dist 1794) — so a guard firing across the room plays near-full + volume, dead centre, at a per-sample volume comparable to the player's own + punches (109 = 90; 47/48 = 100; 49/105 = 110). That is faithful *and* it is + exactly the condition that produces all four complaints. **Only H-D remains + — does the PC generate MORE overlapping combat events than the N64 — and + nothing on the PC alone can settle it.** +4. **⇒ The N64 A/B is now the gating step (user-side, owed).** Batch it: + (a) ROM samples 106 (Skorpion / "Klobb") vs 109 (AK47) as WAVs via + `scratchpad/rom_sfx_decode.py` for the user to settle the Klobb + identification by ear — their identification stands either way, this decides + *which* sound, not whether they heard it; (b) N64 A/B: punch a guard while + others fire — does the same combat sustain, and does the same layering + happen, on N64? If N64 sounds the same, D205's four symptoms are faithful + and the finding closes as NOT-A-BUG. If it does not, the difference is + gameplay-side (guard engagement), and *that* is when the collision/LOS + geometry work becomes justified. +5. **Close D202:** remove the probe set — `[VOL]` (`src/snd.c:556`), + `[DISTVOL]` (`src/game/propobj.c:12780`) + its `#include "audiotrace.h"`, + and the rest listed in GE-ENV-PROBES.md (`[WIRE]`, `[EVT]`/`[STOP-EVT]`, + `[VOICE±]`, `[SLOTWRITE]`, `[ENVELOPE]`, `[VOICES]`, `[RETRIGGER-POST]`, + `[WAVELOOP]`, `[DOORSND]`, `[VOICEMAP]`, `[MISSIONSTATE]`, `[MUSICNOTE]`, + `[EVTQ-DROP]`, `[PASTEND]`). **KEEP `[EXPIRE]`** — part of the M-66b fix, + not a probe. Keep `[VOL]`/`[DISTVOL]` until D205 step 2 is done. Trim + GE-ENV-PROBES.md; set the D202 index status to CLOSED; check README/AGENTS + phase-status lines for "D202 open" wording. +6. Build + smoke after probe removal: `./build-pc.sh ntsc-final`, then + `-level_09` 60 s crash-free with `GE_AUDIOTRACE=1`. +### Environment (unchanged except noted): +- Build: `/c/msys64/usr/bin/bash.exe -lc 'cd /c/Users/james/Source/Repos/gh-fullhist && ./build-pc.sh ntsc-final'` (login shell for MINGW64 PATH; SDL2.dll at `/c/msys64/mingw64/bin`). +- Capture run: `GE_AUDIOTRACE=1 GE_INPUTSCRIPT="$(cat ../scratchpad/exact_run3_script.txt)" ./ge007.x86_64.exe -level_09` from `build-pc/`; kill with `taskkill //F //PID ` (msys `kill` does not work). +- `mupen64/` (N64 emulator, user's A/B reference) at repo root — added to .gitignore this session. +- `scratchpad/` is gitignored: banktest/, exact_match.py, fire scripts, match outputs are all local-only. To re-verify the bank exoneration: `scratchpad/banktest/harness.exe` (rebuild from harness.c against port/src/romdata.c if stale) then `python scratchpad/banktest/diff_bank.py`. +- Matcher constants: `DEC_BASE=0x706d64c0`, PV stride 0x158, `ENV_BASE=0x706d8540`, env stride 0xb0; pitch = 2^((keyBase*100+detune-6000)/1200); WIRE lines are in `audiotrace_wire.log`, NOT audiotrace.log; state addresses print as zero-padded 16-hex without `0x`. +- Uncommitted at handoff time (all probe/docs, nothing behavioral): findings.md (M-71 + D205), HANDOFF.md, GE-ENV-PROBES.md, .gitignore, src/snd.c (`[VOL]`), src/game/propobj.c (`[DISTVOL]`), plus the earlier-session probe set still in place (port/src/libultra.c geTracePrintf, port/src/romdata.c, src/game/mp_music.c `[MISSIONSTATE]`/`[MUSICNOTE]`, src/libultra/audio/{csplayer,event,load}.c) and untracked `port/include/audiotrace.h` — commit them together with this handoff. + +## Done this session (M-69) — D202 full-corpus exact match: 320 scripted-run SFX requests matched per-voice; 286/320 OK, no corruption found. + +Full write-up: findings.md D202 §M-69. `scratchpad/exact_match.py` (with +`rom_sfx_decode.py` / `m63_crosscheck.py`) matches every `sndPlaySfx` request +in a full trace (`scratchpad/exact_run/`: scripted `-level_09`, 61 input +entries, `GE_AUDIOTRACE` + WIRE + `GE_VOICEDUMP`) to its voice's voicedump +stream and slides the ROM-decoded PCM against it. + +- **286/320 OK (CC ≥ 0.85).** The first run's 44 "BAD" were tooling, not audio: + (1) `GE_VOICEDUMP` µs stamps are block-**END** times (records render ahead — + per-sample record time makes sound 29's onset land +263.8 ms vs WIRE +264 + ms, exact); (2) `slide()` needed step-1 refinement around the coarse best hit + (sharp onsets: CC 0.590 → 0.970 one sample later); (3) refs must be clipped + at voice death (`[EVT]` DEACTIVATE/STOP after PLAY — idx=109 then CC 0.999). +- **Remaining ~34 failures are not corruption:** requests with no `[VOICE+]` + (allocation dropped — e.g. t=567204976 idx=46; suspected 8-voice pool + exhaustion under rapid fire, N64 would drop them too) + a few pv10 + misattributions where consecutive requests share a wavetable and the wire + window catches the next request's wire. +- Log-interleaving gotcha: AUDIOTRACE lines from multiple file handles corrupt + some lines; the matcher falls back to a wide wire window when the PLAY line + is unreadable. + +**Next (in order):** +1. Verify the dropped requests are pool-exhaustion drops: check + `[VOICES] allocated=N / max=8` in `exact_run/audiotrace.log` around each + "NO WIRE AT PLAY" timestamp; if the pool is full every time, confirm the + steal/drop path in `alSynAllocVoice` (`src/libultra/audio/synallocvoice.c`, + ~512-sample steal delay) is faithful and close that thread as N64-parity. +2. Tighten wire attribution: WIRE must fall between this request's PLAY and + the next PLAY on the same wavetable (kills the pv10 shared-wavetable cases). +3. Then M-68's remaining items: user by-ear confirmation + N64 A/B on the + door-close-loop duration (M-66b cap), remove the D202 probe set, close D202. + +**Committed:** all of the above is on branch **`feat/phase3-audio`** +(**PR #28**), 2 commits — `31935ad9` (probes: `GE_VOICEDUMP`, `[VOICEMAP]`, +timestamped `[WIRE]`/`[EVT]`/`[STOP-EVT]`, `-VoiceDump`) + `1ac92c7d` +(findings M-67/M-68/M-69 write-ups, GE-ENV-PROBES entries, AUDIO-PLAN status +banner). Build verified green (`ntsc-final`). **Do the next steps above on +`feat/phase3-audio`, not `main`.** + +## Done this session (M-66b) — D202 disposition C IMPLEMENTED + measured: ownerless infinite-loop SFX now expire on PC. Awaiting user by-ear pass, then probe removal + close. + +User chose **C** (extended deviation), as part of the PC port's audio +implementation. M-65's guard stays (slot reclamation under pool pressure); +this adds the audibility fix. Full write-up: findings.md D202 §M-66b. + +**What changed** (`src/snd.h` + `src/snd.c`, all `#ifdef PORT`, N64 zero diff): +new PC-only event `AL_SNDP_PORT_EXPIRE_EVT` (reuses the unused 1<<13 slot). +In PLAY_EVT, after a voice starts, if it is provably ownerless +(LOOPED + FINAL + not RETRIGGER + `state->state == NULL`) **and** its wave +carries an ADPCM loop with `count == -1`, post the expire at 2 s. Handler +re-validates the predicate, ramps volume to 0 over 500 ms, posts END_EVT. +Constants are in **microseconds** (ALMicroTime — the queue is µs-driven). +Not a reused STOP_EVT: its ramp uses the envelope releaseTime (~2.7 ms for +sound 203) and would hard-cut/click. Stale-event safe because +`sndDisposeSound` already removes ALL pending events for a state +(`sndRemoveEvents(..., 0xffff)`); owned loops and retrigger sounds are +untouched. Placed in snd.c (not `port/src/mixer.c`) because **ownership is +only visible in sndp** — the mixer has no ownership field, and scoping there +would force region-fragile DRAM pointer ranges or capping all infinite-loop +voices (music included). + +**Measured** (134 s headless `-level_09`, `GE_AUDIOTRACE=1 GE_AUDIODUMP=1`): +every sound-203 play posts `[EXPIRE]`; each leaked voice gets its VOICE- ~2 s +after its VOICE+; run ends `allocated=0 / max=8`. Per-1-s Goertzel at 256.7 Hz +(the strongest 203 line × 0.7278): **pre-fix user capture = flat at max +t≈39→361 s** (the complaint verbatim); **post-fix = transient ~2–4 s bursts +with full silence between** (gaps t=60–62, 85–87, 94–98, 110–112, 128–131). +Stuck infinite loop is gone; each close rings ~2 s + 0.5 s fade. Scripts + +archived user capture: `scratchpad/d202-m66/` (`check_dump.py`, +`line_track.py`, `user-capture-audiodump.raw`, `user-capture-audiotrace.log`). + +**Next (when the user gives the by-ear OK):** remove the D202 probe set +(`GE_AUDIOTRACE` additions, `[DOORSND]`, `[SLOTWRITE]`, `GE_PULLTRACE`, +`GE_DMEMWIPE`, `GE_NOWET`, `GE_BANKDUMP`, `GE_KEYMAPDUMP`) from +`src/snd.c`, `src/game/propobj.c`, `src/libultra/audio/load.c`, +`port/src/romdata.c`, `port/src/mixer.c`; update GE-ENV-PROBES.md; close D202. +The `[EXPIRE]` line and the guard/expire code STAY (permanent, documented). +Build note for this shell: toolchain at `/c/msys64/mingw64/bin`, and +`cmake --build -j` hit "Cannot create temporary file in C:\\Windows\\" — +`ninja -j1` with `TMP/TEMP=C:/Users/james/AppData/Local/Temp` works. + +## Done this session (M-66) — D202 ROOT CAUSE ESTABLISHED: the audible stuck door loop is sound 203 behaving exactly as ROM + ground-truth code specify — faithful N64 behaviour (original quirk), not a port bug. Disposition A/B/C pending user decision. + +The user capture M-65 asked for arrived (`build-pc/audiodump.raw` 361.7 s + +`build-pc/audiotrace.log`, `-level_09` start with the computer-room double +doors' attract intro). Full write-up: findings.md D202 §M-66. Chain, every +link grounded: + +1. ROM: sound 203 (`METAL_SLIDE_CLOSE_SFX`) wavetable loop `(2471, 7719, + count=-1)` = infinite (libultra `load.c`: "-1 is loop forever"); envelope + `decayTime=-1`. +2. Game code (byte-matched): `doorPlayCloseSound0/1` plays it fire-and-forget, + NULL owner; `[SLOTWRITE]` proves the state is never stored in any door slot + → nothing ever deactivates it except level exit. +3. sndp: preemption skips flag 0x12 (LOOPED|RETRIGGER); `AL_SNDP_DECAY_EVT` + skip stop-scheduling for looped sounds; `decayTime=-1` → priority 0x41, + unstealable by `_allocatePVoice` from any 0x40 sound. Pool: 24 PVoice + (`MUSIC_SYN_CONFIG_MAX_P_VOICES=0x18`) + GE soft limit 8. +4. Result: every metal-door close leaks a looping voice; during attract idle + nothing evicts it → endless door loop, exactly the user's report. All code + paths are shared original source → **identical on N64**. + +**327 ms mystery solved:** the voice plays at -550 cents (keyBase 54 + detune +50 − shift 6000 → ratio 0.72783); 5248 samples / (22050×0.72783) = 327.007 ms +vs observed 327.0±0.1 ms. Spectral lines match ×0.7278; pitch-stretched +template xcorr r=0.835 (unpitched was 0.10 — why all earlier template tests +"failed"). + +**Corrections to M-65 banked:** the "~1400 RMS drone is background music, not +a stuck sound" correction was wrong/incomplete (steady state = leaked 203 loop ++ music composite; the loop dominates the fingerprint); "stuck loop not +reproduced" is now superseded. Timeline corrected too: SFX do continue past +t=39 s — door …80a4 re-closes at t≈360.7 s (5th leaked voice); four leaked 203 +voices alive by t≈40 s; M-65's guard fired twice (t=14.8, 16.4 s) reclaiming +two under pool pressure. + +**Decision (user): C chosen** — extended deviation, cap/expire ownerless loops +to silence the idle loop. **Implemented + measured in M-66b above.** (A) pure +fidelity — remove the M-65 guard; (B) keep the guard only — were the other +options. Once the user's by-ear pass lands: remove the D202 probe set (see +GE-ENV-PROBES.md) and close D202. + +## Done this session (M-65) — D202 PARTIALLY resolved: voice leak root-caused + guarded, `sndCreatePostEvent` un-stubbed (distance attenuation restored). Audible stuck door loop still NOT reproduced. Session was cut by the usage limit AFTER the findings.md write landed — everything below is recorded in full in findings.md M-65. + +**1. Voice leak (the "eventual silence" cascade) — CONFIRMED, MEASURED, guarded.** +`soundIndex=203` (`METAL_SLIDE_CLOSE_SFX`) has `envelope->decayTime == -1` +(genuine ROM data — verified at the source-bank level with new `GE_BANKDUMP`/ +`GE_KEYMAPDUMP` probes) → `SOUND_FLAG_LOOPED` → never posts STOP_EVT, and the +preemption scan refuses to steal looped voices. `doorPlayCloseSound0/1` play it +with a `NULL` owner, so nothing can ever deactivate it. Measured on 115 s of +headless `-level_09`: **7 of 8 voices permanently held by sound 203**, leaving +one usable voice for the whole game — every later SFX gets a state but no +voice and is dropped. Fix: 4-line guard in the `src/snd.c` preemption scan +reclaiming only provably-ownerless loops (`LOOPED && !RETRIGGER && +state->state == NULL`). Measured: acquisitions 171 → 629, count no longer +pinned at 8, residual stuck voices 7 → 1. User confirms "less cascading". +**This is a rule-#2 game-code change justified by measurement, not ground +truth — kept flagged as a documented port-side deviation.** + +**2. `sndCreatePostEvent` un-stubbed (D138) — FIXED.** The D138 stub had been +silently removing ALL distance-based volume attenuation (every caller posts +`AL_SNDP_VOL_EVT`). Its O(n²)-watchdog premise expired when the Phase-3 audio +thread landed. Removed; re-measured clean (`rt=0.996`, drop 0, no watchdog +trips over 115 s). Likely a large part of the "wrong/too-loud/multiple sounds" +impression. + +**3. Audible endless door loop — still NOT reproduced.** Ruled out by +measurement (do not re-walk): wave-level looping (`loop==NULL` on every sound), +decoding past wave end (`[PASTEND]` 0 hits), stale/accumulating DMEM (full +per-frame wipe → unchanged), undamped reverb feedback (`GE_NOWET` → unchanged), +retrigger posts (0), door oscillation (~4 cycles/door/110 s, normal). +Corrections banked: the `romdataFixupMusicSeqTable ... capacity 1` error is +BENIGN (deliberate first call); the ~1400-RMS "drone" in AUDIODUMP captures is +background music (`csplayer.c:535`), not a stuck sound. + +**Next step (what the session ended asking for):** user capture at the failing +door (computer-room double doors, or a level start whose attract intro shows +them opening) with `GE_AUDIOTRACE=1 GE_AUDIODUMP=1 ./ge007.x86_64.exe -level_09`, +then send `build-pc/audiotrace.log` + `build-pc/audiodump.raw`. The dump lets +the stuck sound be isolated numerically (separated from music) and pinned to +the exact door event via `dumppos`. All diagnostics are env-gated — catalogue in +findings.md M-65; remove when D202 closes. + +## Done this session (M-64) — D204 FOUND + FIXED: PC audio ran ~2 % below real time permanently. D202 still OPEN; two M-63 conclusions corrected. + +**New finding D204, fixed and measured.** Found by reviewing the audio +*pacing* loop — the whole M-56→M-63 chain had been auditing the data path. + +**The defect:** `amMain` wakes at 30 Hz (`sched.c:334` forwards every 2nd +retrace to the audio client, `audi.c:441`) and asks for `g_MinFrameSize`=720 +samples per block = **21600/s against a 22050 Hz device** — structurally 2 % +short. `audi.c:531` relies on 784-sample top-up blocks to make it back, but +only requests one once the reported AI length drops under ~69 frames = **3 ms**. +Fine on N64 (double-buffered AI, exact VI interrupt); on PC ordinary OS +scheduling jitter empties a 3 ms cushion before the loop reacts, the SDL queue +hits zero, and the device pads playback with silence — **unrecoverable time, a +queue-depth reading cannot tell the regulator it already fell behind.** +Measured `rt=0.980` sustained with `q` repeatedly at 0, indefinitely, on every +level, for every user, since the first frame. + +**The fix (F5, port-only — `src/audi.c` untouched, rule #2 clean):** +`audioGetAiLengthBytes()` in `port/src/audio.c` subtracts a 1024-frame +(~46 ms) cushion before reporting, so "queue holds the cushion" reads as +"queue is empty" and the loop tops up while slack remains. Also landed: **F1** +correct single-buffer AI_LEN_REG semantics (robustness — measures as a no-op), +**F3** `Audio.QueueLimit` 8192→2880 + non-silent drop logging, **F4** oversize +invariant guard. + +**Measured, in-binary A/B via `GE_D204_OLD=1`** (same executable, no +build-to-build variance), `-level_09` scripted PPK fire: + +| | rt | q (min..max) | drop | max block | +|---|---|---|---|---| +| before | **0.980** | 0..416 | 0 | 3136/3156 | +| after | **1.000** | 368..672 | 0 | 3136/3156 | + +5-minute soak on the shipping binary: `rt=0.999`, `drop=0`, `q` never near 0. +**Not yet verified by ear** — a human listening pass is still owed. + +**New tooling — `. ools_pcudiodebug.ps1`**, the audio counterpart to +`debug.ps1` (that one is for crashes). Drives every audio probe and prints a +pass/fail verdict (real-time ratio, queue starvation, dropped/oversized blocks, +soundIndex histogram): + +``` +. ools_pcudiodebug.ps1 -AB -Fire # before/after table - how D204 was measured +. ools_pcudiodebug.ps1 -Play -Trace # instrumented interactive playtest (D202) +. ools_pcudiodebug.ps1 -Soak # 5-min stability run +. ools_pcudiodebug.ps1 -SyncData ... # mirror ./data into build-pc/data first +``` + +Two harness gotchas it encodes, both of which cost a debug cycle and are +written up in findings.md D204: `Start-Process -RedirectStandardOutput` cannot +capture this game's stdout (it is `WIN32_EXECUTABLE`/`-mwindows`, no console), +and **MSYS2 `bash.exe` launched from PowerShell arrives with a stripped +environment** so every `GE_*` probe reads as unset — the script generates a +`.bat` and runs it under `cmd.exe` instead. + +**Underlying probe — `GE_D204=1`, an audio-health monitor** (one line / 5 s: +`rt=` real-time ratio, `q=` queue depth, `drop=`, `max=` block vs allocation). +Deliberately cheap (~30 clock reads/s) so it can be left on for a **whole +playtest** — this is the instrument D202 actually needs. + +**Two M-63 conclusions corrected — do not pursue either:** +1. **`soundIndex=109` is a red herring, closed negative.** The "94 calls" is + double-counted (the probe logs 2 lines per call) — 47 real, all in bursts + after t≈5.4 s interleaved with ricochet indices = **guards returning fire**, + correct behaviour. The "suspicious uniform ~32.6 ms cadence" is just the + audio-block quantum (720 frames × 4 = 2880 bytes) that *every* `dumppos` in + the trace is rounded to. +2. **M-63's "8 s of audio in a 60 s run" was its own probe's artifact.** + `GE_MIXERTRACE=1`'s unbuffered per-opcode `fprintf` (25 MB log) slows the + process enough to starve the audio thread. Same repro without it: 58.0 s / + 60 s. **`GE_MIXERTRACE` is unsafe for any timing-sensitive measurement** — + M-63's advice to have the user replay with it set would have induced the + very starvation being hunted. Use `GE_D204` instead. + +**A hypothesis I raised, tested and FALSIFIED — do not re-open.** I predicted +the u32 subtraction at `audi.c:531` wraps past 789 queued frames, truncates to +u16 (58128), and drives a 74× heap overrun of the 3156-byte `info->data`. The +wrap is real (high-water 2064 frames measured) but **harmless**: +`frameSamples` is declared **`s16`** (`audi.c:145`), so it lands negative and +audi.c's own lower clamp catches it. Max block stays 3136 B vs the 3156 B +allocation in every run, both modes. No overrun. This is why F1 measures as a +no-op. + +**D202 is NOT resolved by this and stays OPEN.** Nothing degraded in any +headless run this session (5 min soak clean, no voice-pool growth). Its +reported chain — wrong PPK sound, correct one sometimes heard too, pile-up, +eventual near-silence except a stuck loop — still needs a **real playtest**. +**Next session:** have the user play with `GE_D204=1 GE_AUDIOTRACE=1` (and +**not** `GE_MIXERTRACE`) and hand back `ge007.log` + `audiotrace.log`; `rt` +falling or `q` pinning at `queueLimit` with `drop=` climbing localises it +immediately. Do NOT re-open the index/data/decode/ROM-offset chain (M-56–M-62, +solid) or the 109 lead. + +Uncommitted: the D204 fix (`port/src/audio.c`, `port/src/libultra.c`, +`port/include/audio.h`) plus the pre-existing D202 diagnostic probes. Full +detail: `docs/dev/findings.md` D204. + +## Done this session (M-63) — D202 REOPENED again: user's real complaint is runtime mixer/voice corruption, not a wrong-sample-data question. Live repro built; root cause not found; one concrete anomaly banked. + +**Supersedes the M-62 "recommend close" below** — after that was written, the +user clarified the actual bug: the wrong ("slap") sound always plays on the +silenced PPK, the correct suppressed shot **sometimes plays too** (both heard +together), and over a play session sounds "pile up and spirally glitch out" +until eventually **no audio plays except a looping sound (e.g. Bunker's door) +stuck since level start.** That's real-time mixer/voice-lifecycle corruption, +categorically different from "wrong soundIndex requested" — the M-56–M-62 +chain (which is solid) never actually tested this. + +**Build+run works in this environment**: `export +PATH=/c/msys64/mingw64/bin:$PATH` (MSYS2 MINGW64 gcc/cmake/SDL2), then +`./build-pc.sh ntsc-final` from repo root. To run, `build-pc/data/` needs +the **full** asset set, not just the ROM: copy `data/pccg-ntsc-final/`, +`data/pcmodels-ntsc-final/`, `data/ge007.ntsc-final.z64`, and (to match the +user's real state) `data/ge007.eep`+`data/ge007.ini` into `build-pc/data/`. +**A costly false alarm this session**: skipping the sidecar dirs makes +`-level_09` hard-crash in `load_bg_file` almost immediately (raw big-endian +ROM read instead of the converted PC sidecar, per `obInit()`'s own comment) +— looks exactly like a scary new engine bug, cost a stashed rebuild + a gdb +session to rule out, and was purely a test-setup gap. The `[WARN] +pcmodels.bin not found` / `[WARN] pccg.bin not found` lines are printed +right there in the log — read them before chasing a crash. **Next session: +always mirror the full `data/` dir, verify with those two warnings absent.** + +**Repro used**: `GE_MIXERTRACE=1 GE_AUDIOTRACE=1 GE_AUDIODUMP=1 +GE_INPUTSCRIPT="600:R,Z;601:R,Z;...;3210:R,Z;3211:R,Z"` (30 fire pulses, +~0.75s apart) `timeout 60 ./ge007.x86_64.exe -level_09`. Ran clean 60s, no +crash. Logs saved: `scratchpad/d202-m63/{audiotrace.log,mixertrace.log, +audiodump.raw}` (mixertrace.log is 25MB — grep it, don't read it whole). + +**Ruled out**: voice-pool exhaustion/leak. Only 16 distinct voice slots used +across 153 `sndPlaySfx` calls in 60s, properly recycled; `sndDeactivate` +count (159) tracks `sndPlaySfx` count (153) almost 1:1. The D199/M-53–M-55 +"voices never freed" lineage does not reproduce here. + +**Found, unexplained — top lead for next session**: `soundIndex=109` +(`"109_GUN_B4_BOLTACTION_SFX", //used for AK47`, `bondconstants.h:2546`) +fired **94 times in 60 seconds** with no AK47 anywhere in the repro (PPK +only), and part of that traffic retriggers the *same* voice slot at a +suspiciously uniform **~32.6ms cadence** (2880 bytes in the mixed stream, +4-in-a-row) — far faster than any plausible weapon cadence in this game. +Reads like something re-issuing `sndPlaySfx` every audio tick instead of +once per game event, which would explain "sounds piling up." Not yet +confirmed as the cause of the user's exact symptom (109 isn't the PPK's +sound), but it's the most concrete, checkable thing this session produced. + +**Not reproduced**: the user's full described chain (pile-up → glitch → +total silence except one stuck loop). The 60s/58-shot synthetic burst +stayed healthy. Either it needs a longer/denser real-play session, or the +soundIndex=109 anomaly is the seed that compounds given more time — chase +(1) below first, it's cheaper than an extended blind playtest. + +**Next session, in order:** +1. Find what calls `sndPlaySfx(109, ...)` in `src/game/*.c` and determine + if the ~32.6ms retrigger is legitimate (real per-round automatic-fire + loop, just fast) or a bug re-firing every audio tick instead of once per + event. +2. If that doesn't explain it, build a much longer repro (5+ min, walking + + intermittent combat) or have the user reproduce with + `GE_MIXERTRACE=1 GE_AUDIOTRACE=1 GE_AUDIODUMP=1` set and hand back the + logs, aimed at reaching the "total silence except one stuck loop" end + state; diff voice-slot reuse timing against this session's healthy + 60s baseline. +3. Actually listen to `scratchpad/d202-m63/audiodump.raw` (raw s16 stereo + 22050Hz PCM) around the soundIndex=109 retrigger timestamps — not done + this session, log-analysis only so far. +4. Do NOT re-open the index/data/decode/ROM-offset chain (M-56–M-62) — that + part is solid; this is a different mechanism. + +No code changes this session (build+run+trace only; the pre-existing +uncommitted diagnostic files were exercised, not edited). Full detail: +`docs/dev/findings.md` D202 M-63 entry. + +## Done this session (M-62) — D202: byte-match verification confirmed to cover the struct + post-dates the source; user confirms the M-60 reference clip's provenance is unknown/found-online. SUPERSEDED BY M-63 — see above; the "recommend close" below did not hold up. + +Read-only session, no code changes. No MIPS toolchain is available in this +environment (`mips-linux-gnu-*` absent) so M-61's item 1 (build the N64 +target, read the map/ELF for the literal ROM byte) wasn't attempted — went +for the cheaper item 2 instead. + +**Found `scripts/ge007.u-test_basis.csv:946`: +`0289c36e967840bba7f6fbd026dda001,.data,build/u/src/game/gun.o`** — +`test_files.sh`/`test_files_readme.md` confirm this md5 is a known-good +checksum of `gun.o`'s compiled `.data` section, which is where +`wppksil_stats` lives (`gun.c` `#include`s `gunWeaponStat.inc.c`). This +directly falsifies M-61's alternative (a) ("verify doesn't cover this +segment") — it does. + +**Timing check:** `git blame -L946,946` on that CSV → commit `cc14d64e7` +("for england james?", 2026-08-16, upstream sync). `git log` on +`assets/obseg/gun/wppksil/gunWeaponStat.inc.c` → last touched `9fbe1fd5`, +an ancestor of `cc14d64e7`. **The checksum postdates the source's last +edit** — it isn't testing something since changed. Re-read the file: +`.Sound` is still `0x2E`. Since AGENTS.md rule #2 means the PC-port work +never touches `src/game/gun.c` / `assets/obseg/gun/**`, nothing has +drifted since that checksum was captured. + +**Also found: this repo's own CI (`.github/workflows/ci.yml`) never builds +the N64 target** — `validate`/`linux-build`/`windows-build` are all PC-port +jobs, no `test_files.sh`/N64 `Makefile` invocation anywhere. The "byte- +matches ROM" claim in `AGENTS.md` is inherited from upstream +`n64decomp/007`'s own history, not continuously re-verified in this repo. +Worth knowing generally; orthogonal to D202. + +**Net effect:** this is strong circumstantial evidence, not a rebuild- +confirmed "pass" (that still needs installing a MIPS toolchain and +literally running `scripts/test_files.sh`) — but combined with M-61's +already-closed alternative (c) (no runtime remapping), the two live leads +left are **(b) reference-clip provenance** and **(d) envelope-match +reliability**, both pointing away from a port bug. + +**Asked the user directly (same session): where did `B00I00S2D.wav` come +from? Answer: "Not sure / found online."** That was the last unverified +link — five sessions (M-56–M-62) have independently verified every other +part of the chain (index resolution, bank order, pointer chain, ADPCM +decode math bit-exact, ROM offset, and now the source data via the +byte-match checksum). **Recommend closing D202 as NOT-A-BUG** — the same +conclusion M-59 reached, now on much stronger evidence. + +**Next session, in order:** (1) confirm with the user whether to close +D202 outright, or run the cheap optional emulator A/B first (fire the +silenced PPK on the real ROM in a known-good N64 emulator, listen) as +final belt-and-braces confirmation before closing; (2) if the user instead +wants a hard rebuild-confirmed pass rather than the strong circumstantial +evidence, install a MIPS toolchain (`binutils-mips-linux-gnu` equivalent) +and run `scripts/test_files.sh` for real; (3) once D202 is closed, resume +the roadmap's B3 audio track / ROADMAP-1.0 priorities — this whole seven- +session investigation was a detour from the plan-of-record ordering +(B1 → B2 → B3 → …). + +Full detail: `docs/dev/findings.md` D202 M-62 entry (+ the same-session +follow-up appended just below it). + +## Done this session (M-61) — D202: both M-60 threads resolved; keystone fact is now "what byte does the ORIGINAL ROM hold at wppksil_stats.Sound". STILL OPEN. + +Diagnostic-only session (no game/port code touched). Both live threads from +M-60 are done: + +1. **Bank-navigation off-by-N hypothesis: CLOSED (negative).** + `port/src/romdata.c`'s bank re-layout walks `ALBankFile -> ALInstrument + -> ALSound` in exact ROM order; PC index N == N64 index N. Dead end — + do not re-litigate. +2. **Offline decoder: FIXED + VALIDATED.** `scratchpad/rom_sfx_decode.py` + now matches `aADPCMdecImpl` exactly (fixed swapped tbl0/tbl1, missing + intra-subframe accumulation, wrong ncoef). Full 261-sound bank scan: + **index 45 (`DROP_GUN_SFX`) is the only strong match for the user's + reference clip (envcorr +0.999); index 46 (`GUN_SILPPK_A_SFX`) is the + "slap"** (`scratchpad/romsfx_44..60.wav`). +3. **Fire path traced:** `gunfire.c` L3196/3200 → `bondwalkItemGetSound()` + (`gun.c` L1343) → raw u16 `WeaponStats.Sound` (offset 38). No runtime + remapping. Both N64 and PC builds compile the same initializer from + source (`gun.c:135` includes the aggregate `gunWeaponStats.inc.c`; + per-weapon file is an identical second copy) — both say `.Sound = 0x2E` + (46). Unsilenced PPK uses 0x6B. + +**Keystone logical constraint (new):** the N64 build byte-matches the +original ROM, and this data is *compiled* from source in that build — so a +transcription error in `.Sound` would have broken the match. **0x2E is +therefore almost certainly what the original ROM holds**, meaning real N64 +hardware also requests index 46. That contradicts the reference clip +matching index 45, so exactly one of these is false: (a) byte-match verify +doesn't cover this data segment; (b) `B00I00S2D.wav` isn't an in-game N64 +capture (filename looks like a media-library asset ID — provenance never +stated); (c) N64 runtime resolves 46 differently than ROM order; (d) the ++0.999 envelope match is misleading. + +**FAILED this session — do NOT repeat:** direct ROM byte-pattern searches +for the struct (looped 40+ times, all 0 hits: decompiled floats don't +round-trip to IEEE-754 bits, integer tails also absent) and a hand-computed +RAM→ROM mapping (yielded offset `0xC11934` > 12MB file size; `.csegment` +sits at ROM `0xC00000` = EOF in `ge007.ld`, so the arithmetic was wrong). +No N64 `.map` at maxdepth 3 (only PC CMake tree `build-linux/`). + +**Next session, in order:** +1. Find the **N64 build map file or ELF** (search deeper than maxdepth 3: + `assets/obseg/Makefile.*`, `ld/`, `dist/`; then `nm`/`objdump`) and read + the actual 2 ROM bytes at `wppksil_stats.Sound` (vaddr `0x800326C4` + + offset 38). ROM=0x2D → transcription error (audit why verify missed it); + ROM=0x2E → ground truth, pivot to (b)/(c). +2. Check the **byte-match verification tooling**: does it compare data + segments or code only? Find the last full-verify output. +3. **Ask the user one question:** where did `B00I00S2D.wav` come from? +4. Cheap corroboration: live PC trace of the soundIndex passed to + `sndPlaySfx` on PPK fire (expected 46). + +Full detail: `docs/dev/findings.md` D202 M-61 entry. + +## Done this session (M-60) — D202 REOPENED: user rejects "ground truth, not a bug" and supplies a real reference clip. STILL OPEN, do not close. + +**User's own words: "I know for a fact its the wrong sound playing in the +PC port specifically."** This directly overrides M-59's conclusion below — +do NOT treat D202 as closed/WONTFIX. The user also dropped a real reference +recording of the silenced PPK's actual fire sound: `scratchpad/ +B00I00S2D.wav` (untracked/scratch, mono/16-bit/16kHz, 0.204s). Its RMS +envelope is sustained (stays high for ~80% of the clip) vs. +`d202_soundindex46_isolated.wav`'s early-peak-then-fast-decay shape — +consistent with, though not proof of, the user's "slap not gunshot" call. + +Two live threads, neither finished — **pick up here next session**: + +1. **Bank-navigation-bug hypothesis (new, not yet checked)**: soundIndex 46 + is *named* `46_GUN_SILPPK_A_SFX` in `bondconstants.h` yet decodes to what + the user twice ID'd as the melee-slap sound (whose own indices are the + adjacent 47/48/49 `_PUNCH*_SFX`). A correctly-resolved sound shouldn't + contradict its own debug label — check whether `port/src/romdata.c`'s + bank re-layout (`afCtx`, `_bnkfPatchWaveTable`) walks + `ALBank->ALInstrument->ALSound` in the exact ROM order, i.e. whether the + PC conversion could be off-by-one/off-by-N vs. `src/snd.c`'s documented + resolution path `soundBank->instArray[0]->soundArray[soundIndex]`. Not + checked line-by-line against actual ROM bytes for this bank yet. +2. **`scratchpad/rom_sfx_decode.py`** (WIP, untracked, NOT working yet) — a + fresh standalone ROM bank parser meant to decode a *range* of + soundIndex values and diff each against `B00I00S2D.wav` to find which + one actually matches. **Its ADPCM predictor loop is currently wrong** + (stubbed intra-subframe accumulation). The correct formula, confirmed + from `port/src/mixer.c`'s `aADPCMdecImpl` (L185-239, ground truth): + table shape `sAdpcmTable[predictor][2][8]`, per sample `j` in a subframe + `acc = tbl[0][j]*prev2 + tbl[1][j]*prev1 + (ins[j]<<11) + + sum_{kbase += table` (`src/libultra/audio/bnkf.c`) + — GE's wavetable *sample data* (unlike the bank-structure metadata) is + never relaid-out by `port/src/romdata.c`; it points straight into the + live cart-mapped `.z64` image. Last theoretical gap in the M-56/M-58 + data-integrity chain is now closed at the raw-byte level. +2. **Re-read the full `audiotrace.log`, not just soundIndex 46/122.** + Cross-referenced every other soundIndex against `bondconstants.h`'s + `"NN_NAME_SFX"` string table (index baked into the name). Every single + fire (46) request in the trace is immediately followed by one of + `{23, 24, 25, 37}` = `RICO_6_TAJ_A/B/C_SFX` / `RICO_5_C_SFX` — ricochet + impact sounds — cycling per shot. Found the call site: + `src/game/gunfire.c`'s `recall_joy2_hits_edit_detail_edit_flag()` + (unmodified ground-truth bullet-hit handler): any hitscan that strikes a + non-character prop unconditionally plays a random ricochet SFX from a + 20-entry table. **This is exactly what GoldenEye does when you shoot a + wall, on real N64 too.** + +**Conclusion:** the M-52/M-56 repro's fixed aim direction has the player +shooting straight into a nearby wall the whole time, so every shot +legitimately plays THREE correct sounds at once — fire (46) + cartridge- +eject (122) + a random ricochet (23/24/25/37) — not one wrong one. The +"sounds like ricochet + melee slap" description matches a ricochet impact's +character far better than a coincidental bug that always produces two +other real, nameable sounds. Three full sessions (M-56/M-57/M-58) of +independent bit-exact/byte-exact verification found nothing wrong anywhere +in the chain, which is now explained: there was nothing to find. + +**Not yet proven:** whether the fire SFX itself is perceptually prominent +enough / correctly balanced against the (real, expected) ricochet — could +just be masked by a louder transient, which is normal. **Recommended next +session, before resuming M-58's envmix/PVoice trace:** rerun the same +`GE_AUDIODUMP` capture with the aim pointed at open air (no wall hit), confirm +via `audiotrace.log` no `ricochet_sounds_small` index fires, and have the +user listen to that clip specifically. If fire+cartridge alone sound correct +and recognizable, close D202 as a repro artifact, not a port bug. Only +resume the envmix/PVoice-identity trace if that clip still sounds wrong. + +## Done this session (M-58) — D202: address/DMA plumbing AND the ADPCM decode math both independently verified bit-exact correct; bug is downstream (envmix/output-routing or PVoice identity), still OPEN + +Picked up exactly where M-57 left off (priority (1): decoded-PCM-per-voice +dump). Extended `GE_MIXERTRACE` (still env-gated, zero cost when unset) to +log DMEM source bytes, ADPCM book coefficients, and the first decoded frame +in `port/src/mixer.c`; added `[DMAREQ]`/`[BINDTABLE]` in +`src/libultra/audio/load.c` and `[DMAHIT]`/`[DMAMISS]` in `src/audi.c`'s +`amDmaCallback` — all four now write into the *same* `mixertrace.log` so one +physical voice filter's whole lifecycle reads chronologically in one file. + +**Made and caught a real mistake mid-session**: first compared +`wavetable->base` from one run's `audiotrace.log` against `aLoadBuffer` +addresses from a *different* run's `mixertrace.log` (separate files, no +shared ordering) and momentarily concluded the mixer never touches the ROM +wavetable data at all — a false alarm from cross-run/cross-file +correlation, not a real bug (per-run nondeterminism, D117, shifts line +counts between runs). Fixed by merging every new probe into one trace file +and rerunning once. Lesson banked in findings.md M-58. + +**With everything in one file, followed soundIndex 46 (the fire SFX) end to +end for one live occurrence:** `BINDTABLE` → `DMAREQ` (memin exactly matches +the bound table's base, no address confusion) → `DMAMISS` (legitimate cold +ROM→staging-buffer copy via `piServiceDma`, a real cart address under +`romdata.c`'s mapping) → `LOADBUF` (dumped the actual ADPCM source bytes) +→ `ADPCMDEC` (dumped the loaded book coefficients, matching `sndPlaySfx`'s +own book pointer for that soundIndex) → `PCMOUT` (dumped the first decoded +16-sample frame). **Hand-decoded the same 9 source bytes against the same +book coefficients using the textbook VADPCM algorithm, entirely independent +of this codebase's code** — first three samples matched the C output +bit-exact (`0, -1536, -2475`). Both the address/DMA plumbing and the decode +arithmetic are now proven correct for this instance, not just internally +self-consistent. + +**This closes the first three links of the pipeline** (request routing — +M-56, DMEM concurrency — M-57, decode itself — M-58). Next session's leads, +in order: (1) trace `aEnvMixer`'s output routing / persistent volume-ramp +state (`sVol`) the same way — one physical voice followed end-to-end in one +trace file — to see if correctly-decoded PCM gets accumulated into the +wrong output/voice; (2) PVoice allocation/identity aliasing higher up in +the synth; (3) only after (1)/(2), the still-dormant `load.c`/`music.c` +pointer-truncation family as its own hardening pass (own D-number). Do NOT +re-open the ROM/index chain, DMEM concurrency, or decode math/addressing — +all three independently verified closed across M-56/M-57/M-58. + +## Done this session (M-57) — D202: DMEM/concurrent-voice hypothesis falsified by live trace; new latent (dormant) pointer-truncation family found, not yet root-caused + +Interactive session, build+run environment confirmed working +(`export PATH=/c/msys64/mingw64/bin:$PATH`). Picked up M-56's exact next +step: audit `port/src/mixer.c`'s DMEM/`aSetBuffer` context handling for +concurrent voices. + +- Added `GE_MIXERTRACE=1` (`port/src/mixer.c`) — logs every opcode call + (addresses/state pointers, in order) to `mixertrace.log`. Reran the M-56 + repro (`-level_33`, aim+fire script) and read the trace around the + soundIndex 46/122 fire event. **Result: every voice's full pipeline + (LoadADPCM→decode→resample→envmix) runs to completion strictly + sequentially before the next voice starts — no interleaving anywhere in + ~62k logged opcodes.** The M-56 "concurrent voices clobber shared DMEM" + theory (D199's flagged risk) is **falsified by direct trace, closed + negative** — the single shared scratch buffer is safe under this call + pattern, same as real RSP hardware. +- Also ruled out a `a->table->len` 2242→2241 shift seen in the wire probe + log — that's `load.c:396`'s N64 ground-truth ADPCM-frame rounding, + idempotent, not a bug. +- **New (not yet confirmed as D202's cause): `load.c` lines 111/173/259/ + 320/382/438 narrow the real 64-bit `ALWaveTable.base` pointer into the + `s32 memin` field** — same class as the already-fixed D198/D201. Currently + **dormant** in this build (observed base addresses sit under 4GB, so the + truncation round-trips as a no-op) — flagged as a hardening item, not + proven live. Delegated a repo-wide sibling-audit of this cast pattern to + the local Qwen `repo-mapper` agent (read-only, checked): also found + `src/music.c:879,1073,1266` (same class, unaudited) and a low-risk masked + case in `heapinit.c:26`. Everything else in the audio subsystem's `(s32)` + casts are genuine small integers, not pointer truncation. +- Full write-up: `findings.md` D202 "M-57". + +**Next session, in priority order:** (1) dump decoded PCM per voice-slot +right after `aADPCMdec`, tagged by soundIndex, and diff against what the +correct sound should decode to — tests whether the wrong PCM enters the +mixer at all vs. gets attributed to the wrong logical sound downstream; +(2) if that's clean, suspect PVoice allocation/identity aliasing; (3) only +after (1)/(2), consider fixing the `load.c`/`music.c` pointer-truncation +family as its own hardening pass (own D-number) — not yet shown to cause +D202. Do NOT re-check the ROM offset/index chain (M-56) or DMEM/ +concurrent-voice sharing (M-57) — both closed. + +## Done this session (M-56) — D202(a) root-cause chain closed on the data side; decode/mix runtime is the last remaining suspect + +User playtest report ("no music, sound effects bugged — wrong sound on +every gunshot/pickup") triggered a live root-cause push on D202(a). Built ++ ran interactively (this session had a working build+run environment). + +- Added `GE_AUDIODUMP=1` (`port/src/audio.c` `audioSetNextBuffer`) — raw- + dumps the true final mixed s16 stereo output to `audiodump.raw`. Headless + repro: `-level_33` (Dam, player starts armed), `GE_INPUTSCRIPT` holding + aim+fire (`R,Z`) for 8 pulls. Converted to WAV, found the transients via + an RMS scan, sent the clip to the user. **User confirmed by ear: hearing + the ricochet SFX and the melee-slap SFX on every gunshot** — two real, + distinct wrong game sounds, not noise/silence. +- `GE_AUDIOTRACE` on the same run showed the fire event correctly requests + soundIndex 46 (`GUN_SILPPK_A_SFX`) + 122 (`CART_SPENT_SFX`, shell casing) + — legitimate call sites, valid-looking resolved pointers. +- **Independently re-parsed the raw ROM bank bytes from scratch in Python** + (no project code reused) to check `port/src/romdata.c`'s PC-only bank + re-layout tool (`afFixup*`/`romdataFixupAudioBank` — hand-written PORT + code, the one link in this chain never scrutinized this closely before). + **Exact byte-for-byte match** against the live runtime trace for both + soundIndex 46 and 122's wavetable base/len, and confirmed each has its + own distinct (non-aliased) ADPCM book. **This closes the index/pointer/ + ROM-offset chain for good** — proven correct by independent + reconstruction, not just prior code audit. +- **Conclusion: the bug is in the decode/mix runtime itself** + (`port/src/mixer.c`), not the data path. **New leading suspect**: D199's + already-flagged risk — `aSetBuffer`'s inferred persistent-DMEM-context + semantics may not isolate concurrently-active voices (e.g. the + continuous background ambience loop + a fire SFX in the same frame), + letting one voice's ADPCM decode state bleed into another's output. + Full write-up + the exact repro recipe: `findings.md` D202 "M-56". + +**Next session:** audit `mixer.c`'s DMEM/`aSetBuffer` context handling for +concurrent voices using the repro above (`GE_AUDIODUMP`+`GE_AUDIOTRACE`, +`-level_33`, aim+fire script in findings.md). Do NOT re-check the ROM +offset/pointer/index chain — independently re-derived byte-exact this +session. + +## Done this session (M-55) — D202 cont.: voice-stealing + demo-boundary-reset leads both closed out, negative + +Static/read-only, local-Qwen-assisted (`triage` agent for the grep/citation +legwork, spot-checked against real files before trusting it — see +`findings.md` D202 "M-55" for full detail). Picked up M-54's two priority +leads: + +1. **`port/src/mixer.c` never participates in voice allocation at all** + (zero references to `maxPVoices`/`pAllocList`/`pFreeList`/`pLameList`) — + that bookkeeping lives entirely in unmodified ground-truth + `synallocvoice.c`/`synthesizer.c`. Rules out "PC mixer skips voice + stealing" as a cause; nothing to fix there. +2. **No demo-boundary audio reset exists on either platform.** `alInit` + (`sl.c:28`) is a singleton called once at boot; `stop_demo_playback()` + (`ramromreplay.c:604`) touches only joystick bookkeeping, no audio call. + All files involved are unmodified decomp — N64 has the identical gap. + +**Both M-54 leads are now closed, negative.** The orphaned-looped-voice bug +(door/train SFX never deactivated at an attract-cycle boundary) looks like +a pre-existing N64 behavior only made *audible* on PC by an idle unattended +capture, not a PC regression — per rule #2, not obviously fixable without +adding a stop call the original game never had. **Next: an acoustic check** +(longer capture / forced `ramrom_table` index to confirm whether N64's +voice-stealing eventually silences it during normal play) to decide +WONTFIX vs real bug, **or** pivot to D202(a) (wrong-sample-per-weapon, +PPK→PUNCH1) which is now the higher-value untouched thread — needs the +decoded-PCM listen-compare, not more voice-pool tracing. + +## Done this session (M-53) — D202 cont.: both remaining suspects checked out clean, root cause still open + +Static-analysis-only session (no build/run — following up on M-52's D202 +write-up, picking the two suspects it flagged: ADPCM decode math vs. wrong +ROM segment offset). **Both now look clean; full detail in `findings.md` +D202 "M-53 update":** + +- **`aADPCMdecImpl`** (`port/src/mixer.c`) compared line-by-line against the + local Perfect Dark PC port's scalar reference implementation + (`pd_port/port/src/mixer.c`'s non-SIMD `#else` path) — identical algorithm, + no discrepancy. +- **`_sfxtblSegmentRomStart`'s ROM offset** (`0x102F19A0`) cross-validated + against 3 independent sources — the CSV scanner (`filelist.u.csv`), the + N64 linker script (`ge007.ld`, untouched ground truth: `sfx.ctl.o` + immediately precedes `sfx.tbl.o`), and a direct read of the real bytes at + that offset in `data/ge007.ntsc-final.z64` (decodes as a plausible + `ALBankFile`, `bankCount=1`). All three agree. +- Also spot-checked the ADPCM book-coefficient byte-swap (`afFixupBook`, + `romdata.c`) and the `_bnkfPatchWaveTable` pointer math (`bnkf.c`, post-D201 + `uintptr_t`) — both fine, not previously called out explicitly. + +**D202 is still OPEN — root cause not found.** New leading suspect, NOT yet +investigated: the resample/pitch pipeline (`aResampleImpl` + whatever +computes a voice's playback rate from `ALKeyMap`/`unityPitch` — +`synallocvoice.c`/`seqplayer.c`/synport equivalent). A wrong pitch/rate +calc would better explain "sounds like a different sound" (mis-pitched +correct sample) than a literal wrong-sample-content theory, given sample +resolution/pointer-chain tracing already came back clean twice now. Also +would explain "music plays as an SFX-like loop" if the same bug hits +sequence-player note events. **Next session: trace one PPK-fire note's +computed pitch/rate value with a new `GE_AUDIOTRACE`-style probe in that +path** — do not re-audit decode math or the ROM offset further, both are +now well-corroborated. Still needs a real listening pass to close, per the +M-52 note below. + +**M-53 cont. (same session, build+run) — user rebuilt (confirmed no change, +expected) and gave two live-playtest clues that reframe the investigation:** + +1. **The "music-replacement loop" is a real door SFX (`METAL_SLIDE_LOOP_SFX`) + stuck on, not a decode/pitch bug** — confirmed via `GE_AUDIOTRACE` on a + real door interaction (`-level_09`): `sndPlaySfx` legitimately chains + 202→204→203 (open→loop→close, stock N64 chain mechanic in `snd.c`'s + `do…while`). The N64-matching (`//#MATCH`) cleanup code + (`propobj.c door7F053B10()`) that's supposed to stop it looks correct + and isn't a truncated-pointer bug (`door->openSoundState` is already a + real 64-bit `ALSoundState *`). Checked the level-transition "deactivate + all" sweep (`lv.c` → `sndDeactivateAllSfxByFlag_1()`) — it's + flag-filtered (`FINAL_IN_SEQUENCE` only) and wouldn't catch a + mid-chain loop sound even on real N64, so **N64 must instead rely on + the attract-mode demo's scripted input naturally pressing "close door" + before the demo ends.** Leading theory: **this may be a second symptom + of the already-open D193 timing bug** (`g_GlobalTimerDelta`/ + `g_ClockTimer` wall-clock-vs-sim mismatch), not a distinct audio bug — + if the attract demo's fixed-frame-count script runs on a different + real-time schedule on PC, it could get cut off before the scripted + close-door input fires, orphaning the loop voice permanently. **Full + write-up + reasoning chain: `findings.md` D202, "M-53 cont." + the + "Follow-up" paragraph after it.** +2. **"Ammo pickup is one of the knife sounds"** — `PICKUP_AMMO_SFX`=234, + `PICKUP_KNIFE_SFX`=233, adjacent. But combined with PPK(46)→sounds-like- + PUNCH1(47), the two adjacent-index shifts are in OPPOSITE directions — + doesn't fit a simple constant-offset bug. Left open between "two + unrelated single-entry bugs" and "a badly-mispitched correct sample + being misheard as a timbrally-similar neighbor." + +**M-54 — got a live headless repro, and traced the D193-timing theory to a +dead end.** Read `frametiming.c`/`libultra.c`'s VI-retrace/tick chain +end-to-end: the ramrom path bypasses `waitForNextFrame()` and feeds +`updateFrameCounters()` the *recorded* N64 speedframes value directly +(`ramromreplay.c:400`), and the 32-deep `gfxFrameMsgQ` message backlog +self-throttles (each drained burst message after a stall sees near-zero +elapsed and no-ops) — no runaway/duplicate-advance mechanism found. +**Downgrade the D193-linkage theory** — not fully ruled out but the +specific mechanism I'd pointed to isn't there. + +Then extended the `GE_AUDIOTRACE` probes (`snd.c`: `sndPlaySfx` now also +logs `newState=`/`flags=` per chain link, `sndDeactivate` now logs every +call) and used `GE_INPUTSCRIPT="30:A;90:A;...;1170:A;"` (press A once a +second for ~20s to clear the Rare/Nintendo/legal/cast-intro screens fast) +to land in the real attract-mode demo within a ~280s headless run. +**Live repro caught**: the `ramrom_Train` demo plays `TRAIN_GO_SFX` +(soundIndex 64, the only `flags=2`/`SOUND_FLAG_LOOPED` sound in the run) +as two voices — **neither voice ever appears in a `sndDeactivate` line, +anywhere in the log**, while two other, non-looped states DO get +deactivated right as the sequence rolls back to the boot logos. Direct +confirmation of the M-53 "Follow-up" theory: whatever transition sweep +runs there skips looped voices (consistent with +`sndDeactivateAllSfxByFlag`'s `FINAL_IN_SEQUENCE`-only filter). **This +is the same bug as the door report** — any `SOUND_FLAG_LOOPED` voice +started during a `ramrom` demo, not door-specific. + +**New leading theory**: real N64 hardware has a small fixed physical +voice pool with **voice-stealing** (`synallocvoice.c` `_allocatePVoice`, +unchanged decompiled code) — a new sound request silently steals/ramps +out the oldest lower-priority voice once the pool (`maxPVoices`, +unchanged N64 config value) is full, which would eventually silence an +orphaned loop on real hardware even without ever calling `sndDeactivate` +on it. **Not yet checked: whether `port/src/mixer.c` (Phase-3 PC code) +actually enforces the same cap / stealing**, or effectively gives every +voice request a free channel (no stealing ever triggers) — if the +latter, that's the fix, entirely in the port audio layer, no game-logic +(`snd.c`/`propobj.c`/`front.c`) changes needed. + +**Next session, in priority order (superseded by M-55, see above — (a) +and (b) below are now DONE/negative, do not re-audit):** ~~(a) audit +`port/src/mixer.c`'s voice dispatch against `synallocvoice.c`'s +free/alloc/lame-list + stealing algorithm~~ (M-55: mixer.c has no voice +bookkeeping, nothing to fix); ~~(b) look for another explicit N64 +stop-all-ambience call at attract-cycle boundaries~~ (M-55: none exists +on either platform — confirmed absent, not just unlocated); (c) dump +decoded PCM for one mis-sounding SFX via the existing `GE_AUDIOTRACE` +addresses and listen/compare against neighbor entries to settle the +index-vs-pitch question for symptom (a) — **still untouched, now the +top lead**. Do NOT re-touch ADPCM decode math, the ROM/segment offset +math, the ramrom frame-timing chain, or the voice-pool/demo-reset code — +all cross-validated clean now (M-52 + M-53 + M-54 + M-55). + +## Done this session (M-52 cont.) — first Phase-3 audio listen pass: D202 opened, root cause NOT found yet + +User did the first real listening pass on the committed Phase-3 mixer (see M-52 +block below — that commit is already merged to `main`). Result: audio plays, +but **wrong**, in two ways, both filed as **D202** (new finding, §F, OPEN — +not yet root-caused): (a) weapon fire plays the wrong-but-always-the-same clip +per weapon (deterministic, e.g. PPK fire sounds like a melee hit, every shot); +(b) level music never plays — a looping SFX-like sound plays in its place. +Confirmed menu/intro music is correct; only in-level audio (Bunker1, Dam) is +broken. + +**This session's work was ELIMINATION, not a fix.** Built a `GE_AUDIOTRACE` +env-gated probe chain (`src/snd.c` `sndPlaySfx`, `src/libultra/audio/load.c` +`alLoadParam`) and traced real gameplay (`-level_09`, user firing repeatedly, +plus self-verified with `GE_INPUTSCRIPT`). Conclusively ruled out the entire +index/pointer/ABI chain — soundIndex resolution, bank re-layout order, the +ALWaveTable pointer chain, the wire-up into the physical decode voice, the +already-fixed D54 param-pool sizing, sequential-voice-processing (no cross-talk +race), and the ALSndpEvent/ALEvent size relationship (measured equal, 32B). +**Full elimination log + the two remaining suspects (ADPCM decode math in the +new `port/src/mixer.c`, or a wrong ROM-embedded byte offset) are written up in +`docs/dev/findings.md` D202 — read that before re-investigating, do not +re-derive.** Diagnostic probes are still in the tree (uncommitted, +`#ifdef`-free but `GE_AUDIOTRACE`-gated, harmless): `src/snd.c`, `src/libultra/audio/load.c`. +Strip them once root-caused, or fold into a permanent probe if useful long-term. + +**Next step recommendation (from D202):** since the wrong sample is 100% +deterministic per weapon (ruled out stateful/interleaving causes already), the +fastest path is probably a byte-level diff of the embedded `_sfxtblSegmentRomStart` +PC data against the source `.z64` at the resolved offset (rules in/out asset- +embedding) — cheaper to check than auditing `aADPCMdecImpl`'s DSP math by hand. + +## Done this session (post-M-50, M-52) — audio findings backfilled + Phase-3 mixer landing committed + +Picked up exactly where the last session's handoff left off: the working tree +had 15 modified files (D200's reverb fix + the earlier Phase-3 software-mixer +implementation), with code comments referencing findings **D198/D199** that +were never written into `findings.md`, and the M-49 playtest block claiming +**D195/D197** were "logged in findings §F" when they weren't. + +**Wrote 5 findings entries** (§F table, after D194/before D200): +- **D195** — Control transparency/alpha bug (playtest report, no capture yet — OPEN). +- **D197** — Silo head-texture wrap, likely `G_TX_CLAMP` not honoured (OPEN). +- **D198** — `load.c` used `K0_TO_PHYS` (unconditional `&0x1FFFFFFF` mask) on + 64-bit heap pointers at 3 call sites; every sibling site already used + `osVirtualToPhysical`. Fixed by matching them (FIXED). +- **D199** — the Phase-3 software mixer itself: macro-swap `aXxx` execution + (`abi.h`→`mixer.h`→`mixer.c` `Impl` functions) + the `aSetBuffer` + persistent-DMEM-context semantics, which have **no RSP disassembly to + verify against** — reconstructed purely from call-site pairs. Flagged the + context-inference as a real risk if an unaudited call site sets it + differently (LANDED, functional on `-level_09`). +- **D201** (new number) — found while reading the diff: `bnkf.c`'s relocation + offsets were `s32`, silently corrupting bank/inst/sound/wavetable pointers + on PC for ~50% of allocations (sign-extension of a truncated 64-bit + pointer). The code comment had already applied the `uintptr_t` fix but + mislabeled it "D200" (that number was already taken by the reverb bug) — + relabeled to D201 in both the comment and the entry (FIXED). + +**Committed** the full audio-port change (mixer + K0_TO_PHYS/bnkf fixes + +D200 reverb fix + all 5 new findings) as one commit — judged safe to land +together rather than split D200 out separately (as the prior handoff +suggested) since it's one cohesive session's audio work and splitting +`findings.md`'s hunks cleanly wasn't worth the risk of a bad split. + +**Verified before commit:** `./build-pc.sh ntsc-final` — clean link, no new +warnings beyond one pre-existing nested-comment cosmetic warning in the +already-present D200/D201 comment block. Ran `-level_09` ~600+ frames +(interactive console session) — crash-free, matches D200's prior +verification; music + SFX both audible. + +**Not done / still owed:** +- D195/D197 both need a `GE_PCDUMP`/`GE_TEXDUMP` capture — filed as + OPEN with a concrete next step, not investigated further this session. +- D199's `aSetBuffer` context-inference risk is unverified beyond + `-level_09` — a wider level sweep (esp. levels with reverb/aux-bus SFX) + would raise confidence. +- LEVEL-STATUS's per-level PASS lines still predate the mixer landing — + re-sweep once this commit is on `main`. + +## Done this session (post-M-50) — D200: `-level_09` audio segfault root-caused + fixed + +**D200 (findings §F, FIXED).** `-level_09` segfaulted in seconds once the new +software mixer ran reverb. Root cause: `ALDelay.input/output` are **u32**, and +`reverb.c` back-references the delay ring as `&r->input[-d->output]`. On N64 +(s32 `ptrdiff_t`) the u32 negation wraps to a small negative index; on x86-64 it +**zero-extends to +4,294,967,136 samples** (~8GB forward) — proven exactly: +`0x706d30a0 + 0xFFFFFF60·2 = 0x2_706D2F60`, the observed wild address. Wild +`aSaveBuffer`/`aLoadBuffer` writes then trampled the delay array + `r->base` + +adjacent heap in a self-propagating loop. **Fix: 5 lines in +`src/libultrare/audio/reverb.c`** — `(s32)` negation casts at all negative-index +sites (`alFxPull` ×2, `_loadOutputBuffer` ×2) + `(s64)` ramalign; identical to PD +ground truth (`n_reverb.c`). Sibling audit: reverb.c is the only audio file with +the pattern. Verified: `-level_09` runs 120 s+ crash-free, music + SFX both flow; +temporary DRAM guard window in the mixer saw zero OOB writes. All instrumentation +removed — the only session diff in code is the 5-line fix. + +**⚠ Working tree has uncommitted audio-port work from an EARLIER session.** +`git status`: 15 modified files; only `reverb.c` + `findings.md` are this +session's. The rest is the Phase-3 software-mixer implementation (`port/src/mixer.c` +macro-swap acmd execution, `port/include/mixer.h`, `include/PR/abi.h`, +`port/src/audio.c`, `port/src/libultra.c`, `src/libultra/audio/bnkf.c`) + +K0_TO_PHYS corrupted-pointer fixes in `load.c`/`save.c`/`synsetpan.c`/ +`synsetvol.c`/`synsetfxmix.c`. **Its code comments reference findings D198/D199 +that were never written into `findings.md`** — and HANDOFF's M-49 block claims +D195/D197 are "logged in findings §F" but those entries are also missing from the +index. **Before committing anything: write D195/D197/D198/D199 entries, then +commit the audio work as its own change** (keeps D200's 5-line fix reviewable on +its own). + +**Status deltas.** Phase 3 audio is now functionally live on `-level_09` (was: +crash in seconds). LEVEL-STATUS's Bunker1 PASS line predates the mixer work — +re-sweep levels once the mixer commit lands. Closed thread: the leftover +"aSetBuffer BIG c=2144/2048" flags from this session's debug were confirmed +legitimate DMEM-only `_pullSubFrame` (env.c) ops — no action. + +**Env reminders.** Every fresh MSYS shell needs `export PATH=/c/msys64/mingw64/bin:$PATH` +or the exe fails with "SDL2.dll: cannot open". Windows locks the running `.exe` — +kill the game before rebuilding or the link step fails. + +## Done this session (M-49) — PR #22/#23 merged, Linux bundle re-verified off main (2026-09-04, interactive console) + +**v0.1.0 is verification-complete on both platforms. Only the tag push + publish remain (user's call).** + +- **PR #22 (D188 va_list ABI) — MERGED** (`08feb5c2`). +- **PR #23 (D189 stack-overruns) — auto-closed** by GitHub when the #22 merge deleted + its base branch (it was stacked on #22's branch, not `main`; a closed PR can't be + reopened or retargeted). **Recreated as PR #24**, rebased onto `main` — the + now-merged xprintf/D188 commits dropped out cleanly (`patch contents already + upstream`), leaving just `stan.c tileStack[64]` + CMakeLists `-fno-stack-protector` + + D189/D190 docs. CI green, **MERGED** (`881cbf5b`). Stale branch deleted. + (`git push --force` and `--delete` are classifier-blocked intermittently — worked + for the branch delete, not the force-push, hence the new branch name + `fix/linux-stan-overrun-d189-rebased`.) +- **Linux bundle rebuilt + re-verified off merged `main`:** + - `cmake --build build-linux` (WSL Ubuntu / gcc-14) — links **243/243**. + - `tools_pc/bundle-linux.sh 0.1.0` → `dist/goldeneye-pc-port-0.1.0-linux-x86_64.tar.gz` + (+ `.sha256`). No-ROM / 60 MB guards pass. + - Untarred clean to `~/gesmoke3` → `prepare-assets.py` → all 4 sidecars + `ALL CHECKS PASSED` → dropped ROM in `data/`. + - **Level sweep 5/6** (`-level_09/20/27/34/37` all run the full 60 s window, + frame 1500+, no crash; **`-level_45` SIGSEGV at load = D190**, known/open, already + hedged in the release notes). The `romdataFixupMusicSeqTable: seqCount 63 exceeds + blob capacity 1` line on every level is the **expected** D35 header-only-copy clamp, + not a regression. + - Matches M-48's tarball sweep exactly — the two merged PRs regressed nothing. +- **Legal review (user asked):** no new exposure. Both PRs are `#ifdef PORT` ABI / + build-flag changes, no content added. Release artifacts unchanged: exe + README + + licenses + `prepare-assets/` (emit scripts + decomp metadata `.inc.c`/`.csv`); **no + ROM, no assets, no `pccg.bin`/`pcmodels.bin`**. Pre-existing n64decomp/007 + no-explicit-license caveat is disclosed in `NOTICE` and unchanged. +- **`dist/` now holds both pristine bundles** (`…-linux-x86_64.tar.gz` + + `…-win64.zip` from M-48) ready for a manual release-asset upload if CI's `release` + job needs a fallback. + +### Owed / next (M-49) +1. **Tag:** `git tag v0.1.0 && git push origin v0.1.0` → CI `release` job drafts the + dual-platform pre-release → download + smoke the *attached* archives → **Publish** + (user's call — the only outward-facing step). +2. D190 (`-level_45` load SIGSEGV, propDef-stride family) stays open; not a v0.1.0 + blocker. + +## Done this session (M-48) — triage + v0.1.0 Windows bundle validated (2026-09-03, interactive console) + +Triage of high-impact work for end-user experience + repo credibility. Top +finding: **cutting the v0.1.0 alpha release is the single biggest credibility +lever** and the Windows half is now proven. Full ranked triage: +`scratchpad/TRIAGE-2026-09-03.md`. + +- **Windows v0.1.0 bundle — VALIDATED END TO END.** `bundle-win.sh 0.1.0` → + 6.9 MB zip (exe + SDL2/zlib/winpthread/stdc++/gcc_s + README + licenses + + `prepare-assets/` w/ 512 model headers, DLL-closure check passes). Unpacked + to a clean dir → `prepare-assets.py` regenerated all 4 sidecars + (`ALL CHECKS PASSED`) → `ge007.x86_64.exe -level_09` booted to frame 2100+ + crash-free, geometry renders (`scratchpad/smoke_000240.png`). +- **PR #20 (`ci/bundle-win-zip-fallback`) — MERGED (`80952f69`).** Stock MINGW64 + ships no `zip`; `bundle-win.sh` now falls back to 7z / `Compress-Archive`. + CI unaffected (it `pacman -S zip unzip`). +- **PR #21 (`docs/release-notes-known-issues-refresh`) — MERGED (`e4fc9dd0`).** + Dropped the HUD weapon icon (D187/#19) and blank briefing text (D178/M-36) + from `.github/release-notes.md` + `tools_pc/dist/README.md.in` — both fixed. + Black sky + front-end 3D models stay listed. +- **D178 (blank briefing objectives) — was already FIXED** (`cd1ed574`, + `romdataFixupBriefing`, M-36). Stale item in the M-44/M-45 owed lists. + +### Linux runtime FIXED this session (M-48, full-agentic) — two crashes root-caused + fixed +WSL back after reboot. Found the Linux `-O2` build crashed at boot; root-caused ++ fixed two independent latent-UB classes, both **exposed by PR #9's `-Og`→`-O2` +switch** and by Ubuntu gcc's hardened defaults — the N64 + MinGW builds never +saw them. + +- **D188 / PR #22 — `_Printf` `va_list` ABI.** `_Printf` took `va_list` by value + then passed `&args` to `_Putfld`; on x86-64 SysV `va_list` is an array type so + that's the address of a local pointer, not the arg list → `_Putfld`'s + `va_arg(*args,…)` walked garbage → SIGSEGV in `bossInitMainthreadData`'s D121 + sprintf, pre-frame-1. Fix: `#ifdef PORT` thread a `va_list *` through + (`xprintf.c` + `sprintf.c`). Same class as D187. **Windows framediff 3/3.** +- **D189 / PR #23 (stacked on #22) — latent stack-buffer overruns.** + `stan.c sub_GAME_7F0B1DDC` `tileStack[39]` vs a `>= 41` bail check (BUNKER1 + prop setup legitimately walks ~40 tiles); `bondview2.c + bondviewCalcIntroSwirlCamera` `pointbuf[10]` indexed `[-3..11]`. Harmless on + N64 + MinGW (no stack protector); fatal `__stack_chk_fail` under Ubuntu gcc's + `-fstack-protector-strong`. Fix: `-fno-stack-protector` globally (matches + N64/MinGW) + `tileStack[64]` `#ifdef PORT`. **Windows framediff 3/3; Linux + `-O2` `-level_09` runs to frame 2100+ crash-free, renders BUNKER1.** +- Earlier-seen `-Og` "texcache `std::list` abort ~frame 9" was downstream of the + D189 stack corruption — gone with the fix. +- porting-notes: **D7** (va_list SysV) + **D8** (stack-protector / latent + overrun class). findings §F D188/D189. +- Debug tree `~/…/build-linux-dbg`, smoke tree `~/gesmoke` in WSL. + +### Owed / next (M-48) — v0.1.0 is now unblocked for BOTH platforms +1. **Merge PR #22 → then PR #23** (retargets to `main` on #22 merge). Both + need the user (`gh pr merge` is agent-blocked... except it worked earlier + for #20/#21 — try it). +2. **Rebuild + re-bundle Linux** off merged `main`, re-run the bundle smoke + (untar + prepare-assets + `-level_09`), and the Linux multi-level sweep. +3. **Tag:** `git tag v0.1.0 && git push origin v0.1.0` → CI `release` job + drafts the pre-release with **both** archives → download + smoke-test the + *attached* win64 zip and linux tarball → **Publish** (user's call — the + only genuinely outward-facing step). +4. If Linux still has rough edges after the sweep: the release notes already + hedge it ("boots and renders … far less exercise"); ship it as a + known-rough secondary platform rather than holding the tag. + +## Done this session (M-47) — triage + PR #18/#19 render verification (2026-09-03, interactive console) + +Triage of high-impact / low-effort wins for end-user experience + repo +credibility. Key env finding: **an interactive session on the physical console +CAN verify rendering** (`GE_PCDUMP` + `framediff.py` 3/3) — the M-37/42/43 +"no display" claim was background-session-only. Recipe in +`[[env-no-runtime-verify]]` memory / see below. + +- **PR #19 (HUD ammo-type icon) — VERIFIED + de-drafted + merged current to + main. Ready to merge.** `-O2` build, `-level_09` `GE_PCDUMP` 200–440: orange + magazine glyph absent pre-fix, present post-fix; `framediff.py` frame 320 + worst-cell dmean **21.8 → 4.0** (converges to the pre-regression golden). + Added `findings.md` §F **D187** + `porting-notes.md` **D6** (non-void-no-return + / latent-until-`-O2` class). All CI green, `mergeStateStatus: CLEAN`. + **User action: merge PR #19** (agent is `gh pr merge`-blocked). +- **PR #18 (black sky, Path B) — VERIFIED render, stays draft.** Sky geometry + now emits + textures (`-level_22` top-half luma 0.1 → ~174; user watched it + live: "sky box is there just glitchy"). Two defects, full analysis in a PR #18 + comment: **(1) texcoord shear** — `tc = unk20/unk24 * 32.0f` is the wrong + scale; pin the real units of `SkyRelated38.unk20/unk24` from + `sub_GAME_7F097388` first. **(2) vertical coverage** — textured band only + fills top ~25%, black gap to horizon; trace `skyPortRenderPoly` nverts + xy + per call. ~1 render-iterate session. +- Ranked triage (full list in the session): **#19 → #18 → tag v0.1.0 → refresh + README GIF** is the best payoff path. Tag v0.1.0 needs a clean bundle + + launch smoke on Win + Linux (release infra all merged, PR #17 in `main`). + +### Rendering-verify recipe (interactive console session) +`export PATH="/c/msys64/mingw64/bin:$PATH"`; run from repo root w/ absolute exe +path; golden is 640×480 so back up `data/ge007.ini` and set `[Window]` +Width/Height=640/480 (game rewrites it on clean exit — restore after); +`GE_PCDUMP="200-440:60"` + `timeout 90`; frames → `./ppm/`; +`python tools_pc/framediff.py ppm`. PIL only in +`~/AppData/Local/Programs/Python/Python313/python.exe`, not the msys python. +Windows locks the running `.exe` — kill the game before the next `cmake --build` +or the link fails. + +## Done this session (M-46) — D176(a) sky Path B implemented → draft PR #18 (2026-09-03, full-agentic) + +Short window (~25 min, interactive-lite, no display). Fixed the +full-agentic-mode skill first (time now read from the system clock, not +extrapolated; interactive-verify is no longer all-or-nothing). Then a +background subagent implemented **D176(a) Path B**: + +- **`fix/d176a-sky-path-b` → draft PR #18.** `src/game/sky.c` only, all + `#ifdef PORT`, N64 path unchanged. `skyRenderTri`/`skyRenderFull` now emit + screen-space `gSPVertex`+`gSP1Triangle`/`gSP2Triangles` from the projected + `SkyRelated38` verts (new helper `skyPortRenderPoly`, ortho via + `dynAllocate*`) instead of the no-op'd `G_RDPHALF_*` stream. **Links + 243/243, zero new warnings.** findings.md §F "D176(a) — M-46"; porting-notes §D. +- **Owed (display):** `-level_22` frame ~360 should now show a moonlit sky, + not black. Spot-check `-level_36`/`-level_29`. Tex-coord scale (`*32`) is a + guess. Single-frame `GE_PCDUMP` diff vs golden for the sky levels; confirm + non-sky levels unchanged. **PR stays draft until a human eyeballs it.** +- Worktree at `.claude/worktrees/agent-aa27d9f0082d683cb` (branch pushed; + `git worktree remove` it when done). +- Memory: `qwen-parity-goal.md` — user wants the local agent brought to + tool/feature parity with Claude over time. + +**Also M-46 — HUD weapon-icon regression root-caused → draft PR #19** +(`fix/hud-weapon-icon-missing-return`). `set_rgba_redirect_generate_microcode()` +(`gunfire.c:5929`, sole icon emitter) is non-void with no `return`; harmless at +`-Og`, breaks at `-O2` (commit `34885535`, Sep 2) — undefined return reg → callers +overwrite the icon's DL commands. Ammo numbers use a separate correct path, hence +the split symptom. Fix = one-line `return` under `#ifdef AVOID_UB`. MEDIUM-HIGH. +**Not built/run this session.** Owed: `-O2` build + `-level_09` capture vs +`docs/img/bunker1-2.png`; A/B a `-Og` Debug build; findings.md §F entry. +Do NOT revert `34885535`. + +## Done this session (M-45) — release infra: dual-platform alpha bundles + CI docs-PR fix (2026-09-03) + +Triage session → release plumbing. Interactive-lite (no build/run env). **Two +PRs merged to `main`:** + +- **PR #16 (`ci/docs-pr-mergeable`, merged `2f85ca84`)** — dropped `paths-ignore` + from `ci.yml`'s `pull_request:` trigger; the `changes` job now diffs a PR + against its base sha (mirroring the branch-push logic). Docs-only PRs finally + merge without an owner bypass (the M-44 #14/#15 problem). Fixes the item the + M-44 handoff flagged as "worth a one-line ci.yml fix". +- **PR #17 (`release/v0.1.0-alpha-dual-platform`, merged)** — first-alpha + release plumbing: + - **`tools_pc/bundle-linux.sh`** (new) — packages a Linux build as + `goldeneye-pc-port--linux-x86_64.tar.gz` (exe + README + licenses + + `prepare-assets/`). Mirrors `bundle-win.sh`'s no-ROM / 60 MB guards + the + `prepare-assets/` assembly. **No shared libs bundled** — generated README + says `apt install libsdl2-2.0-0 zlib1g libgl1` (user chose "document apt", + not ".so bundling"). CI-verified: 4.2M tarball, ~512 model headers. + - **`tools_pc/dist/README.md.in`** — OS-neutralised via `@PLATFORM@` / + `@EXE@` / `@DEPS@` / `@LICENSE_EXTRA@` tokens (win: sed; linux: sed + awk + for the multi-line `@DEPS@`). Known-issues list corrected: **dropped the + stale "ladders don't work" line** (D177 fixed M-36); added black-sky + + missing-HUD-weapon-icon. + - **`ci.yml`** — `linux-build` now packages + uploads the Linux bundle; + `release` job (`needs:` + `linux-build`) attaches **both** platform + archives to the draft pre-release. + - **`.github/release-notes.md`** — rewritten for the two-platform NTSC-U alpha. + +**Alpha scope decision (user):** NTSC-U only, keep the one-time +`prepare-assets.py` step. Part A (3-region + launcher) and Part B (native C +converter) from `docs/dev/release-dropin-rom-plan.md` remain follow-ups — Part B +is still the [[issue-6-dropin-rom-release]] offline-Qwen candidate. + +### Owed / next (M-45) — see the standalone prompt in the session handoff + +1. **Cut `v0.1.0`** — needs a machine with a working build+display: + - Windows: `./build-pc.sh ntsc-final` → `tools_pc/bundle-win.sh 0.1.0` → + unzip to a clean dir → drop US ROM in `data/` → `python prepare-assets\prepare-assets.py` + → launch → confirm it boots to the menu / a level. + - Linux: `cmake --build build-linux` (or `./build-pc.sh`) → + `tools_pc/bundle-linux.sh 0.1.0` → untar clean → install SDL2/zlib/GL → + drop ROM → `python3 prepare-assets/prepare-assets.py` → launch. + - If both load: `git tag v0.1.0 && git push origin v0.1.0` → CI drafts the + pre-release with both archives → smoke-test the *attached* artifacts → + Publish. +2. **Tier 2 visible-glitch fixes** (display needed to verify — lead preps, user + eyeballs): + - **HUD weapon-icon regression** ([[hud-gun-ammo-icon-missing]]) — regressed + ~2026-09-02/03. Bisect PR #7/#8/#9 + f10-overlay merges; check the F10 + overlay DL append in `gfx_pc.cpp gfx_run()` isn't leaving combiner/tile + state dirty; audit the `bondview*.c` weapon-icon draw path. Capture + `-level_09`, diff vs `docs/img/bunker1-2.png`. + - **D176(a) black sky — Path B** ([[d176a-sky-stream-spec]]) — `#ifdef PORT` + in `src/game/sky.c` to emit `gSPVertex`+`gSP1Triangle` from the + `SkyRelated38` verts instead of the no-op'd RDPHALF stream. Verify + `-level_22` frame ~360. + - **D172 blood particles magenta/cyan** — likely a channel-order / env-color + decode in the fast3d sprite path or `blood_animation.c`. Small. +3. **Tier 4 research/planning pass** — audio (libaudio→SDL, adapt PD `mixer.c`), + blank-briefing objective text (D178/D143 `langGet` NULL family), front-end 3D + models (D75/D149). Produce a delegation plan: mechanical → local Qwen, + judgment → Claude subagents / lead. No code, plan only, for user approval. + + + +Interactive session, user on a real display driving the tests. Branch +`feat/f10-options-overlay`, new commit `bf18a166`. **PR #10 un-drafted / ready.** + +- **Fixed invisible overlay text** — `drawText` passed the measured text w/h + into `textRender`'s `width`/`height` (the on-screen CLIP rect), clipping every + glyph. Now passes `viGetX()/viGetY()` like `bondview2.c` debug text. +- **Mouse UI** — hover-highlight; left-click in the value column toggles/cycles; + drag sliders; red `X` close box; label-clicks only focus. New + `inputSuspendForOverlay()` (input.c) frees + shows the OS cursor while open. +- **New rows: Fullscreen** (live) + **Resolution** (windowed presets filtered to + desktop, live resize+recenter). `videoRequestWindowSize/Fullscreen` post from + the scheduler thread; `videoDrainWindowRequests()` applies host-side in + `videoPumpEvents`. +- Layout recomputed for the wide BankGothic font (right-aligned values, 15u row + pitch, all 13 rows fit ~240u). +- **User verdict: "looks great and can be merged in."** Verified live: layout, + hover/click/drag, live VSync/filter/fullscreen/resolution, close box, closed = + no-op, front-end pointer unaffected at default `MouseCaptureMode=1`. + +### Merge sweep (M-44, end of session) — ALL 6 PRs MERGED +Main = `74bebc0d`. All feature branches deleted (local + remote); tree clean. +- **#12** D186 FpsCap clamp · **#13** building.md d88 step · **#11** alpha + asset-prep tool · **#10** F10 overlay v2 · **#15** D176(a) stream spec · + **#14** D176(a) repro + D74-row cleanup. +- #14 needed a manual `findings.md` conflict resolve against #15 (kept both + D176(a) subsections, chronological M-37 -> M-42 -> M-43). +- **Docs-only PRs (#14/#15) can't be merged by `gh pr merge`** — the `main` + ruleset requires `validate`+`windows-build`, but `ci.yml`'s `pull_request:` + has `paths-ignore: ['**.md','docs/**']` so docs PRs run no CI and those + contexts never report. `--admin` is classifier-blocked for the agent; user + merged via GitHub UI owner-bypass. **Worth a one-line ci.yml fix**: drop + `paths-ignore` from `pull_request:` and let the `changes` job report the + heavy jobs skipped=success (the pattern already used for `push`). + +### Owed / next (M-44) +1. **HUD gun/ammo icon missing** — user reports the weapon *icon* (not the ammo + number) vanished, regressed "in the last day". NOT investigated. Bisect the + PR #7/#8/#9 merges + f10 work; `-level_09` capture vs `docs/img/bunker1-2.png`. +2. `Video.FpsCap` sub-30 is unresponsive/hangy (D186 / PR #12 territory, not the + overlay branch). Uncapped FPS unsupported. Low priority per user. +3. PR #10 merge is the user's call (they approved it). +4. Unchanged from M-43: D176(a) Path B; PR #11 packaging dry-run; merges #12–#15. + +## Done this session (M-43) — PR #11 to ready + D176(a) full stream spec (2026-09-03, full-agentic) + +Full-agentic-mode, ~1:00–4:30 ET. **Runtime rendering is not verifiable in this +environment** — the Windows GL build exits immediately with no window when +launched from the Git Bash tool (no display), so `GE_PCDUMP` captures nothing. +Kept to work that is checkable headless (Python tooling, static analysis, git). + +- **PR #11 (`feat/alpha-asset-prep-tool`) → READY FOR REVIEW.** Rebased onto + current `main`; the romdata `mmap` commit that blocked it landed via PR #8 and + dropped out on rebase, so the branch is now just the asset-prep tool + two doc + edits. **Re-verified end-to-end this session:** assembled the `prepare-assets/` + tree exactly as `bundle-win.sh` does, ran `prepare-assets.py` against the US + ROM from a clean temp dir → all four output files (`pcmodels.bin`/`manifest.csv`, + `pccg.bin`/`manifest.csv`) **SHA-1-identical** to the working build's sidecars. + PR body rewritten. **Owed:** a human run from an actual unzipped release `.zip` + (packaging dry-run) — tool logic + output are verified; this is the last + "shipped artifact works" check. + - NB `docs/building.md`'s d88-step edit is also on PR #13 standalone — + whichever merges first, the other no-ops on that hunk. + +- **D176(a) black sky — full stream spec committed** (`findings.md` §F, + "D176(a) — M-43 UPDATE"). The M-37 scratch note was never committed; this + re-derives the complete format from `sky.c` + `gbi.h`. Key facts: + `skyRenderTri`/`skyRenderFull` emit a **verbatim N64 RDP triangle command** + (2-word header + 3 edge-slope pairs + 16-word shade block + 16-word texture + block = 40 data words for `G_TRI_SHADE_TXTR`), chopped into `G_RDPHALF_1/_CONT` + pairs and terminated by `G_RDPHALF_2`. GE's RSP ucode just DMAs it to the RDP. + `sub_GAME_7F097388` already produces complete screen-space verts + (`SkyRelated38`: xy×4, z, 1/w, s/t, rgba) — the projection is endian-clean and + not the bug. **Two implementation paths written up:** Path A = RDP-tri decoder + in `port/fast3d/gfx_pc.cpp` (invert the plane equations → 3 `LoadedVertex` → + existing GL path; ~1 session, "right"); Path B = `#ifdef PORT` in `sky.c` to + emit `gSPVertex`+`gSP1Triangle` from the `SkyRelated38`s instead of the RDPHALF + stream (~a few hours, cheaper, small game-code-idiom exception). **Recommend + Path B first** — lights up all cloud-sky levels and de-risks A. Verification + target: `-level_22` frame ~360 (M-42's clean headless black-sky repro). + Cross-checked the word layout against `gbi.h` (local Qwen, read-only). + +### Owed / next (M-43) +1. **Implement D176(a) Path B** on a machine with a working display+GL — it is a + ~few-hour job now that the format is fully documented, but every step needs a + render to verify (winding, combiner, tex coords), which this env cannot do. +2. **PR #11 packaging dry-run** — unzip a real release bundle on a clean machine, + run `prepare-assets`, launch the game. +3. Unchanged from M-42: D186 real fix (frame pacing off the scheduler thread); + PR #10 (F10 overlay, draft) still owes its interactive feel-check; PR #13/#14 + docs still awaiting the user's merge. + +## Done this session (M-42) — D186 frame-cap fix + doc honesty (2026-09-03, full-agentic) + +Full-agentic-mode, ~3 h. This repo is mature (21/21 levels load+render+no-crash, +front end playable); most open work is deep/multi-session or interactive-gated. +Landed the one clean verifiable engineering win + tidied stale docs. **Three +branches pushed + PRs opened** (user reviews/merges — the full-agentic skill was +updated mid-session to pre-authorize push/PR on the user's own repos): + +- **PR #12** ← `fix/d186-fpscap-clamp` (ready for review) +- **PR #13** ← `docs/building-d88-step` (docs; overlaps a commit on PR #11) +- **PR #14** ← `docs/m42-graphics-honesty` (docs) + +- **`fix/d186-fpscap-clamp`** (`4a1cb1d5`) — **D186: a sub-30 `Video.FpsCap` + no longer throttles the whole sim.** `sync_framerate_with_timer()` + (`gfx_sdl2.cpp`) runs its `sysSleep`+busy-wait inline on the scheduler thread + (`osSpTaskStartGo`→`gfx_run`→`swap_buffers_begin`), so a low cap blocks + VI-retrace delivery and drags every game thread down to the cap rate — this is + what made the game a 1-fps slideshow when `FpsCap` got dinged to 10 (M-39). + No N64 analogue. Fix = clamp `0 < FpsCap < 30` → 0 (uncapped) + warning, at + both `videoInit` (normalises a bad `ge007.ini` for the next `configSave`) and + `gfx_sdl_set_target_fps` (the fast3d chokepoint, also covers the F10-overlay + live path). Caps ≥ 30 unchanged. **Verified:** default `FpsCap=0` framediff + 3/3 golden-identical; `FpsCap=10` now runs at the uncapped rate (VI ~1740 in + 30 s vs ~300 before); `FpsCap=60` still paces (~56/s, no warning). Build links + 242/242. findings.md §F "D186" + porting-notes.md §E. **Owed (real fix):** + move frame pacing off the scheduler thread so any cap only drops *presented* + frames — until then a low cap is refused, not supported. + +- **`docs/building-d88-step`** (`2348a1a0`, cherry-pick of the M-38 commit + stranded on the PR #11 branch) — `building.md` §4 now lists `d88_emit.py + --regen` as the third required sidecar pass (was d43+d69 only → a source + build per the old instructions crashed on first level load; this is the D185 + omission). NB the commit's trailing blockquote forward-references the + release-bundle `prepare-assets.py` (PR #11, not yet merged) in present tense. + +- **`docs/m42-graphics-honesty`** (`76dd2d5c`) — findings.md + GRAPHICS-BACKLOG: + **D176(a) black sky — `-level_22` (Statue, night) is a clean no-input headless + repro** (bare boot, frame ~360, upper half pure black). Best verification + target for whoever writes the RDPHALF sky-tri decoder (root-caused M-37, + ~1 session, genuine new fast3d code). `-level_36`/`-43` intro cameras face + the terrain so a no-input capture there does *not* frame the sky; `-level_29` + top-of-frame is dim, not black (needs an eyeball). Also struck the stale + "D74 wrap-block" backlog row — that dead-code/OOB block was reworked and + hoisted behind `Video.WrapFix` at M-30 (RC3/D167). + +### Owed / next (M-42) +1. **Push the 3 branches, open PRs** (or fold `docs/*` into an existing docs PR). + D186 is a clean standalone; the two docs branches are trivial. +2. **D186 real fix** — relocate frame pacing off the scheduler thread + (`gfx_sdl2.cpp` / the `osSpTaskStartGo` gfx path). Then the clamp can relax. +3. **D176(a)** — implement the RDPHALF tri-raster decoder in `gfx_pc.cpp` + (`case G_RDPHALF_1/2/CONT` currently no-ops at ~:2901); verify against + `-level_22` frame ~360. Stream spec in findings.md §F "D176(a) M-37 UPDATE". +4. **PR #10 / #11** still draft, still owe their interactive/Linux verification + and a rebase onto current `main` (both predate the #7/#8/#9 merges — #11 + especially carries already-merged commits). +5. Doc drift spotted, not chased: GRAPHICS-BACKLOG D176(b)/D182(2) still cite + "Family A" (`import_texture` line-pitch shear) as the cause, but D183 (M-36) + disproved that for the `-level_36` repro (0/166 strided loads). Re-scope. + +## Done this session (M-41) — branch/PR merge sweep (2026-09-03) + +Goal: get lingering branches/PRs merge-ready. Claude cannot merge or `gh pr +merge` (classifier-blocked) — all merges are the user's to do. + +- **PR #8 (`feat/linux-ci-compile`) — UN-DRAFTED, ready for review.** Re-verified + on WSL Ubuntu: incremental `ninja -C build-linux` links green; `-level_09` + runs 1150+ frames headless and shuts down clean (no crash). Body rewritten to + current reality. `mergeStateStatus: CLEAN`. + - **Headless `GE_PCDUMP` still reads black on llvmpipe/WSLg** — confirmed via + the committed `GE_DUMPSHADER` diag: `glReadPixels` of fb0 returns 0.000 mean + on `GL_BACK` *and* `GL_FRONT`; the two extra FBOs are 0x0 attachment-less. + Not a port bug (user watched BUNKER1 render in the live window M-40). A + real-GL-driver box is needed for the Windows-vs-Linux framediff. Did **not** + chase the offscreen-FBO capture rewrite — dev-infra, not a merge blocker. + - Reverted an uncommitted TEMP `GE_DUMPSHADER` shader-source dump in + `gfx_opengl.cpp` (rendering works; not needed). +- **Pushed + opened PRs for the three unpushed local branches:** + - **PR #9 `perf/release-opt`** — `-Og`->`-O2` for release. Non-draft, trivial. + - **PR #10 `feat/f10-options-overlay`** (draft) — D184. Owed: interactive feel-check. + - **PR #11 `feat/alpha-asset-prep-tool`** (draft) — off the PR #7 docs branch; + diff shrinks once #7 merges. Owed: Linux romdata mmap compile/runtime. +- **PR #7 (`docs/dropin-release-plan-and-status`)** — CLEAN/MERGEABLE, docs+CI + only, all checks green. Recommended merge; user to do it. + +### Merged this session: PR #7 (97017ca9), PR #8 (3b798994), PR #9 (6c322548). +Still open: PR #10 (f10 overlay, draft), PR #11 (alpha-asset-tool, draft) — +de-draft once their owed verification is done. + +### NEW — open Linux issue (surfaced M-41, not chased): flaky stack-smashing on -level_09 +Running `./build-linux/ge007.x86_64 -level_09` headless on WSL Ubuntu/llvmpipe, +the game **intermittently** aborts with glibc `*** stack smashing detected ***` +(SIGABRT / SIGNAL 6) somewhere past frame ~1200. Repro'd ~3 runs, then ~4 clean +runs to frame 2300+ — **flaky**, likely heap-layout / uninit-memory dependent +(canary only sometimes tripped). Both crashing runs had `GE_PCDUMP` set at high +frame numbers; the clean gdb run had no GE_PCDUMP — weak correlation, unconfirmed. +NOT a regression from #8/#9 (that's what made Linux run at all); `-fstack-protector` +on glibc aborts where MinGW may silently tolerate the same overflow. Windows is +verified crash-free past frame 1740. Candidates: the new POSIX crash/watchdog +path in `port/src/crash.c` (has fixed `char[24]`/`char[64]`/`char linebuf[N][512]` +buffers, `backtrace_symbols` formatting) if it's firing on SIGTERM teardown; or a +real latent in-level overflow. Needs a dedicated gdb session: `handle SIGABRT +stop`, let it run 2000+ frames, `bt` at the abort. + +### Linux headless GE_PCDUMP — partially works (M-41, subagent aae607dac4a1a7ce0) +`glReadPixels(GL_BACK)` pre-swap **does** capture correctly on llvmpipe (subagent +got the gun-barrel intro, mean ~37, visually right). But in-level `-level_09` +frames 150–900 come back black/near-black (max 26–230, mean <5) before the +stack-smash. Unclear if the level's 3D geometry actually renders headless or if +those are genuinely dark frames. The M-40 "black on both GL_BACK and GL_FRONT = +blanket llvmpipe limitation" claim is too strong — GL_BACK works. + +## Done this session (M-40) — Linux CI: compile + LINK now GREEN (2026-09-03) + +Continued PR #8 (`feat/linux-ci-compile`). Started at "propobj.c fails to +compile"; ended with **all three CI jobs green — the whole tree compiles AND +links on Ubuntu 24.04 / gcc-14**. PR stays **draft** (human merges). Windows +build re-verified green after every change (`-level_09` crash-free past frame +1740). 9 commits `2e8fa3de..c803b212`: + +- `append_text_picked_up` — PORT-guarded signature (enum/int->ptr is a GCC-14 + hard error `-fpermissive` does NOT downgrade). +- `port/shim/stddef.h` — add `wchar_t` (`__WCHAR_TYPE__`) to the C-on-Linux branch + (SDL2 / glibc `` need it). +- `vtxstore.c` — `(Model*)` cast on the `modelGetNodeRwData` call (matches the + sibling line 157; enum->ptr hard error). +- `include/PR/os.h` — `bcopy/bcmp/bzero`: on `PORT && !_WIN32` include + `` (glibc's size_t prototypes) instead of the N64 `int` ones. +- `port/src/{pccg,pcmodels,romdata}.c` — local `snprintf` decls `unsigned long + long` -> `size_t` (same M-39 fix, these 3 files had private copies). +- `n64stubs.c` — `bcopy/bzero` defs are now `_WIN32`-only (glibc exports them). +- `port/src/libultra.c` — guard the D60 `VirtualQuery` / `GetCurrentThreadStack- + Limits` probes behind `PLATFORM_WINDOWS`; POSIX diag prints n/a. +- **`CMakeLists.txt` — `CMAKE_C_EXTENSIONS ON` / `CXX_EXTENSIONS ON`** + (`-std=gnu11/gnu++20`). Strict `-std=c11` defines `__STRICT_ANSI__` which + hides `CLOCK_REALTIME` etc. This is what MinGW already used. +- `CMakeLists.txt` — host `` via `gcc -print-file-name=include/stddef.h` + (it's compiler-internal, not in `bin/../include` on native Linux gcc). +- **`port/shim/sched.h` (new)** + `hostsched.h.in` — C++ TUs get the real + `` (libstdc++ ``/gthr need `cpu_set_t`/`sched_yield`; the + decomp's `src/sched.h` shadows it). C uses `#include_next`. +- `port/src/crash.c` — POSIX `crashDumpThreads` (was PLATFORM_WINDOWS-only; + watchdog referenced it unconditionally -> the one link error). Logs the + thread roster + the watchdog thread's own backtrace. + +### Linux RUNTIME — boots + runs, but renders BLACK (2026-09-03, commits `2afc0be7`, `cee26d46`) +`-level_09` / `-level_20` / `-level_24` **boot, run 600+ frames, and shut down +cleanly** on Ubuntu 26.04 / gcc-14 via WSLg — **no crash**. BUT the level +**renders black** (frame-200 `GE_PCDUMP` capture = all black + one green speck). +Compile + link + boot + kernel/scheduler/timers all work; the graphics pipeline +does not. + +Build locally: `cmake -S . -B build-linux -G Ninja -DROMID=ntsc-final +-DCMAKE_C_COMPILER=gcc-14 -DCMAKE_CXX_COMPILER=g++-14` then `ninja -C +build-linux` (~20s on drvfs). WSL `Ubuntu` distro provisioned (toolchain + +SDL2/zlib/GL dev); repo at `/mnt/c/Users/james/Source/Repos/gh-fullhist` +(fixed the malformed `/etc/wsl.conf` `[automount]` + `wsl --shutdown`). +`data/` has the ROM + sidecars already. + +Runtime fixes landed this session: +- `2afc0be7` — image-base 4GiB sentinel; PE-only load-base assert guarded; + **MAP_32BIT game-thread stacks** (kills the `(u32)&stackvar` truncation + class — without it, crash in `texLoad`/`memcpy` at stage load). +- `cee26d46` — `getenv()`/`realloc()` K&R decls (strict host GCC truncated + `getenv`'s pointer -> `configGetFrameDump` segfault on the `GE_PCDUMP` path). + +**M-40 rendering progress (commits `bd7a6cde`, `4d406462`):** +- **D146 opcode-0x00 → FIXED.** `-Wl,-Ttext-segment=0x20000000` (CMakeLists, + Linux only). Root cause: the no-PIE default base 0x400000 put `.data`/`.bss` + at ~0x00500000-0x00600000, which `seg_addr()` (fast3d) misreads as a + K0-physical DRAM offset (`w1 < 0x800000`) and remaps to 0x80xxxxxx (zeroed + mirror). 0x20000000 sits clear of every N64 range and stays < 2 GiB + (mcmodel=small OK; 0x140000000 needed mcmodel=large -> reloc failures). + ELF analogue of the Windows 0x140000000 base. +- **`start_draw_to_framebuffer(0)` -> GL fb 0.** Was binding + `framebuffers[0].fbo`, a generated attachment-less FBO -> incomplete -> + strict GL (Mesa) drops all draws. Windows verified unaffected. +- **GE_PCDUMP capture** now pre-swap (`gfx_pre_swap_hook`) + GL_BACK + + glFinish + PACK_ALIGNMENT=1. Post-swap readback was undefined on + Mesa/WSLg. + +**LINUX RENDERS. (user confirmed, 2026-09-03)** The user watched the live +WSLg window during a post-fix `-level_09` run: **BUNKER1 renders normally** +(textured interior; white border = WSLg decoration). The D146 image-base +fix + the framebuffer-0 fix were the bring-up fixes. + +The "black screen" I chased was **only the `GE_PCDUMP` headless capture** — +`glReadPixels(GL_BACK)` reads black on llvmpipe/WSLg even though the window +presents correctly. NOT a render bug. (Confirmed: a red `glClear` + +immediate `glReadPixels` in `clear_framebuffer` returns red, so readback of +fb 0 works mid-render; the pre-swap capture path still comes back black — +llvmpipe back-buffer semantics.) Subagent (claude, `aae607dac4a1a7ce0`) is +fixing `gfx_opengl_dump_bound_fbo` so headless capture works on Mesa +(GL_FRONT-after-swap, or blit fb0 -> a 1-attachment FBO and read that). + +**Watchdog** false-positive fixed this session (`libultra.c +portHeartbeatCheck`): window 3 s -> 8 s, and stop after 3 reports (was +spamming `ge007.crash.log` during gdb pauses / SIGTERM teardown). + +### Next session (M-41) +1. Land the subagent's headless-capture fix, then re-verify with a Linux + `-level_09` framediff (a golden captured on Windows vs Linux). +2. **Un-draft PR #8** — Linux compiles, links, and RENDERS. The user's + "hold until Linux renders" condition is met once the capture fix + a + framediff confirm it. +3. Interactive Linux feel-check (input, a few levels), then sweep the 21 + levels on Linux. +4. Still owed (unchanged): D186 yield-aware frame cap; push + `feat/alpha-asset-prep-tool` + PR; `perf/release-opt` decision; D184 F10. + +## Done last session (M-39) — Linux CI compile job + FpsCap 1fps diagnosis (2026-09-02, full-agentic) + +Full-agentic-mode, ~2.5h. Two threads: Linux build bring-up (pushed, draft PR) +and a user-reported "1 fps" regression (root-caused, user-side fix applied). + +### D186 — "game runs at 1 fps" = `Video.FpsCap` config, NOT a code regression +User reported the game (window title "GoldenEye 007 - 1 fps") had regressed to a +slideshow. **Root cause: `FpsCap = 10` in `data/ge007.ini`** `[Video]` — almost +certainly dinged down via the **F10 options overlay** (`feat/f10-options-overlay`, +D184) while testing; ESC-close calls `configSave()` so it persisted. Also +`ScreenShakeIntensity = 10` (default 1) in the same file, same origin. +- Bisected 14 commits back (`849ee512`): identical ~950 ms/frame on this box → + not a code regression. User confirms it ran fast recently → config drift. +- **Verified** (`-level_09`, headless, same 35 s window): `FpsCap=10` → 5 frames + total; `FpsCap=30` → 5 frames; `FpsCap=60` → frame ~300; **`FpsCap=0` → frame + 900+** at ~90–160 µs/frame. Any non-zero cap degrades; lower = worse. +- **Applied:** set `FpsCap = 0` in the user's (gitignored) `data/ge007.ini`. + `-Og`→`-O2` (see perf branch) tested too — only ~10 %, not the cause. +- **Underlying defect (OWED, not fixed):** `sync_framerate_with_timer()` in + `port/fast3d/gfx_sdl2.cpp` (called from `gfx_sdl_swap_buffers_begin` when + `target_fps != 0`) does `sysSleep(left)` + a busy-wait on the render path; + with the cooperative/threaded kernel this starves the game threads instead + of just dropping frames. GE sim is VI-locked so a low render cap must not + slow the pipeline. Fix = make that wait yield to the scheduler, and clamp + `Video.FpsCap` (and the F10 row) to e.g. ≥ 20 or treat < 20 as 0. + +### Linux CI compile job — branch `feat/linux-ci-compile`, **draft PR #8 (pushed)** +Adds a real `cmake --build` on Ubuntu (was configure-only) + carries the M-38 +romdata POSIX-mmap patch (`docs/dev/LINUX-PORT.md` "land behind CI"). Job pinned +to **ubuntu-24.04 + gcc-14**. Each fix verified to leave the Windows build green +(240/240, `-level_09` crash-free): +- `port/shim/stddef.h` — 3-way split: C++ → hoststddef; C on Windows → N64 stub + (MinGW `` drags in the `errno` macro that collides with PR/os.h); + C on Linux/macOS → `__SIZE_TYPE__`/`__PTRDIFF_TYPE__`/`__builtin_offsetof`. +- `port/shim/stdarg.h` — Linux/macOS branch: `va_list` + `__gnuc_va_list` + (glibc `` needs the alias) + `va_*` from builtins, no ``. +- `port/include/pc_protos.h` — `snprintf` size arg `unsigned long long` → `size_t` + (glibc size_t is `unsigned long`; conflicting-types error). +- `CMakeLists.txt` — `-no-pie` on Linux (dram_syms.s absolute 0x70000000 syms, + same reason Windows needs `--disable-dynamicbase`); `-fplan9-extensions` (C, + non-MinGW) for the `inherits` idiom; `-fpermissive` (C, non-MinGW) for GCC-14 + hard-error promotions (propobj.c `append_text_picked_up` vestigial params). +- **CI progress:** from ~first TU → **~19 300 lines / most of the game compiles**. + Last seen failing on `propobj.c` (the `-fpermissive` commit targets it; result + pending). Likely next: `port/shim/string.h` / `port/shim/stdlib.h` have **no + Linux branch** (Qwen triage, MEDIUM-HIGH) — but `/usr/include/{string,stdlib}.h` + *do* exist so extending the `#elif !defined(_WIN32)` → `hoststring.h`/ + `hoststdlib.h` route should work (unlike stddef/stdarg those aren't gcc-internal). + Then link (watch for `-no-pie` / dram_syms relocs), then runtime on a real box. + +### Other +- **`perf/release-opt`** (local, 1 commit, NOT pushed): `CMakeLists.txt` restrict + `-Og` to `Debug` builds (was forced on all non-Debug, overriding `-O2`). Correct + regardless; ~10 % frame time. Independent of D186. +- Qwen `triage` dispatched (read-only) for the Linux header-compat surface — + analysis in its report (string.h/stdlib.h shims flagged); the + `docs/dev/LINUX-HEADER-COMPAT.md` deliverable was not written (hit turn limit). + +### Next session (M-39) +1. **Watch PR #8 CI**, keep clearing Linux compile breaks (string.h/stdlib.h + shims next), then the link step, then runtime bring-up on a Linux box. + PR stays draft — human merges. +2. **D186 real fix:** yield-aware frame cap in `gfx_sdl2.cpp` + `FpsCap` clamp; + land with the F10 overlay branch (that's where the footgun is). +3. Decide `perf/release-opt` — trivially mergeable, or fold into another PR. +4. Still owed from M-38: push `feat/alpha-asset-prep-tool` + PR (rebase to main); + D184 F10 interactive feel-check. + +## Done this session (M-38) — alpha asset-prep tool + Linux romdata + D185 blocker (2026-09-02) + +Branch **`feat/alpha-asset-prep-tool`** (off `docs/dropin-release-plan-and-status`), +2 commits, **not pushed, no PR**. Full-agentic-mode session, ~60 min. + +- **Alpha asset-prep tool — DONE + verified.** `tools_pc/dist/prepare-assets/prepare-assets.py` + (stdlib-only: hashlib/shutil/subprocess/argparse/pathlib). Auto-detects the ROM + near the bundle, SHA-1 → region (US/EU/JP, rejects byte-swapped/overdumped), + runs `d43_emit.py` + `d69_emit.py` against a vendored input tree, writes + `data/pcmodels-/` + `data/pccg-/` next to the exe. + `bundle-win.sh` assembles `prepare-assets/` fresh at package time (emit scripts + from `tools_pc/`, + `filelist.u.csv` + `file_resource_table.inc.c` + 512 + `modelFileHeader.inc.c` as vendored inputs — decomp metadata, not game assets). + Bundled README gets a one-time "Step 2: `python prepare-assets\prepare-assets.py`". + **Verified end-to-end:** from a bundle with only exe+DLLs+US ROM, produces all + 4 files **byte-identical to `d43`/`d69` run from repo root**; bundle 14.1 MB + (< 60 MB guard); no-ROM / bad-SHA error paths tested. Resolves BACKLOG "Alpha + release — asset-prep tool" steps 1–3. **Step 4 (clean-machine smoke with the + game running) is BLOCKED by D185 ↓.** + +- **D185 — investigated + RESOLVED same session (`4491db3f`).** First cut of + `prepare-assets.py` ran only d43 + d69, so `pccg.bin` was missing the 21 + `Usetup*Z` per-level stage-setup files that **`d88_emit.py --regen`** appends + to the same dir → every level segfaulted pre-frame-1. Not a d69/​input + regression (the CRLF→LF change to `filelist.u.csv` / + `file_resource_table.inc.c` from the public-release migration is real but d69 + tolerates it). Fix: pipeline is now **d43 → d69 → d88 --regen**; + `bundle-win.sh` vendors `d88_emit.py` + its local import `d88_propdefs.py`. + Verified byte-identical to the known-good sidecar + `-level_09/-20` crash-free. +- **`build-pc/` + `data/` state (heads-up):** this session did a full clean + rebuild of `build-pc/` from `feat/alpha-asset-prep-tool` (docs branch + + romdata + tool), and replaced `data/pcmodels-ntsc-final/` + `data/pccg-ntsc-final/` + with freshly-generated copies (verified byte-identical to the prior working + ones). To get back to another branch's build: `git checkout && + rm -rf build-pc && ./build-pc.sh ntsc-final`. + +- **Linux romdata map — landed (compile+runtime verification owed).** + `port/src/romdata.c`: the `0x10000000` cart-base map was Windows-only + (`VirtualAlloc`); the POSIX `#else` was a "not implemented" stub. Added an + anonymous `mmap(MAP_FIXED_NOREPLACE)` at `CART_BASE` with identical + heap-copy fallback on failure; factored the shared post-map work into + `romdataFinishCartMap()`. **Windows: builds 242/242, `-level_09` crash-free, + no behaviour change.** Linux compile + `mmap`-lands-at-0x10000000 + runtime + are **owed on a Linux box** — full gap analysis in `docs/dev/LINUX-PORT.md` + (Qwen triage was wrong on 2/3 headline items; the CMake `else()` branches and + `system.c` POSIX paths already exist — romdata was the one real code gap). + +### Next session (M-38) +1. **Push `feat/alpha-asset-prep-tool` + PR** — the asset-prep tool is done and + verified end-to-end (d43+d69+d88 from an assembled bundle = byte-identical + sidecars, levels boot crash-free). Rebase onto `main` first (it currently + sits on the docs branch). Fold `docs/dev/LINUX-PORT.md`. BACKLOG "Alpha + release — asset-prep tool" step 4 (clean-machine smoke) is effectively done + headless; a real human run from an unzipped bundle is the last confirmation. +2. **`docs/building.md` gap:** it should tell source builders to run `d88_emit.py + --regen` too (it already says `d43`/`d69`) — same omission that caused D185. +3. **Linux:** turn the CI Ubuntu `validate` job into a real compile job (same + PR as the romdata mmap patch) → then runtime bring-up on a Linux machine + (`docs/dev/LINUX-PORT.md` has the checklist). +4. D184 F10 overlay interactive feel-check still owed (unchanged from M-37). + +## Done this session (M-37) — F10 options overlay (approach C) (2026-09-02) + +Branch **`feat/f10-options-overlay`**, port-layer only, **not pushed, no PR**. +Finding **D184**. Implements `docs/dev/OPTIONS-MENU-PLAN.md` §2 approach (C). + +- **New:** `port/src/optionsoverlay.c`, `port/include/optionsoverlay.h` — an + immediate-mode overlay drawn as its own fast3d 2D DL appended after the game + DL. Closed ⇒ `optionsOverlayEmit()` returns NULL, nothing appended, golden + dumps byte-identical. +- **Hooks:** `port/fast3d/gfx_pc.cpp gfx_run()` (append after `gfx_run_dl`, + before `gfx_flush`); `port/src/video.c videoPumpEvents` (F10 toggle next to + F12; ESC closes; wheel → scroll; `videoRequestLiveConfig()` + + `liveCfgDirty` apply of VSync/FpsCap/TextureFilter at `videoStartFrame`); + `port/src/input.c inputComputePad` (controller 0 swallowed + nav routed while + open — mirrors the D180 WI-1 pattern); `port/src/config.c` + + `port/include/config.h` (`configForEachOption` + `configSetOptionMeta` + side table, `CONFIG_OPT_*` enum). +- **v1 rows:** VSync, FpsCap(0–360), MSAA(1/2/4/8, restart), TextureFilter, + MouseAimSpeed, MouseTurnSpeed, MouseInvertY, MouseCaptureMode, + ScreenShakeIntensity(0–3). Left/right adjust; live where applicable; + `configSave()` on close. +- **Diagnostic:** `GE_OPTIONSOVERLAY=1` auto-opens at boot (env-gated, left in). +- **Verified:** build links **241/241**. **Runtime checks NOT done** — this + headless env kills the GUI process after ~8 s and `gfx_opengl_dump_bound_fbo` + writes 0-byte PPMs, so `-level_09` framediff, the 60 s soak, and the + overlay-frame eyeball are **owed on an interactive machine**. +- **Owed feel-check:** open F10 in-level and in the front end; confirm layout + sanity, live VSync/filter apply, and that closed = no visual change. + +**Also M-37 (on `main`, committed):** +- **PR #5 (D177 ladders) confirmed merged** (`955349db`); branch + both agent + worktrees removed; D177 fully closed (climb test no longer owed). +- **D176(a) black sky — ROOT-CAUSED** (`4acadada`, findings.md "D176(a) — M-37 + UPDATE"). `gfx_pc.cpp:2901` deliberately no-ops `G_RDPHALF_*`; GE's cloud/water + sky emits geometry *only* as that stream. No PD shortcut. Fix = decode the + RDPHALF sky-tri stream in fast3d (~1 session). Scratch: `D176a-sky-rootcause.md`. +- **D176(b)** — offline ROM decode blocked (§7 toolchain absent). Next: grep the + converted `bg_sevx_all_p` GDL for opcode 0xC0, then runtime GE_DTEX image-ID + correlation. Scratch: `D176b-rom-groundtruth.md`. +- Commit trailers stripped from the F10 branch per the no-session-trailers rule. + +### Next session +1. **Finish D184:** interactive machine — F10 framediff (`-level_09`), 60s soak, + overlay-frame eyeball + nav feel. Tune row coords / arrow-repeat if needed. + Then push `feat/f10-options-overlay` + PR. (Contains one stray commit + `3c419e8d` D177-doc-tidy — harmless, merges fine.) +2. OR **D176(a) fix:** first `-level_22`/`-level_29` headless to confirm also + black (proves emit path), then write the RDPHALF sky-tri decoder in + `port/fast3d/gfx_pc.cpp` per the scratch note. +3. `main` has 3 uncommitted doc edits (Qwen 3→3.8, mermaid setup) — the user's + own concurrent work; leave for them to commit. + +## Done this session (M-35) — native-PC input + options-menu QoL run (2026-09-02) + +Branch **`qol/native-pc-input-menu`** (from `def0e0ac`), draft PR open, +**nothing merged to `main`**. Full review: `docs/dev/M-35-QOL-REVIEW.md`; +playtest handoff: `docs/dev/M-35-PLAYTEST-CHECKLIST.md`; WI-3 design: +`docs/dev/OPTIONS-MENU-PLAN.md`. Findings **D180** (input), **D181** +(screen-shake hook). Commits `91b7c5aa` → `849ee512` (+ docs). + +- **WI-1 (D180) — DONE, feel-check owed.** `Input.MouseCaptureMode` (0 = + legacy always-grab, **default**; 1 = Quake-style click-to-lock). + `port/src/input.c` + `video.c`, port-only, no `#ifdef PORT`. Mode 1: free + OS cursor until you click the window; ESC / focus-loss / entering a + front-end menu frees it; re-entering a stage while armed re-locks. + `reconcileGrab()` per poll off `current_menu`. Mouse buttons withheld + from the game while free in-stage. Controller paths untouched. **B3:** + `Input.MouseAimSpeed` default 25 → 16. +- **WI-2 (D180) — DONE for capture mode, feel-check owed.** When capture + mode is on and the cursor is free in a menu, the D165/D169 pointer + P-controller takes its target from the **absolute** OS cursor position + mapped onto the live virtual front-end rect → true 1:1, no drift. Route + (a), port-only. Legacy relative path unchanged; 1:1 menu tracking + requires `MouseCaptureMode = 1`. +- **WI-3 — foundation only.** `configRegisterFloat` / `configRegisterUInt` + + clamps landed (`port/src/config.c`); `configSave` unified. First + route-(b) hook: **`Game.ScreenShakeIntensity`** (D181, `src/fr.c + viShake`, `#ifdef PORT`, default 1.0 = exact no-op). **The options-menu + surface is a design doc only** (`OPTIONS-MENU-PLAN.md`) — recommended a + port-layer fast3d 2D overlay (F10), not a `src/` menu edit; resume + checklist in that doc. +- **Verified:** build 241/241; `-level_09` framediff 3/3 within threshold + (nonclear Δ ≤ 0.01pp, phash noise = documented D117); `GE_STARTMENU=7` + boot crash-free; golden dumps unaffected at defaults. +- **Resume point for a tuning session:** run `M-35-PLAYTEST-CHECKLIST.md` + item 0 first (default-config regression), then the click-to-lock and + aim-speed feel items. If `MouseAimSpeed = 16` still overshoots, drop the + default further in `port/src/input.c` and note it on the PR. + +## Where things stand + +- **BUNKER1 (`-level_09`) is playable-ish.** Loads, renders recognisably + (textured rooms, storage racks, floor; skeletal guards render as + humanoids), and **survives a guard firefight** — 45 s+ crash-free + (D103–D120). **Silo (`-level_20`) also loads + renders clean.** + Committed through `f2beae4b` (M-12: D121 WS1 boot, D122 propDef fix). +- **Input layer (Phase 3) landed and playtested** (D118, M-9/M-10). + `port/src/input.c` is real: keyboard+mouse + SDL_GameController → N64 + pad. Core aim/move works. **M-24 mouse-look rework** (`port/src/input.c`, + port-only): mode-aware map — aim mode (RMB) drives the analog stick past + ±60 and emits no C-buttons; hipfire keeps digital C-up/C-down pitch. + **D118b (mouse-Y inverted) and D118c (aim + mouse-down → crouch) FIXED**; + **D118a residual** — hipfire pitch still digital vs analog yaw (minor). + `config.c` INI load/save now implemented → `ge007.ini` is written on + first run and re-read; `[Input]` `MouseEnabled` / `MouseAimSpeed` (50) / + `MouseTurnSpeed` (100) / `MouseInvertY` (0) are live-tunable. Rebinding / + gamepad hotplug still TODO. Weapon switch on kbd = A button + (`Space`/`Z`/`E`) — no dedicated key. +- **Recent fixes (M-10, `d1e93b76`):** D119 guard-attack crash + (`weapons_held[]->chr` type-pun) fixed; D120 blood-stain hang guarded + (not fixed — `d43_emit.py` opcode-0x18 converter gap). +- **Cosmetic backlog (parked, `docs/GRAPHICS-BACKLOG.md`):** D75 front-end + 3D model transforms (Nintendo logo misplaced, gun-barrel Bond absent, + cast models absent), D76 disclaimer screen partial, D77 no audio, + D74 dead wrap-block. **D114/D116 "HUD/text X-mirror" — CLOSED, NOT A BUG + (M-33/D168): every `GE_PCDUMP` was vertically flipped; the game renders + correctly on hardware. PPM writer fixed.** + +## Done this session (M-33 part 2) — build unblocked, D168/D169 verified (2026-09-01) + +**Environment sorted.** The build works from the migrated tree: `PATH=/c/msys64/mingw64/bin` ++ ROM copied to `data/ge007.ntsc-final.z64` + the three sidecar regens +(`d43_emit.py` / `d69_emit.py` / `d88_emit.py --regen`, all pass clean) → `./build-pc.sh +ntsc-final` links 242/242, `-level_09` runs crash-free past frame 1200. **No `assets/*.bin` +extraction needed** — the committed `assets/` is self-sufficient; the ROM is runtime-only. +The old repo at `C:/Users/james/Source/Repos/007` has the ROM + a prior build. + +- **D168 — VERIFIED.** Rebuilt with the flip fix, captured `-level_09` frame 320: + the exit arrow points up, ammo "7|93" reads normally bottom-right — matches + `docs/img/bunker1-2.png`. The legal screen, RAREWARE logo and level-name + captions all render upright with normal text. **D114/D116 conclusively closed + as capture artifacts.** `tools_pc/golden/*.png` regenerated from a fresh + correct run (`framediff.py --update`) — stopgap flip replaced. +- **D75 — SHRUNK.** Upright bare-front-end capture (`GE_PCDUMP="700-1200:25"`): + **gun-barrel Bond renders and animates fine** (frame ~1075 walk, ~1150 fire, + upright tuxedo silhouette in the iris). The "Bond absent" reports were the + flipped capture. **Nintendo logo IS genuinely broken** — frame ~775 shows two + overlapping white ellipsoids shifted left, no wordmark (real `logoinst` + transform/geometry bug, not a flip). Cast roll not re-captured. Annotated in + `findings.md` §F "D75 Bug 2 — M-33 UPDATE" + `GRAPHICS-BACKLOG.md`. +- **D169 — FIXED** (`940f9426`, `port/src/input.c`, port-only). Menu pointer + clamp bounds now come from `getPlayer_c_screenwidth/height/left/top()` (the + live 440×330 front-end field) instead of hard-coded 320×240; estimator seeds + from the real `cursor_h_pos/v_pos`. Builds + links clean; `GE_STARTMENU=7` and + `-level_09` crash-free; framediff 3/3. **Interactive feel-check owed** — + headless can't move the mouse, so "every grid tile reachable" is inferred + from the corrected clamp math. +- **Task 3 — attract screenshots captured.** `docs/img/attract-{bunker1,silo,dam}.png` + committed (`ea98a4b3`), upright, pre-spawn intro-camera views. **Not wired into + the README** (developer to place; keep `archives-1`/`bunker1-1` as the top pair). + `-level_24` (Archives per `playtest.sh`) booted to an outdoor rocky area, not + the interior — didn't ship it; worth a look. + +Commits this session: `7ce4fead` `06d3eb22` `7292fc0e` (part 1) → `a0c7f2f5` +(developer: operating manual + doc cross-refs) → `940f9426` `ad134615` `ea98a4b3`. + +### Next +- **Interactive feel-checks owed:** D169 (mouse reaches every level tile; + file-select/main-menu unregressed) and the M-24/M-31 mouse-look knobs. +- **D75 residual:** Nintendo logo `logoinst` model renders as white blobs — + real transform bug; recheck cast roll with `GE_PCDUMP="1400-2200:25"`. +- **RC1** (wallet-Bond photo "180°"): needs a mode-select capture — interactive + nav, not done. Re-judge against upright. +- Wire the attract screenshots into the README once placement is decided. +- `-level_24` boots to the wrong area — investigate. + +## Done this session (M-33 part 1) — public-repo migration smoke-test + honesty pass (2026-09-01) + +First session in the migrated public tree (cleaned history, `docs/` restructured +into `docs/` + `docs/dev/`). Migration findings from part 1: + +- **Doc paths were stale** — `CLAUDE.md`/`AGENTS.md` referenced pre-restructure + filenames (`PORT-LEARNINGS.md`, `PCPortResearch.md`, `AGENT-WORKFLOW.md`). + **Fixed by the developer in `a0c7f2f5`.** +- **Task 1 — doc honesty pass (`7ce4fead`).** `README.md`: "Not playable yet" + callout; Status platform bullet rewritten (Windows-only testing; Linux build + unverified at runtime); Linux build steps flagged untested; `building.md` + + `ci.yml` comment clarified. +- **Task 2 — `GE_PCDUMP` vertical flip = D168 (`06d3eb22`).** Writer fixed; + D114/D116/D75 annotated across `findings.md`, `GRAPHICS-BACKLOG.md`, + `TEXTURE-GLITCH-ANALYSIS.md`, `porting-notes.md §D2`, `GE-ENV-PROBES.md`. +- **Task 4 — D169 root-cause writeup (`7292fc0e`)** — 320×240 vs 440×330 clamp + mismatch in the menu pointer P-controller. + +## Done this session (M-32b) — tactical follow-up (2026-08-31) + +3 commits on `master` (`49ce620a`, `7071b110`, + this reconciliation). + +- **Debug-scaffolding strip (`49ce620a`).** Removed inert env-gated + `GE_D54`/`GE_D63`/`GE_D90` probe blocks from compiled `src/` + (`memp.c`, `bg.c` incl. the bare non-PORT `d63bgprimarycount` static + + entry log, `blood_animation.c`, `dyn.c`, `rsp.c`, `bondview_r.c`, + `csplayer.c`, `load.c`, `seqplayer.c`; 161 lines). `GE_D54`/`GE_D90` now + fully gone; `GE_D63` partially (remaining blocks all `#ifdef PORT` + + getenv, inert — strip candidates for later). Build green; `-level_09` + + `-level_20` render crash-free (19 frames each). `docs/GE-ENV-PROBES.md` + updated. +- **D75 Bug 2 runtime probe (`7071b110`).** `GE_D75=1` in `title.c` + `sub_GAME_7F007F30`. Booted bare front end through the gun-barrel: + Bond + gun models absent; **both Model instances valid** (nMtx 21/1); + **`render_pos` valid + fresh each frame** (== that frame's `dynAllocate` + mtxlist base, clean double-buffer, o2p unchanged) → the "stale arena" + hypothesis (D115 #5) is **RULED OUT** — do not land a persistent + `render_pos` buffer. **Zero fast3d DL warnings** → not D144/D146 either. + Failure is downstream in `drawjointlist`/`dotube` vtx/node-DL resolution + or an off-screen `basemtx`. Next: a drawjointlist-level probe. Full + write-up: §F "D75 Bug 2 — RUNTIME PROBE". +- **RC3 Depot eyeball — INCONCLUSIVE, default stays OFF.** Captured Depot + (`-level_30`) with `GE_WRAPFIX=1` vs `0`. Depot is too dark in the + boot-camera area to judge the non-PoT (65×65 / 96×48) ceiling/panel + surfaces the fix targets; the visible corrugated-container walls look + unchanged and un-regressed with the fix on. Not enough signal to flip + `cfgWrapFix=1`. Needs a **live human eyeball** on Depot with + `Video.WrapFix=1` (walk to a lit area / the ceiling), or a `GE_DTEX` + capture on the specific surface. `port/src/video.c` `cfgWrapFix` unchanged. +- **Sidecars regenerated** (`d43_emit.py` / `d69_emit.py` / `d88_emit.py + --regen`, ntsc-final) — ready for a campaign playtest. + +### Next (M-32b) +- **RC3** — human eyeball Depot with `Video.WrapFix=1`; flip `cfgWrapFix=1` + in `port/src/video.c` if a lit non-PoT surface is visibly better and + `-09/-20/-34` don't regress. +- **D75 Bug 2** — drawjointlist/`dotube`-level probe (resolved vtx/nodeDl + ptrs, whether any tris emit, composed `basemtx*render_pos` for joint 0). +- **D143** blank briefing/objective text — untouched this session. +- **D154 / D152+ / D160** — still playtest-gated. + +## Done this session (M-31/M-32) — 6-agent parallel burst #2 (2026-08-31) + +6 worktree subagents, files partitioned; 5 merged to `master` +(`0c918ab4`..`1753ac7a` cherry-picked, then a reconciliation commit). +Combined tree builds clean (240/240); `-level_09`/`-level_20` render +crash-free (framediff phash = documented D117 intro-pan noise, nonclear +coverage stable to 0.04pp); 6/6 texture-heavy levels (Depot/Facility/ +Runway/Archives/Streets/Caverns) boot+render+no-crash. + +| Item | Result | Verified | +|---|---|---| +| **D154** (`bg.c`) | Existing wall-shoot GBI-parser port was mostly right, but `vtxoff = gdl->dma.par & 0xf` was **always 0** — the PC `Gdma_le` shim maps `.par` to word0 bits 0-23 (packed length), not the N64 params byte. Fixed → `((u32)gdl->words.w0 >> 16) & 0xf`. Also ported a sibling raw parser: `bgTestBulletHitBackground` G_SETTILE back-scan (~3841). `GE_D154=1` diag added. | build 242/242; **playtest-gated** (needs firefight into a wall on an idle machine) | +| **D152+** (`snd.c`, `libultra.c`) | Audit: every compiled-audio `osSetIntMask(OS_IM_NONE)` is **balanced** — the §F "unbalanced early-return" guess was wrong. Real fixes: `sndSetSfxSlotVolume` now holds the mask across its `ALSoundState` walk (matches its twin `sndDeactivateAllSfxByFlag`) + `sndApplyVolumeAllSfxSlot` batches its loop under one recursive hold (kills the mission-failed fade lock-storm); `portThreadWrapper` → `imThreadExitRelease()` releases an orphaned lock on thread exit (kills the transient-thread-died leak + pthread-id-reuse re-wedge). Steal-lock kept as backstop. | build 240/240; `-level_09/-20` crash-free; **fade-out repro playtest-gated** | +| **RC3 / D167** (`gfx_pc.cpp`) | fast3d never stored the N64 tile `mask` — wrapped every repeating texture at GL image size, not `1<render_pos` → transient `dynAllocate` arena (D115 item #5). Needs a runtime probe. | write-up only | +| **D160 / D148** (docs) | "propDef command-index walk desync" hypothesis **DISPROVEN** — exhaustive static trace: every record type Dam emits has `converter PC bytes == sizepropdef()×4`, stream tiles byte-exact, walk in lockstep, all 21 levels clean. Dam rappel cutscene is runtime AI-script / `CAMERAMODE_POSEND` cinematic render (D75 family). `GE_D160=1` diag already ships. | needs live Dam-to-exit playthrough | +| **Docs** | `TEXTURE-GLITCH-ANALYSIS.md` §0 status table reconciled (RC2 FIXED / RC4 RETRACTED / D159+D161 FIXED / RC3 done-behind-knob); new `docs/GE-ENV-PROBES.md` (full env-var table); GE_D63 strip-safety checklist (all inert; only `bg.c:2876/2880` is a bare non-PORT `static` + entry log worth deleting for N64 hygiene). | — | + +**Worktree-harness note:** 3 of the 6 agent worktrees were created from a +**stale commit** (`0a4b3bae`, ~30 behind); a mid-flight `git reset --hard +master` per agent recovered them. Watch for this on future bursts — check +each agent's HEAD in its first report. + +### Next +- **D75 Bug 2** — runtime `GE_PCDUMP` + capped probe on the front-end + animated-model `render_pos` / `dynAllocate` path (the one in-scope lead). +- **RC3** — eyeball Depot with `Video.WrapFix=1`, flip `cfgWrapFix=1` in + `port/src/video.c` if clean. +- **D154 / D152+ / D160** — all playtest-gated; verify during the campaign + playthrough (wall-shoot / mission-fail / Dam exit). +- Strip the `bg.c:2876/2880` bare D63 entry log. + +## Done this session (M-12) — 2 commits + +- **WS1 / D121 (`02c12068`)** — bare `./build-pc/ge007.x86_64.exe + -level_XX` auto-injects that stage's `memallocstringtable[]` `-m*` pool + row (`#ifdef PORT` in `boss.c bossInitMainthreadData`). No manual `-m*` + args any more. Gotchas recorded: setting `g_DebugAndUpdateStageFlag=1` + is the WRONG fix (routes boot through the title-stage intro), and + `tokenSetString` *replaces* the whole token buffer so the injected + string must carry `-level_XX` forward. See §H D121. +- **D88.4 was already resolved** (committed `ff563812`/`4ccc0d3c` weeks + ago). The "OPEN — cross-level blocker / `setupDoor` crash" note in the + old §F index / plan was **stale**. `d88_emit.py --regen` = 21/21 pass. +- **D122 (`f2beae4b`)** — the real cross-level blocker was prop/item + **model loading**. `tools_pc/d88_propdefs.py` had a per-type handler + table + a generic fallback; 6 `inherits ObjectRecord` propDef types + (47 TINTED_GLASS, 39 VEHICHLE, 40 AIRCRAFT, 45 TANK, 13 AUTOGUN, 20 + AMMO/MultiAmmoCrate) had **no handler** → fell to the generic + word-granular `bswap32`, which swapped the `[s16 obj][s16 pad]` word as + one u32 → model id `obj` in the wrong half → OOB `PitemZ_entries[]` + deref → crash in `modelLoad` (`loadobjectmodel.c:393`) / + `modelInitRwData` (`model.c:6249`). Fixed in the converter + (`OBJ_TAIL_DESC` handler) + matching `sizepropdef()` `#ifdef PORT` + stride. BUNKER1/Silo were never affected (they don't emit those types). + Full write-up: §F/§H **D122**. Confidence: crash fix high; types + 39/40/45 tail layout medium (structs have "locs unconfirmed" notes — + nudge if a level actually drives a tank/vehicle). + +**Uncommitted docs** (tangled with the in-progress big docs restructure — +`HANDOFF.md`, `AGENTS.md`, `CLAUDE.md`): the D121 + D122 entries in +`docs/PCPortResearch.md` §F/§H and this file's edits. Commit or fold in. + +## Done this session (M-17) — 4 commits, 13→18/21 PASS + +- **D126** (`e851e2ab`) — objective sub-records `criteria_picture` (30), + `criteria_roomentered` (32), `criteria_deposit` (33), + `setup_objective_text` (35) each end in a `T *next` list pointer that + `set_parent_cur_obj_*` / `setup_briefing_text_entry_parent` write while + walking the setup stream. Widens 4→8B and 8-aligns at offset 16 on PC → + struct is 24B/6w (N64 16/20); the old N64-sized emit meant the runtime + 8-byte `->next` store clobbered the *next* propdef record's header → + walk desync → command indices drifted ~100 → `setupDoor` `linkedDoor` + resolution landed on the wrong record. Fixed `d88_propdefs.py` + (`PROPDEF_PC_BYTES[30/32/33/35]=24` + typed handler) + + `loadobjectmodel.c` `sizepropdef` PORT returns 6. **Cleared C3r Bunker2 + + C4 Depot + C6 Surface2** in one change. +- **D127** (`1129f63d`) — `sndPlaySfx` guards a bogus `ALSound*` (the + converted libaudio bank has fewer/rearranged `soundArray` slots than + N64; audio is Phase-3 parked). **Cleared C7 Surface1.** +- **D128** (`749feb9f`) — `sub_GAME_7F0B37EC` special-portal marker used + the hardcoded N64 8-byte `bg_portal_data_entry` stride / `controlbytes1@6`; + on PC the struct is 16B with `controlbytes1@10`, so `|= 2` scribbled + into the middle of a portal's `offset_portal` pointer → `bg.c:5723` + crash on any level with a `specialportalarray` entry. `#ifdef PORT` + uses the struct accessor. **Cleared C5 Control.** +- **D129** (``) — `langGet` bounds-checks the slot id and + resolved bank pointer. Bare `-level_XX` boots that reach the + cast/credits text path (Cuba, `bondviewRenderCredits`, D76 area) + reference banks the menu flow never loaded. Reduces the Cuba bare-boot + end-credits crash; **Cuba itself loads + renders 300+ frames fine** and + the credits work through the real front-end flow. + +## Done this session (M-18) — 1 fix, 18→20/21 PASS + +- **D130** (`port/src/romdata.c`, uncommitted) — Facility `-level_34` + + Runway `-level_35` C2 crash (`import_texture_i8` AV on wild ptr + `0x72181ee8`). Root cause was NOT the model-GDL relocation the + D124-Facility addendum / `BRIEF-C2gdl-model-reloc.md` suspected — + disproved: `texLoadFromGdl` never copies a `G_SETTIMG` on these levels, + `texWriteLoadToTmem*` is never called, and `gdl` in `sub_GAME_7F0762E0` + is still a segmented `0x05xxxxxx` value so its `& 0x00ffffff` masks are + correct. Real cause: **`romdataFixupFont` corrupts BankGothic/ZurichBold + glyph indices 0/1/2** — the in-place N64 24B → PC 32B `fontchar` relayout + aliases `dst`/`src` for low glyph indices (stride grew 8B < the 24B field + read span), so a field write clobbers a later field's source mid-loop + (glyph 1 `width` = `bswap(index)`, glyph 2 `pixeldata` ≈ `0x020002e8` → + `+= font_base` → wild). Facility's title "Chemical Warfare Facility #2" + renders `#` → `gDPLoadTextureBlock(gdl, curchar->pixeldata=wild, …)` → + fast3d AV. Fix: stage all 6 N64 fields in a local `f[6]` before writing. + §F/§H **D130**; PORT-LEARNINGS §A. Verified: -level_34 0/~14, -level_35 + 0/4; -level_09/20/24 unregressed. + +## Done this session (M-19) — D131 (Jungle), D63 scaffolding removal + +- **D131** (`port/fast3d/gfx_pc.cpp`) — Jungle `-level_37` C2m crash. + `explosionRenderPropSmoke` passes a compiled `.bss` matrix symbol + (`&dword_CODE_bss_8007A100`) through `osVirtualToPhysical()` — a + `u32`-returning shim — into `gSPMatrix`, truncating the `0x1_00000000` + module high word → w1 = `0x401c68e0` → wild deref in `gfx_sp_matrix` + when the first explosion draws (~frame 300). NOT `gimgfixup` / the + model-GDL path (that premise was already dead per D130). Fix: `seg_addr` + restores the module high word for a fallthrough w1 in + `[0x40000000, 0x70000000)`. Covers ~30 latent `osVirtualToPhysical()` sites (explosion/glass/blood/bondview2). Verified: + Jungle renders to frame 1500–2400+ crash-free; `-level_20`/`-level_24` + unregressed. §F/§H **D131**, PORT-LEARNINGS §A. +- **D63 scaffolding removed** — dead TEMP D63 probe calls stripped from + `port/src/libultra.c` (watchdog thread + activity ring + slot watchers) + and the `#ifdef PORT` probe blocks in `blood_animation.c`, `dyn.c`, + `front.c`, `rsp.c`, `title.c`. All inert (env-gated / no side effects); + build links clean. `port/fast3d/gfx_pc.cpp` D63 trail code left for a + later pass. Bug class D24-implications + `osYieldThread` watch item added + to PORT-LEARNINGS §E. + +## Done this session (M-23) — WS6 Facility playthrough: D135/D137/D138/D139 FIXED, D136/D140/D118c open + +First real human WS6 playtest. User speed-ran Dam OK, then hit a chain of +crashes on Facility (`-level_34`) — each one a subsystem the 21-level sweep +never exercises (no weapon fire, no stage unload, no pause menu). Fixed four, +two new open, Facility now near-complete. **Committed `ec51d9c5`** (the +older D121/D122/D126/D128/D130/D131 write-ups are still uncommitted, tangled +in the docs-restructure — fold those separately). + +- **D135 (FIXED)** `src/game/propobj.c` — firefight crash. `bgTestHitOnObj` + (bullet-ray vs object triangle geometry, via `propobjFindHit` on every + shot that resolves on an object model) is an **unported N64 GBI parser**: + raw 8-byte-`Gfx` byte/word indices desync on the PC 16-byte `Gfx` stream → + walks off the DL → AV `0x709ae02a`. Ported `#ifdef PORT` to `gdl->words.w0/ + .w1` shifts (mirrors PD `pd_port/src/game/bg.c:3635`). Second fault after + the parser fix: `objHit` `propobj.c:9717` `% impact_sounds->thing2_len` int + div-by-0 — the ported texnum-recovery (`*(s16*)(phys(w1)-8)`) returned a + bogus `texturenum` → OOB `g_HitTypeSounds[]`. Fixed: PORT texnum branch + always `-1` (`isnd_default`, safe; generic impact sound/decal, park w/ D77). +- **D137 (FIXED)** `src/game/gunfire.c` — right-mouse (raise crosshair) crash. + `gunDrawSight` `s32 sp54` holds a `Gfx*` the whole fn (`texSelect`/ + `display_image_at_position` take `Gfx**`); 4-byte slot → `*(Gfx**)&sp54` + splices adjacent stack → wild DL write in `texSetRenderMode`. `#ifdef PORT`: + `sp54` is `Gfx *`. §A. +- **D138 (FIXED)** `src/snd.c` — the "1 room from the end" *freeze* (not a + crash — kernel-heartbeat watchdog). `sndCreatePostEvent` → + `alEvtqPostEvent` (`libultra/audio/event.c:110`) walks `evtq->allocList`; + parked audio thread never drains it, `chrobjSndCreatePostEvent` posts per + objTick per ambient-sound object, Facility is dense with machinery → the + list walk stalls the main thread. `#ifdef PORT`: `sndCreatePostEvent` is a + no-op until audio lands (D77). No gameplay effect. +- **D139 (FIXED, UNVERIFIED)** `src/game/cleanup_objects.c` — stage-unload + crash (`lvlUnloadStageTextData` → `cleanupObjects` → `objFree`, `obj->prop` + = packed-float garbage). Root cause: `cleanupObjects` walks propDefs with + `(u8)obj[0]` for the type, which works only because the header word + `[u16 extrascale][u8 state][u8 type]` is **big-endian** (type = low byte). + On LE the low byte is `extrascale` → the walk never matches `PROPDEF_END`, + runs off the blob, dispatches the switch on garbage → `objFreePermanently` + on non-objects → crash. `#ifdef PORT`: use `((PropDefHeaderRecord*)obj)-> + type` (offset 3) like every other consumer (`sizepropdef`, proplvreset). + **Not confirmed** — when the user hit pause to trigger a fast teardown + test, the pause menu itself crashed first (D140). `-level_09` unregressed + (91.65%). §F **D139**, PORT-LEARNINGS §C (BE header byte on LE). +- **D140 (OPEN)** — **pause menu crashes.** `maybe_mp_interface` → + `bondviewRenderWatch` (`bondview2.c:8604`) → `bondviewTransformManyPos- + ToViewMatrix(g_CurrentPlayer->field_23C = NULL, objheader->numMatrices=9)` + → `matrix_4x4_copy(src=0x0)` AV. The watch model's per-node view-matrix + cache (`struct player.field_23C`) is never allocated (or `field_23C` is at + the wrong PC offset — `struct player` layout, `docs/AUDIT-M6-player- + offsets.md`). Blocks the WS6 pause-menu / objective-status checks and the + watch gadgets. D75 3D-model-transform family. Files: `src/game/bondview2.c`, + `src/bondtypes.h` (`struct player`). +- **D118c (OPEN, low pri)** — in manual-aim mode, mouse-down → **crouch**. + Mouse Y emits C-down; `bondview2.c:5351` maps C-down in `insightaimmode` + to `crouchDown`/zoom as well as aim pitch. No input-layer-only fix; needs + the deferred analog-aim `#ifdef PORT` hook (§F D118). Documented, parked. +- **New harness** `tools_pc/repro_gdb.sh ` — nohup-launch a level, attach + gdb, dump full `bt full` + locals on the first SIGSEGV/SIGFPE, rolling + frame dump to `ppm/`. Needed because `ge007.crash.log`'s own backtrace is + `#01 = null` (native unwinder fails). Also `build-pc/d136_cmds.txt` — a + gdb cmd file that traces every `objFree` call (`type`/`obj`/`prop`/`model`) + then catches the fault; the "last line before FAULT" localises D136-class + walk-off bugs. **Attach the winpid via `ps -W | grep ge007.x86_64 | + awk '{print $4}'`** — `repro_gdb.sh`'s own `ps -p $!` derivation is wrong. +- **Follow-up (not done):** the BG room-geometry hit-test in `bg.c` + (`~3373-3646`, walks the D85-widened `ptr_expanded_mapping_info`) is the + identical unported GBI parser to D135 — latent, fires on shooting + walls/floor. Same mechanical port. §F D135 + PORT-LEARNINGS §B. +- **Controls** (for the new session's own playtesting): kbd — WASD/arrows + move, `A`/`D` sidestep, mouse X turn (analog), mouse Y look (digital + C-button), LMB/LCtrl fire, RMB/LShift aim, `Space`/`Z`/`E` = A (action AND + weapon-switch — GE overloads it), `X`/`R`/`F` = B (reload), `Q` = L, + `Enter`/`Tab` = Start, `Esc` quit. No dedicated weapon key. + +## Done this session (M-24) — mouse-look rework + real INI parser + playtest QoL + +Pre-playtest QoL, port-layer only, no `src/` / game-logic change. +**Full review sheet: `docs/M-24-QOL-REVIEW.md`** (rationale, per-change +risk, verification, what to check by playing). Committed `f3ec5170`, +`65ed0315`, `33506aee`. + +- **`port/src/input.c` mouse-look rework.** Read `bondviewProcessInput` / + `MoveData`: GE aim is **mode-dependent** — hipfire yaw = analog stick-X, + pitch = digital C-up/C-down (stick-Y = move, no pitch); aim mode (R held) + yaw+pitch = analog stick past ±60, and C-up/C-down there = crouch/lean/ + zoom, *not* aim. New map keyed on our own RMB/LShift (hold-to-aim proxy): + aim mode pushes `stick_x/y` into the 61..80 band and emits **no** + C-buttons; hipfire keeps digital C-pitch. GE's native pitch is inverted + (C-up→look down) — hidden so mouse-down looks down; `MouseInvertY` flips. + - **D118b FIXED** (mouse-Y inversion), **D118c FIXED** (aim+down→crouch — + no `src/` hook needed, aim look no longer emits C-down). + - **D118a residual**: hipfire pitch digital vs analog yaw. Minor (precise + vertical aim is an aim-mode activity). Full fix = the deferred analog + `#ifdef PORT` `bondview.c` hook. +- **`port/src/config.c` — INI load/save implemented** (was a stub that only + logged TODO). Parses `$S/ge007.ini` (`[Section]` blocks, `Key = value`, + `#`/`;` comments), clamps ints to registered bounds, writes defaults on + first run, rewrites on clean exit. Unblocks live tuning of the mouse + knobs + all future video options. Note: constructor-registered options + present at `configLoad()` time (main.c:52) are captured; anything + registered later would miss the first save. +- **New knob** `Input.MouseTurnSpeed` (hipfire yaw %, default 100), split + from `MouseAimSpeed` (aim-mode %, default 50) since they feed different + game curves. +- Verified: `-level_09` boots crash-free to 900+ frames @ 91.65% coverage + (unregressed); `-level_20` crash-free; `ge007.ini` written then re-read + with no unknown-key warnings. Build green (`ntsc-final`). +- **`port/src/video.c` QoL:** **F12** takes a screenshot + (`ppm/shot_NNN.ppm`, dumped on the render thread via the existing + `gfx_opengl_dump_bound_fbo`); window **focus loss frees the mouse** + (`inputSetMouseGrab(0)` on `SDL_WINDOWEVENT_FOCUS_LOST`, re-grab on + gain) so alt-tab works. `inputSetMouseGrab()` also zeroes the aim delta + and suspends mouse reads while released. +- Alt-Enter fullscreen toggle already existed (`gfx_sdl2.cpp:299`). +- **Gamepad hotplug** — `SDL_CONTROLLERDEVICEADDED/REMOVED` → `inputRescanPads()` + (close all + re-open). Caveat: the game latches `inputConnectedMask()` at + `osContInit` (boot), so a pad added later merges into controller 0 for + play but won't appear as a separate channel — plug it before launch for + multi-pad. +- **Mouse-wheel = weapon cycle** — a notch queues a 2-poll A-button press; + GE's default scheme (`invButtons = A_BUTTON`, `bondview2.c:5162/5326`) + cycles the weapon forward on a bare A edge. Both wheel directions cycle + forward (no clean backward input without the A+fire combo). +- **Window title shows live FPS** (`wmAPI->set_window_title`, ~1 Hz). +- **Still TODO (not started):** key rebinding, on-screen (in-render) FPS + overlay, in-game options menu. + +## Done this session (M-25) — 3 commits, port-layer QoL + +Free-roam QoL pass, `port/` only, zero `src/` / game-logic change. Full +review sheet: **`docs/M-25-QOL-REVIEW.md`**. All defaults reproduce prior +behavior exactly. + +- **`9ebed821` — `[Video]` ge007.ini knobs.** `Video.VSync` / `FpsCap` / + `MSAA` (1/2/4/8) / `TextureFilter` (0 nearest, 1 bilinear) / `Fullscreen`, + wired in `video.c videoInit`. Plus **config auto-migration**: `config.c` + now rewrites `ge007.ini` once when the build registers a key the file + never had (so a new section actually appears) — an up-to-date file with a + user comment is left untouched. +- **`5a7a1035` — `tools_pc/playtest.sh`** takes a level *name* + (`playtest.sh bunker2`) or number, `--list` prints the 21-level table, and + a crash now auto-runs `addr2line` on the faulting PCs. +- **`863f436b` — `[Input]` ge007.ini knobs.** `MouseYScale`, + `MouseSmoothing` (0 = off = today), `PadDeadzone` (7000), `PadTriggerPct` + (23), `PadLookInvertY`. +- Verified per commit: build green, `-level_09` crash-free to frame 620 @ + 91.6% (unregressed), `-level_20` crash-free. +- **Silo `-level_20` capture freeze — D117/D134 load sensitivity, NOT a + real bug and not an M-25 regression.** Full-length captures froze at + ~frame 320 (silo→Bond camera descent) twice, but only on a machine still + loaded from the sweep (the pre-M-25 baseline froze the same way). On an + idle machine the user drove Silo live past ~1800 frames, VI pacemaker + healthy, no stall. See `docs/M-25-QOL-REVIEW.md`. + +## Done this session (M-26) — port-layer QoL batch (A/B/C/E) + +Low-risk QoL, `port/` only, zero `src/` change. All defaults reproduce +prior behaviour. Full review: **`docs/M-26-QOL-REVIEW.md`**. + +- **A — save-on-exit + `[Window]` persistence.** `atexit(portAtExit)` in + `main.c` (`videoSaveWindowState()` + `configSave()`), fires on every + clean quit (`exit(0)` in `videoPumpEvents`); `abort()` paths skip it. + New `[Window]` ini section (`Width/Height/X/Y/Maximized`, sentinels = + old behaviour) in `video.c`; `Video.Fullscreen` now round-trips. +- **B — `level_sweep.sh` STALLED verdict.** Frames rendered then froze + with the process still alive → `STALLED (froze at N/M frames)` instead + of a bogus PASS (the Silo case). Retried once like NO-FRAMES. +- **C — raw mouse input.** `Input.MouseRawInput=1` (default 0) sets the + SDL relative-scale / warp hints off in `inputInit` — no OS pointer accel. +- **E — `--help` / `--version`** in `main.c` before `crashInit()`: build + id, usage, the 21-level `-level_XX` table. +- **D — `[Debug]` ini flags.** `Debug.FrameDump` (mirrors `GE_PCDUMP`), + `Debug.InputLog` (mirrors `GE_INPUTLOG`); env var wins, ini is the + fallback. `config.c` `configGetFrameDump()`/`configGetInputLog()`. +- **Verified:** build green (`ntsc-final`); `-level_09` + `-level_20` + boot crash-free 6/6 GE_PCDUMP frames (unregressed); `ge007.ini` gains a + populated `[Window]` block on exit. + +## Done this session (M-27 continued) — FULL front-end loop works end to end + +**User playtested Dam → exit → post-mission report → Facility briefing → +Start, exactly like the retail game.** No crashes, no hangs. 10 commits +`bb6ac627`..`0fdb14a3` (all committed). + +- **D142** (`src/bondconstants.h`) — GCC/mingw compiles an all-non-negative + `enum` as UNSIGNED, so `for (s = SP_LEVEL_EGYPT; s >= SP_LEVEL_DAM; s--)` + in `fileGetHighestStageDifficultyCompletedForFolder` never terminated → + SELECT FILE screen froze the game. Fix: `#ifdef PORT` negative sentinel + in `LEVEL_SOLO_SEQUENCE`. PORT-LEARNINGS §D3. +- **D143** (`src/game/textrelated.c`) — `langGet()` returns NULL for a + string slot the PC menu flow hasn't loaded; `textRender/textMeasure/ + textWrap` faulted on it. NULL guards → blank text instead of crash. + (A string bank still isn't loading — briefing/objective text is blank. + Cosmetic, chase later.) +- **D144 / D146** (`port/fast3d/gfx_pc.cpp`) — a front-end 3D model + (MISSION COMPLETE dossier / mode-select wallets, D75 family) emits a + malformed compiled sub-DL (`seg5+0x9ee4`): unresolved matrix pointer, + then garbage opcodes. D144 = bad matrix ptr → identity; D146 = unknown + opcode → end the DL (was `sysFatalError`→`abort()`, no crash log) + + `fast3d_ptr_ok()` guards on `gfx_sp_vertex/movemem/set_vertex_colors`. + Model renders wrong/absent (GRAPHICS-BACKLOG D149) but the game survives. +- **D145** (`port/src/video.c`, `gfx_sdl2.cpp`, `input.c`) — bare **ESC was + `exit(0)`** in two event handlers; on the debrief screens ESC is the + natural "back" key so paging with it quit the game (clean exit, no log, + looked like a crash). ESC now = N64 B (back/cancel); quit = window-X / + Alt+F4. +- **D147** (`port/src/libultra.c`) — **the end-of-Dam hang.** `sndPlaySfx` + → `alEvtqPostEvent` on the main thread races the `amMain` audio thread + in `sndRemoveEvents` on the same `ALEventQueue`; both "lock" with + `osSetIntMask(OS_IM_NONE)` which was a **no-op** → list corruption → + infinite spin. `osSetIntMask` is now a process-wide **recursive mutex** + (`OS_IM_NONE` acquires, `OS_IM_ALL` token releases). Thread dump + confirmed. PORT-LEARNINGS §D4. Verified `-level_09` 1200 frames clean. + +**New test tooling (committed):** +- `tools_pc/debug.ps1` — `.\tools_pc\debug.ps1 [-level_09] [-Menu 13] + [-NoBuild]`. Builds, runs under gdb, prints FATAL line + backtrace + + crash log on exit → `gdb.txt`. Use this for every playtest so a + crash/hang always leaves a trace. +- `GE_INPUTSCRIPT=":,…;…"` (`port/src/input.c`) — headless + scripted controller-0 input (buttons pulse, `SUP/SDOWN/SLEFT/SRIGHT/ + SNONE` sustain). Sole input source when set. +- `GE_STARTMENU=` (+ `_PAGE`, `_DIFF`) (`src/game/lv.c`) — boot + straight into a front-end menu (13=MISSION_COMPLETE, 10=BRIEFING, + 7=MISSION_SELECT, 12=MISSION_FAILED, 6=MODE_SELECT). Skips a save/unlock + so it lands on Dam regardless; good for crash-testing a screen, not for + reproducing real objective state. + +### Still open (cosmetic / not blocking, parked in GRAPHICS-BACKLOG) +- **D148** — Dam level-end cutscene (Bond rappelling down the dam) doesn't + play; cuts straight to the report. Scripted-cutscene / cinematic-camera + + rappel anim; D75 family. +- **D149** — front-end MISSION COMPLETE / mode-select 3D models garbled + or absent (the D144/D146 corrupt DL). D75 family. +- **D143 side effect** — briefing / objective text renders blank (a lang + string bank not loaded in the PC menu flow). Find which bank. + +### Next +- Continue the mission list: Facility → Runway → … playtest with + `tools_pc/debug.ps1`, same loop. In-level ABI/GBI crashes are the + expected work (D122/D135/D147 pattern). +- The D75 front-end / cutscene 3D-model family (D148/D149) is now the + biggest *visible* gap but is cosmetic — below crashes. + +## Done this session (M-30) — user batch defect list; commits + docs + +### M-30b — 6-agent parallel burst (2026-08-31), all merged + verified + +7 commits `84a8693e`..`1b69c8ba`. Merged tree builds clean; `-level_09/-20/-30/-34/-37/-28` +render + no crash; menu + Depot visually verified; Agent D's 21/21 with-input +(walk+fire) sweep crash-free. Three texture-pipeline fixes compose correctly. + +| Dxx | Fix | Verified | +|---|---|---| +| **D158** | KF7 muzzle-flash `((f32*)stackpad2)[-8]` negative-stack write → `0xc000001d` on fire. Real `#ifdef PORT` local. | Caverns survives sustained KF7 fire; stackpad audit found no others | +| **D159** | `texSwapAltRowBytes` (odd-row byte-swap for an N64 RDP TMEM XOR fast3d doesn't emulate) scrambled every ~1:1-viewed texture = the user's "interlaced textures". `#ifdef PORT` skip. | menu wallet-photo: comb → clean portrait; `-level_09` unregressed | +| **D161** | Depot ceiling: 16×16 CI8 tile drawn with TLUT off (`G_TT_NONE`) → fast3d did a palette lookup on stale palette → blue speckle / radial rays. Decode CI+`G_TT_NONE` as intensity. | Depot ceiling: blue garbage → dark industrial roof; `-30/09/34` pass | +| **D164** | Legal/disclaimer screen drew 1 line — `legal_text_end = &legalscreen_MRD` assumed linker adjacency (false on mingw). `#ifdef PORT` bound on the array. | high-confidence static (nm-verified); needs a boot to eyeball | +| **D165** | Front-end mouse cursor: velocity² feel → P-controller 1:1 pointer. `Input.MenuPointerMode` (def 1), `Input.MenuPointerSpeed`. | build + menu smoke; user feel-check pending | +| **D166** | Hipfire vertical aim: fixed digital threshold → speed-proportional C-button duty cycle. `Input.HipfirePitchSpeed`. | build; user feel-check pending | +| **D160** | Dam rappel cutscene (D148) not root-caused — `GE_D160=1` diagnostic shipped. Hypothesis: propDef command-index walk desync (D122/D132 family). | **needs: user runs Dam to exit with `$env:GE_D160=1`, pastes `D160:` lines** | + +Also this session (pre-burst): D157 (campaign unlock — user-confirmed), RC2 +(`Video.FixMipTextures`), D120 (PointUsage), D74 (`Video.WrapFix` opt-in). + +### M-30 state / next +**⚠ Before the next playtest: `./build-pc.sh ntsc-final` AND the full sidecar +regen** (`python tools_pc/d43_emit.py ntsc-final && python tools_pc/d69_emit.py +ntsc-final && python tools_pc/d88_emit.py ntsc-final --regen`) — D120 changed +`d43_emit.py` (PointUsage) and needs the regen; `debug.ps1` rebuilds the exe +but does NOT regen sidecars. (Done on this session's machine already.) + +**Campaign progression + saving works (D157, user-confirmed).** The user can +now do the full Track A playthrough — each level boundary exercises the same +save path. `GE_UNLOCK_ALL=1` jumps to any level; `GE_SAVELOG=1` traces the +save chain if a later level misbehaves. + +Fixed + committed this session (all on `master`, unpushed): +- **D165 / D166** (M-31, `port/src/input.c`, port-only) — input polish. + D165: the front-end cursor is now a true ~1:1 pointer (P-controller + estimating the game cursor through `front.c`'s own integrator) instead of + the M-30 velocity² feel; `Input.MenuPointerMode` (default 1) falls back to + the old mode, `Input.MenuPointerSpeed` scales it, `GE_INPUTLOG` traces + `menuptr est/tgt/eff/stick`. D166: hipfire mouse pitch is speed-proportional + C-button pulses, not a digital threshold (`Input.HipfirePitchSpeed`). + Build green; `-level_09` + `GE_STARTMENU=6` smoke crash-free. **User + feel-check owed** (headless can't drive the mouse) — tune MenuPointerSpeed + up if the menu sweep feels short. +- **D157** — campaign never unlocked (objective-difficulty byte read at the + wrong offset on LE). Root-caused + user-confirmed. §F D157. +- **RC2** (`Video.FixMipTextures`, default on) — LOD textures were uploaded + ~1.3× too tall; clip to base-tile height. Modest visual win, no regression. +- **D120** — `d43_emit.py` now emits the opcode-0x18 `PointUsage[]` chain + (was zero-filled → blood-decal walk cycled, guarded). Interactive verify + pending (BUNKER1 firefight). +- **Menu #4** — stripped the per-frame D63 log flood that collapsed the + front-end to 5fps (`d0789358`). +- **Menu #2** — front-end mouse-pointer mode (`current_menu != RUN_STAGE` → + mouse drives the cursor both axes, LMB=A/RMB=B); Y-invert fixed. +- **RC4 RETRACTED** — the palette decode is correct RGBA5551, the analysis + doc's "spec" was ARGB1555. Don't "fix" it. +- **21/21 level sweep PASS** (first all-green incl. Cuba) — no regressions. + +Still open / next: +- **D148** (Dam exit rappel cutscene doesn't play) — user will see it right + after Dam. D75 front-end/cutscene-model family, cosmetic, deep. Not started. +- **RC1 / wallet-Bond photo garble** — D75 family (`texLoadFromGdl` model-GDL + expansion broken for front-end models). `TEXTURE-GLITCH-ANALYSIS.md` §6b. +- **Depot ceiling blue-speckle** — B2 light-shaft/UV bug on that surface; + RC2 didn't touch it. Needs `GE_DTEX`/RenderDoc on the specific surface. +- **D154** (`bg.c` wall-shoot GBI parser, committed `072b5c44`) — needs a + firefight-into-a-wall to verify, then close. +- In-level ABI/GBI crashes as the campaign playtest proceeds (D122/D135 + pattern) — the expected work. +- Latent: `fileSetDifficultyStageTime` `times[]` OOB read for 007-difficulty + late levels (retail has it too; edge case, don't fix without growing + `save_data` which breaks EEPROM layout). + +### BLOCKER (M-30) — campaign progression not persisting — **FIXED (D157), CONFIRMED by user playtest** +User replayed Dam on Agent post-fix: `gdb.txt` shows `obj 0 objdiff=1` / +`obj 1,2 objdiff=2` (skipped on Agent) / `obj 3 objdiff=0 status=1` → +`objectiveIsAllComplete=1` → `end_of_mission_briefing` → `fileUnlockStage... +stage=0 diff=0` → `fileWriteSave slot=0 bitflags=10`. `data/ge007.eep` slot 0 +now carries the Dam completion time; the Agent checkmark shows in mission- +select **and persists across a kill + restart**. Campaign progression works. +(`GE_SAVELOG` diagnostics — `#ifdef PORT`, env-gated — left in +`objective_status.c` / `boss.c` / `file.c` / `file2.c` for now; strip once a +few more level boundaries are playtested. `dump_objectives.py` per-criterion +`MinDif=` is only valid on the `type=23` lines — garbage on 2-word sub-records, +harmless.) + +
D157 root cause +`D157` (`src/bondtypes.h`, `#ifdef PORT`): the type-23 objective record's +`MinDificulty` is a BE `s32` at word 3; `d88_propdefs.py` bswap32's it, so on +LE the value moved from byte 0xF to 0xC, but `struct objective_entry.difficulty` +still read `s8`@0xF → **0 for every objective**. `get_difficulty_for_objective()` +returned Agent(0) for all → `objectiveIsAllComplete()` on Agent evaluated +objectives that should be difficulty-gated out (Dam "Neutralize all alarms" = +Secret Agent, "Install covert modem" = 00 Agent) → always incomplete → +`end_of_mission_briefing()` (the unlock write) never ran. **User was right — +you don't shoot an alarm on Agent Dam.** Fixed the struct tail order under +`#ifdef PORT`; also fixed `dump_objectives.py` (same byte-offset bug — it +showed "Agent" for everything). **Next: user replays Dam on Agent (just reach +the exit) under `GE_SAVELOG=1`; expect `MinDif=1/2/2/0` on the crit lines, +`objectiveIsAllComplete=1`, a `fileWriteSave slot=N ... TIMES` line, and +Facility unlocked in mission-select.** If obj 3 (Agent, flag 0x1000) still +shows incomplete, the level isn't setting that stage flag on exit — separate +issue. `GE_UNLOCK_ALL=1` remains the playtest workaround meanwhile. +
+ +
Original M-30 blocker writeup (superseded by D157) +User completed Dam, hit "previous" from the debrief to mission select, +**Facility still locked**. `data/ge007.eep` has zero completion times → the +unlock save isn't landing (or is wiped on reload). Chain: +`bossReturnTitleStage` (`objectiveIsAllComplete()` gate) → +`end_of_mission_briefing` (`briefingpage`/`selected_difficulty` guard) → +`fileUnlockStageInFolderAtDifficulty` → `fileOverwriteSaveSlotWithNewSave` +(needs a slot with `SAVEFLAG_DORESET`) → `fileWriteSave` (`fileGamePakProbe`) +→ EEPROM; then `fileValidateSaves` CRC-checks every slot on menu re-entry and +`fileResetSave`s any mismatch. +**M-30 GE_SAVELOG run 1 (user's gdb.txt) — narrowed:** +`bossReturnTitleStage stage=33` (Dam) fires with **`objectiveIsAllComplete=0`** +→ `end_of_mission_briefing` never called → no unlock write. `fileValidateSaves` +/ CRC / DORESET-slot all fine (first-boot fresh-save init, expected). So the +blocker is **objective-completion detection**: `objectiveIsAllComplete()` bails +at obj 0 (`get_status_of_objective(0) != COMPLETE`). Dam obj 0 = 4× +DESTROY_OBJECT (ALARM tags). This is D126/D151 propDef-decode family — either +`objective->ObjRefID` at the wrong byte offset on LE (criteria records +`_bswap32`'d, `MissionObjectiveRecord.ObjRefID` s32@4 vs the criterion's +`u16` tag), or the `sizepropdef` walk stride is off for a criterion type. +Note `sizepropdef(PROPDEF_OBJECTIVE_START)` PC-branch returns **4** and +`d88_propdefs.py` emits **16 B** for type 23 — but `MissionObjectiveRecord` +(bondtypes.h) is header+ObjRefID+TextID+MinDificulty+`*nextentry` = 20 B N64 +/ 24 B PC. If `nextentry` is real+serialized, both the stride and the +converter are 1-2 words short → whole-stream desync. (`struct objective_entry`, +the other view of type 23, is only 16 B and has no nextentry — the two +structs disagree; the "// maybe wrong..." comment flags it.) +**Diagnostic committed (``):** `GE_SAVELOG=1` now also dumps +each criterion's `type / ObjRefID / TextID / sizepropdef stride / first 3 +words / currentstatus` (gated by `g_savelogObjOnce` so it only fires on the +`bossReturnTitleStage` call, not the per-frame poll). +**Next: user does ONE genuine full Dam completion (all objectives, walk out +the exit) under `GE_SAVELOG=1`; the `SAVELOG: crit ...` lines pin the exact +field/stride that's wrong.** Also noted but not chased: +`fileSetDifficultyStageTime` `offset = ((diff*20)+levelid)*10` indexes +`times[]` up to `[98/99]` but the array is `u8 times[76]` — OOB for +high level/difficulty combos (writes into the next slot's checksum). Not the +Dam/Agent case (index 0) but a latent save-corruption bug. +
+ +## Done this session (M-30) — user batch defect list; earlier items + +User playtest batch report (5 items; #1 disregarded): + +- **#4 main-menu hang (file-select → main menu → back → 5fps, no input, force-close).** + Crash log was D146 spam + `D63 [1479] from=... No stack`. **Root cause: the + D63 debug trail in `gfx_pc.cpp` fired ~10 `sysLogPrintf` lines + a full trail + walk on EVERY unknown GBI opcode, unconditionally** — on the D149 corrupt + front-end model DL that ran per-frame and collapsed the menu to ~5fps with + input starved. **FIXED `d0789358`:** stripped the D63 trail (statics, + per-G_DL recording, default-case dump); the rate-limited D146 "ending DL" + log (cap 20) stays. Headless: FILE_SELECT / MODE_SELECT / MISSION_SELECT all + boot + render 900+ frames clean, 0-1 D146 lines, no hang. `-level_09` 3/3 + framediff, `-level_20` 91.6% unregressed. **User to re-verify the live + menu-nav transition** — the exact folder→mode→back path wasn't reproducible + headless, but the per-frame log flood (the "5fps no input") is gone. +- **#2 file-select mouse only moves side-to-side.** The front-end cursor is + stick-driven (`frontUpdateControlStickPosition` reads `joyGetStickX/Y`); the + port only fed mouse-X to the stick in menus (mouse-Y → C-buttons, ignored by + menus). **FIXED ``:** `port/src/input.c` — when + `current_menu != MENU_RUN_STAGE` (and `!= MENU_INVALID`, so bare `-level_XX` + in-game aim is untouched) mouse velocity drives stick X+Y as a pointer, no + C-buttons / aim band, LMB=A (select) RMB=B (back). Reads the game global + `current_menu` for UI context only (enum MENU is ABI int) — no logic change. + New knob `Input.MenuPointerSpeed` (%, default 100). **User to verify feel.** +- **#5 menu + saving for playtest.** EEPROM saves (`osEeprom*` → + `data/ge007.eep`, write-through, M-29 `bf5e4d3d`) audited — sound; progress + persists across restart *once the menu works* (#4). No change needed; verify + after #4. +- **#3 "interlaced" textures** (`screenshots/interlaced texture example.jpg`). + **RC2 (mip contamination) FIXED, default on** (``, + `port/fast3d/gfx_pc.cpp`). New `GE_DTEX=1` probe confirmed it: every LOD + texture uploaded ~1.3x too tall (64x64 I4 → 64x87), the extra rows being mip + bytes rendered as image rows. `import_texture()` now clips a full-width LOD + block to the SETTILESIZE base height; GL builds correct mips from a correct + base. Knob `Video.FixMipTextures` (=0 → old behaviour, byte-identical to + golden). BUNKER1 side-wall textures visibly cleaner with it on; Silo + unregressed. **`-level_09` framediff phash on frames 320/440 is D117 noise, + not a regression** — M-30 verified: BUNKER1's intro camera pans continuously + for 1000+ frames and its speed varies run-to-run, so *any* fixed capture + frame lands on a different pan moment each run (a re-baseline attempt to a + "settled" 700-1000 window failed the same way — there is no static window in + the intro). Trust `nonclear` (coverage %, stays stable) + the 21-level sweep + for `-level_09`/`-level_20`; the golden phash frames are only meaningful for + gross breakage. Depot M-30 A/B: RC2 makes the wall panels mildly cleaner, + does NOT touch the ceiling blue-speckle (separate B2 light-shaft/UV bug), + makes nothing worse. **21/21 sweep PASS with the fix on.** + - **Still open:** the wallet-Bond *photo* garble is RC1 / D75 front-end model + family (the model GDL / `texLoadFromGdl` expansion is broken for front-end + models — fast3d walks into `0xfafbfbfc` garbage, not a missing opcode). + Separate larger track — see `docs/TEXTURE-GLITCH-ANALYSIS.md` §6b. + - RC4 (palette off-by-one, `gfx_pc.cpp:835`) and RC3 (wrap period) still + open — small, in the same doc §6. +- Regenerated `docs/LEVEL-OBJECTIVES.md` for all 21 solo levels (Track A); + committed loose reference docs/tools. + +## Done this session (M-29) — race-to-release plan; audio DEFERRED; 2 commits + +**Direction (user, M-29):** ship the solo campaign start→finish, fastest. +Three tracks: A = full 21-level playtest (release gate, user batch-reports a +defect list from a solo playthrough); B = **audio DEFERRED** — ship silent, +add sound post-launch (`docs/AUDIO-PLAN.md` stays valid for later, do NOT +start it); C = solo quick wins, priority: **B2 textures** > B3 blood-stain +converter > B5/B6 menu input > C2 (D75 models) > C4 (D148 cutscene) > +C1 (mirrored HUD — user unsure it's worth it). Full plan + +`docs/SMALL-FIXES.md` mapping in the `m29-release-push` memory. + +- **`0bd0ceec`** — aim sensitivity (SMALL-FIXES B4 / BACKLOG B3): + `Input.MouseAimSpeed` default 50→25 (overshot), new `Input.AimBand` knob + (5..40, def 20). Port-only, `ge007.ini`-tunable. Needs playtest feel check. +- **`bf5e4d3d`** — file-backed EEPROM saves (BACKLOG B5 / Phase 4): + `osEeprom*` in `port/src/libultra.c` now back `data/ge007.eep` (2 KB / + 16Kbit, lazy-load + write-through, PD pattern). `osEepromProbe` → + `EEPROM_TYPE_16K`. Verified: game writes the file (checksum @blk 0 + + save_data @blk 4), `-level_09` unregressed, persists across runs. + Campaign progress now survives quit. +- **SMALL-FIXES B1 (D74 wrap block) — DEFERRED.** The in-place one-liner + (`G_TX_CLAMP` test + `[t]` index) activates never-run code and boot-crashes + `-level_09` (0 frames). Left inert with a NOTE in `gfx_pc.cpp`. Needs the + hoist-out-of-vertex-loop rework + per-level visual check. +- **B1 3-point filtering — wired up as opt-in (`a418f4d0`), NOT default.** + The sm64ex 3-point shader path existed but was unreachable + its min-filter + row was mip-less. Fixed the row (trilinear), aniso default 0→4, exposed + `Video.TextureFilter=2`. **Default stays 1 (bilinear)** — 3-point softens + textures at normal distance and (tested) did NOT fix the Depot roof. +- **B2 (Depot wrong textures) — filtering RULED OUT, still not fixed.** + `docs/BRIEF-B2-depot-textures.md`. The roof renders as blue speckle + + radial rays converging to a point; identical with 3-point+trilinear+aniso + at 640×480 native, so NOT grazing aliasing. Decode path (TLUT bswap, + `palette_to_rgba32`, `import_texture_*`) audited clean. It's a texture-data + / UV / light-shaft-effect bug on that surface. **Next step: `GE_DTEX` + probe** to identify what draws it (params in the brief). This is a + RenderDoc/probe-class investigation — D114/D116 rabbit-hole family, needs + a focused session, not inline. +- **`7d7f5fb2` D155 — Facility outro-cutscene "hang" FIXED.** User report. + `waitForNextFrame()` passed `deltaFrames` unclamped; on the port + `osGetCount()` is wall-clock (D117) so a real-time stall at the + cutscene→debrief asset load (worse with a local LLM thrashing the box) + made it balloon to hundreds → `g_ClockTimer` huge → `modelTickAnim` + `while(numticks--)` × N chrs + dozens of `for(ispeed`/`playspeed` → `frameb` huge/NaN → + `floorFloatToInt` garbage → ~2^31 iterations. `#ifdef PORT` guards: snap a + non-finite/`|x|>=1e6` `frameb` to `framea`, and fall back `frame`/`frame2` + to the pre-loop values in `modelTickAnim`. One-shot `stderr` diagnostic + dumps the model speed fields when it fires. **User to re-verify the + Facility outro.** If it still hangs OR the diagnostic prints, the NaN + source is upstream (suspect a D100/D140-style misaligned `Model` field, or + the cutscene data). §F D155+D156, PORT-LEARNINGS §E. +- **D153 reminder cost real time this session:** back-to-back `-level_09` + runs during builds boot-crash under machine thrash; only regression-test on + a settled machine (clean run = 600+ frames fine). + +## Done this session (M-28) — D150 (watch page crash) + D151 (watch text blank) + +- **D150** (`src/str.c`, `#ifdef PORT`, uncommitted) — user found a crash + opening **level objectives on the watch**. AV in `strcat` (`str.c:25`, + NULL src). The watch BRIEF/OBJECTIVES pages (`options.c:3940-4068`) build + text with `strcat(buf, langGet(id))`; `langGet` returns NULL on PC for the + briefing/objective string banks the in-level watch flow never loads + (same missing-bank issue as the D143 "briefing text renders blank" note). + Fix: NULL-tolerant `strcpy/strncpy/strcat` under `#ifdef PORT`. Note these + are `__nonnull__` builtins to GCC so a plain `if (src==NULL)` is optimised + away — the guard laundras the ptr through an empty `__asm__` (`GE_IS_NULL`). + Build green; `-level_09` 600+ frames @ 91.67% unregressed. **Watch-page + repro is interactive — needs user re-verify.** §F/§H D150, PORT-LEARNINGS §C. +- **D151** (`src/bondtypes.h`, `#ifdef PORT`, uncommitted) — the D150 + "objective/briefing text renders blank" consequence, ROOT-CAUSED and fixed. + NOT a missing bank: the per-level lang bank IS loaded in-level + (`langLoadToAddr`, `prop.c:1274`). The propDef records that carry the watch + text (types 35 `WatchMenuObjectiveText`, 23 `ObjectiveStart`) store a plain + `s32` slot id in their 3rd word; `struct watchMenuObjectiveText` / + `struct objective_entry` decode it as `u16 reserved; u16 text;` and read + `text` at offset `0xA` — works only because on BE the id is in the low 16 + bits. `d88_propdefs.py` `_bswap32`'s the whole word → `text` @0xA reads 0 → + `langGet(0)` → NULL → blank. Fix: `text` is a full `u32` at the word offset + under `#ifdef PORT` (N64 `u16 reserved; u16 text;` kept under `#else`). No + converter change / no sidecar regen. Compiles clean (link needs the running + game closed). **Interactive re-verify pending** — open the watch, all of + OBJECTIVES / mission background / M / Q / Moneypenny should now show text. + §F **D151**, PORT-LEARNINGS §C. + +### D152 (OPEN) — mission-failed → permanent black screen = `osSetIntMask` lock deadlock +User killed Trevelyan in Facility (`Ctrl` = **fire**, no crouch bind) → failed +the objective → fade to black, never returns; process alive. **Live `gdb.txt` +inspected** (game left running under `debug.ps1`): NOT the front-end +mission-failed *screen* — it's a **deadlock on `s_imLock`** (the D147 +recursive-mutex behind `osSetIntMask`). Rendering froze at frame 12600. +`mainThread` in `sndSetScalerApplyVolumeAllSfxSlot`→`alEvtqPostEvent`→ +`osSetIntMask(OS_IM_NONE)`→`pthread_mutex_lock` (waiting); `amMain` in +`sndPlayerVoiceHandler`→`alEvtqNextEvent`→same, also waiting; **neither owns the +lock** → a leaked unbalanced `OS_IM_NONE` (libaudio early-return paths) or a +transient thread that acquired-and-exited holds it. The mission-failed **audio +fade-out** (`sndSetScalerApplyVolumeAllSfxSlot` posts `AL_SNDP_RELEASE_EVT` per +`ALSoundState` per frame) is what exercises the window. D147 family; D147's +"cannot deadlock" claim is now falsified. §F **D152**, PORT-LEARNINGS §D4. +**MITIGATED (M-28, `port/src/libultra.c`, port-only, uncommitted).** +`osSetIntMask` is now a **self-healing** logical lock: same recursive +acquire/release as the D147 mutex on the normal (microsecond) path, but a +waiter blocked **> 2 s** (`OS_IM_STUCK_NS`) declares the holder leaked it, +logs `LOG_ERROR` with the stale owner + the stealing caller's return +address, and steals the section. A leaked/unbalanced `OS_IM_NONE` (or an +acquire-and-exit transient thread) can no longer wedge the game forever — +worst case is a ~2 s audio hiccup + a log line that names the leak. Builds +clean. Still OPEN: the exact leaking call site (read it from the next +`D152: ... stealing from owner=` log) + the proper narrow fix (dedicated +`ALEventQueue` lock). Next: replay the Facility mission-fail; if the steal +fires, `addr2line` the logged caller. +Separate minor gap: no crouch keybind (BACKLOG B3/B4). + +### D154 (WRITTEN, UNVERIFIED, UNCOMMITTED) — `bg.c` room hit-test GBI parser port +`bgTestRayIntersectionInRoom` (`src/game/bg.c:3331`) is the D135 sibling the +docs kept flagging: an unported N64 GBI parser (`((u8*)gdl)[k]` / `((u32*)gdl)[i]` +byte/word indexing) walking the 16-byte PC room DL as if it were 8-byte N64 +`Gfx`. Fires on **shooting walls/floor** (`bgTestBulletHitBackground`), which no +level-sweep exercises. Ported under `#ifdef PORT` (N64 path verbatim under +`#else`): header fields via the `.dma` view like `bgBuildRoomVtxBounds`; G_TRI1 ++ G_TRI4 vertex-index nibbles recovered from `(u32)gdl->words.w0/.w1` +(derivation table cross-checked twice); `texturenum` -> -1 like D135 (KSEG0 +deref invalid for converted GDLs; only flavours the impact decal/sound, D77). +Builds clean. **NOT verified** — a no-input capture never calls this function, +so it needs a real firefight into a wall on an **idle** machine; couldn't get a +clean run this session (D153/driver-crash under machine thrash — see below). +Next session: build, `-level_09`, fire at a wall, confirm no crash + `-level_09` +framediff green, then commit. §F **D154**. + +### D153 — `-level_09` frame ~900 crash = load flakiness, NOT a bug (closed) +Chased for ~30 min in M-28: `-level_09` crashed `0xc0000005` at a +nondeterministic frame 900↔1400, 6/6 runs — **but every one was while the +machine was hammered with concurrent builds + multi-run loops.** On a quiet +machine `-level_09` ran past frame 2400, 0 heartbeats, no crash. It's the +D117/D134 host-scheduling flakiness the docs warn about every session (M-25 +Silo ~frame 300 is the same). Not a regression, not D150/D151/D152. +`romdataFixupMusicSeqTable: seqCount 63 exceeds blob capacity 1` is a benign +expected warning (header-only first call). **Lesson: never regression-test +while builds/other game runs are in flight.** §F **D153** (closed). + +
M-27 earlier — front-end/pause bucket: D140 + D141 FIXED + +Started the parked "front-end / transitions / pause / watch" bucket. +**Pressing Start in-level no longer crashes** — the pause / watch menu +renders (weapon page verified via a temp `GE_AUTOPAUSE` input probe, +since removed). Cosmetics unchanged and still parked: mirrored watch text +(D114/D116), dark/faint weapon model (D75). + +- **D140 (FIXED)** `src/game/bondview.h`, `src/game/bondview2.c` — + pause-menu crash `bondviewRenderWatch` → `bondviewTransformManyPos- + ToViewMatrix(field_23C=NULL)`. `something_with_watch_object_instance` + (N64 player +0x230) is a **`struct Model` + RW-pool punned into a + 0x184-byte field-run**; `field_23C`/`watch_scale_destination`/ + `pause_watch_related_adjust` are actually `.render_pos`/`.scale`/ + `.animframe1`. PC `sizeof(struct Model)` grows → the aliases break → + NULL `render_pos`. Fix: real inline `struct Model` + + `u32 watchRwPool[192]` under `#ifdef PORT`; the 3 named reads redirect to + the member via `GE_WATCH_{ANIMFRAME,SCALE,RENDERPOS}` macros (N64 `#else` + = verbatim field). D56 branch of `sub_GAME_7F07E7CC` now uses the inline + pool. D100/D102 pattern. §F/§H **D140**, PORT-LEARNINGS §A. +- **D118d (FIXED, feel unconfirmed)** `src/game/options.c` — user bug report: + watch inventory list over-scrolls with keyboard W/S (one press skips several + items). GE's "slam the stick" fast-scroll is a raw per-frame level check + (`joyGetStickY < -0x46`); keyboard/digital pads sit at max every frame. + `#ifdef PORT` drops the stick term → list-nav goes through the latched + single-step path (one step per press). Build green, `-level_09` unregressed; + couldn't verify the feel headlessly (INVENTORY watch page unreachable without + page-cycling input) — **needs an in-game check**. Latent sibling in `front.c` + menu nav (same `joyGetStick*InRange` level-check pattern). §F **D118d**. +- **D141 (FIXED)** `src/game/gunfire.c` — the crash D140 exposed. + `set_enviro_fog_for_items_in_solo_watch_menu` walks `bodymodel->Switches[]` + (a `ModelNode*` array) with raw byte offsets `+0x48`/`+0x5c`, `j += 4` + (4-byte-pointer constants) → PC 8-byte stride reads garbage → bogus node + → AV in `modelGetNodeRwData`. Fix: `#ifdef PORT` uses + `Switches[18 + (j>>2)]` / `Switches[23 + (j>>2)]`. §F **D141**, + PORT-LEARNINGS §B (cf. D128). +- **Verified:** build green (`ntsc-final`); `-level_09` framediff 3/3, + `-level_20` crash-free (both unregressed); autopause probe → watch page + renders 400+ frames, no `ge007.crash.log`. +- **D139** (stage-unload, M-23, still UNVERIFIED) not exercised — it needs + a real level-exit teardown, not a pause. Next front-end item. +- **Still in the bucket:** D139 verify; unpause / watch-page navigation + (the temp probe didn't test exiting the watch); level exit → MISSION + COMPLETE → debrief → auto-advance; main-menu / mission-select walkthrough + (WS2); watch objectives page + gadgets. + +**Update (M-27 end): the level-exit → MISSION COMPLETE → debrief → +auto-advance → next briefing → Start loop is DONE and playtested (D142– +D147).** D139 got exercised for free. Remaining bucket items (unpause / +watch-page nav) are minor. +
+ +
M-24 "Next task" (superseded — this is M-50 narrative archive, not live work; §H authoritative) + +**Scope call (user, M-23):** D139 (stage unload) + D140 (pause menu / watch) +are the same **not-yet-built front-end/transition layer** — level exit → +debrief → next briefing, pause, the watch. Bare `-level_XX` skips all of it +and the sweep never touches it. Do **not** build a throwaway exit path just +to verify D139 — its fix is unambiguous (LE reads the wrong header byte) and +committed; it'll be exercised for free once that layer is built. Both go in +a **parked "front-end / transitions / pause / watch" bucket** for one +dedicated session later (see `docs/PLAN-linear-level-sweep.md` WS2). + +**M-24 = keep doing in-level playtesting.** In-level crashes (D135/D137/D138 +type — pointer-width / ABI / GBI-parser bugs) are the real work and show up +during normal `-level_XX` play. + +1. **Resume WS6** `docs/LEVEL-PLAYTEST.md`, bare `-level_XX`. Do the in-level + ~80% (spawn, geometry, guards, doors, lifts, switches, pickups, each + difficulty-gated objective *registers* COMPLETE via `objective_status`, + alarms/reinforcements). **Defer** the exit-trigger / auto-advance / pause / + watch checks — those need the parked front-end bucket. Facility first + (it's playable start→~end now), then down the mission list. +2. Each new in-level crash: `tools_pc/repro_gdb.sh `, get the real + `bt full`, expect another `#ifdef PORT` ABI/layout fix + (D122/D126/D132/D135/D137/D139 pattern). Log as the next Dxx. +3. **Re-run `tools_pc/level_sweep.sh`** once — D135/D138 touch shared code + paths; sweep is boot-only so it only proves no *boot* regression. Refresh + `docs/LEVEL-STATUS.md`. +4. `bg.c` hit-test sibling port (§F D135 follow-up — identical unported GBI + parser to D135, `~3373-3646`) — do it before a level where wall-shooting + is heavy (i.e. soon). + +**Parked bucket — front-end / transitions (STARTED M-27):** +- ~~D140 (pause menu → `bondviewRenderWatch` → NULL `field_23C`)~~ FIXED M-27 + (+ D141, the watch-page crash it exposed). Pause now renders. +- D139 verify (stage unload / `cleanupObjects`) — still needs a real exit +- unpause / watch-page navigation (M-27 probe only tested opening it) +- level exit → MISSION COMPLETE → debrief → auto-advance to next briefing +- main menu / mission-select / difficulty-select playtest (WS2) +- the watch (objectives page, gadgets) — needed for the WS6 exit/objective + checks, D75 3D-model-transform family + +**Verification per fix:** target level boots to ≥1 non-degenerate frame +(`tools_pc/pixcount.py`), AND `-level_09` + `-level_20` unregressed +(`tools_pc/framediff.py`). Regen sidecars after any converter change. + +
+ +
M-22 and earlier "Next task" (superseded — §H authoritative) + +-1. **D134 DONE (M-22)** — the frame-2 boot hang that every session since + M-13 wrote off as "sweep flakiness / D117 / machine load" was a **real, + fixed bug**: the SP/DP task-done event was posted `OS_MESG_NOBLOCK` into + the sched `interruptQ` that the 60 Hz VI pacemaker also fills, so a slow + synchronous fast3d frame let a retrace backlog swallow the done event → + permanent stall. Fix in `port/src/libultra.c` (`portPostEventForce` + + 2 reserved slots in `portPostVIEvent`). `-level_09` 6/6 boots to frame 600, + 0 heartbeats (pre-fix 1/3). §F/§H **D134**, PORT-LEARNINGS §E. + +0. **D132 DONE (M-21, commit `fa296b17`)** — propDef union-index slots for + types 14/19/38/44 (LINK/SWITCH/LOCK_DOOR/SAFE_ITEM) now emitted at the + right PC offsets (`d88_propdefs.py` + `loadobjectmodel.c sizepropdef` + PORT). + +1. **`-level_09` (BUNKER1) boot crash — DISPROVEN as a regression** (M-20, + coordinator: 3 runs / 1800 frames crash-free on `1fc3cff6`; re-confirmed + M-20 this pass: 690+ frames at 91.7% coverage). Was sweep flakiness / + D117 nondeterminism under machine load. Do not investigate. +2. **Intro "renders mostly black" is NOT a regression — it is the known + D75/D76 parked-cosmetic steady state** (M-20 / **D133**). Verified by + building M-17 (`9ec6121e`, whose handoff claimed "the entire intro + renders") in a scratch worktree and capturing the same `GE_PCDUMP` + window: coverage is **pixel-identical** to HEAD (legal screen 6677 + non-clear px = 2.17% both builds; logo-ish frames ~7%; black gaps in + between). The 2D/text layers draw; the animated character-model layers + (Nintendo-logo transform, gun-barrel Bond, cast models) never appear — + exactly D75. The M-17 "entire intro renders" handoff line was + aspirational, not a measured state. **Parked below level/crash work + (`docs/GRAPHICS-BACKLOG.md`).** Silo (#3, fly-down hang) is the same + class or D117 nondeterminism — not a fresh regression. +3. **Facility `-level_34` + Jungle `-level_37` re-verify FAILED this pass** + (M-20, machine lightly loaded). Facility: boot crash `frames=0`, PC + `0x1400c3b77` (addr2line unresolved). Jungle: renders frames 1–2 then + kernel-heartbeat hang ("no frame rendered", `frames=2`). Both were + claimed PASS in M-18/M-19 (D130/D131). **Needs a clean-machine + re-verify** before deciding regression vs. flakiness — see + `docs/LEVEL-STATUS.md`. `-level_09`/`-level_20` still PASS, so the + capture harness is sound. + +After that: all 21 load+render → hand `docs/LEVEL-PLAYTEST.md` to the +user for the WS6 completion pass (real input, per-level objective +checklist). + +
+ +**Regen gotcha:** `d69_emit.py` rewrites `data/pccg-/pccg.bin` from +scratch with only the 52 bg/stan rows — you MUST follow it with +`d88_emit.py --regen` to re-add the 21 `Usetup*Z` rows, or every level +falls back to the raw N64 setup and crashes identically in +`proplvreset2`. Always run the full `d43 && d69 && d88 --regen` chain. + +Sweep note: `level_sweep.sh` was flaky this session (spurious NO-FRAMES on +known-good levels under machine load / the 24 s watchdog + D117 +nondeterminism). Verify individual levels with a 35–45 s window and +`GE_PCDUMP="60-500:40"` when a sweep row looks wrong. + +Sweep runner: `tools_pc/level_sweep.sh` (bare `-level_XX`, `GE_PCDUMP="80-260:40"`, +24 s watchdog, `taskkill //F //IM ge007.x86_64.exe`; **`export PATH=".../mingw64/bin:$PATH"` +before `addr2line`** — the script's own symbolication no-op'd without it). + +The M-13 matrix below is partially superseded by M-14 (see §H D125 for the current +12/21 status) — treat §H as authoritative. + +| Class | Levels | Site | +|---|---|---| +| **C1** | Dam 33, Runway 35, Frigate 26, Statue 22, Streets 29, Cradle 41 | `chrIsNotDeadOrShot` chraction.c:4483 — `self` = AI-list rodata ptr | +| **C2** | Facility 34, Jungle 37 | `import_texture_i8`/`gfx_tex_normalize_source` — bad tex ptr | +| **C3** | Aztec 28, Bunker2 27 | `propobj.c` door model / `linkedDoor` walk | +| **C4** | Depot 30 | `prop.c:902` `sp4C->room` after `walkTilesBetweenPoints` | +| **C5** | Control 23 | `bg.c:5723` `portal_pts->numPoints` | +| **C6** | Surface2 43 | `loadobjectmodel.c:393` `PitemZ_entries[modelid].header` (D122 cont.) | +| **C7** | Surface1 36 | `snd.c:653` `sndSetupSound` (audio — parked subsystem) | + +**Done M-13:** C1 fixed (**D123**, `tools_pc/d88_propdefs.py` — the C1 +crash was D122 fallout: widened `Vehichle/AircraftRecord.ailist` slot +zeroed instead of carrying its read-before-write int id). C2 split: +Jungle's texture crash fixed (**D124**, `port/src/gimgfixup.c` — compiled +explosion-DL sync keyed on an already-erased marker); Facility+Runway +diagnosed (model-GDL relocation misaligns `dst`, `objecthandler_2.c` / +`texLoadFromGdl`, D80/D82/D83 area) — NOT fixed. + +Full re-sweep after both: **12 / 21 PASS** (`docs/LEVEL-STATUS.md`). +9 crashes remain in 6 classes: **C2** Runway+Facility (model-GDL align — +biggest), **C3** Aztec+Bunker2 (`docs/BRIEF-C3-C6-prop-model.md`), +**C2m** Jungle (explosion-DL `G_MTX`), **C6** Surface2 (`PitemZ` modelid), +**C5** Control (BG portal), **C4** Depot (BG tile/room), **C7** Surface1 +(`sndSetupSound`). Priority order + files in `docs/LEVEL-STATUS.md` "Next". + +Uncommitted M-13 edits: `tools_pc/d88_propdefs.py`, `port/src/gimgfixup.c`, +§F/§H D123+D124, `docs/LEVEL-STATUS.md`, `docs/LEVEL-PLAYTEST.md`, +`docs/PORT-LEARNINGS.md`, `tools_pc/level_sweep.sh`, the BRIEF-C* files, +this file. Fold into the docs-restructure commit set. + +Re-run `tools_pc/level_sweep.sh` after each fix; keep `-level_09` + +`-level_20` green (`framediff.py`). All-21-PASS → hand the user +`docs/LEVEL-PLAYTEST.md` for WS6. + +**Verification per fix:** target level boots to ≥1 non-degenerate frame +(`tools_pc/pixcount.py` > a few %), AND `-level_09` + `-level_20` +unregressed (`tools_pc/framediff.py`). Regen sidecars after any converter +change: `python tools_pc/d43_emit.py ntsc-final && python +tools_pc/d69_emit.py ntsc-final && python tools_pc/d88_emit.py ntsc-final +--regen`. + +**Parked (do NOT start here):** WS2 front-end menu playtest (needs a +human driving; cosmetics D75/D76/D77/D114/D116 all out of scope — +`GRAPHICS-BACKLOG.md`). Audio (Phase 3), saves (Phase 4). + +## Environment / build + +```sh +export PATH="/c/msys64/mingw64/bin:$PATH" && ./build-pc.sh ntsc-final # ~5 s +``` + +Run from the **repo root**, not `build-pc/`. + +- **Repro (BUNKER1, skips attract mode):** + `./build-pc/ge007.x86_64.exe -level_09` + (per-level `-m*` pool sizes are now auto-injected — D121/WS1; pass them + by hand only to override). `boss.c:337` decodes `-level_XX` as + `d0*10 + d1 - 0x210`; LEVELID == the `-level_XX` number. +- **Sidecar regen** (after any converter change; `data/` is gitignored): + ```sh + python tools_pc/d43_emit.py ntsc-final && \ + python tools_pc/d69_emit.py ntsc-final && \ + python tools_pc/d88_emit.py ntsc-final --regen + ``` + If `data/` is missing: `cp baserom.u.z64 data/ge007.ntsc-final.z64` + first (see §F "`data/` deletion + recovery"). +- **Frame capture:** `GE_PCDUMP="-:"` → `./ppm/` + (gitignored). Analyse with `tools_pc/pixcount.py` (non-black content + check) and `tools_pc/framediff.py ` (structural regression vs + `tools_pc/golden/` — the port is **NOT frame-deterministic**, D117; use + `--mask` for the HUD, not an exact compare). `tools_pc/ppm2bmp.py` to + view. +- **Crashes:** `ge007.crash.log` (repo root). Symbolicate + `addr2line -e build-pc/ge007.x86_64.exe -f -C ` (image base + `0x140000000`). Frames past the true chain may be stale (D56). +- **gdb:** launch mode is too slow for timing-dependent faults. **Attach** + mode is fast: `gdb -batch -x cmds.txt -p ` (`` = 4th + column of `ps -p `; game must already be running, e.g. `nohup + … &`). A hardware watchpoint on a global catches a bad write in < 1 min. +- Standalone probe compiles need `-std=c11`. + +## Non-negotiables (full list in AGENTS.md) + +1. N64 build files (`Makefile`, `tools/`, `rsp/`, `ld/`) untouched. +2. Game logic unmodified except narrow, documented `#ifdef PORT` + ABI/layout/format exceptions — each logged as a Dxx in §F/§H with the + N64 line kept verbatim under `#else`. Anything beyond a narrow, + obviously-correct exception: **stop and write it up** with a confidence + rating, don't hack it in. +3. Prefer an **offline sidecar converter** (`tools_pc/d*_emit.py`, + D43/D69/D88 pattern) over a runtime fixup for a whole ROM-asset format.