diff --git a/.github/release-notes.md b/.github/release-notes.md index 2e1d2c50..1799c6f6 100644 --- a/.github/release-notes.md +++ b/.github/release-notes.md @@ -30,6 +30,9 @@ Small hotfix batch from user playtest feedback on v0.2.0: confirmed it's proximity/line-of-sight-triggered exactly as on the N64 (walk toward the room), not a port bug — the full scripted beat, including the execution shot, fires correctly (D263). +- **Removed the `Skip intro` F10 toggle** — user testing found it breaks + audio. It was already off by default; it's now also pulled from the menu + entirely until root-caused (D216). ### What's new since v0.1.0 @@ -66,8 +69,8 @@ Small hotfix batch from user playtest feedback on v0.2.0: per-mission pickups, as on N64). - **Steam Deck first-run preset**: on SteamOS the first launch seeds Deck-friendly defaults (native 1280×800 fullscreen, VSync, MSAA 4, 150% - draw/LOD distance); an existing `ge007.ini` always wins. Two more F10 rows: - *No hit flash* (suppresses the damage-flash overlay) and *Skip intro*. + draw/LOD distance); an existing `ge007.ini` always wins. One more F10 row: + *No hit flash* (suppresses the damage-flash overlay). - **Modern dual-stick controller layout** (the scheme used by the console re-releases): left stick move/strafe, right stick look, right trigger fire, left trigger aim, A/X use, B/Y crouch/cancel, **RB/LB cycle weapons**. diff --git a/docs/dev/findings.md b/docs/dev/findings.md index 901fcfad..89ad801b 100644 --- a/docs/dev/findings.md +++ b/docs/dev/findings.md @@ -445,7 +445,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D213 | **On-screen FPS readout — `Video.DisplayFPS` (M-83 QoL wave, port-only, PD parity).** `port/src/optionsoverlay.c` samples frame time once per emitted frame (`fpsTick()` via `sysGetMicroseconds()`, ~0.5 s window) and, when enabled, emits a small top-right green "N FPS" DL even while the F10 panel is closed. Config-only knob (kept off the 13-row panel, which is at its documented layout limit — matches PD, whose `DisplayFPS` is also file-only). **Default 0 ⇒ `optionsOverlayEmit()` still returns NULL when the panel is closed ⇒ golden dumps byte-identical.** | **LANDED, port-only.** Build clean; `bunker1` no-op confirmed. **Owed:** eyeball that the counter reads a sane ~30 and doesn't overlap the HUD. | | D214 | **Keyboard rebinding — `[Input.Bind]` config section (M-83, port-only, PD parity, Phase 4 debt).** `port/src/input.c` gains 12 gameplay actions (Forward/Back/StrafeLeft/StrafeRight/TurnLeft/TurnRight/Fire/Aim/Action/Cancel/LeanLeft/Start), each a comma-separated list of up to 4 SDL scancode names (`Input.Bind.Forward = W,Up` …). `inputRebuildBinds()` (called from `inputInit()`, after `configLoad()`) parses them via `SDL_GetScancodeFromName`; unknown names log a warning and are skipped. The old hardcoded `keyDown(ks, SDL_SCANCODE_*)` block in `inputComputePad` is replaced by `actHeld(ks, IA_*)`. Mouse buttons (fire = LMB, aim = RMB) stay hardwired; the mouse-wheel weapon-cycle A-pulse is untouched. **Defaults reproduce the previous FPS layout exactly — verified: all 20 default key names round-trip to the identical `SDL_Scancode` the old code used (`SDL_GetScancodeFromName` test, 0 mismatches).** Config-file only (no in-overlay text entry — matches PD, whose rebinding is also file-driven); a fresh `ge007.ini` gets the full `[Input.Bind]` block with the defaults visible. | **PARTIAL — M-84: code PASS (alt/ctrl); one doc-only fix.** LANDED, port-only, no `#ifdef PORT`; build clean. One doc bug fixed (no code change): `config.c` groups keys by the substring *before the last `.`*, so the section header is **`[Input.Bind]`**, not `[Bind]` — earlier finding/commit/QOL text was wrong. Working syntax: put `Fire = Left Alt` under `[Input.Bind]`, or write a fully-qualified `Input.Bind.Fire = Left Alt` line anywhere (`config.c:307`). Separately: the pause→**watch menu** does not wire Esc→back (still Enter to open/close/back) — pre-existing, `options.c`, minor. | | D215 | **1P weapon viewmodel never drew in-level — `gunRenderFirstPersonGunModels` builds its `ModelRenderData` from a reinterpret across separately-declared adjacent globals that only works on N64 (M-83, D3x/ABI class; retires VIEWMODEL-RESEARCH.md).** The N64 line `renderdata = *(ModelRenderData *)&D_80035CC0;` (`gunfire.c`) casts the lone `u32 D_80035CC0 = 0;` to a whole `ModelRenderData` and reads on into the next declared global, `u32 D_80035CC4[] = {1, 3, 0, 0, …}` (`gun.c`) — yielding `{ basemtx=0, zbufferenabled=1, flags=3, rest=0 }` **only because the N64 linker lays the two globals out contiguously in declaration order and pointers are 4 B**. On x86-64 it breaks twice: `ModelRenderData`'s three pointer members (`basemtx`/`gdl`/`mtxlist`) widen 4→8 B so `flags` is no longer at struct offset 8, and C guarantees nothing about the two globals' relative placement. Measured (`GE_DVM=1`, BUNKER1): `renderdata.flags == 0`, so **every** weapon-model DL node was gated off at `modelRenderNodeGundl` (`(renderdata->flags & 1)`), and `subdraw()` for the weapon emitted just its `gSPSegment` — 1 gfx command instead of ~150. All three VIEWMODEL-RESEARCH.md ranked suspects were ruled out by the same probe first: `field_87F==1` (gate open), `RootNode` valid, `numRecords 22 < 192` (pool fine), node walk visits 24+ valid `DL`/`SWITCH`/`BSP` nodes. **Fix:** `#ifdef PORT` → set the three fields explicitly (`zbufferenabled=TRUE; flags=3;`), N64 line verbatim under `#else`. ABI/layout only, no logic change. | **FIXED (`src/game/gunfire.c`, `#ifdef PORT`).** Build clean. `GE_DVM=1` post-fix: `flags=0x3`, weapon `subdraw()` now emits **153** gfx commands (was 1). Regression sweep crash-free. `GE_DVM` gate+render probes left in, env-gated (cf. `GE_D193*`). **Owed:** one BUNKER1 eyeball that the gun model is actually visible + correctly posed (headless can't see pixels; the DL is now provably generated). The committed `bunker1` golden will need re-baselining — the gun now renders in-frame. | -| D216 | **`Game.SkipIntro` — boot straight to the file-select menu (M-83, port + one `#ifdef PORT` game-code line, PD parity, Phase 4 quick win).** New `s32 portSkipIntro` (`port/src/video.c`, `configRegisterInt("Game.SkipIntro", 0, 1)`), read once in `src/game/lv.c` at `LEVELID_TITLE` stage load — in the `else` of the existing `GE_STARTMENU` `#ifdef PORT` block. When set it does what every intro stage's own `!is_first_time_on_main_menu` branch does (`front.c`): `is_first_time_on_main_menu = FALSE; prev_keypresses = TRUE; maybe_is_in_menu = TRUE; menu_update = MENU_FILE_SELECT;` — i.e. it reuses the game's own post-intro route rather than inventing one, so the legal screen + Nintendo/Rare/GoldenEye logo attract loop is skipped. `GE_STARTMENU` still wins when both are set. Default 0 ⇒ untouched. | **LANDED + VERIFIED this session.** Build clean. Headless: default boot frame 90 = the TWYCROSS classification screen (10% non-black), `SkipIntro=1` frame 90 = the **SELECT FILE menu** (folders/photos/cursor all render, 89% non-black), no crash across repeated boots. Screenshots captured + eyeballed. **Owed:** nothing critical — a human boot to confirm the menu is fully interactive (cursor moves, a save slot opens). | +| D216 | **`Game.SkipIntro` — boot straight to the file-select menu (M-83, port + one `#ifdef PORT` game-code line, PD parity, Phase 4 quick win).** New `s32 portSkipIntro` (`port/src/video.c`, `configRegisterInt("Game.SkipIntro", 0, 1)`), read once in `src/game/lv.c` at `LEVELID_TITLE` stage load — in the `else` of the existing `GE_STARTMENU` `#ifdef PORT` block. When set it does what every intro stage's own `!is_first_time_on_main_menu` branch does (`front.c`): `is_first_time_on_main_menu = FALSE; prev_keypresses = TRUE; maybe_is_in_menu = TRUE; menu_update = MENU_FILE_SELECT;` — i.e. it reuses the game's own post-intro route rather than inventing one, so the legal screen + Nintendo/Rare/GoldenEye logo attract loop is skipped. `GE_STARTMENU` still wins when both are set. Default 0 ⇒ untouched. | **LANDED + VERIFIED this session.** Build clean. Headless: default boot frame 90 = the TWYCROSS classification screen (10% non-black), `SkipIntro=1` frame 90 = the **SELECT FILE menu** (folders/photos/cursor all render, 89% non-black), no crash across repeated boots. Screenshots captured + eyeballed. **Owed:** nothing critical — a human boot to confirm the menu is fully interactive (cursor moves, a save slot opens). **REGRESSED (v0.2.1 user testing, 2026-09-15): user reports enabling `Skip intro` breaks audio.** Not root-caused this session — leading suspect is that the intro/attract stages perform some audio-subsystem init (title/attract music kickoff, or a first-touch of the mixer) that `portSkipIntro`'s jump to `MENU_FILE_SELECT` bypasses. **Mitigation applied:** the `Game.SkipIntro` row was pulled from the F10 options overlay (`port/src/optionsoverlay.c`) so players can no longer enable it from the menu; the config var, default (0/off), and `lv.c` hook are untouched (so an existing `ge007.ini` with it already set would still trigger the bug — worth a follow-up hard-disable if that turns out to matter). Do not re-add the menu row without root-causing the audio interaction first. | | D217 | **1P weapon viewmodel + third-person Bond model textures render garbled — wrong/stale palette on model-DL CI textures (M-84, user QA; surfaced by D215 now that the viewmodel draws).** BUNKER1/Facility: the PP7 viewmodel has yellow+blue scrambled texels and a solid **green** patch by the hammer; Bond's in-game **watch face** shows the same green mismatch; user reports the **third-person Bond world model** does this "sometimes" too. Signature matches the **D161 family** — a CI-format tile drawn without the expected TLUT state, so fast3d's `import_texture` indexes a **stale `rdp.palette`**. D161 fixed the explicit `G_TT_NONE` case; this looks like model DLs not emitting `gDPLoadTLUT` through the port path, or the palette segment not resolving for weapon/character geometry. **Not a D215 regression** — D215 only un-gated the DL. | **FIXED (M-90/M-92/M-97; PR #47 texpool alignment + PR #49 CI-cache palette key) — the green-texture defect is fixed and merged, confirmed by two independent human playtests. The white missing-texture residual (AK47 / NPC-held world-model weapons) is split out as D228.** Status cell had gone stale at OPEN; corrected M-105. History: root cause characterised M-86; headless repro: `-level_34` FACILITY ~frame 300–400, PP7 **grip = flat green block** + green ring at the barrel joint + green-cast hand; barrel/slide upper correct black. Also green on `-level_30` DEPOT. **Clean on `-level_09` BUNKER1** (A/B control — same PP7, identical DL). **Two separate defects found:** ① **Wrong palette bound (the visible green — upstream of fast3d).** The grip is a CI4 solid-fill of palette index 0. Green `(24,65,0)` == RGBA5551 `0x1A00` == `rdp.palette[0]` at grip-import time. The grip's `count=1` model `G_LOADTLUT` is fed **different source bytes per level** for the same tile: BUNKER `00 01`→`PD_BE16`→`0x0001` (black ✓), FACILITY `1a 00`→`0x1A00` (green ✗). fast3d byteswaps + stores faithfully (proven correct by BUNKER) — the wrong bytes *arrive* from the model-DL / segment-address path. Strong tell: every *larger* model TLUT has `raw[0]==raw[1]==0x0000` with real colour from index 2 ⇒ GE's model-TLUT payload likely carries a **4-byte (2-entry) leading zero pad**, OR the port's model-TLUT source pointer is **4 bytes / 2 entries too low** (then the `count=1` grip load reads the tail of the preceding CI blob — BUNKER lucky, FACILITY not; fits "widespread across levels + weapons"). Ruled out this session: not tail-overflow (index 0 is in-range; matches M-85 `GE_D217Z`), not the fast3d byteswap. **Next:** dump the grip TLUT `G_SETTIMG` target vs the ROM/`pcmodels` sidecar PP7-grip material bytes; confirm the 4-byte offset vs N64; fix in model-DL segment resolution or `tools_pc/d43_emit.py` blob placement (sidecar regen). ② **CI texture-cache collision (proven, real, separate; fix landed).** `gfx_texture_cache_lookup` (`gfx_pc.cpp:522`) keyed CI textures on `{addr, palette_addrs[0/1], palidx, size_bytes}` with **no palette-content check**; GE reissues `gDPLoadTLUT` from repeated scratch-arena addresses with different content, so a later material could take a stale HIT and sample another material's decoded palette. **Fix on branch `fix/d217-model-texture-palette` (commit `f6fc0399`):** FNV-1a hash of the 512-byte `rdp.palette` computed only in `gfx_dp_load_tlut()`, added to the CI cache key (`port/fast3d/gfx_pc.{cpp,h}`, fast3d-internal, no `#ifdef`). Cheap, cannot worsen a decode; **owes a full 21-level `verify.sh sweep`** before merge (touches every palettized world texture's import path — cache-churn risk only). Does **not** fix defect ①. Screenshots: session scratchpad `d217/`, `fac_gun_crop.png`. Cross-ref `docs/dev/GRAPHICS-BACKLOG.md`. **User playtest matrix (M-87, raw notes — not yet triaged):** DAM/PPK: watch still green; AK47 viewmodel has white (missing-texture) patches on the magazine + barrel. FACILITY: PPK garbled **green AND blue**; 3rd-person Bond world model shows greenness on level load-in; AK47 has "weird blue / glitched" textures. JUNGLE: M16 viewmodel is orange in spots. Confirms multi-weapon (PP7/AK47/M16), multi-color (green/blue/white/orange) — consistent with the upstream model-TLUT-source theory (different trailing garbage per weapon/material) rather than one single fixed offset. **Scope clarified (M-87):** not just Bond's 1P viewmodel / 3P world model — **NPC-held weapon models (other characters' guns in third person) show the same glitched-color/white-missing texture symptom.** So the defect is in the shared weapon-model-DL/TLUT path used by *any* character holding that weapon, not something specific to Bond's model or the 1P viewmodel rig — widens the likely fix surface (weapon model loading in general) beyond "Bond's gun." **M-87 clarification:** not uniform across levels — severity and glitch *type* both vary per level (some levels show it lightly, some not at all in a given session, and the specific wrong-color/missing pattern differs), consistent with the per-material trailing-garbage theory rather than one fixed global offset. **M-88 update: defect ① is NOT a `tools_pc/d43_*.py` sidecar bug — the theory in this row is refuted; the model converter never touches texture/palette bytes at all.** Root cause narrowed to the runtime `texWriteLoadToTmemAddr`/`gfx_dp_load_tlut` TLUT-address path (`src/game/tex.c` + `port/fast3d/gfx_pc.cpp`, both out of this session's file scope). See full write-up: §H "D217 — M-88 defect ① root-cause narrowing". **M-89 update: the `gfx_dp_load_tlut` pitch/width=1 hypothesis is REFUTED** (hand-derived: it already computes `pitch=1` for every model-DL TLUT call via the GBI `width-1` packing — a proposed `pitch=1` patch would be a no-op, do not apply it) — **the addressing math is correct by inspection**. New leads, neither yet probed: (a) does `g_Textures[texturenum]`/`_imagesSegmentRomStart` actually resolve to identical ROM bytes for this weapon's grip texture on BUNKER1 vs FACILITY (an image-table/asset-resolution path distinct from the already-ruled-out per-model `TextureID`/`d43_emit.py`/`Globalimagetable`/D68 path), or (b) does the shared `texpool` arena (`image.c` `texLoad`, two-ended `leftpos`/`rightpos` allocator) serve a stale/aliased `tex->data` for what should be a fresh per-level decode. See full write-up: §H "D217 — M-89: the `gfx_dp_load_tlut` width=1/pitch hypothesis is REFUTED". **M-90: defect ① ROOT CAUSE FOUND + FIXED** — neither lead (a)/(b) was it: the texpool arena base for the weapon pools lands **%8==4 on PC** (memp bump drift from pointer-bloat allocation sizes), and `texAlignIndices` pads CI rows to *absolute* 8-byte boundaries, so every palette in a misaligned pool sits **4 bytes early** while the model-DL `G_LOADTLUT` still reads the N64 offset — the grip's count=1 load lands on the next tenant's header slot (`texnum=26` → LE s16 → `0x1A00` = the green), and larger loads read two zero-pad entries then the real palette (this is M-86's unexplained "leading zero pad" tell). **Fix:** `texInitPool` rounds the arena base up to 8 under `#ifdef PORT` (no-op on N64; ≤7-byte pool shrink) — ABI/layout-class edit, no logic touched. Verified: FACILITY 188/188 decodes aligned + draw reads `pal0=0001` (was `1a00`) + 0 green pixels in frames 250–450 (was ~1300); BUNKER1 + DEPOT still clean. **Pending:** full 21-level sweep result + human playtest eyeball of the M-87 weapon matrix. See §H "D217 — M-90". **M-92: sweep 21/21 PASS + human eyeball done — PP7/PPK/watch CONFIRMED FIXED by this alignment fix. AK47 (viewmodel + world model) and NPC-held world-model weapons (white/missing-texture patches, e.g. magazine/receiver) are a DISTINCT, still-OPEN residual — not fixed by this PR, not yet root-caused. PR #47 should still merge (real, confirmed partial fix); reopen defect ① scoped to the AK47/NPC-world-model white-texture failure mode. See §H "D217 — M-92". **M-97: independently re-confirmed by a second human playtest** (this branch merged up to date with `main`, fresh build, `-level_34` FACILITY): PP7/PPK grip+hammer clean, watch face clean, no green anywhere; **NPC-held AK47 still shows white missing-texture patches.** Two independent playtests, same split — the green defect is fixed, the white defect is not. The white residual is now split out under its own label, **D228** (rather than leaving defect ① ambiguously half-open), so this row can close on the green mechanism it actually describes. | | D218 | **Wide FOV exposes the per-level draw-distance / fog boundary — "blue artifacting" down long peripheral sightlines (M-84, user QA on the D211 FOV slider).** BUNKER→**Dam main tunnel** at `FovScale` ~150: geometry / backdrop past the level's fog-end distance shows at the widened frustum edges (blue = Dam's fog tint). Not a render bug — `fogLoadCurrentEnvironment` (`bgfog.c:305`) does `viSetZRange(Visibility.BlendMultiplier, Visibility.FarFog)`, so **`Visibility.FarFog` is both the far clip plane and the fog-saturation distance**; levels tune it for the stock ~60° FOV. | **Logged (M-84), fix proposed + parked.** Plan: `Video.DrawDistance` (percent, 100–400, default 100 ⇒ no-op) `#ifdef PORT` multiplier on `FarFog` at `bgfog.c:305` (+ the direct `arg0->Visibility.FarFog` reads just below, so fog stays consistent) plus `Video.DrawDistanceAutoFov` (0/1, default 1 — couples draw distance to `FovScale` unless `DrawDistance` is set explicitly). Clamp effective multiplier ≲2.5× (small znear ⇒ far-field z-fighting risk). **M-87: user confirms still occurring** at high FOV, and — reversing the earlier "not implemented by user request" — **now wants `Video.DrawDistance` implemented as a real QoL feature**, plus scope it to cover **LOD/detail-distance in general**, not just the fog/far-clip boundary this row root-caused. Open question for that broader ask: does GE have a *separate* geometry/object LOD-swap or draw-culling distance mechanism (distinct from `Visibility.FarFog`) worth exposing too, or is fog-distance the only meaningful "how far can you see" knob in this engine? Check for a per-object visibility-radius / simple-model-swap system before assuming `FarFog` is the whole story. **FIXED as designed, awaiting visual verification (M-121).** Implemented exactly the parked plan: `f32 portDrawDistanceMultiplier(void)` (`port/src/video.c`) — an explicit `Video.DrawDistance != 100` wins outright, otherwise `Video.DrawDistanceAutoFov` (default on) couples the multiplier 1:1 to `Video.FovScale`, clamped to `[1.0, 2.5]` (never shrinks draw distance; the 2.5x ceiling is the finding's own far-field z-fighting caution). Applied in `fogLoadCurrentEnvironment` (`bgfog.c`) to a new local (`scaledFarFog`) used at *both* the `viSetZRange` far-clip call and the `g_CurFogDetails.scaled_far_fog_dist` fog-blend read just below it, so the clip plane and the fog math stay consistent per the original plan's own note — the level's `EnvironmentRecord` itself is never mutated (it's shared/reloaded, not a per-frame copy). Both new config keys added to the F10 overlay (`Video.DrawDistance` slider, `Video.DrawDistanceAutoFov` toggle) alongside the existing `FovScale` row. Identity at both defaults (`DrawDistance=100`, `FovScale=100` ⇒ `portFovScale==1.0f`) by construction. **Broader LOD-swap question from M-87 ANSWERED — yes, and now exposed too (same session, M-121 continued).** `src/game/model.c` has a real, distinct geometry-LOD system: `MODELNODE_OPCODE_LOD` nodes whose children are shown/hidden per-frame by `modelUpdateDistanceRelations()` testing a view-space `distance` (scaled by `getPlayer_c_lodscalez()`, itself now FOV-consistent per this session's D222 fix) against each node's `MinDistance`/`MaxDistance` — a genuine detail-swap mechanism, separate from the fog-visibility-fade this row already fixed. It even has an existing ground-truth multiplier hook, `g_ModelDistanceScale`, already used transiently by the game itself (`chr.c`'s 0.3125f close-up override, `front.c`/`title.c`'s menu-model `modelSetDistanceDisabled`). Rather than write to that shared global (would fight those existing overrides), added a second, composed multiplier at the same call site: `f32 portLodDistanceMultiplier(void)` (`port/src/video.c`), driven by new `Video.LodDistance` (percent, 25-400, default 100) and `Video.LodDistanceAutoFov` (**default OFF**, unlike `DrawDistanceAutoFov` — this is meant as a standalone performance lever the user can pull *down*, not something that silently adds cost as FovScale widens) — `distance *= portLodDistanceMultiplier()`, inverse-scaled so `LodDistance=200` keeps full detail twice as far (more cost) and `LodDistance=50` drops to the lower LOD twice as soon (less cost). Both new keys in the F10 overlay alongside `DrawDistance`. Identity at `LodDistance=100`. Rebuilt clean, crash-free at `LodDistance=40` (aggressive) on Dam and at defaults on bunker1. **M-121 live playtest, iterated to a working state.** Two real issues found and fixed while testing live on Dam with the user: (1) **regression** — the D222 fix (above) correctly widened `c_lodscalez` for the screen-edge cull planes, but that same value is *also* read directly by `propobj.c`'s `chrobjFogVisRangeRelated()`/`sub_GAME_7F054C58()` (the character/prop fog-visibility-fade cutoff) — uncompensated, this made guards fade/disappear *sooner* at wide FOV than before D222, the opposite of the intent. Fixed by dividing that same `c_lodscalez` reading by `portDrawDistanceMultiplier()` at both call sites (`#ifdef PORT`), so the "how far can you see" question these two share is answered consistently. (2) **tuning** — a straight 1:1 `Video.FovScale` auto-coupling on `portDrawDistanceMultiplier()` was not aggressive enough; live-tested up twice (1x → 2x → 4x `FovScale`, clamp ceiling raised 2.5x → 4.0x → 6.0x each time) before guards held up and the Dam tunnel's blue-glow artifact receded to the user's satisfaction. Re-verified end-to-end that the fog *ramp* itself (not just the far-clip cutoff) scales with the multiplier — `g_ScaledFarFogIntensity`/`scaled_far_fog_dist` both derive from the same scaled far value — so if a similar artifact resurfaces on another level, the multiplier magnitude or a level-specific tunnel-length edge case are the first things to check, not this math. **User confirms this is good for now.** All three (D222 cull fix, D218 draw-distance + fog-visibility-fade fix, D249 LOD-distance) verified crash-free and landing in the same PR. | | D219 | **PPK explosions render purple on Dam (M-87, user QA, "not 3D but noting").** Effect-color defect, not a texture-decode one — likely the D172 particle-color family (`gfx_lod_tile_offset` / `G_CC_INTERFERENCE` channel-order class) recurring on a different particle type/weapon than the D172 fix covered, or a fresh instance of the same class. | **RESOLVED (M-115, 2026-09-12)** — byte-order fix in `src/game/image.c` scoped by pixel format (16-bit family bswap16-at-store, 32-bit identity); user playtest-confirmed. Superseded history below, which began: **OPEN — observed, not investigated.** Next: `GE_PCDUMP`/`GE_TEXDUMP` a Dam PPK explosion; check whether it's the same `gfx_lod_tile_offset` tile-sampling defect D172 fixed for blood, on an explosion DL D172's fix didn't touch. **Status: re-checked at runtime M-90 — mechanism covered by the D172 fix on current `main`; close pending a human by-eye pass (folds into PR #47 playtest).** **M-89 triage (local-Qwen, static analysis only, not yet runtime-confirmed):** the explosion DLs (`assets/oddtextures.c` `globalDL_0x078/0x120/0x1c8/0x270…`) use the same `G_CYC_2CYCLE`+`G_CC_INTERFERENCE,G_CC_MODULATEIA2`+`G_TL_TILE`, tile0=IA smoke/tile1=RGBA fire mechanism as the already-fixed D172 blood/spark records — no separate/unfixed code path found in `explosion.c` or the asset DLs. If still purple after D172's fix (which the user's Dam report postdates — needs re-confirming on current `main`), the next suspect is the GLSL combiner implementation of `G_CC_INTERFERENCE`+`G_CC_MODULATEIA2` for cycle 2 specifically (`gfx_generate_cc`, `port/fast3d/gfx_pc.cpp:312-494`), not the tile-offset fix. **Next, concrete:** run Dam (`-level_33`) with `GE_D172=1` while triggering a PPK/grenade explosion and check for "BUG" lines in the log (silence = D172's fix already covers this DL and the row can likely close on a re-test; needs a scripted-fire repro, not yet built). **M-90 runtime re-check DONE (scripted-fire repro built with `GE_INPUTSCRIPT`): the D172 fix demonstrably covers Dam's particle DLs — row can close on a human by-eye pass.** Ran `-level_33` headless with `GE_D172=1`, 64 scripted Z-pulses (`GE_INPUTSCRIPT="200:Z;215:Z;..."`, ~15 controller-reads apart) and a `GE_PCDUMP` window over the same span. Result: **40/40 particle multitex tris log `SAMPLED1=tile1 (tmem=392)`** — the correct RGBA fire tile, i.e. exactly the post-D172-fix behaviour (pre-fix these read `SAMPLED1 = tile0 (tmem=0 fmt=IA)`, the smoke texture); **zero** "BUG"/unknown-GBI/D146-abort lines; **0** purple/magenta pixels (`r>120, b>120, g<90, r-g>50, b-g>50`) across all 28 captured frames. Caveat: the user's M-87 "purple" report may predate the D172 fix on their binary (playtests run on whatever build was current); this re-check proves the sampling path is correct on current `main`, which is as far as a headless pass can take it — a final by-eye look at a live Dam PPK firefight is the honest closer (cheap for the user, folds into the D217 PR #47 playtest). **M-108 — REOPENED, the by-eye closer FAILED.** User played a live Dam (`-level_33`) PPK firefight on the current `main` build (post-D233/D234 fixes, same session as D243) and confirmed explosions are **still blue/purple**, directly contradicting M-90's clean scripted-repro result (40/40 correct-tile samples, 0 purple pixels, 28 frames). This means the M-90 `GE_INPUTSCRIPT` scripted-fire repro was very likely **not exercising the same explosion instance/DL the live firefight hits** — plausible gaps: a different explosion variant (grenade vs. bullet-impact spark vs. enemy-death vs. PPK-specific muzzle/hit effect) using a DL the scripted test's Z-pulses didn't trigger, or a state/lighting/prim-color condition (M-89's alternate suspect: the `G_CC_INTERFERENCE`+`G_CC_MODULATEIA2` cycle-2 GLSL combiner implementation, `gfx_generate_cc`, `port/fast3d/gfx_pc.cpp:312-494`) that only manifests under conditions the synthetic repro didn't reproduce. **Do not trust the M-90 all-clear for this finding going forward** — it proved one particle path clean, not that Dam's live-fire purple is fixed. Next: a live `GE_PCDUMP`/`GE_D172=1` capture during an actual purple explosion in real play (not a scripted approximation) is now the only way forward — need to pin down exactly what triggers it (PPK gunshots hitting a surface/enemy? a thrown/tossed explosive? which). **User has now confirmed (2026-09-12) this is NOT Dam-exclusive — game-wide, not just suspected.** Treat as confirmed scope, not pending a second-level check. **User wants this actively fixed, not just triaged** — next session should prioritize a live `GE_PCDUMP`/`GE_D172=1` capture during a real explosion (any level) over further static analysis, since the M-89/M-90 static passes are exhausted on this finding without producing a fix. **New HIGH-interest lead (M-110): `explosion.c` never clears `G_LIGHTING` before drawing any of the 15 particle DLs** (`globalDL_0x078` through `0x9a8` in `assets/oddtextures.c`, all structurally identical, all `G_CC_INTERFERENCE`+`G_CC_MODULATEIA2`) — grepped every particle-draw call site in `explosion.c` (`gSPClearGeometryMode` calls at lines 883/1495/1718/2227 only ever clear `G_CULL_BOTH`/`G_FOG`/`G_CULL_FRONT`, never `G_LIGHTING`) and confirmed `G_CC_INTERFERENCE,G_CC_MODULATEIA2` is used nowhere else in the codebase, so it's safe to key a probe on it. The port's vertex path (`port/fast3d/gfx_pc.cpp:1301-1346`) branches hard on `rsp.geometry_mode & G_LIGHTING`: if lighting is on when these particles draw, the authored RGBA tint in `v->v.cn[]` gets reinterpreted as a normal vector, and the resulting SHADE color (fed into `G_CC_MODULATEIA2`'s `COMBINED*SHADE`) becomes a scene-light color instead of the DL's actual red/orange fire tint — a mechanism that would plausibly read as "wrong color, state-dependent, game-wide" exactly as reported, and would explain why a synthetic scripted-fire repro (M-90, no nearby lit models) came back clean while live firefights (guards/lit models on screen) don't. **Not yet confirmed** — this depends on whatever last touched `geometry_mode` before the particle draw in a given frame, which the game's own N64-accurate command order controls; it is not yet established whether this is a genuine port bug (fast3d misordering/dropping a `G_GEOMETRYMODE` command relative to real hardware) or something that would reproduce identically on N64 (in which case it isn't actually the D219 mechanism, since N64 doesn't show purple explosions). **Instrumented, not fixed:** added a `#ifdef PORT`/`GE_D172=1`-gated log line to the existing D172 multitex-tri probe (`port/fast3d/gfx_pc.cpp` ~line 1740, `gfx_sp_tri1`) that now also prints `geometry_mode` and flags `LIGHTING=ON <-- D219 SUSPECT` whenever one of these particle triangles draws with lighting on. Sanity-checked against the known-clean M-90 repro (headless `-level_33`, 40 scripted Z-pulses): probe fires exactly 40/40 as before, all show `LIGHTING=off` — consistent with the already-known-clean case, confirming the probe works and doesn't false-positive. **Next, concrete:** run `build-pc/d172_silo.bat` (or equivalent, `GE_D172=1`) and trigger a real explosion (any level, any explosive source) near visible character models; grep the log for `D219 SUSPECT`. A hit confirms the mechanism and turns this into a real fix (clear `G_LIGHTING` in the port's particle-draw path or in fast3d's geometry-mode tracking, whichever turns out to be the actual divergence from N64); a miss across a real purple-explosion capture rules this whole lead out. **M-112 (this session): decode-chain code review + two new inert diagnostic probes; runtime capture NOT obtained (environment limitation, see below). Confidence: LOW-MEDIUM on a narrowed next step, no fix applied.** Read the full N64 texture decompression ground truth for these assets end to end: `texLoadFromDisplayList`→`texLoad`→`texInflateNonZlib`/`texInflateZlib`→(`texReadUncompressed` | `texInflateHuffman`/`texInflateRle`+`texChannelsToPixels` | `texBuildLookup`+`texInflateLookup`/`texInflateLookupFromBuffer`) (`src/game/image.c`), plus `gimgFixupGlobalimagetable`/`gimgSyncCompiledGlobalDLs` (`port/src/gimgfixup.c`) and `import_texture_rgba16` (`port/fast3d/gfx_pc.cpp:653-681`). Findings: (1) `IMAGE_FIRE_0..14` are **not** `sImageTableEntry`-table images (unlike `IMAGE_IMPACTMULTI`/`IMAGE_IMPACTREDBRICK*`/flare/ammo images, which live in the 32 `sImageTableEntry` arrays at segment offset 0xAC8+ and get their `G_SETTIMG` marker synthesized at runtime by `texSelect`, `src/game/othermodemicrocode.c:318`) — FIRE_N's `gsDPSetTextureImage(...IMAGESEG(IMAGE_FIRE_N))` is baked directly into the compiled `globalDL_0x078..0x9a8` display lists (segment offset 0x000-0xA50) alongside the interleaved, correctly-rendering `IMAGE_SMOKE_0..14` (IA16). Both FIRE's and SMOKE's markers go through the *same* `gimgFixupGlobalimagetable` part-1 DL-bswap step and the *same* `texLoadFromDisplayList`→`texLoad` resolution — no separate/divergent table-offset math applies to FIRE_N specifically (this narrows away part of the open brief's option (a): there is no `sImageTableEntry`/`s_tbl_offs[]` bounds bug possible here, because FIRE_N never goes through that array class at all). (2) `texChannelsToPixels`'s `TEXFORMAT_RGBA16` case and `texReadUncompressed`'s `TEXFORMAT_RGBA16` case are decomp ground truth (byte-exact, `#ifdef PORT` free) and are format-generic, not texture-specific — a bug there would require a same-format sibling to also be wrong, and IMPACT/FLARE RGBA16 sprites (same `G_IM_FMT_RGBA,G_IM_SIZ_16b`, same underlying `texLoad`) are the only same-format comparison point on hand, **but this session did not runtime-confirm whether those actually render correct colors in game** — flagging as an unverified assumption inherited from earlier sessions' "shared decoder is proven correct" framing, not re-proven here. (3) `texSwapAltRowBytes` is already an unconditional no-op on `PORT` (D159, `src/game/image.c:2219`, `return;` before any format switch) so it cannot be format- or texture-selective on the port — ruled out as a contributor for FIRE specifically (it's equally a no-op for every other texture). **New, added, NOT yet exercised:** two matching `GE_D219RAW`-gated diagnostic probes (inert unless the env var is set, `#ifdef PORT`, no behavior change): (a) in `texInflateNonZlib` (`src/game/image.c`, right after the per-image header read, and again right after decode) — logs `texnum`, `format`, `width`, `height`, `compmethod` from the bitstream header, then the first row of decoded RGBA5551 pixel words, for every image shaped 16x14 (the FIRE tile shape); (b) in `import_texture_rgba16` (`port/fast3d/gfx_pc.cpp`) — logs the same pixel words as read back at upload time, so a session can diff "as decoded" vs "as consumed" to rule pool corruption/mis-addressing in or out. **Environment blocker hit this session:** could not get the built `ge007.x86_64.exe` to produce *any* stderr/log output when launched from this delegated agent's shell, across four different invocation strategies (direct foreground exec, backgrounded `&`/`nohup`, `cmd /c start foo.bat` detached, `cmd /c foo.bat` synchronous with a hard timeout) — the process either exits or is killed before a single `osSyncPrintf`/`sysLogPrintf` line reaches the redirected log file, even though every print call in this codebase does an explicit `fflush(stderr)`. A plain GUI sanity check (launching `notepad.exe` the same way) worked fine, ruling out a blanket "no GUI" sandbox restriction, so the failure is specific to this binary/environment combination and its cause was not identified. Per `env-no-runtime-verify` (session memory): runtime verification is session-dependent; treat this session's empty captures as inconclusive, not as evidence of anything. **Next, concrete (for a session that CAN capture runtime output, interactive or otherwise):** (i) run any level with `GE_D219RAW=1` during an explosion/PPK firefight (or simply on level load, since these are likely eagerly cached) and read the logged `compmethod` for `IMAGE_FIRE_N` — cross-reference against a `GE_D219RAW`-instrumented capture of a *confirmed-correct-on-screen* RGBA16 sprite from the sibling `sImageTableEntry` families (e.g. `IMAGE_IMPACTMULTI`) to see whether FIRE uses a different `TEXCOMPMETHOD_*` (the compressed-bitstream compression method, not the pixel format) than a texture confirmed to render correctly — `TEXCOMPMETHOD_LOOKUP`/`_HUFFMANLOOKUP`/`_RLELOOKUP` (`texBuildLookup`+`texInflateLookup*`) build/consume a packed-color lookup table with a completely different bit-layout code path than `_HUFFMAN`/`_HUFFMANPERHCHANNEL`/`_RLE` (`texChannelsToPixels`, per-plane R/G/B/A order); if FIRE's method differs from the working comparison texture's, that pinpoints exactly which decode function to scrutinize next, rather than continuing to treat "the RGBA16 decoder" as one monolithic suspect. (ii) if the compmethod matches, the bug is genuinely upstream of decode (ROM/asset-extraction data itself, not the decompression code) — at that point compare the decompressed byte stream against a reference N64 emulator's texture-dump of the same asset (this repo has no such reference on hand) to establish definitively whether the ROM bytes for FIRE really do decode to blue/purple on real hardware too (in which case the bug is actually downstream in the color-combiner path after all, reopening the D172/G_CC_INTERFERENCE lead this session did not have budget to re-examine) or whether the extraction into this repo's `assets/oddtextures.c`-adjacent ROM data diverges from the real cart image for just these 15 offsets. Probes left in tree, both inert by default: `GE_D219RAW` (`src/game/image.c` `texInflateNonZlib`, `port/fast3d/gfx_pc.cpp` `import_texture_rgba16`). diff --git a/port/src/optionsoverlay.c b/port/src/optionsoverlay.c index 96dd61fb..e1abea30 100644 --- a/port/src/optionsoverlay.c +++ b/port/src/optionsoverlay.c @@ -127,9 +127,11 @@ static struct Row rows[] = { /* D232: the community "no damage flash" toggle (suppresses the red/green * hit-flash overlay in bondview2). */ { "Game.NoHitFlash", "No hit flash", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 }, - /* D216: read once at title-stage load (lv.c) -- toggling it takes effect - * the next time the title screen is reached. */ - { "Game.SkipIntro", "Skip intro", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 }, + /* D216/Game.SkipIntro: user report (v0.2.1 testing) that it breaks audio + * -- pulled from the menu until root-caused. Not exposed to players; the + * config var + lv.c hook stay in place (dead unless an existing ini has + * it set, which no menu path can do any more). Do not re-add without + * fixing the underlying issue first. */ /* D257: everything-unlocked goodie (default ON). Consumed at startup by * main.c -- applies from the next launch. */ { "Game.AllUnlocked", "All unlocked", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 },