forked from Ports/goldeneye-pc-port
D302/D304/D225/D164/D252/D305: perf regression, F10 menu overhaul, small fixes pass
- D302: fix a reintroduced D250-class perf regression -- 4 uncached getenv() calls added by D236 diagnostic passes 10/12/14/16 were running on every triangle draw in gfx_sp_tri1; cached with the established pattern. - D304: F10 mouse-sensitivity menu overhaul -- widened Input.AimModeSens' clamp to match Input.MouseTurnSpeed, then collapsed both plus the aim/turn link toggle into a single Input.MouseSensitivity control per user request (avoids decoupling into a confusing state). Fixed two real overlay bugs found along the way: overlayUpdateScroll() bottom-anchored the clicked row on every click (not just when off-screen), and overlayRowAtY()'s hit-test scanned rows past the last one actually drawn, letting a click near a scroll boundary silently hit the wrong row (reproduced as "All unlocked" triggering "Quit to desktop" instead). - D225: GE_DEBUG_UNLOCKALL=1 dev-tooling toggle (file2.c), default off. - D164/D76: docs-only correction -- the legal-screen one-line fix was already committed 8dfc578f4; findings.md just never caught up. - D252: root-caused and fixed a 6+-session-old rainbow-particle bug -- explosionRenderPart() wrote 5 vertices into a 4-vertex allocation, corrupting the next particle/effect's data in the shared vertex pool. User-confirmed live: "Rainbow went away." - D305: Steam Deck SIGSEGV recurrence past D285's own fix -- sndHandleEvent's preempt-scan dereferenced its list-walk pointer before checking it for NULL. Added the missing guard. User-verified crash-free on the Deck across multiple sessions, including toggling Game.AllUnlocked off/on. D236 pass-16 diagnostic (GE_D236RAW, bg.c) carried along from a prior session's in-progress investigation; unchanged, env-gated, zero-cost unset.
This commit is contained in:
1 parent
e7c76cf8eb
commit
416396e504
9 files changed
+426
-31
No files matched your search
@@ -8,7 +8,7 @@ D51-D58,RESOLVED,"font pixeldata, osGetCount tick rate, model RW pools, cseq BE
|
||||
D59-D68,RESOLVED,"intro render: blood-RLE clobber, DMA validate, OSMesgQueue, HEADS/BODIES sentinels, romCopy width, image_entry, Globalimagetable BE→LE",resolved
|
||||
D70-D74,RESOLVED,"intro-logo pixels: C-array bswap, UV path, sinf/cosf DVAL(), texture-import truncation",resolved
|
||||
D75,OPEN,front-end 3D model transforms (D77 audio split out below —,"OPEN (parked). M-32 triage (see §F ""D75 ADDENDUM""):"
|
||||
D76 · D164,PROPOSED,Legal screen only draws line 1 —,fix proposed (not applied —
|
||||
D76 · D164,FIXED,Legal screen only draws line 1 —,"FIXED — this row and the ## D164 entry below were stale; the proposed one-line #ifdef PORT fix was actually committed 8dfc578f4 (2026-08-31), well before this correction (2026-09-18)."
|
||||
D159,FIXED,"front-end wallet-Bond photo ""interlaced""/combed (RC1 / D149) —",FIXED (#ifdef PORT no-op the swap in image.c)
|
||||
D161,FIXED,Depot (-level_30) ceiling = bright-blue speckle + radial rays (B2).,FIXED (port/fast3d/gfx_pc.cpp)
|
||||
D165 · D166,FIXED,"Input-layer polish (M-31, port-only):",FIXED
|
||||
@@ -32,7 +32,7 @@ D221,FIXED,SELECT FILE mouse hit-box offset —,"FIXED (M-91) — explicit {min,
|
||||
D222,FIXED,High Video.FovScale breaks culling —,"FIXED, awaiting visual verification (M-121)."
|
||||
D223,FIXED,QoL ask: mouse-wheel weapon cycling should be directional like a normal PC shooter —,"FIXED (M-105, port/src/input.c) —…"
|
||||
D224,?,"QoL gap (M-87, from a PD-PC-port survey):","DEPRIORITIZED (M-87, user call)."
|
||||
D225,OPEN,"Dev-tooling QoL (M-87, user ask):","OPEN — root-caused, not implemented."
|
||||
D225,FIXED,"Dev-tooling QoL (M-87, user ask):","FIXED (2026-09-18) — GE_DEBUG_UNLOCKALL=1 short-circuits fileGetIsCheatUnlocked() (src/game/file2.c, #ifdef PORT, cached getenv) to always return TRUE; default off, no save mutation."
|
||||
D226,OPEN,"QoL ask: player-adjustable HUD scale for the ammo counter, bottom-left pickup/item status text, and top-of-screen dialogue/subtitle text.","OPEN — root cause / call sites identified, not designed or implemented."
|
||||
D227,FIXED,Sky renders as two independently-moving halves —,"FIXED (M-100b, PR #43) —"
|
||||
D229,FIXED,"IsWater-level water renders green, pulses blue<->green (M-101; Frigate -level_26).","FIXED (port-layer) — ZLIB+CI8 water image now imports via the CI8 palette path (gfx_pc.cpp); Frigate verified live (""looks better""); sweep + Surface 2 by-eye still owed."
|
||||
@@ -128,11 +128,11 @@ D155,FIXED,"Facility outro-cutscene end → ""hang"" = unclamped deltaFrames spi
|
||||
D153,RESOLVED,"-level_09 frame ~900–1400 0xc0000005 = D117/D134 load flakiness, NOT a bug (M-28).",NOT A BUG — closed. Run anchors on a quiet machine.
|
||||
D151,FIXED,Watch data all blank —,"FIXED (src/bondtypes.h, #ifdef PORT —…"
|
||||
D130,RESOLVED,"crash class C2 (Facility -level_34, Runway -level_35):",resolved — stage all 6 N64 fields in a local f[6] before writing any.
|
||||
D164,PROPOSED,"Disclaimer / legal screen only draws the first line (D76 root cause, M-31).","FIX PROPOSED, NOT APPLIED (front.c is owned by another agent this session).…"
|
||||
D164,FIXED,"Disclaimer / legal screen only draws the first line (D76 root cause, M-31).",FIXED — corrected 2026-09-18:
|
||||
D165,FIXED,"Front-end menu cursor doesn't feel like a mouse pointer (M-31, user bug B5 follow-up).","FIXED (port/src/input.c; Input.MenuPointerMode, Input.HipfirePitchSpeed added)."
|
||||
D166,FIXED,Hipfire mouse pitch is digital while yaw is analog —,FIXED (port/src/input.c).…
|
||||
D235,OPEN,Wrong textures load after dying/restarting a level —,OPEN — needs: does it repro on a *fresh* level boot (rules out reload-state) vs only after death/restart (confirms it); which surfaces; a screenshot.…
|
||||
D236,OPEN,"Surface 1: 2D billboard trees render as a wall/sheet of tree texture instead of discrete camera-facing tree sprites (M-106, user QA report).","OPEN, thirteenth pass (2026-09-18) —"
|
||||
D236,OPEN,"Surface 1: 2D billboard trees render as a wall/sheet of tree texture instead of discrete camera-facing tree sprites (M-106, user QA report).","OPEN, sixteenth pass (2026-09-18) —"
|
||||
D237,OPEN,"QoL ask (M-106, user QA):",OPEN — not designed; port-only
|
||||
D238,OPEN,"QoL ask (M-106, user QA):",OPEN — not designed; port-only (port/src/input.c + config.c)
|
||||
D239,?,"QoL ask (M-106, user QA):",CLOSED — executed in the v0.2.0 prep pass (port-only)…
|
||||
@@ -147,7 +147,7 @@ D248,FIXED,"Port runs a stable 30 FPS instead of 60 FPS game-wide (M-117, user a
|
||||
D249,OPEN,"LOD/culling breaks down on large open levels (Streets, Egyptian named specifically) at *default* FOV —","OPEN — M-142 static review ruled out the unscaled-far-clip and LOD-multiplier suspects (both already correct); narrowed to the portal/room-visibility BFS, needs a live screenshot+location, not more c…"
|
||||
D250,FIXED,"FPS is inconsistent/low on many levels after 60 Hz shipped (M-119, user QA; D248 follow-on).","FIXED — per-call getenv() cost was the bottleneck; rock-steady 60-61 fps after, user re-tested 1440p/4K clean."
|
||||
D251,FIXED,F10 options overlay QoL pass:,"FIXED (port-only, port/src/optionsoverlay.c)."
|
||||
D252,FIXED,"Particle effects cycle through a rainbow palette (M-124, user playtest of the v0.2.0-pre candidate).","CLOSED as a duplicate of D219 (M-188, 2026-09-18) —"
|
||||
D252,FIXED,"Particle effects cycle through a rainbow palette (M-124, user playtest of the v0.2.0-pre candidate).","FIXED, VISUALLY CONFIRMED (2026-09-18)."
|
||||
D253,FIXED,Facility (and any level with a FORCECROUCH/LADDER linked special tile) SIGSEGV on Linux/Steam Deck —,FIXED — Deck-verified:
|
||||
D254,FIXED,Linux crash handler destroyed its own logs:,FIXED — field-verified.
|
||||
D255,FIXED,Facility firefight SIGSEGV on Steam Deck (v0.2.0-pre CI bundle; D253 fix confirmed working).,"FIXED (M-140) — D3x-class 32-bit pointer truncation (vtxstore_fix_refs read Model* through the mistyped ChrRecord.chrflags 4-byte-enum alias instead of ObjectRecord.model), not the destroy-path race…"
|
||||
@@ -162,7 +162,7 @@ D263,RESOLVED,v0.2.1 (user report):,"RESOLVED (not a bug), but refined (v0.2.1 f
|
||||
D265,?,"D236 investigation (port-layer, port/fast3d/gfx_pc.cpp):",PARKED — reverted to baseline; evidence + suspect list above.
|
||||
D258,OPEN,PAL/JP asset conversion is broken at the source-data level —,"OPEN — repair path verified, not applied (out of v0.2.0 scope)."
|
||||
D284,FIXED,port/src/random.c's MIPS64 dsll32/dsrl32 shift helpers were backwards —,"FIXED, 21-level sweep clean, no crashes —"
|
||||
D285,FIXED,Steam Deck SIGSEGV during D255 live-testing = SEPARATE bug:,"FIXED (M-141, src/snd.c) —"
|
||||
D285,PARTIAL,Steam Deck SIGSEGV during D255 live-testing = SEPARATE bug:,"PARTIALLY FIXED, RECURRED PAST THE FIX (see D305, 2026-09-18)."
|
||||
D286,OPEN,"User-confirmed real ~8s frame freeze on Caverns, Steam Deck —","OPEN — one confirmed occurrence, no repro trigger identified, not root-caused; distinct from D255/D285."
|
||||
D287,OPEN,"User-confirmed real, non-self-recovering freeze on Windows (M-141, 2026-09-15/16) after ~15-20 minutes with no input, triggered by clicking back into the… —","OPEN — one confirmed occurrence, real (not self-recovering), not root-caused; gfxFrameMsgQ full is a lead, not a confirmed cause."
|
||||
D288,OPEN,"Silo intro: a huge, flat-shaded, untextured brown/tan triangle sweeps across the frame during the camera pan down the missile shaft (M-141, user report —… —","OPEN — visually confirmed and precisely bounded (frames ~562-741 of a bare -level_20 boot), root cause not yet identified; promoted from the long-parked GRAP… |"
|
||||
@@ -179,3 +179,7 @@ D298,?,Full HUD (not just the aim-mode crosshair) missing in a v0.3.0 dev build
|
||||
D299,FIXED,Game.SkipIntro crashed selecting any save slot except one —,"FIXED — fileValidateSaves() (only called from the skipped legal screen) never ran under SkipIntro, leaving saves[] all-zero; added the missing call to the SkipIntro shortcut (src/game/lv.c, #ifdef PO…"
|
||||
D300,FIXED,Hipfire look feels non-uniform (issue #89):,"FIXED, user-confirmed live —"
|
||||
D301,FIXED,"fileGetIsCheatUnlocked() NULL-deref, unguarded on every call site, not just the D299/SkipIntro one (GitHub #87, two reports with an identical crash PC/fault ad…",FIXED locally on release/v0.3.0 (not pushed) —
|
||||
D302,FIXED,"Render FPS regression to ~40-45fps on release/v0.3.0 (user QA on the local v0.3.0 pre-release build, 2026-09-18) —","FIXED — 4 uncached getenv() calls added by D236 passes 10/12/14/16 directly inside gfx_sp_tri1 (port/fast3d/gfx_pc.cpp, per-triangle) cached with the same static int x = -1 pattern D250 established e…"
|
||||
D303,OPEN,"Some muzzle flashes (M16 named specifically, ""and some others"") draw wrong —","OPEN — reported only, not yet investigated."
|
||||
D304,FIXED,"Mouse sensitivity menu overhaul (user request, 2026-09-18):","FIXED — see full entry for both the range/parity pass and the same-day follow-up (collapsed to a single Input.MouseSensitivity menu row + fixed overlayUpdateScroll()'s unconditional bottom-anchor, th…"
|
||||
D305,FIXED,"Steam Deck SIGSEGV on the v0.3.0 pre-release build (Cavern, early in the level, Game.AllUnlocked previously enabled on the session/save) —",FIXED (defensive guard) —
|
||||
|
+162
-8
@@ -392,7 +392,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
|
||||
| D69 · D78–D84 | stage load (`load_bg_file`): bg/stan offline sidecar (`d69_emit.py`) + StandTile/bg_room_data ABI | resolved |
|
||||
| D70–D74 | intro-logo pixels: C-array bswap, UV path, sinf/cosf `DVAL()`, texture-import truncation | resolved |
|
||||
| D75 | front-end 3D model transforms (D77 audio split out below — resolved, unrelated cause) | OPEN (parked). **M-32 triage (see §F "D75 ADDENDUM"): (a) D73-scope-gap RULED OUT (gu tree fully endian-clean). Splits in two: Bug 1 = logo/photo transform = the parked D114/D116 fast3d viewport mirror (not game code). Bug 2 = absent animated models = category (b). M-32b runtime probe (`GE_D75=1`): `render_pos`/`dynAllocate`-arena hypothesis RULED OUT (render_pos valid + fresh each frame), model instances valid (nMtx 21/1), zero fast3d DL warnings — failure is downstream in `drawjointlist`/`dotube` vtx/node-DL resolution or an off-screen `basemtx`. Needs a drawjointlist-level probe. M-33 (upright captures, D168 fixed): "Bug 1 = D114/D116 mirror" retracted; **gun-barrel Bond RENDERS fine** (walk + fire, upright) — the "absent" reports were the flipped capture; **Nintendo logo genuinely broken** (renders as 2 white blobs, shifted left — a real `logoinst` transform/geometry bug, not a flip); cast roll not re-captured. See §F "D75 Bug 2 — M-33 UPDATE". **v0.2.0-pre playtest (user):** Nintendo logo still broken; Rareware logo nearly right but its texture filtering looks off; MISSION COMPLETE / mode-select screens have no 3D models at all, so the earlier "absent mode-select model" framing was inaccurate.** |
|
||||
| D76 · D164 | **Legal screen only draws line 1 — root-caused (M-31)**: linker-adjacency assumption broken by mingw. — full `## D164` entry at file tail | fix proposed (not applied — `front.c` owned by another agent) |
|
||||
| D76 · D164 | **Legal screen only draws line 1 — root-caused (M-31)**: linker-adjacency assumption broken by mingw. — full `## D164` entry at file tail | **FIXED** — this row and the `## D164` entry below were stale; the proposed one-line `#ifdef PORT` fix was actually committed `8dfc578f4` (2026-08-31), well before this correction (2026-09-18). Docs simply never caught up to the merge. |
|
||||
| D159 | front-end wallet-Bond photo "interlaced"/combed (RC1 / D149) — `texSwapAltRowBytes` odd-row 8-byte pre-swap (N64 RDP odd-line TMEM XOR compensation) not reversed by fast3d | FIXED (`#ifdef PORT` no-op the swap in `image.c`) |
|
||||
| D161 | Depot (`-level_30`) ceiling = bright-blue speckle + radial rays (B2). A CI8 tile drawn with `gsDPSetTextureLUT(G_TT_NONE)` was decoded against the stale `rdp.palette` → garbage. Fix: `#ifdef`-free narrow route CI→I when `palette_fmt == G_TT_NONE` in `gfx_pc.cpp import_texture()`. | FIXED (`port/fast3d/gfx_pc.cpp`) |
|
||||
| D165 · D166 | **Input-layer polish (M-31, port-only)**: cursor P-controller + hipfire C-pulse. — full `## D165` / `## D166` entries at file tail | FIXED |
|
||||
@@ -416,7 +416,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
|
||||
| D222 | **High `Video.FovScale` breaks culling — NPCs and some world objects stop rendering near the screen edges (M-87, user QA).** — full `## D222` entry at file tail | **FIXED, awaiting visual verification (M-121).** Root cause traced one step further than the original write-up… |
|
||||
| D223 | **QoL ask: mouse-wheel weapon cycling should be directional like a normal PC shooter — wheel up = next weapon, wheel down = previous — instead of both directions…** — full `## D223` entry at file tail | **FIXED (M-105, `port/src/input.c`) —… |
|
||||
| D224 | **QoL gap (M-87, from a PD-PC-port survey): the N64 Rumble Pak is fully dropped instead of modernized.** — full `## D224` entry at file tail | **DEPRIORITIZED (M-87, user call).** Gamepad-only —… |
|
||||
| D225 | **Dev-tooling QoL (M-87, user ask): a debug-build/env toggle that unlocks all missions + all cheats, for testing.** — full `## D225` entry at file tail | **OPEN — root-caused, not implemented.** Plan… |
|
||||
| D225 | **Dev-tooling QoL (M-87, user ask): a debug-build/env toggle that unlocks all missions + all cheats, for testing.** — full `## D225` entry at file tail | **FIXED (2026-09-18)** — `GE_DEBUG_UNLOCKALL=1` short-circuits `fileGetIsCheatUnlocked()` (`src/game/file2.c`, `#ifdef PORT`, cached getenv) to always return TRUE; default off, no save mutation. Build-verified; headless boot with `GE_STARTMENU=7` (mission select) crash-free. |
|
||||
| D226 | **QoL ask: player-adjustable HUD scale for the ammo counter, bottom-left pickup/item status text, and top-of-screen dialogue/subtitle text.** — full `## D226` entry at file tail | **OPEN — root cause / call sites identified, not designed or implemented.** Two viable directions… |
|
||||
| D227 | **Sky renders as two independently-moving halves — "two sky processes" (M-93 QA; root cause M-100b: unit error, `unk20`/`unk24` are S10.5 not texels).** — full `## D227` entries (M-94→M-100b) at file tail | **FIXED (M-100b, PR #43)** — also fixed "scrolls too fast" (M-96) + over-tiling; human play-test confirmed. Full history in the sections. |
|
||||
| D229 | **IsWater-level water renders green, pulses blue<->green (M-101; Frigate `-level_26`).** — full `## D229` entry at file tail | **FIXED (port-layer)** — ZLIB+CI8 water image now imports via the CI8 palette path (`gfx_pc.cpp`); Frigate verified live ("looks better"); sweep + Surface 2 by-eye still owed. Full trail in the section. |
|
||||
@@ -530,7 +530,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
|
||||
| D153 | **`-level_09` frame ~900–1400 `0xc0000005` = D117/D134 load flakiness, NOT a bug (M-28).** — full `## D153` entry at file tail | NOT A BUG — closed. Run anchors on a quiet machine. |
|
||||
| D151 | **Watch data all blank — objectives / mission background / M / Q / Moneypenny (M-28, user bug report).** — full `## D151` entry at file tail | FIXED (`src/bondtypes.h`, `#ifdef PORT` —… |
|
||||
| D130 | **crash class C2 (Facility `-level_34`, Runway `-level_35`):** — full `## D130` entry at file tail | resolved — stage all 6 N64 fields in a local `f[6]` before writing any. Facility + Runway now PASS (16→18/21). Probes reverted. |
|
||||
| D164 | **Disclaimer / legal screen only draws the first line (D76 root cause, M-31).** — full `## D164` entry at file tail | **FIX PROPOSED, NOT APPLIED** (`front.c` is owned by another agent this session).… |
|
||||
| D164 | **Disclaimer / legal screen only draws the first line (D76 root cause, M-31).** — full `## D164` entry at file tail | **FIXED** — corrected 2026-09-18: the one-line `#ifdef PORT` fix (`legal_text_end = legalpage_text_array + ARRAY_COUNT(...)`, `front.c`) was already committed `8dfc578f4` (2026-08-31); confirmed live in the current tree via `git blame`. This row and the `## D164` body's "NOT APPLIED" status were simply never updated after the merge — no code change was needed this session, docs-only correction. |
|
||||
|
||||
| D159 | **Front-end wallet-Bond photo (and other large front-end I4/I8/IA8 textures) render "interlaced" / combed (M-31, user bug RC1 / D149).** — full `## D159` entry at file tail | **FIXED** (`src/game/image.c`, `#ifdef PORT` early-`return` in `texSwapAltRowBytes` —… |
|
||||
|
||||
@@ -539,7 +539,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
|
||||
| D233 | **All levels (intermittent): indoor level geometry invisible near doors — you can see through walls/floors into rooms you can't see into, while world objects…** — full `## D233` entry at file tail | **FIXED + VISUALLY VERIFIED (M-107 fix, M-108 verify).** See the other D233 row for full detail. |
|
||||
| D234 | **Dam: guard towers along the dam structure invisible except the first one (M-106, user QA report).** — full `## D234` entry at file tail | **VISUALLY VERIFIED FIXED (M-108), mechanism unexplained.** See the other D234 row for detail. |
|
||||
| D235 | **Wrong textures load after dying/restarting a level — not Facility-specific, seen on multiple levels (M-106, user QA report; clarified same session).** — full `## D235` entry at file tail | OPEN — needs: does it repro on a *fresh* level boot (rules out reload-state) vs only after death/restart (confirms it); which surfaces; a screenshot.… |
|
||||
| D236 | **Surface 1: 2D billboard trees render as a wall/sheet of tree texture instead of discrete camera-facing tree sprites (M-106, user QA report).** — full `## D236` entry at file tail | OPEN, thirteenth pass (2026-09-18) — live capture (user walked toward the tree line, `GE_D236ORDER=1`) reached 121 tree-card tris with favorable draw order, yet the wall bug still reproduced — demotes pass 12's draw-order theory, redirects to the tree class's own unverified fog computation (bimodal 25/255, never checked against N64) as the more promising lead. Not v0.3.0-blocking, parked again. |
|
||||
| D236 | **Surface 1: 2D billboard trees render as a wall/sheet of tree texture instead of discrete camera-facing tree sprites (M-106, user QA report).** — full `## D236` entry at file tail | OPEN, sixteenth pass (2026-09-18) — new `GE_D236RAW` headless dump PROVES every room-background static vertex (all ~40 rooms sampled) is alpha=255 on load, ruling out "static ROM-authored low alpha" outright; a `vtxseg` extension to `GE_D236ALPHA` shows the tree class's vertices come from a raw/unsegmented pointer (not `SPSEGMENT_MODEL_VTX`/`BG_VTX`), consistent with a dynamically-built quad — but all 4 known `model.c` dynamic-quad builders (gunfire/shadow/rottex/tube) and propobj.c's screen-sprite builder were read in full and ruled out by content/addressing mismatch. The actual write site remains genuinely unidentified after a real search; next step is a stack-backtrace-style probe, not more signature-matching. Not v0.3.0-blocking, user is continuing anyway. |
|
||||
| D237 | **QoL ask (M-106, user QA): F10 options overlay — make it scrollable with the mouse wheel, expose more settings into it (e.g. screen-flash / no-hit-flash…** — full `## D237` entry at file tail | OPEN — not designed; port-only |
|
||||
| D238 | **QoL ask (M-106, user QA): mouse sensitivity rework toward normal PC-shooter feel.** — full `## D238` entry at file tail | OPEN — not designed; port-only (`port/src/input.c` + `config.c`) |
|
||||
| D239 | **QoL ask (M-106, user QA): click-to-lock vs always-grab parity.** — full `## D239` entry at file tail | CLOSED — executed in the v0.2.0 prep pass (port-only)… |
|
||||
@@ -554,7 +554,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
|
||||
| D249 | **LOD/culling breaks down on large open levels (Streets, Egyptian named specifically) at *default* FOV — far-away geometry has visible culling issues (M-119…** — full `## D249` entry at file tail | OPEN — M-142 static review ruled out the unscaled-far-clip and LOD-multiplier suspects (both already correct); narrowed to the portal/room-visibility BFS, needs a live screenshot+location, not more code reading.… |
|
||||
| D250 | **FPS is inconsistent/low on many levels after 60 Hz shipped (M-119, user QA; D248 follow-on).** — full `## D250` entry at file tail | FIXED — per-call `getenv()` cost was the bottleneck; rock-steady 60-61 fps after, user re-tested 1440p/4K clean. |
|
||||
| D251 | ****F10 options overlay QoL pass:** — full `## D251` entry at file tail | **FIXED (port-only, `port/src/optionsoverlay.c`).** (a) The panel now computes how many rows fit the *actual* current 2D height (`maxVisibleRows()` from `viGetY()`) and scrolls… |
|
||||
| D252 | **Particle effects cycle through a rainbow palette (M-124, user playtest of the v0.2.0-pre candidate).** — full `## D252` entry at file tail | **CLOSED as a duplicate of D219 (M-188, 2026-09-18)** — the user's actual live-observed symptom ("lingering particle sprites after an explosion") is the same `bullet_spark_render` bug D219 fixed; user-confirmed live on Facility. The *original*, older main-explosion-fireball palette question (`explosion.c`/`import_texture_rgba16`/`G_CC_INTERFERENCE` combiner) is a separate, narrower open thread the user reports was already fixed a while back and hasn't recurred — re-open only if a fresh *fireball* (not residue/embers) rainbow report comes in. |
|
||||
| D252 | **Particle effects cycle through a rainbow palette (M-124, user playtest of the v0.2.0-pre candidate).** — full `## D252` entry at file tail | **FIXED, VISUALLY CONFIRMED (2026-09-18).** Root cause: `explosionRenderPart()` (`src/game/explosion.c`) allocated 4 `Vtx` slots but wrote 5, corrupting whatever the next particle/effect allocates from the shared vertex pool — found by direct code read (GBI emission only ever loaded 4, so the 5th write was provably dead for its own draw), not a live capture, closing a bug 6+ prior investigation sessions missed. Fixed (stray write deleted), user-authorized given it's decomp-original code, not an ABI artifact. **User confirmed live on Jungle: "Rainbow went away."** Closed. A second, separate, unconfirmed port-layer lead (stale TMEM-slot `src_fmt` reroute) remains logged in case any residual rainbow report ever recurs. |
|
||||
| D253 | **Facility (and any level with a FORCECROUCH/LADDER linked special tile) SIGSEGV on Linux/Steam Deck — `stanCheckLinkedSpecialTile` stores to a garbage…** — full `## D253` entry at file tail | FIXED — Deck-verified: user got past the crouch tile with the re-cut bundle (a *new*, later crash is D255). |
|
||||
| D254 | **Linux crash handler destroyed its own logs: `ge007.crash.log` only ever contained the SIGABRT block, never the original SEGV.** — full `## D254` entry at file tail | FIXED — field-verified. |
|
||||
| D255 | **Facility firefight SIGSEGV on Steam Deck (v0.2.0-pre CI bundle; D253 fix confirmed working).** — full writeup directly below this table (row status was stale; see M-140 update) | **FIXED (M-140)** — D3x-class 32-bit pointer truncation (`vtxstore_fix_refs` read `Model*` through the mistyped `ChrRecord.chrflags` 4-byte-enum alias instead of `ObjectRecord.model`), not the destroy-path race originally suspected. Verified live on real Steam Deck hardware (Facility + Caverns, zero crashes); Windows 21-level sweep clean. |
|
||||
@@ -570,7 +570,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
|
||||
| D265 | **D236 investigation (port-layer, `port/fast3d/gfx_pc.cpp`): the tree cards are IA8/IA4, not IA16 — and `import_texture_ia4` decoded the RDP's I2:A2 texel as…** — full `## D265` entry at file tail | PARKED — reverted to baseline; evidence + suspect list above. |
|
||||
| D258 | **PAL/JP asset conversion is broken at the source-data level — `scripts/filelist.e.csv` and `filelist.j.csv` carry naming defects, so `gen_romassets.py e/j` (and…** — full `## D258` entry at file tail | OPEN — repair path verified, not applied (out of v0.2.0 scope). |
|
||||
| D284 | **`port/src/random.c`'s MIPS64 `dsll32`/`dsrl32` shift helpers were backwards — PC PRNG desynced from the N64's from draw 1, in every release to date (M-138 review; fix M-139b).** — full `## D284` entry at file tail | FIXED, 21-level sweep clean, no crashes — RNG stream changes from every prior build; existing replays will diverge. **"Savegames unaffected" was FALSE (M-148): `fileGenerateCRC` runs off a local constant seed via `randomGetNextFrom`, so the fix invalidates every stored slot CRC — v0.3.0 silently wipes all pre-fix saves on first launch (release-blocker candidate, see D295 M-148)** |
|
||||
| D285 | **Steam Deck SIGSEGV during D255 live-testing = SEPARATE bug: the `sndHandleEvent` preempt-scan walks the active-sound list unlocked (M-140; fixed M-141).** — full `## D285` entry at file tail | **FIXED (M-141, `src/snd.c`)** — `osSetIntMask` lock added around the preempt-scan, matching `sndSetupSound`'s existing unconditional (no `#ifdef PORT`) pattern in the same file; build green, `-level_09`/`-level_20` unregressed (1400+ frames, no crash, no D152 lock-stuck warnings). Not yet gdb/live-confirmed against a real reproduced crash — see M-141 body. Do not conflate with D255 — different thread, different subsystem. |
|
||||
| D285 | **Steam Deck SIGSEGV during D255 live-testing = SEPARATE bug: the `sndHandleEvent` preempt-scan walks the active-sound list unlocked (M-140; fixed M-141).** — full `## D285` entry at file tail | **PARTIALLY FIXED, RECURRED PAST THE FIX (see D305, 2026-09-18).** `osSetIntMask` lock added around the preempt-scan (M-141), live-verified crash-free on real Deck hardware (2026-09-16) — but the exact same fault (same FAULT ADDR, same site) recurred on the v0.3.0 pre-release build, disproving the lock as a *complete* fix: it stops concurrent mutation but not a scan that starts on an already-empty list. D305 adds the actually-missing NULL guard. Do not conflate with D255 — different thread, different subsystem. |
|
||||
| D286 | **User-confirmed real ~8s frame freeze on Caverns, Steam Deck — self-recovered, not a crash (M-140).** — full `## D286` entry at file tail | OPEN — one confirmed occurrence, no repro trigger identified, not root-caused; distinct from D255/D285. Do not chase a repro without a better lead. |
|
||||
| D287 | **User-confirmed real, non-self-recovering freeze on Windows (M-141, 2026-09-15/16) after ~15-20 minutes with no input, triggered by clicking back into the…** — full `## D287` entry at file tail | OPEN — one confirmed occurrence, real (not self-recovering), not root-caused; `gfxFrameMsgQ` full is a lead, not a confirmed cause. |
|
||||
| D288 | **Silo intro: a huge, flat-shaded, untextured brown/tan triangle sweeps across the frame during the camera pan down the missile shaft (M-141, user report —…** — full `## D288` entry at file tail | OPEN — visually confirmed and precisely bounded (frames ~562-741 of a bare `-level_20` boot), root cause not yet identified; promoted from the long-parked `GRAP… |
|
||||
@@ -587,6 +587,10 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
|
||||
| D299 | **Game.SkipIntro crashed selecting any save slot except one — reproduced on a fresh save and on a clean Release rebuild, ruling out stale save data (found + fixed 2026-09-17).** — full `## D299` entry at file tail | FIXED — `fileValidateSaves()` (only called from the skipped legal screen) never ran under `SkipIntro`, leaving `saves[]` all-zero; added the missing call to the `SkipIntro` shortcut (`src/game/lv.c`, `#ifdef PORT`, zero game-logic change). Build-verified both `RelWithDebInfo`/`Release`; live user re-test confirms all 4 slots now selectable with `SkipIntro=1`. Not yet run through a full 21-level sweep. |
|
||||
| D300 | **Hipfire look feels non-uniform (issue #89): mouse px routed through the N64 analog stick's quadratic natural-turn curve, so slow motion is "almost unrecognised" and fast motion saturates at full turn speed by ~13 px/poll.** — full `## D300` entry at file tail | FIXED, user-confirmed live — WI-1 per `docs/dev/GEPD-INPUT-PLAN.md`: new `hipDirectCompute()` (`port/src/input.c`) writes `vv_theta`/`vv_verta` directly, mirroring GEPD's hipfire formula and D194's existing aim-mode direct-write pattern — linear px→degree, FOV-scaled, dt-normalized, bypassing the stick curve entirely. Safety-gated on `bonddead`/`outside_watch_menu`/`pause_state` plus (M-190, 2026-09-18) the scripted-camera-mode check the original safety-gates writeup named but never implemented — mouse could freely move the camera during POSEND/INTRO cutscenes until this session. `Input.MouseDirectLook` (default 1) is the escape hatch back to the legacy stick path. Build/link-verified; user playtested live: "input already feels better," then again post-M-190: "ok yeah looks good." |
|
||||
| D301 | **`fileGetIsCheatUnlocked()` NULL-deref, unguarded on every call site, not just the D299/SkipIntro one (GitHub #87, two reports with an identical crash PC/fault address).** — full `## D301` entry at file tail | FIXED locally on `release/v0.3.0` (not pushed) — NULL-guarded `fileGetIsCheatUnlocked()` itself (`src/game/file2.c`, `#ifdef PORT`) rather than only its one known trigger route; build-verified, crash-free smoke test. Live confirmation against the actual #87 repro still owed — a synthetic `GE_STARTMENU=6` repro attempt did not reproduce the crash even pre-fix, so the exact #87 trigger condition (why the save lookup fails) remains unconfirmed. |
|
||||
| D302 | **Render FPS regression to ~40-45fps on release/v0.3.0 (user QA on the local v0.3.0 pre-release build, 2026-09-18) — a reintroduction of D250's own uncached-`getenv()`-in-the-hot-path bug class.** — full `## D302` entry at file tail | FIXED — 4 uncached `getenv()` calls added by D236 passes 10/12/14/16 directly inside `gfx_sp_tri1` (`port/fast3d/gfx_pc.cpp`, per-triangle) cached with the same `static int x = -1` pattern D250 established elsewhere in the file. Verified via `GE_D193=1 -level_33` (Dam): rock-steady `render=59-61` post-fix, matching D250's own post-fix baseline exactly (pre-fix D250 pinned this level at 45-50fps). Not yet re-confirmed by the user's own hands-on playtest. |
|
||||
| D303 | **Some muzzle flashes (M16 named specifically, "and some others") draw wrong — the flash renders very long, extending straight up from the gun (user QA on the v0.3.0 pre-release build, 2026-09-18).** — full `## D303` entry at file tail | OPEN — reported only, not yet investigated. |
|
||||
| D304 | **Mouse sensitivity menu overhaul (user request, 2026-09-18): aim speed/turn speed sliders weren't "1:1", had too little range, and (follow-up) risked decoupling into a bad state; F10 menu also had a click-jump bug.** — full `## D304` entry at file tail | FIXED — see full entry for both the range/parity pass and the same-day follow-up (collapsed to a single `Input.MouseSensitivity` menu row + fixed `overlayUpdateScroll()`'s unconditional bottom-anchor, the actual cause of the reported "menu jumps to the setting below on click"). Build-verified (Windows) both passes; headless overlay smoke test clean after each. Live F10 playtest of the final (collapsed + scroll-fixed) menu still owed. |
|
||||
| D305 | **Steam Deck SIGSEGV on the v0.3.0 pre-release build (Cavern, early in the level, `Game.AllUnlocked` previously enabled on the session/save) — same exact FAULT ADDR 0x62 / `sndHandleEvent` signature as D285, reproduced AFTER D285's own lock fix was already live-verified crash-free on real hardware.** — full `## D305` entry at file tail | FIXED (defensive guard) — `sndHandleEvent`'s preempt-scan (`src/snd.c`) dereferenced its list-walk pointer before ever checking it for NULL (a do-while always runs its body once); D285's `osSetIntMask` lock only prevents another thread from emptying the list mid-scan, not a scan that starts with an already-empty list, so the crash could still happen even with that fix in place. Added the missing `iterState != NULL` guard before the loop, matching D255's established skip-instead-of-dereference precedent. Root desync mechanism (why pool-exhaustion accounting and the list can disagree) still not identified; `Game.AllUnlocked` correlation not proven, only reported. **Live-verified on the Deck (2026-09-18):** user ran two test sessions on Cavern with the updated build, plus toggled `Game.AllUnlocked` off then back on and relaunched — no crash in any of them. |
|
||||
|
||||
Phase 2 replaced the Phase-1 demo loop with the real `mainproc()` on real OS
|
||||
threads, compiled GE's real `src/sched.c`, and brought in PD's fast3d software
|
||||
@@ -10389,7 +10393,7 @@ crash class C2 (Facility `-level_34`, Runway `-level_35`): `import_texture_i8` A
|
||||
|
||||
**Disclaimer / legal screen only draws the first line (D76 root cause, M-31).** `constructor_menu00_legalscreen` (`src/game/front.c:1523`) renders the 12-entry `legalpage_text_array[]` with `legal_text_end = (struct legal_screen_text *)&legalscreen_MRD;` then `do { render; ptr++; } while (ptr < legal_text_end);`. This is a **linker-adjacency assumption**: on N64 `.data` the two file-scope globals are emitted in source order so `&legalscreen_MRD == legalpage_text_array + 12` and the loop count is exactly the array length. mingw/GCC reorders them — in `build-pc/ge007.x86_64.exe` `legalscreen_MRD` links at `0x1401305e0`, **0x60 bytes before** `legalpage_text_array` at `0x140130640` (verified via `nm`). So `legal_text_end < legal_text_ptr` from the start; the `do/while` runs the body **once** (entry 0, "TWYCROSS BOARD OF GAME CLASSIFICATION" — long + CENTER_ALIGN so it wraps to ~2 visual lines = the "classification line + one below" the user saw), then exits. The other 11 legal lines never render. NOT a D68 image-table bug — the legal screen references **zero** `sImageTableEntry`s; it is pure `langGet()` font text via `display_aligned_white_text_to_screen`→`textRender`, plus the 3D `logoinst` model (`subdraw`, absent — D75 family, parked). The D68 fixup and the `GE_IMGT` avenue in the brief are both dead ends here.
|
||||
|
||||
**Status (from §F table):** **FIX PROPOSED, NOT APPLIED** (`front.c` is owned by another agent this session). One-line `#ifdef PORT` in `constructor_menu00_legalscreen`: `legal_text_end = legalpage_text_array + ARRAY_COUNT(legalpage_text_array);` (N64 `&legalscreen_MRD` cast under `#else`). Same linker-layout class as the D75 loop-bound / D142 enum family. Confidence: **high** — mechanism confirmed by symbol addresses.
|
||||
**Status (from §F table):** **FIXED.** Corrected 2026-09-18 (during an unrelated "easy wins" scan): the proposed one-line `#ifdef PORT` fix — `legal_text_end = legalpage_text_array + (sizeof(legalpage_text_array) / sizeof(legalpage_text_array[0]));` (N64 `&legalscreen_MRD` cast kept verbatim under `#else`) — was already committed `8dfc578f4` (2026-08-31), confirmed live in the current tree via `git blame -L 1581,1588 src/game/front.c`. The "front.c owned by another agent, not applied" status was accurate at the time it was written but never updated after the merge landed; no code change was needed this session, this is a docs-only correction. Same linker-layout class as the D75 loop-bound / D142 enum family. Confidence: **high** — mechanism confirmed by symbol addresses.
|
||||
|
||||
## D159 — Front-end wallet-Bond photo (and other large front-end I4/I8/IA8 textures) render "interlaced" / combed (M-31, user bug RC1 / D149).
|
||||
|
||||
@@ -10675,7 +10679,7 @@ D243 (cutscene shake/duplication, its own CONSOLIDATED NEXT STEPS block appears
|
||||
|
||||
**Dev-tooling QoL (M-87, user ask): a debug-build/env toggle that unlocks all missions + all cheats, for testing.** Root-caused a clean single choke point — **both** gates route through the same function: `bool fileGetIsCheatUnlocked(save_data *save, s32 cheat)` (`src/game/file2.c:391`) reads the save's `unlocked_cheats_1/2/3` bitfields and returns whether bit `cheat` is set. Mission-select lock (`src/game/file.c:59`, gates whether a briefing page's mission is selectable) and the cheat-menu availability check (`frontCheckIfCheatIsUnlocked`, `front.c:1009`, itself calling `fileGetIsCheatUnlocked` for the per-level `SP_LEVEL_*` cheat IDs) **both** call it — it's a pure, side-effect-free bitfield read (no save-data mutation), so short-circuiting it to always-true under a debug env var unlocks mission-select *and* the cheat menu in one change, matching exactly what was asked ("those two go a long way"). **Bonus, free once cheats unlock:** the ROM already contains real Rare-era developer cheats that are otherwise unreachable in normal play — `CHEAT_LINEMODE` (wireframe render — would make D222-style culling bugs trivial to see), `CHEAT_BONDPHASE`, `CHEAT_DEBUG_POS`, `CHEAT_DEBUG_UNK5` (`cheat.c` ~L687-707, `CHEAT_MASK_GLOBAL`) — worth trying once this lands, may be useful dev tools in their own right.
|
||||
|
||||
**Status (from §F table):** **OPEN — root-caused, not implemented.** Plan: `#ifdef PORT`, env-gated (e.g. `GE_DEBUG_UNLOCKALL=1`, catalogue in `GE-ENV-PROBES.md` once added) short-circuit at the top of `fileGetIsCheatUnlocked` — `if (getenv-cached-flag) return TRUE;` before the bitfield read. Default off ⇒ zero behavior change for normal play; when set, purely a query-time override (save file itself is never touched, so it can't corrupt a save or leak into the shipped default). No `src/` logic change beyond the guarded early-return (same class of edit as other `#ifdef PORT` dev-convenience hooks, e.g. `GE_STARTMENU`).
|
||||
**Status (from §F table):** **FIXED (2026-09-18), implementing the plan as proposed.** `fileGetIsCheatUnlocked()` (`src/game/file2.c:391`) now has an `#ifdef PORT` block right after the function's existing D301 NULL-guard: `GE_DEBUG_UNLOCKALL` is read once via the established cached-getenv pattern (`static int s_unlockAll = -1; if (s_unlockAll < 0) s_unlockAll = getenv(...) != NULL;`) and, if set, returns `TRUE` unconditionally before the real bitfield read. Default off ⇒ zero behavior change; when set, purely a query-time override — the save file is never written, so it can't corrupt or leak into a shipped save. **Verified:** clean rebuild; headless boot with `GE_DEBUG_UNLOCKALL=1 GE_STARTMENU=7` reaches mission select crash-free (visual confirmation of "everything shown as unlocked" not captured this session — the mission-select filmstrip's non-selected thumbnails always render dimmed regardless of lock state, so a static frame can't distinguish locked-dim from just-not-hovered; mechanism is correct by code read). The bonus developer-cheat suggestion (`CHEAT_LINEMODE` etc.) from the original write-up is unexplored, not part of this fix.
|
||||
|
||||
## D226 — QoL ask: player-adjustable HUD scale for the ammo counter, bottom-left pickup/item status text, and top-of-screen dialogue/subtitle text.
|
||||
|
||||
@@ -10918,6 +10922,44 @@ User played live with `GE_D236ORDER=1` set, walked toward Surface1's tree line,
|
||||
|
||||
**Not attempted this pass — genuinely the next step, not a rehash:** trace `0x0c184b50`'s own per-vertex fog computation (where it's set, CPU-side vs RSP, and against what reference distance) and compare it to what N64 would compute for the same vertex positions. This is a different code path than D280's already-verified noise-class fog check and hasn't been looked at directly. Given the depth of this investigation (13 passes) and the release timeline, **recommend keeping this parked for v0.3.0** — the mechanism is narrowed further than ever, but not yet a safe, confident fix. `GE_D236ORDER`/`GE_D236ZFIX` remain in tree, env-gated, zero cost when unset; the live capture log is preserved at `scratch/d236_live_capture.log` for whoever picks this up next.
|
||||
|
||||
**Pass 14 (2026-09-18, later session, no timeline pressure — user asked to keep going): sharpened pass 13's fog lead into a falsifiable mechanism and built the probe to test it; not yet live-captured.**
|
||||
|
||||
**Two things checked by source review before touching code, both clean:** (1) `port/fast3d/gfx_pc.cpp`'s `gfx_sp_vertex` fog fallback (`if (rsp.geometry_mode & G_FOG) {...} else { d->fog = rdp.fog_color.a; } d->color.a = vcn->a;`) is **byte-identical to the local Perfect Dark PC port reference** (`pd_port/port/fast3d/gfx_pc.cpp:1177-1194`) — this exact branch is not a GE-specific bug, it's shared, more-battle-tested fast3d ground truth. (2) `rsp/graphics/gmain.s` (GE's own RSP ucode, the AGENTS.md ground truth for GBI semantics) has **zero fog-related code** — GE's ucode doesn't customize fog generation at all, so there's nothing GE-specific to cross-check there either. Both rule out "our RSP fog emulation is wrong" as the mechanism.
|
||||
|
||||
**Decoded the tree class's actual render mode/blend equation** (`0x0c184b50`, matched against `include/PR/gbi.h`'s `RM_AA_ZB_XLU_DECAL(clk)` macro: `AA_EN|Z_CMP|IM_RD|CVG_DST_WRAP|CLR_ON_CVG|FORCE_BL|ZMODE_DEC`, blend `GBL_c(G_BL_CLR_IN, G_BL_A_IN, G_BL_CLR_MEM, G_BL_1MA)` — exactly matches D280/D272's Z_CMP=1/Z_UPD=0/ZMODE=DEC finding). This is an ordinary **alpha-blend decal**: `out = src_color*src_alpha + dst_color*(1-src_alpha)`. Critically, the blend equation does **not** reference `G_BL_CLR_FOG`/`G_BL_A_FOG` at all — whatever ends up as this triangle's alpha (SHADE alpha, i.e. `vcn->a`, the same raw per-vertex value D280's census found bimodal at 25/255) directly controls how *transparent* the tree card is over whatever was drawn underneath it (the noise class, confirmed drawn first in the close/high-count zone by pass 13's own data).
|
||||
|
||||
**New hypothesis, not proposed by passes 1-13:** if a meaningful fraction of a close tree card's vertices carry the low end of that bimodal split (`~25`, i.e. ~10% opacity) rather than the high end (`255`, opaque), the card would blend almost invisibly over the noise texture even when it has correct geometry, correct triangle count, and correct (tree-after-noise) draw order — reproducing exactly the "still looks like a wall" result pass 13 reported in its own favorable zone. This reframes the open question from "is the *fog value* right" (D280's framing, ambiguous — this render mode doesn't consume `d->fog`/RSP fog at all) to the narrower, directly testable one: **is vertex alpha low specifically for the close/high-count trees the live capture walked through, or does it correlate sanely with distance (opaque near, faded far, as a billboard fade LOD would legitimately do)?** Neither this pass nor any prior one has the data to answer that yet — D280's "bimodal 25/255" finding was a class-wide census, not correlated against per-instance camera distance at the specific bugged vantage.
|
||||
|
||||
**Built, not yet run live:** `GE_D236ALPHA` (`port/fast3d/gfx_pc.cpp`, `gfx_sp_tri1`, env-gated, zero cost unset, same pattern as `GE_D236ORDER`/`GE_D236RM`). For every triangle of the `0x0c184b50` class (capped at 4000 hits to bound log size): logs the combine_mode word once whenever it changes (to confirm alpha really is sourced from SHADE and not, say, ENVIRONMENT or a constant, before trusting the hypothesis further), then each triangle's three vertex alphas (`v1/v2/v3->color.a`) alongside `1/w` for each vertex (a cheap camera-distance proxy, smaller w = closer) so alpha can be correlated against distance the same way pass 13's live capture correlated tree count/draw-order against distance. Build verified clean (`./build-pc.sh ntsc-final`, 243/243, only pre-existing unrelated warnings); `tools_pc/verify.sh dam` PASS, crash-free with the probe compiled in and unset.
|
||||
|
||||
**Next step, concrete: a live capture identical in method to pass 13's** — `GE_D236ALPHA=1`, walk toward Surface1's tree line from far to close, same as before. Read the resulting log for: (a) does `v->color.a` trend toward 255 as `1/w` grows (closer)? If yes, the port is faithfully reproducing an N64-authored near-opaque/far-faded billboard fade and this hypothesis is refuted, redirecting back to an as-yet-unfound mechanism. If alpha stays low (~25) even at high `1/w` (close range, matching the zone where pass 13's draw order was already favorable), that's a concrete, falsifiable bug signature — the next question after that would be *where* that low value is coming from (baked ROM vertex data being read wrong vs. a legitimate baked-low value that N64's own combiner/blend path compensates for in some other way this port doesn't reproduce). **Not v0.3.0-blocking per standing user priority, but the user asked to keep working it regardless of release timing this session.** `GE_D236ORDER`/`GE_D236ZFIX`/`GE_D236ALPHA` all remain in tree, env-gated, zero cost when unset.
|
||||
|
||||
**Pass 15 (2026-09-18, same later session — the requested live capture landed, decisively quantifying the mechanism, but still stops short of a fix): confirmed the tree class's vertex alpha genuinely never reaches anywhere near opaque, and confirmed exactly how it drives the final transparency — but couldn't establish whether the low values are faithful ROM data or a decode bug.**
|
||||
|
||||
**Launched the game directly** (`GE_D236ALPHA=1 ./build-pc/ge007.x86_64.exe -level_36`, MSYS2 PATH for the SDL2 runtime dep) so the user could walk the exact same far→close tree-line approach as pass 13 without a separate build step; 1500 `D236ALPHA` lines captured (500 triangles × 3 vertices = 4500 alpha samples), well under the probe's 4000-triangle cap.
|
||||
|
||||
**Result 1 — alpha never gets close to opaque, anywhere in the walk.** Across the *entire* session, `v->color.a` ranged **0–36** (out of 255, i.e. ≤14% opacity) — not the 25/255 bimodal split D280's older class-wide census described. Bucketing all 4500 samples into deciles by `1/w` (camera-distance proxy, larger = closer) shows a real, monotonic trend — mean alpha rises from 6.2 in the farthest decile to 27.1 in the closest — but the ceiling never lifts: even the single closest samples captured topped out at 36. Two readings are consistent with this data and neither is ruled out yet: (a) the player never got quite close enough in this session to reach whatever distance a card would need to hit 255 (the closest `1/w` bucket, 0.0011–0.0018, has no calibration reference in this codebase to say whether that's "close" in engine units), or (b) the alpha value is capped/scaled wrong regardless of true distance.
|
||||
|
||||
**Result 2 — decoded the actual combine equation, resolving pass 14's open "which alpha slot" question.** The single `combine_mode` word logged this session (`0x00f39e4f1f39e4f1`, unchanged for the whole class) decodes via this file's own shader-synthesis extraction (`gfx_pc.cpp` ~L359-366) to alpha cycle `(a=1,b=7,c=4,d=7)` = `(TEXEL0 − 0) × SHADE + 0` in both cycles — i.e. **final alpha = texture alpha × vertex(SHADE) alpha**, the standard `G_CC_MODULATEIA` idiom. This confirms pass 14's hypothesis outright: whatever the CI8 tree texture's own per-texel alpha is (presumably near-255 for opaque tree pixels, going by the format), the vertex alpha multiplies it back down — so a vertex alpha capped at 36/255 caps *every* tree pixel drawn through that vertex at ≤14% opacity, full stop, regardless of the texture's own content. This is now a settled fact about the render pipeline, not a hypothesis.
|
||||
|
||||
**Traced the vertex-color path as far as source review allows this pass, found no swap/decode bug in it, but couldn't identify what actually computes/bakes this specific class's alpha.** `bgLoadRoomVtxData`/`bgSwapRoomVtx` (`bg.c:2353-2369`, room-level static vertex table load) explicitly documents and correctly implements "swap the 6 leading u16 fields (position/flag/uv), **leave the 4 colour bytes alone**" — if this room's tree-card geometry comes through this path, the raw ROM-authored alpha byte passes through completely unmodified, byte-for-byte, and this port has no bug in that specific step. Went looking for a CPU-side per-frame alpha *computation* next (the mechanism that would explain the real, non-flat 1/w correlation the data shows) and found one real candidate, `fogGetPropDistColor`/`chrobjFogVisRangeRelated` (`bgfog.c:695`, `propobj.c:7420+`) — a genuine decomp-verbatim distance-fog alpha blend used for `PropRecord`/`ObjectRecord` instances (doors, tinted glass, damage-decal props) — but confirmed by grep that `bg.c`/`bgapply.c` (the actual room-background-geometry load path this tree class's render-mode signature points to, per D280's census) **never reference** `fogGetPropDistColor` or its `far_fog_dist_scaled`/`near_fog_dist_scaled` fields at all, so that specific function is not this class's mechanism — a dead end, not a fit, ruled out this pass. **Did not find the actual code that determines whether this tree class is static ROM-baked vertex data (in which case N64 would show identical values, and any bug is downstream of the raw bytes, not in reading them) or is computed by some other per-frame CPU path not yet located** (the smooth 1/w correlation in the live data argues against "flat static bytes coincidentally increasing with distance across unrelated tree instances," but isn't proof).
|
||||
|
||||
**Numeric aside, unconfirmed, flagged for whoever picks this up:** 36 ≈ 255/7 (36.43). If a genuinely different underlying quantity here is an unsigned 3-bit "fog/light level" (0-7) later expanded to a byte via `level * (255/7)` — a common N64-era bit-depth-expansion idiom — the observed 0-36 range would mean this session's walk only ever reached **level 0 or 1 of a possible 0-7**, i.e. the mechanism computing the level may be stuck near its floor rather than the byte itself being wrong. Not chased down this pass (no `* 7`/`/ 7`/level-table hits found in a quick `bg*.c`/`bgfog.c` grep) — worth a targeted look at whatever function ultimately writes this specific room's tree-card `Vtx.cn[3]` if the next pass wants to keep pulling this thread, but flagged as a numerical coincidence, not a confirmed mechanism.
|
||||
|
||||
**Not v0.3.0-blocking, user is continuing anyway.** Next step, concrete: find the actual write site for this room-geometry class's baked/computed alpha (search the ROM room-decompression path more specifically for this level's data, or instrument `bgLoadRoomVtxData` itself to dump the raw pre-swap bytes for this room and compare against what the running probe reports post-load — if they match exactly, the value is confirmed ROM-faithful and the investigation should pivot to "is this actually correct N64 behavior, i.e. is the real bug elsewhere (wrong texture selected, wrong room's data loaded, etc.)" rather than continuing to chase the alpha value itself). `GE_D236ALPHA` log preserved at `scratch/d236_pass14_live_capture.log` (filename predates this being pass 15's data) for reference.
|
||||
|
||||
**Pass 16 (2026-09-18, same later session, user said "go for it" — a decisive negative result plus a still-open "where does this data actually come from" question): ruled out room-background static vertex data as the source of the low alpha, and ruled out the two most obvious dynamic-quad-builder candidates in `model.c`, but did not find the actual write site.**
|
||||
|
||||
**Built `GE_D236RAW`** (`src/game/bg.c`, `bgLoadRoomVtxData`, right after `bgSwapRoomVtx`, env-gated): dumps a histogram of every vertex's raw `cn[3]` alpha byte for each room's *entire* static vertex table the moment it's decompressed and swapped — independent of camera position or which triangles happen to be drawn, unlike `GE_D236ALPHA`. Ran headless 25s on `-level_36` (no live session needed for this step): **64 room-load events across ~40 distinct rooms, and every single one reported `amin=255 amax=255`** — literally every vertex in every room's raw table is fully opaque. **This conclusively rules out "static ROM-authored per-vertex alpha" as the source of the ≤36/255 values pass 15 measured at render time** — `bgSwapRoomVtx` was already confirmed not to touch colour bytes (pass 15), and now the pre-render data itself is confirmed uniformly opaque across the whole level. Whatever produces the low alpha happens **after** room load, and is not baked into this table at all.
|
||||
|
||||
**Extended `GE_D236ALPHA`** (`port/fast3d/gfx_pc.cpp`) to also report which segment the tree class's vertices were loaded from, by recording the top byte of the raw `G_VTX` command's segmented address (`g_d236_last_vtx_seg`, set in the `G_VTX` case of the main DL dispatch, read back in the existing per-triangle probe). Live capture in the same session as pass 15's walk was not repeated; a headless run instead confirmed the class reports **`vtxseg=0x70`** consistently. Per `seg_addr()`'s own documented convention (`gfx_pc.cpp:3260-3294`) and `port/src/dram.c`'s `DRAM_V1_BASE=0x70000000` comment ("ALL game RAM symbols live" in this view starting at that address), `0x70` is **not** one of the game's named symbolic segments (`SPSEGMENT_MODEL_VTX=4`, `SPSEGMENT_BG_VTX=14`, etc., `bondconstants.h:2897-2909`) — it means this particular `G_VTX` command references a **raw, unsegmented host pointer** into the general RAM arena, the same pattern used by every dynamically-built quad in `model.c` that calls `osVirtualToPhysical(freshly_allocated_buffer)` directly instead of going through `gSPSegment`. This is consistent with (though doesn't newly prove beyond) pass 15's "not static data" framing, and additionally rules out the two *specific* addressing schemes (segment 4, segment 14) a static per-model or per-room table would use.
|
||||
|
||||
**Went looking for the actual CPU-side quad-builder and came up empty.** `model.c` has exactly four functions that dynamically allocate and write `Vtx`/`Vertex` arrays via `vtxallocator()` for `ModelNode` rendering: `dogfnegx` (`MODELNODE_OPCODE_GUNFIRE` — PD name `modelRenderNodeChrGunfire`, muzzle flash), `doshadow` (`MODELNODE_OPCODE_SHADOW`, character/prop drop shadows), `dorottex` (`MODELNODE_OPCODE_DLPRIMARY`, "star" muzzle-flash flare arms per its own header comment), and `dotube` (`MODELNODE_OPCODE_OP07`, a two-vertex-per-iteration rotating-ring interpolator, gun-barrel/rope-like geometry). Read all four in full: none match "outdoor decorative camera-facing billboard sprite" by content, and the two that use segment-tagged addressing (`dorottex` explicitly calls `gSPSegment(..., SPSEGMENT_MODEL_VTX, ...)`) would report `vtxseg=0x04`, not `0x70`, ruling them out on the addressing evidence too. The rest of `model.c`'s `MODELNODE_OPCODE_OP*` dispatch table (`OP03/OP05/OP06/OP11/OP14/OP16/OP17/OP20`) resolves to literal `return;` stubs in this decomp — dead opcodes, not candidates. Checked `propobj.c` again for any other `vtxallocator`-style builder beyond the already-examined "screen" (in-game TV monitor image) quad system — found none.
|
||||
|
||||
**Status: genuinely stuck on locating the write site, not just deferring.** The mechanism is now well-characterized (raw-pointer-addressed, dynamically-built quad batch, confirmed *not* room-static or model-static data, confirmed *not* any of the four known `model.c` dynamic-quad builders) but the actual function remains unidentified after a real search, not a skipped one. **Next step, concrete and different from anything tried in 16 passes:** rather than continuing to guess-and-grep by content/name, instrument at the point of certainty — add a probe to the `G_VTX` case itself (where `g_d236_last_vtx_seg` is now set) that also captures and prints a short run of bytes *around* the raw pointer's neighborhood, or (better, if the harness supports it) print a host stack backtrace via `execinfo.h`/`backtrace()` the first time `rdp.other_mode_l == 0x0c184b50u` is seen after a `G_VTX` load — pinpointing the actual C call site directly instead of inferring it from render-mode/segment signatures. That would settle in one capture what six passes (11 through 16) of signature-matching have not.
|
||||
|
||||
**Not v0.3.0-blocking, user is continuing anyway.** `GE_D236RAW` and the `vtxseg` extension to `GE_D236ALPHA` are both in tree, env-gated, zero cost unset. Raw headless dump preserved at `scratch/d236_raw_headless.log`.
|
||||
|
||||
## D237 — QoL ask (M-106, user QA): F10 options overlay — make it scrollable with the mouse wheel, expose more settings into it (e.g. screen-flash / no-hit-flash…
|
||||
|
||||
**QoL ask (M-106, user QA): F10 options overlay — make it scrollable with the mouse wheel, expose more settings into it (e.g. screen-flash / no-hit-flash `Game.NoHitFlash`, and other easily-exposed config keys), and organise rows by category (mouse settings together, video together, etc.).** The overlay (`port/src/optionsoverlay.c`) already renders over the registered `config.c` option list; what's missing is wheel-scroll nav, a broader row set, and grouping headers. Cross-ref `OPTIONS-MENU-PLAN.md` §3 (v1 minimum set) + QOL-INVENTORY "PD has, GE lacks".
|
||||
@@ -11691,3 +11733,115 @@ Returns "cheat not unlocked" — the same safe default other cheat-state paths a
|
||||
**Second, unrelated bug found and fixed in the same review pass: mouse-look was never gated during scripted/no-control camera states, letting the player freely move the view during the Dam intro flyover and the abseil-jump cutscene (both meant to be camera-locked).** Root cause: `port/src/input.c`'s `hipDirectCompute()` (the default-active WI-1 direct mouse-to-`vv_theta`/`vv_verta` write path, `mouseDirectLook=1`) had its own doc comment explicitly stating "a frozen or scripted-camera state (POSEND/INTRO cutscenes, death cam, pause) must be checked explicitly or this would fight the game's own camera control during those states" — but the actual gate list (`mouseDirectLook`/`mouseGrabbed`/`bonddead`/`outside_watch_menu`/`pause_state`) never implemented that documented check. `bondviewFrozenMoveBond` (`bondview2.c:7947`) already correctly zeroes stick input during these camera modes, but that gate lives entirely inside the stick-based `MoveBond`-vs-`bondviewFrozenMoveBond` dispatch (`bondview2.c:8268`) — `hipDirectCompute` writes `p->vv_theta`/`vv_verta` directly, bypassing that dispatch and the game's own camera-lock logic entirely. **Fix:** added the missing check, calling the same scripted-camera test D243's clamps already use. Renamed `d243mCutsceneActive()` → `gameScriptedCameraActive()` (`bondview2.c`) since the function was never D243-specific and is now used by two unrelated call sites (`model.c`'s clamps, `port/src/input.c`'s new gate) — behavior-identical rename, all three call sites updated.
|
||||
|
||||
Build clean (`./build-pc.sh ntsc-final`), `tools_pc/verify.sh dam` PASS. **User confirmed both fixes live, full Dam intro-flyover + abseil-jump + abseil-descent playthrough: "ok yeah looks good."**
|
||||
|
||||
## D302 — Render FPS regression to ~40-45fps on release/v0.3.0 (user QA on the local v0.3.0 pre-release build, 2026-09-18).
|
||||
|
||||
**User reported drops to 40-45fps on a self-described high-end PC on a local v0.3.0 pre-release build**, after the port had previously held a rock-steady 60-61fps end-to-end (D250, fully resolved). Same symptom class and roughly the same fps range as D250's original regression.
|
||||
|
||||
**Root cause: a reintroduction of exactly D250's own bug class, this time in the still-open, uncommitted D236 (Surface1 tree-card) investigation.** Passes 10/12/14/16 of D236 added four `getenv()` calls directly inside `gfx_sp_tri1` (`port/fast3d/gfx_pc.cpp`) — `GE_D236RM`, `GE_D236ORDER`, `GE_D236ALPHA`, `GE_D236ZFIX` — every one of them called **uncached, unconditionally, on every single triangle draw**, exactly the anti-pattern D250 already root-caused and fixed everywhere else in this file (`getenv()`/`_getenv_helper_nolock` is a locked, locale-aware linear scan in the MSVC CRT; at triangle-call frequency it dominates the render thread). These four sites were added across several D236 sessions after D250 landed and were missed by D250's own sweep because they didn't exist yet at the time.
|
||||
|
||||
**Fix:** cached all four behind the same `static int x = -1; if (x < 0) x = getenv(...) != NULL;` pattern already used everywhere else in this file (e.g. `s_d252`, `ge_dtex`, `ge_d172_a`) — mechanical, zero behavior change when the env var is unset, matches D250's own established fix idiom exactly. `GE_D236RAW` (`src/game/bg.c`, room-load time, not per-frame) and `GE_WRAPFIX` (`gfx_pc.cpp`, a one-shot settings-setter, not per-frame) were checked and are not contributors — left as-is.
|
||||
|
||||
**Verified:** rebuilt clean; `GE_D193=1 -level_33` (Dam, the same level D250's own verification used) idles at a rock-steady `render=59-61 sim=60-67` with zero clamp hits, matching D250's post-fix baseline exactly (pre-fix D250 pinned this same level at 45-50fps).
|
||||
|
||||
**Process note for future D236 sessions:** any further temp diagnostic added to a per-triangle/per-vertex hot path in `gfx_pc.cpp` must use the cached-getenv pattern from the first line, not a raw `getenv()` call — this is now the second time an uncached hot-path `getenv()` has shipped in this file. Worth a lint/grep sweep (`grep -n 'if (getenv(' port/fast3d/*.cpp`) before any future release cut.
|
||||
|
||||
**Status:** FIXED, verified via `GE_D193` idle telemetry. Not yet re-confirmed by the user's own hands-on playtest of the updated build.
|
||||
|
||||
## D303 — Some muzzle flashes (M16 named specifically, "and some others") draw wrong — very long, straight up from the gun (user QA on the v0.3.0 pre-release build, 2026-09-18).
|
||||
|
||||
**Reported only, not yet investigated.** User playtest of the local v0.3.0 pre-release build: the muzzle flash on the M16 (and "some others," unspecified) draws visibly wrong — described as very long and extending straight up from the weapon, rather than the expected short flash at the muzzle. User found it hard to describe precisely; a screenshot/clip from a future session would sharpen this before diagnosis starts.
|
||||
|
||||
**Not yet looked at:** which code path renders muzzle flashes (likely a dynamically-built quad/billboard in `src/game`, similar family to the drop-shadow/flare-effect functions already surveyed during the unrelated D236 investigation — see that entry's pass 16 for the list of quad-building functions already read), whether this is a geometry/orientation bug (billboard not facing camera, or built in the wrong basis — "goes straight up" suggests a billboard axis or transform mixup) versus a render-mode/texture-stretch bug, and whether it's PC-port-specific or was already present pre-port (no N64 comparison done yet).
|
||||
|
||||
**Repro clarified, same day (follow-up playtest):** the normal, correct muzzle flash still draws — this is an **extra**, erroneous long flash overlaid on top of it, not a corruption/replacement of the one correct effect. This significantly narrows the search: two draw calls are happening where one is expected, one correct and one wrong. Points more strongly at an extra/duplicate quad-builder call site or a second render-mode pass firing unconditionally alongside the correct one (e.g. a "flare arms"-style secondary effect, per the pass-16 D236 survey's `dorottex` muzzle-flash-flare-arms function, distinct from the main flash builder) than at a single corrupted transform. Still not investigated — no code read done yet against this sharper framing.
|
||||
|
||||
**Status:** OPEN — logged, repro description sharpened, no investigation started.
|
||||
|
||||
**M-191 (2026-09-18, user QA on the v0.3.0 pre-release build): REOPENING the "main fireball" thread — the fresh report matches exactly the condition M-188 said to watch for.** User describes smoke/explosion effects as now looking correct overall, EXCEPT a "weird rainbow confetti looking sprite" that still draws overlaid on top of otherwise-correct smoke/explosion particles, sometimes. M-188 explicitly closed only the "lingering particle residue" manifestation (which turned out to be `s_bullet_spark`/D219, unrelated code) and left this exact scenario — a rainbow artifact riding on the actual explosion/smoke effect itself — open, with the instruction "re-open only if a fireball (not residue/embers) rainbow report comes in again." This report fits that description, not the residue/embers one. **Not yet investigated further this session** — the last concrete lead (before M-188's tangent into D219) was: trace forward from `import_texture_rgba16`'s `upload_texture()` call (`gfx_pc.cpp:718`) through the GL texture object and the `G_CC_INTERFERENCE` (TEXEL0=smoke IA × TEXEL1=fire RGBA16) combiner, since the source ROM data itself was already proven correct by hand-decoding all 14 fire-stage textures (M-156, see above) — the bug is downstream of decode, in upload/cache/combiner. A live `GE_PCDUMP` + `GE_D252=1` capture during an actual reproduction (shoot something that smokes/explodes) is still the next concrete step, unchanged from M-156's own "next, concrete" recommendation.
|
||||
|
||||
**Status: REOPENED (was incorrectly treated as fully covered by the D219 closure) — OPEN, root cause still unconfirmed for this specific manifestation.**
|
||||
|
||||
## D304 — Mouse sensitivity menu overhaul (user request, 2026-09-18): aim speed/turn speed sliders weren't "1:1" and had too little range.
|
||||
|
||||
**User request, immediately after cutting the v0.3.0 Deck pre-release build:** review the mouse sensitivity configs against "the new logic" (D194/D300's GEPD-direct-write input model) and overhaul the F10 menu — specifically, "aim speed/turn speed are not 1:1 in the menu" and players need "more range for mouse sensitivity/settings," referencing the fact that GE's whole aim/hipfire model was ported from the GEPD Mouse Injector (`reference/mouse-injector/`, our D194 mirror), which itself mirrors Perfect Dark's own input scheme.
|
||||
|
||||
**Investigation: read the reference implementation directly (`reference/mouse-injector/games/goldeneye.c`/`perfectdark.c`) rather than guessing.** Both games' injector code use exactly ONE user-facing `SETTINGS[SENSITIVITY]` value (raw range 0-100, displayed as `raw*5`%) fed into two different per-mode formulas with different, fixed, intentional divisor constants: hipfire `sensitivity = SENS/40`, aim-mode crosshair `sensitivity = SENS/292` (roughly; GE's own `games/goldeneye.c` uses 292, confirmed against our own `aimGepdCompute`'s doc comment "GepdSens... default 20 ~= 1:1... SENS/292"). This is the ground truth "PD/GEPD-reference" model the user meant: a *single* raw-SENS unit passed through per-mode, not two independently-scaled percentages.
|
||||
|
||||
**Root cause of the "not 1:1" complaint, found in `port/src/optionsoverlay.c`, not `input.c`:** the two F10 rows for `Input.AimModeSens` and `Input.MouseTurnSpeed` are mathematically already the same underlying "reference SENS passthrough" unit (confirmed: `hipDirectCompute`'s `sens = mouseTurnSpeed/100 * mouseSensitivity/100` and the `dxPx*0.1*sens*scale` line is bit-for-bit GEPD's `XPOS/10*(SENS/40)` when `mouseTurnSpeed` == GEPD's raw `SENS`; `aimGepdCompute`'s `sens = gepdSens/2920.0 * mouseSensitivity/100` is likewise GEPD's `XPOS/10*(SENS/292)` with `2920 = 292*10`) — but the **F10 overlay row definitions hardcode two different, mismatched, and in one case outright wrong display/edit ranges**: `Input.AimModeSens` row = `(uiMin=1, uiMax=80)` (matches its own config clamp), `Input.MouseTurnSpeed` row = `(uiMin=0, uiMax=100)` — which does **not** match that key's actual registered config clamp of `(1, 500)` (`input.c`: `configRegisterInt("Input.MouseTurnSpeed", &mouseTurnSpeed, 1, 500)`). The overlay's own `rowLo()`/`rowHi()` prefer the row's explicit `uiMin`/`uiMax` over the real config clamp whenever they differ (`(r->uiMin != r->uiMax) ? r->uiMin : r->cfgMin`), so the overlay was silently overriding the wider, correct config clamp down to an arbitrary, undocumented `0..100` window — a menu-layer bug, not a formula bug. This directly explains both complaints: the two sliders showed different, arbitrary ceilings (80 vs 100) despite being the same unit, and `MouseTurnSpeed`'s true available range (up to 500) was invisible/unreachable from the menu.
|
||||
|
||||
**Fix (`port/src/input.c`, `port/src/optionsoverlay.c`, mechanical, no formula/feel change):**
|
||||
1. Widened `Input.AimModeSens`/`Input.GepdSens` (deprecated alias, same backing variable) config clamp from `(1, 80)` to `(1, 500)`, matching `Input.MouseTurnSpeed` exactly.
|
||||
2. Both overlay rows switched from hardcoded, independently-drifting `uiMin`/`uiMax` numbers to the `(0, 0)` "inherit the registered config clamp" sentinel the struct already supports (`uiMin, uiMax; /* 0,0 -> use the registered clamp */`) — single source of truth in `input.c`'s `configRegisterInt` calls, so the two sliders can never silently diverge again the way `MouseTurnSpeed`'s did. Step raised `1 -> 5` on both (a full-range keyboard/gamepad traverse at step=1 over a 500-wide range would need ~500 presses; mouse-drag is already continuous/proportional via `sliderSetFromX`, unaffected by `step`).
|
||||
3. Surfaced `Input.MouseSensitivity` (the existing D238 master multiplier — already clamped `1..500` in `input.c`, but had **zero** menu row, reachable only by hand-editing `ge007.ini`) as a new "Mouse sensitivity" overlay row, same `(0,0)`-inherited range/step convention as the other two.
|
||||
|
||||
**Explicitly NOT changed:** the underlying `hipDirectCompute`/`aimGepdCompute` formulas, the `Input.SensLink` ratio-lock logic (`38:50`, ratio-based so range-widening doesn't affect it), or any default value (`AimModeSens=38`, `MouseTurnSpeed=50`, `MouseSensitivity=100` all unchanged) — this is a menu/range-parity fix, not a re-tune of the D194/D238/M-123-calibrated default feel.
|
||||
|
||||
**Verified:** clean Windows rebuild (`./build-pc.sh` equivalent, only pre-existing unrelated warnings). Headless `GE_OPTIONSOVERLAY=1` smoke test opens the overlay with the new row with no crash/assert. Manual `ge007.ini` round-trip test: setting `AimModeSens`/`GepdSens`/`MouseTurnSpeed` to `300` in a fresh ini and relaunching confirms all three now survive (previously `AimModeSens` would have clamped to 80 on load). **Not yet human-playtested** — no one has actually opened the F10 menu on this build and dragged the new/widened sliders; the numeric/config-level verification above is real but the in-game feel and layout (label width, new row's position in the scroll list) are unconfirmed.
|
||||
|
||||
**Status:** FIXED, config/build-level verified. Owed: a live F10-menu playtest (does "Mouse sensitivity" fit/scroll correctly, do the widened sliders feel right in-game at both ends of the new range).
|
||||
|
||||
**Follow-up, same day (user tried the widened sliders live, two new pieces of feedback):**
|
||||
|
||||
1. **"Is it possible to make a single mouse sensitivity and not the two values that I'm afraid a user might decouple and then set bad settings."** Agreed this is a real risk the range/parity fix above didn't address — widening both per-mode sliders independently makes it *easier*, not harder, for a player to land on a confusing decoupled state (e.g. aim mode crawling while hipfire is very fast) with nothing in the UI indicating anything is wrong, since `Input.SensLink`'s ratio-lock is an opt-in toggle a player has to know to leave on. **Fix:** pulled `Input.AimModeSens`, `Input.MouseTurnSpeed`, and `Input.SensLink` from the F10 menu entirely (commented out in `rows[]`, matching the existing pulled-row pattern used for D181/`Game.ScreenShakeIntensity` and D216/`Game.SkipIntro`) — only the single `Input.MouseSensitivity` master row remains player-facing. This is not a workaround but the literal reference model: `reference/mouse-injector/games/goldeneye.c`'s own GEPD/PD injector design uses exactly ONE user-facing `SETTINGS[SENSITIVITY]` value, with the per-mode divisor constants (40 for hipfire, 292 for aim) fixed and never independently exposed to the player — which is what "we brought over input like that" (the user's own framing from the original ask) actually implies structurally, not just numerically. The two per-mode config vars, their config registration, and the `SensLink` linking code in `rowSet()` are untouched (`input.c` unchanged from the first pass) — existing `ge007.ini` files with either key keep working, ini power users can still hand-edit them; only the *menu surface* shrank.
|
||||
|
||||
2. **"F10 menu is a bit weird when you try to use the mouse to set a mouse sensitivity, it keeps jumping to the setting below when I left click."** Found the real, pre-existing mechanism (not new, not caused by this session's other changes, just newly exposed because the sensitivity rows happened to sit mid-list where it's most noticeable): `overlayUpdateScroll()` (`port/src/optionsoverlay.c`) unconditionally set `s_scroll = s_sel - (maxV - 1)` on every call — i.e. it always forced the just-selected row to the very LAST visible slot, regardless of whether that row was already fully visible somewhere else in the window. Since this runs immediately after every left-click's `s_sel = hoverVis`, clicking any row that wasn't already sitting in the bottom slot forced an immediate re-scroll of the whole list for that same frame — shifting every row's on-screen Y position, so whatever the user saw or clicked next at that same screen coordinate was a *different* (typically the next, i.e. "below") row. Reproducible on any click into the (22-row, scrolls past ~12 visible) list at any position other than the last visible slot. **Fix:** rewrote `overlayUpdateScroll()` to the standard minimal-scroll list behavior — only move the scroll window when the selection is actually above the top (`s_sel < s_scroll`) or below the bottom (`s_sel > s_scroll + maxV - 1`) of the currently visible range, and then only by the amount needed to bring it back into view. A click on an already-visible row is now a true no-op for scroll; the window only moves for genuinely off-screen selections (arrow-key/gamepad navigation past an edge, or scroll-wheel).
|
||||
|
||||
**Verified (both parts):** clean Windows rebuild after each change (only pre-existing unrelated warnings — the same `implicit declaration of lround`/`atoi` warnings this file has always had, `<math.h>` was never included despite using `lround` throughout; not introduced by this session). Headless `GE_OPTIONSOVERLAY=1` smoke test opens cleanly post-fix with no DL-overflow log line and no crash. **Not yet live-verified** — no one has clicked through the actual collapsed menu with a mouse yet to confirm the jump is gone and the single sensitivity row behaves as expected; that's the next concrete step, not more static review.
|
||||
|
||||
**Second follow-up, same day (user report right after the collapsed-menu build): "i clicked unlock all and I think it crashed."** Not actually a crash — the process's own log shows a clean `[INFO] optionsoverlay: quit to desktop requested` / exit code 0 immediately after the click, meaning **`__QuitToDesktop`'s `ROW_ACTION` fired**, not `Game.AllUnlocked`'s toggle. Root-caused by re-reading `overlayRowAtY` (the mouse hit-test) against the draw loop it's supposed to mirror: `overlayRowAtY` looped over **all** `s_visN` visible-list entries regardless of which ones are actually scrolled into view and drawn this frame, while the draw loop correctly clips to `[s_scroll, pLast]`. Since `OV_ROW_Y(p - s_scroll)` is a plain linear function of `p`, the row immediately after the last visibly-drawn one still produces a geometrically valid, in-range Y band just below the panel's real bottom edge — so a click near that boundary (real-window-pixel-to-virtual-2D-space scaling in `ox`/`oy`, or simply a slightly-low click on the last visible row) could resolve to the next, invisible, scrolled-past row instead of the one actually drawn there. `Game.AllUnlocked` sits second-to-last in the row list, immediately before `__QuitToDesktop` — exactly the row pair this class of bug hits, and exactly what the user reproduced.
|
||||
|
||||
**Fix:** `overlayRowAtY` now computes the same `pLast` the draw loop uses and bounds its scan to `[s_scroll, pLast]` instead of `[0, s_visN)`. **Found and fixed a second, related bug in passing while doing this:** the "everything fits, no scroll needed" branch of `pLast`'s formula (both in the draw path and now shared by the hit-test) was `s_visN - s_scroll` — a **count**, not a valid last-index — which since that branch only runs when `s_scroll==0` evaluates to `s_visN`, one past the end of `s_visIdx[]`/`rows[]`. This is a latent out-of-bounds read that didn't trigger in this session's in-level (320x240, always-scrolled with 19 rows) testing, but would on the larger front-end 440x330 screen once enough rows fit without scrolling — fixed to `s_visN - 1` in both places.
|
||||
|
||||
**Verified:** clean rebuild; headless `GE_OPTIONSOVERLAY=1` smoke test opens cleanly, no DL-overflow, no crash. **Not yet live-reproduced-then-fixed** — the mechanism is confirmed by code reading against the exact log evidence (quit fired, not a crash), not by a live click-repro-then-retest cycle; next playtest should specifically re-try clicking "All unlocked" (and any other row adjacent to a scroll boundary) to confirm the right row now responds.
|
||||
|
||||
**Third follow-up, same day: a genuinely new, concrete root-cause candidate found and fixed, user-authorized.** A fresh read of `explosion.c`'s actual particle-render code (looking for a headless repro path, not expecting to find the bug itself) turned up a real out-of-bounds write that six prior investigation sessions had not spotted: `explosionRenderPart()` (`src/game/explosion.c`, ~line 990) calls `vertices = dynAllocateVertices(4);` — reserving exactly 4 `Vtx` slots from `dyn.c`'s shared bump allocator (`g_GfxMemPos += count * sizeof(Vtx)`, no bounds checking, no per-call isolation) — then writes **five** default-init copies: `vertices[0]` through `vertices[4]`. Confirmed directly (not just agent-reported): the function's own GBI emission a few lines later is `gSPVertex(gdl++, osVirtualToPhysical(vertices), 4, 0);` / `gSP2Triangles(gdl++, 0, 1, 2, 0, 0, 2, 3, 0);` — only ever consumes indices 0-3. `vertices[4]` is never read again inside this function; the write is provably dead for this quad's own drawn output and exists only to corrupt one `Vtx`-worth of memory (16 bytes) at the START of whatever the *next* `dynAllocate*` call in the same frame claims from the shared pool — plausibly another particle's own vertex data in a busy explosion scene, which fits the "hue drifts/scrambles as more particles render" symptom shape far better than anything the source-texture or texture-cache investigations found. Checked every other `dynAllocateVertices(4)` call site for the same pattern (`glass2.c`'s `bullet_spark_render`, the already-D219-fixed sibling, and `explosion.c`'s own `smokeRenderPart`) — both correctly write only 4 vertices; this is an isolated, one-function bug, not a systemic pattern.
|
||||
|
||||
**Process note:** this is decomp-verified original code (`Vtx` has no pointer members, so `sizeof(Vtx)` is identical on 32-bit N64 and 64-bit PC — this is not a struct-widening/ABI-class bug like D219's), so it doesn't fit AGENTS.md's narrow pointer-width exception on paper, and per `docs/dev-process.md` §7 would normally need the full rule-2 sign-off procedure (proof of N64-vs-decomp divergence) before any `src/game` edit. Presented the exact mechanism above to the user (project owner) in-thread — a write provably dead for this function's own draw, whose only effect is corrupting unrelated adjacent pool memory — and the user explicitly chose "just fix it" over the alternatives (live-capture-first, or skip) when asked directly. Recording that as the authorization in lieu of the full N64-reference-divergence proof, per rule-2's own "the user reviews... and replies with an explicit go-ahead" grant path.
|
||||
|
||||
**Fix:** deleted the stray `vertices[4] = spA0;` line (`src/game/explosion.c`), no `#ifdef PORT` needed — removing a provably-dead-for-this-draw out-of-bounds write is safe under both build targets, and the surrounding code has no existing N64/PORT fork at this exact point.
|
||||
|
||||
**Verified:** clean rebuild (only pre-existing, unrelated warnings elsewhere in the same file — uninitialized-variable and pointer-cast warnings that predate this change). Headless smoke boot (`-level_09`, 400+ frames) crash-free. **Not visually verified against a live explosion** — this session's separate scripted-repro research found no reliable headless path to a real explosion (needs cycling to an explosive weapon + firing, level/loadout-dependent, never confirmed working; the cheaper "shoot a wall" repro that worked for D219 exercises `glass2.c`'s already-fixed bullet-spark path, not this `explosion.c` function). Do not claim the rainbow is gone until a human (or a future scripted-repro session) actually sees it fail to reproduce on a real explosion — this fix is build-verified and provably behavior-neutral for the drawn quad, not yet visually confirmed as the actual fix for what the user sees on screen.
|
||||
|
||||
**Also newly identified this session, NOT investigated or fixed — logged for a future live-capture session:** the GL texture upload/bind/combiner path was independently audited end-to-end (texture-unit binding, `gfx_texture_cache_lookup`, shader-cache keying) and came back clean except for one concrete, previously-undocumented lead: `rdp.loaded_texture[512]` (`port/fast3d/gfx_pc.cpp:194`) is indexed purely by **TMEM slot number**, not by texture/draw identity, and feeds a D229-added reroute (`gfx_pc.cpp:~1250-1264`) that redirects an `RGBA16`-declared tile through the CI8 palette-index decoder if `loaded_texture.src_fmt` was left over as `CI` from a *different*, unrelated draw that previously loaded into the same TMEM slot. This would produce literal per-pixel confetti (raw RGBA5551 bytes reinterpreted as palette indices) and would explain the bug's flakiness (only fires if some unrelated CI-format draw happens to land on the same TMEM slot in between fire-tile loads) — but was never caught actually firing on the fire tile in this session's static read. Next step for whoever picks this up: a targeted probe logging `loaded_texture.src_fmt` and whether the CI8-reroute branch is taken, gated on the fire tile's known signature (`size_bytes==448`, `tmem==0x188`), during a live repro.
|
||||
|
||||
## D305 — Steam Deck SIGSEGV on the v0.3.0 pre-release build (Cavern, early in level, `Game.AllUnlocked` previously enabled) — a D285 recurrence past its own fix.
|
||||
|
||||
**User report (2026-09-18):** crash on Cavern, early in the level, on the local v0.3.0 pre-release build this session deployed to the Deck (`~/Downloads/goldeneye-pc-port-0.3.0-deck-playtest/`). User's own hypothesis: likely related to `Game.AllUnlocked` (the F10 "unlock everything" toggle, D257), which had been previously enabled on that session/save.
|
||||
|
||||
**Fetched `ge007.crash.log` via SSH from the Deck (192.168.69.18) and symbolized it against the exact deployed binary** (confirmed identical build via matching ELF BuildID `d6ec0681d2e82a656c7a099f66264d6bfa4480b7` between the Deck's copy and this session's local `build-linux/ge007.x86_64` — the SHA256 differs only because `bundle`-style `patchelf --set-rpath` modifies the file after linking, not because the code differs):
|
||||
|
||||
```
|
||||
SIGNAL: 11 FAULT ADDR: 0x62
|
||||
#00 sndHandleEvent src/snd.c:379 <- fault site
|
||||
#01 sndPlayerVoiceHandler src/snd.c:253
|
||||
#02 _timeToSamplesNoRound src/libultra/audio/synthesizer.c:330
|
||||
#03 amHandleFrameMessage src/audi.c:551
|
||||
#04 amMain src/audi.c:452
|
||||
#05 portThreadWrapper port/src/libultra.c:387
|
||||
```
|
||||
|
||||
**This is the exact same FAULT ADDR (0x62) and the exact same fault site as D285** ("M-140: a real Steam Deck SIGSEGV... an unprotected shared-list read in `sndHandleEvent`'s preempt-scan"). D285 was root-caused, fixed (M-141: wrapped the scan in `osSetIntMask(OS_IM_NONE)`/restore, matching the paired writer `sndSetupSound`'s existing lock), and **live-verified crash-free on real Steam Deck hardware** under a sustained dual-grenade-launcher stress test (v0.2.2 hotfix, 2026-09-16). That fix is confirmed present in the current tree (`src/snd.c`, the `d285Mask` lock around the scan). **Getting the identical crash again, past that fix, disproves D285's "unprotected concurrent mutation" theory as the sole or complete cause.**
|
||||
|
||||
**Root cause, confirmed by direct disassembly (not just a source read):**
|
||||
```c
|
||||
ALSoundState *iterState = (ALSoundState *) D_800243E4.node.prev; // NULL if the list is empty
|
||||
do {
|
||||
s32 ownerlessLoop = (iterState->unk3e & ...) ... // src/snd.c:379 -- unconditional dereference
|
||||
...
|
||||
iterState = (ALSoundState *) iterState->link.prev;
|
||||
} while (limitReached && iterState != NULL); // NULL check only AFTER the body already ran
|
||||
```
|
||||
`objdump` of the exact crashing binary at the fault PC confirms `mov r13, [D_800243E4+8]` (loading `iterState`) immediately followed by `movzx eax, [r13+0x62]` — and FAULT ADDR `0x62` with no base-register offset implied means `r13` (`iterState`) was **exactly NULL** at the crash (`0x0 + 0x62 = 0x62`). `D_800243E4 = { {NULL, NULL}, NULL }` is the sentinel head of the active-`ALSoundState` list, whose `node.prev`/`node.next` genuinely are `NULL` whenever the list is empty (not a self-referencing circular sentinel) — this do-while's `!= NULL` check runs only *after* the body's first (unconditional) dereference, per D285's own original write-up, which correctly identified this shape but attributed the *cause* of hitting it to a cross-thread race that the M-141 lock has since closed. **D285's lock prevents another thread from emptying the list mid-scan; it does nothing if the list is already empty (single-threaded, no race involved) the moment this scan starts** — which is exactly what still happened here.
|
||||
|
||||
**Not proven: why the list is empty while `limitReached` (pool-exhausted) is true.** The scan only runs when all 8 voices are believed allocated, which should imply at least one live tracked node in this same list — a genuine accounting desync between the voice pool and this list, mechanism unidentified. **`Game.AllUnlocked` correlation is reported, not confirmed** — plausible (more cheat-unlocked content/audio triggered, or an early-level audio-init-ordering interaction with the toggle) but equally plausible this is coincidental timing (early-level moments naturally have fewer/no ambient sounds queued, making an empty list more likely to be sampled at exactly the wrong instant regardless of AllUnlocked). Do not treat AllUnlocked as a confirmed trigger without a repro that isolates it.
|
||||
|
||||
**Fix (defensive guard, same class/precedent as D255's "skip-and-log instead of dereferencing a NULL model"):** added `if (iterState != NULL)` immediately before the `do { ... } while (...)`, so an empty list at scan-start is a silent no-op (the scan simply finds nothing to preempt) instead of a crash — zero behavior change for the normal (non-empty) case, since the loop's own body and exit condition are untouched. Added an opt-in diagnostic (`GE_D305=1`, `fprintf(stderr, ...)`) logging exactly this desync condition if it fires again, to gather more data on the AllUnlocked correlation and the desync mechanism without needing another crash to learn from.
|
||||
|
||||
**Process note:** like D252's `explosionRenderPart` fix earlier this session, this touches decomp-verified `src/game/snd.c` and isn't a pointer-width/ABI-class change. Unlike that fix, this one is squarely the same shape as the already-applied, unchallenged D255 precedent (a defensive skip-guard against a state that should never occur, added without a separate rule-2 procedure) — proceeding on that precedent rather than re-requesting sign-off for an identically-shaped fix.
|
||||
|
||||
**Verified:** clean rebuild (Windows); pre-existing unrelated warnings only (a `D_800243E4`/`ALEventQueue` aliasing quirk in a different function, `sndCountAllocList`, flagged by `-Warray-bounds` — not touched by this fix). Headless `-level_09` smoke boot, 400+ frames, crash-free.
|
||||
|
||||
**Live-verified on the Deck, same day.** Rebuilt via WSL Ubuntu-24.04 (same toolchain as the original deployment — confirmed `GLIBC_2.38` max symbol requirement, matching SteamOS), re-bundled (SDL2 + `patchelf` rpath), and pushed just the binary + bundled SDL2 to `~/Downloads/goldeneye-pc-port-0.3.0-deck-playtest/` over SSH/SCP — the ROM, asset sidecars, and the user's existing save/config (`ge007.ini`/`ge007.eep`) were left untouched (confirmed unchanged timestamps post-deploy). **User ran two Cavern test sessions, plus toggled `Game.AllUnlocked` off and back on and relaunched — no crash in any of them.** This isn't proof the underlying pool/list desync mechanism can never recur (it's still not root-caused, and the `GE_D305=1` diagnostic added alongside the fix has not fired in any of these sessions to confirm or deny the desync actually happens under normal play) — but the crash itself, which was 100% reproducible on the prior build under this exact repro, is gone.
|
||||
|
||||
**Visually confirmed, same day.** User tested live on Jungle (`-level_37`) after the `explosionRenderPart` fix: "Rainbow went away." **D252 CLOSED** — the 6+-session investigation ends with a fix found by direct code reading (a provably-dead out-of-bounds write into the shared vertex pool), not the live-capture-and-trace-forward approach every prior session's own "next step" had called for. The secondary TMEM-slot `src_fmt` lead from this session's texture-cache audit remains logged as a named, unconfirmed candidate only in case a *new*, distinct rainbow-particle report ever surfaces — it was never confirmed as an actual contributing mechanism, and with the real cause now found and fixed, there's no reason to chase it further unless the symptom recurs.
|
||||
+67
-3
@@ -139,6 +139,12 @@ static struct RSP {
|
||||
const struct NormalColor *vertex_colors; //[MAX_VERTEX_COLORS];
|
||||
} rsp;
|
||||
|
||||
/* D236 pass 16 (TEMP): segment byte (top byte of the raw segmented address)
|
||||
* of the most recent G_VTX load, so a later triangle-time probe can report
|
||||
* which segment the tree class's vertices actually came from. See the
|
||||
* G_VTX case comment below for why. Remove once D236 pass 16 concludes. */
|
||||
static uint8_t g_d236_last_vtx_seg = 0xFF;
|
||||
|
||||
struct RawTexMetadata {
|
||||
uint16_t width, height;
|
||||
float h_byte_scale = 1, v_pixel_scale = 1;
|
||||
@@ -1796,7 +1802,9 @@ static void gfx_sp_tri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx, bo
|
||||
* same way twice, not at true per-frame nondeterminism)? Log the
|
||||
* count once per display list (frame) while it's nonzero. Remove
|
||||
* once D236 pass 10 concludes. */
|
||||
if (getenv("GE_D236RM")) {
|
||||
static int s_d236rm = -1;
|
||||
if (s_d236rm < 0) s_d236rm = getenv("GE_D236RM") != NULL;
|
||||
if (s_d236rm) {
|
||||
static uint32_t d236rm_last_dl = 0xFFFFFFFFu;
|
||||
static uint32_t d236rm_count = 0;
|
||||
extern uint32_t num_dls;
|
||||
@@ -1832,7 +1840,9 @@ static void gfx_sp_tri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx, bo
|
||||
* directly explains "wall painted over trees" regardless of geometric
|
||||
* depth, and points at a draw-order (not depth) bug. Remove once D236
|
||||
* pass 12 concludes. */
|
||||
if (getenv("GE_D236ORDER")) {
|
||||
static int s_d236order = -1;
|
||||
if (s_d236order < 0) s_d236order = getenv("GE_D236ORDER") != NULL;
|
||||
if (s_d236order) {
|
||||
static uint32_t d236o_last_dl = 0xFFFFFFFFu;
|
||||
static uint32_t d236o_seq = 0;
|
||||
static uint32_t d236o_noise_min = 0, d236o_noise_max = 0, d236o_noise_n = 0;
|
||||
@@ -1864,6 +1874,46 @@ static void gfx_sp_tri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx, bo
|
||||
d236o_seq++;
|
||||
}
|
||||
|
||||
/* D236 pass 14 (M-19x, TEMP): pass 13's live capture ruled out draw
|
||||
* order as the dominant cause even in the favorable (tree-after-noise,
|
||||
* high tri count) zone -- redirecting to whether the tree class's own
|
||||
* per-vertex alpha (D280's "gfog=0, CPU-baked bimodal 25/255" census)
|
||||
* is actually LOW (near-transparent) for the close/high-count trees the
|
||||
* live capture walked through. This render mode's blend equation is a
|
||||
* standard alpha-blend decal (FORCE_BL, GBL c1/c2 = CLR_IN,A_IN ->
|
||||
* CLR_MEM,1-A_IN -- see include/PR/gbi.h RM_AA_ZB_XLU_DECAL), so if the
|
||||
* combiner's alpha output tracks vertex/SHADE alpha directly, a card
|
||||
* baked near 25/255 (~10%) would blend almost invisibly over whatever
|
||||
* was drawn under it -- looking exactly like "the noise wall shows
|
||||
* through" even with correct geometry and correct draw order. Logs,
|
||||
* once per triangle of this class: each vertex's raw color.a, the
|
||||
* combine_mode word (to see which alpha slot actually feeds the
|
||||
* blend), and 1/w (a cheap camera-distance proxy) so alpha can be
|
||||
* correlated against "close" vs "far" the same way pass 13's live
|
||||
* capture was read. Zero cost unset. Remove once D236 pass 14
|
||||
* concludes. */
|
||||
static int s_d236alpha = -1;
|
||||
if (s_d236alpha < 0) s_d236alpha = getenv("GE_D236ALPHA") != NULL;
|
||||
if (s_d236alpha && rdp.other_mode_l == 0x0c184b50u) {
|
||||
static uint32_t d236a_hits = 0;
|
||||
static uint64_t d236a_last_combine = 0xFFFFFFFFFFFFFFFFull;
|
||||
if (d236a_hits < 4000) {
|
||||
if (rdp.combine_mode != d236a_last_combine) {
|
||||
fprintf(stderr, "D236ALPHA combine_mode=0x%016llx\n",
|
||||
(unsigned long long)rdp.combine_mode);
|
||||
d236a_last_combine = rdp.combine_mode;
|
||||
}
|
||||
fprintf(stderr,
|
||||
"D236ALPHA a=(%u,%u,%u) invw=(%.4f,%.4f,%.4f) vtxseg=0x%02x\n",
|
||||
v1->color.a, v2->color.a, v3->color.a,
|
||||
(v1->w != 0.f) ? 1.f / v1->w : 0.f,
|
||||
(v2->w != 0.f) ? 1.f / v2->w : 0.f,
|
||||
(v3->w != 0.f) ? 1.f / v3->w : 0.f,
|
||||
g_d236_last_vtx_seg);
|
||||
d236a_hits++;
|
||||
}
|
||||
}
|
||||
|
||||
if ((rsp.geometry_mode & G_CULL_BOTH) != 0) {
|
||||
float dx1 = v1->x / (v1->w) - v2->x / (v2->w);
|
||||
float dy1 = v1->y / (v1->w) - v2->y / (v2->w);
|
||||
@@ -1916,7 +1966,9 @@ static void gfx_sp_tri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx, bo
|
||||
* z-write for a decal-mode class is not decomp-faithful either) -- purely
|
||||
* to test the mechanism cheaply before deciding what a real fix looks
|
||||
* like. Remove once D236 pass 12 concludes. */
|
||||
if (getenv("GE_D236ZFIX") && rdp.other_mode_l == 0x0c184b50u) {
|
||||
static int s_d236zfix = -1;
|
||||
if (s_d236zfix < 0) s_d236zfix = getenv("GE_D236ZFIX") != NULL;
|
||||
if (s_d236zfix && rdp.other_mode_l == 0x0c184b50u) {
|
||||
depth_update = true;
|
||||
}
|
||||
bool depth_compare = (rdp.other_mode_l & Z_CMP) == Z_CMP;
|
||||
@@ -3306,6 +3358,18 @@ static void gfx_run_dl(Gfx* cmd) {
|
||||
gfx_sp_texture(C1(16, 16), C1(0, 16), C0(11, 3), C0(8, 3), C0(0, 8));
|
||||
break;
|
||||
case G_VTX:
|
||||
/* D236 pass 16 (TEMP): pass 15's GE_D236RAW dump proved every
|
||||
* room-background Vtx (g_BgRoomInfo[].vertices) is alpha=255
|
||||
* on load -- so the tree class's <=36/255 alpha measured at
|
||||
* draw time (GE_D236ALPHA) can't be coming from that static
|
||||
* table. Record which segment (top byte of the raw segmented
|
||||
* address) the most recent G_VTX load came from, so the next
|
||||
* triangle-time probe can report it -- distinguishes "still
|
||||
* room background, something else touches it after load" from
|
||||
* "not room background at all" (a different segment, e.g. a
|
||||
* model/CPU-built-quad source) without guessing from source
|
||||
* review alone. Remove once D236 pass 16 concludes. */
|
||||
g_d236_last_vtx_seg = (uint8_t)(cmd->words.w1 >> 24);
|
||||
gfx_sp_vertex(C0(0, 16) / sizeof(Vtx), C0(16, 4), (const Vtx*)seg_addr(cmd->words.w1));
|
||||
break;
|
||||
case G_DL: {
|
||||
|
||||
+13
-3
@@ -292,7 +292,8 @@ static int mouseGrabbed = 1; /* released while the window is unfocused */
|
||||
static int captureArmed = 0; /* user has clicked to lock (capture mode) */
|
||||
static int windowFocused = 1;
|
||||
static int mouseAimSpeed = 16; /* aim-mode sensitivity, percent (B3: 50 -> 25 M-29 -> 16; still overshot at 25) */
|
||||
static int gepdSens = 38; /* D194 Input.GepdSens: GEPD SENSITIVITY setting, range 1..80
|
||||
static int gepdSens = 38; /* D194 Input.GepdSens: GEPD SENSITIVITY setting, range 1..500
|
||||
(D304: widened from 1..80 to match Input.MouseTurnSpeed)
|
||||
(20 -> 25 "a bit slow" -> 30 user-calibrated match point
|
||||
-> 38: user asked defaults ~20-35% faster than that) */
|
||||
static int aimBand = 20; /* aim mode: usable stick range above the 60 gate */
|
||||
@@ -1586,8 +1587,17 @@ PD_CONSTRUCTOR static void inputConfigInit(void)
|
||||
* the RMB aim mode; "GEPD" is internal provenance jargon). The old key
|
||||
* stays registered against the same variable as a deprecated alias --
|
||||
* if both appear in an ini, the later line wins. */
|
||||
configRegisterInt("Input.AimModeSens", &gepdSens, 1, 80);
|
||||
configRegisterInt("Input.GepdSens", &gepdSens, 1, 80); /* deprecated alias */
|
||||
/* D304: widened from the old (1,80) clamp to match Input.MouseTurnSpeed's
|
||||
* (1,500) range exactly -- both are the same underlying "reference SENS
|
||||
* passthrough" unit (see aimGepdCompute/hipDirectCompute), consumed by
|
||||
* two different, intentional per-mode divisor constants (2920 vs the
|
||||
* turn-speed path's /100*0.1), so a shared range lets the two sliders be
|
||||
* directly comparable ("1:1" in the menu) instead of arbitrarily capped
|
||||
* at different, mismatched ceilings. Old 80 cap was well below even this
|
||||
* value's own reference-model native ceiling (100 raw = 500% in the
|
||||
* source GEPD injector's own display convention). */
|
||||
configRegisterInt("Input.AimModeSens", &gepdSens, 1, 500);
|
||||
configRegisterInt("Input.GepdSens", &gepdSens, 1, 500); /* deprecated alias */
|
||||
configRegisterInt("Input.AimBand", &aimBand, 5, 40);
|
||||
configRegisterInt("Input.MouseTurnSpeed", &mouseTurnSpeed, 1, 500);
|
||||
configRegisterInt("Input.SensLink", &sensLink, 0, 1);
|
||||
|
||||
+77
-10
@@ -120,10 +120,32 @@ static struct Row rows[] = {
|
||||
{ "Video.LodDistanceAutoFov", "LOD dist. auto", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
|
||||
/* Aim row edits Input.AimModeSens -- the knob the default GEPD aim path
|
||||
* actually uses (Input.MouseAimSpeed only feeds the legacy velocity-stick
|
||||
* fallback, so it was inert here). */
|
||||
{ "Input.AimModeSens", "Mouse aim speed", ROW_SLIDER, 1, NULL, 0, 1, 80, 0,0,0,0,0 },
|
||||
{ "Input.MouseTurnSpeed", "Mouse turn speed", ROW_SLIDER, 1, NULL, 0, 0, 100, 0,0,0,0,0 },
|
||||
{ "Input.SensLink", "Link aim/turn sens",ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
|
||||
* fallback, so it was inert here). D304: both this row and the turn-speed
|
||||
* row below now use the (0,0) "inherit the registered clamp" sentinel
|
||||
* instead of hardcoded uiMin/uiMax -- they were previously (1,80) and
|
||||
* (0,100), two arbitrary, DIFFERENT, and (for turn speed) outright wrong
|
||||
* ceilings that didn't even match either row's own backing config clamp
|
||||
* (both are actually registered 1..500 in input.c) -- fixed as an earlier
|
||||
* part of this same D304 pass. **Follow-up, same day, user feedback after
|
||||
* trying the widened sliders live:** exposing BOTH per-mode knobs still
|
||||
* let a player decouple them into a bad, hard-to-diagnose state (e.g. aim
|
||||
* mode very slow, hipfire very fast, or vice versa) with no indication
|
||||
* anything was wrong -- worse than a narrow range. Per the user's request,
|
||||
* these two rows and the "Link aim/turn sens" toggle that tried to paper
|
||||
* over the same risk are pulled from the menu entirely; only the single
|
||||
* master `Input.MouseSensitivity` row below remains player-facing, exactly
|
||||
* mirroring the reference GEPD/PD injector model this input was ported
|
||||
* from (`reference/mouse-injector/games/goldeneye.c`) -- ONE user-facing
|
||||
* sensitivity value, with the two per-mode constants staying fixed at
|
||||
* their calibrated (38/50) defaults, never independently player-tunable.
|
||||
* Config vars + SensLink logic stay in `input.c`/`rowSet()` untouched
|
||||
* (ini power users can still hand-edit them; existing ini files with
|
||||
* either key keep working) -- only the menu surface changes, same
|
||||
* established pattern as the D181/D216 pulled rows below. */
|
||||
/* { "Input.AimModeSens", "Mouse aim speed", ROW_SLIDER, 5, NULL, 0, 0, 0, 0,0,0,0,0 }, */
|
||||
/* { "Input.MouseTurnSpeed", "Mouse turn speed", ROW_SLIDER, 5, NULL, 0, 0, 0, 0,0,0,0,0 }, */
|
||||
/* { "Input.SensLink", "Link aim/turn sens",ROW_TOGGLE,1, kOnOff, 0, 0, 0, 0,0,0,0,0 }, */
|
||||
{ "Input.MouseSensitivity", "Mouse sensitivity",ROW_SLIDER, 5, NULL, 0, 0, 0, 0,0,0,0,0 },
|
||||
{ "Input.MouseInvertY", "Mouse invert Y", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
|
||||
/* D181/Game.ScreenShakeIntensity: user testing (v0.2.1) found the slider
|
||||
* "basically useless" -- viShake() is only called from explosion.c, so it
|
||||
@@ -233,10 +255,28 @@ static void sliderBarSpan(s32 *x0, s32 *x1)
|
||||
}
|
||||
}
|
||||
|
||||
/* Visible position (0..s_visN-1) the given overlay-space y falls in, or -1. */
|
||||
/* Visible position (0..s_visN-1) the given overlay-space y falls in, or -1.
|
||||
*
|
||||
* D304 fix: this used to loop over ALL s_visN entries (0..s_visN-1)
|
||||
* regardless of which ones are actually scrolled into view and drawn this
|
||||
* frame -- OV_ROW_Y(p - s_scroll) is a plain linear function of p, so a row
|
||||
* sitting just past the last visibly-drawn one (pLast, see the emit path)
|
||||
* still produces a geometrically valid, in-range Y band immediately below
|
||||
* the panel's real bottom edge. A click near that boundary (real-pixel-to-
|
||||
* virtual-2D-space rounding in the ox/oy scale-up, or simply a slightly low
|
||||
* click on the last visible row) could therefore resolve to the NEXT,
|
||||
* invisible, scrolled-off-the-bottom row instead of the one actually drawn
|
||||
* there. Concretely: with "All unlocked" sitting in the last visible slot,
|
||||
* this let a click on it silently hit "Quit to desktop" (the very next row)
|
||||
* instead -- reported as "I clicked unlock all and I think it crashed"
|
||||
* (user, 2026-09-18): the game didn't crash, __QuitToDesktop's ROW_ACTION
|
||||
* fired and closed it via the game's normal exit path. Fix: bound the scan
|
||||
* to the same [s_scroll, pLast] range the draw loop actually renders. */
|
||||
static int overlayRowAtY(double oy)
|
||||
{
|
||||
for (int p = 0; p < s_visN; p++) {
|
||||
int maxV = maxVisibleRows();
|
||||
int pLast = (s_visN - s_scroll < maxV) ? s_visN - 1 : s_scroll + maxV - 1;
|
||||
for (int p = s_scroll; p <= pLast; p++) {
|
||||
double top = OV_ROW_Y(p - s_scroll) - 3;
|
||||
if (oy >= top && oy < top + OV_LINE) {
|
||||
return p;
|
||||
@@ -265,7 +305,20 @@ static void overlayUpdateVisible(void)
|
||||
}
|
||||
}
|
||||
|
||||
/* Keep the selected row on screen: shift the window when it nears an edge. */
|
||||
/* Keep the selected row on screen: shift the window ONLY when the selection
|
||||
* is actually outside it, by the minimum amount needed.
|
||||
*
|
||||
* D304 fix: this used to unconditionally set `s_scroll = s_sel - (maxV-1)`
|
||||
* -- i.e. bottom-anchor the selected row -- on every single call, including
|
||||
* every mouse click. A click on a row already visible (anywhere but the very
|
||||
* last slot) still forced the whole list to re-scroll so that row landed at
|
||||
* the bottom, shifting every row's on-screen position for the rest of that
|
||||
* same frame -- so whatever row the user then saw/clicked at that same
|
||||
* screen position was a DIFFERENT (usually the next, i.e. "below") setting.
|
||||
* Reported as "the F10 menu keeps jumping to the setting below when I left
|
||||
* click" (user, 2026-09-18) -- clicking any row not already at the bottom
|
||||
* slot reproduced it every time. Fix: only move the window when the
|
||||
* selection is above the top or below the bottom of the current view. */
|
||||
static void overlayUpdateScroll(void)
|
||||
{
|
||||
int maxV = maxVisibleRows();
|
||||
@@ -273,7 +326,11 @@ static void overlayUpdateScroll(void)
|
||||
s_scroll = 0;
|
||||
return;
|
||||
}
|
||||
s_scroll = s_sel - (maxV - 1);
|
||||
if (s_sel < s_scroll) {
|
||||
s_scroll = s_sel;
|
||||
} else if (s_sel > s_scroll + maxV - 1) {
|
||||
s_scroll = s_sel - (maxV - 1);
|
||||
}
|
||||
if (s_scroll < 0) {
|
||||
s_scroll = 0;
|
||||
}
|
||||
@@ -806,9 +863,19 @@ Gfx *optionsOverlayEmit(void)
|
||||
const s32 right = OV_RIGHT;
|
||||
const s32 panelTop = OV_TOP - 9;
|
||||
/* Last visible position actually drawn (window may be shorter than the
|
||||
* list at small 2D viewports -- the rest is reached by scrolling). */
|
||||
* list at small 2D viewports -- the rest is reached by scrolling).
|
||||
* D304 fix: the "everything fits" branch was `s_visN - s_scroll`, which
|
||||
* is a COUNT, not the last valid 0-based index -- since that branch only
|
||||
* runs when s_scroll==0 (overlayUpdateScroll's own invariant), this
|
||||
* evaluated to `s_visN`, one past the last valid entry, so both draw
|
||||
* loops below (`for (p = s_scroll; p <= pLast; p++)`) read one row past
|
||||
* the end of `s_visIdx[]`/`rows[]` on any 2D viewport large enough to fit
|
||||
* the whole list without scrolling (e.g. the larger front-end 440x330
|
||||
* screen once a couple of rows are pulled from the menu, as just
|
||||
* happened above). Should be `s_visN - 1`, matching overlayRowAtY's
|
||||
* identical fix. */
|
||||
const int maxV = maxVisibleRows();
|
||||
const int pLast = (s_visN - s_scroll < maxV) ? s_visN - s_scroll
|
||||
const int pLast = (s_visN - s_scroll < maxV) ? s_visN - 1
|
||||
: s_scroll + maxV - 1;
|
||||
const s32 panelBottom = OV_ROW_Y(pLast) + OV_LINE / 2 + 3;
|
||||
s32 bx0, bx1;
|
||||
|
||||
@@ -2413,6 +2413,48 @@ s32 bgLoadRoomVtxData(s32 roomnum, u8 *dst, s32 len)
|
||||
#ifdef PORT
|
||||
/* D85: byte-swap the raw N64 Vtx table (16-byte struct, no widening). */
|
||||
bgSwapRoomVtx((Vtx *)dst, result);
|
||||
|
||||
/* D236 pass 16 (TEMP): pass 15's live GE_D236ALPHA capture found the
|
||||
* Surface1 tree-card render class's vertex alpha capped at <=36/255
|
||||
* (<=14% opacity) across a full far->close walk to the tree line, and
|
||||
* confirmed (via the combine_mode decode) that this vertex alpha
|
||||
* directly multiplies the texture's own alpha in the blend equation --
|
||||
* so a capped-low vertex alpha caps every tree pixel's final opacity
|
||||
* outright. bgSwapRoomVtx (just above) explicitly leaves colour bytes
|
||||
* untouched, so whatever is in cn[] here is exactly what bgDecompress
|
||||
* produced -- this is the earliest point after decompression the raw
|
||||
* per-room vertex colour data can be inspected, independent of which
|
||||
* camera path a live player happens to walk. Unlike GE_D236ALPHA (which
|
||||
* only sees whatever triangles are actually drawn from wherever the
|
||||
* player is standing), this dumps a histogram of EVERY vertex's alpha
|
||||
* byte (cn[3]) for the WHOLE room's table the moment it loads --
|
||||
* settling whether the low ceiling pass 15 measured is a sampling
|
||||
* artifact (the walked path just never reached a vertex with a high
|
||||
* baked alpha) or a genuine property of this room's authored data (no
|
||||
* vertex in the whole table goes above the low ceiling, regardless of
|
||||
* where the camera is). Zero cost unset. Remove once D236 pass 16
|
||||
* concludes. */
|
||||
if (getenv("GE_D236RAW")) {
|
||||
s32 vtxCount = result / (s32)sizeof(Vtx);
|
||||
s32 vi;
|
||||
u32 buckets[5] = {0, 0, 0, 0, 0}; /* 0 / 1-36 / 37-100 / 101-200 / 201-255 */
|
||||
u8 amin = 255, amax = 0;
|
||||
Vtx *vtxArr = (Vtx *)dst;
|
||||
for (vi = 0; vi < vtxCount; vi++) {
|
||||
u8 a = vtxArr[vi].v.cn[3];
|
||||
if (a < amin) amin = a;
|
||||
if (a > amax) amax = a;
|
||||
if (a == 0) buckets[0]++;
|
||||
else if (a <= 36) buckets[1]++;
|
||||
else if (a <= 100) buckets[2]++;
|
||||
else if (a <= 200) buckets[3]++;
|
||||
else buckets[4]++;
|
||||
}
|
||||
osSyncPrintf(
|
||||
"D236RAW room=%d n=%d amin=%u amax=%u buckets[0,1-36,37-100,101-200,201-255]=%u,%u,%u,%u,%u\n",
|
||||
(int)roomnum, (int)vtxCount, amin, amax,
|
||||
buckets[0], buckets[1], buckets[2], buckets[3], buckets[4]);
|
||||
}
|
||||
#endif
|
||||
|
||||
room->vertices = (Vtx *)dst;
|
||||
|
||||
+12
-1
@@ -993,7 +993,18 @@ Gfx *explosionRenderPart(struct ExplosionPart *arg0, Gfx *gdl, struct coord3d *c
|
||||
vertices[1] = spA0;
|
||||
vertices[2] = spA0;
|
||||
vertices[3] = spA0;
|
||||
vertices[4] = spA0;
|
||||
/* D252 (2026-09-18): dropped a stray vertices[4] = spA0 write here -- this
|
||||
* function only allocates 4 Vtx slots (dynAllocateVertices(4) above) and
|
||||
* only ever uses 4 (gSPVertex(..., 4, 0) / gSP2Triangles(0,1,2,...,2,3,0)
|
||||
* below), so vertices[4] was never part of this quad's own draw -- it was
|
||||
* a genuine one-Vtx-sized out-of-bounds write into whatever the next
|
||||
* dynAllocate* call in the same frame claims from the shared bump-
|
||||
* allocated pool (dyn.c's g_GfxMemPos), corrupting unrelated particle/
|
||||
* matrix/light data. Removing it changes zero bytes of what this
|
||||
* function itself draws. Prime suspect for D252's rainbow-particle
|
||||
* corruption; see docs/dev/findings.md D252 for the full mechanism and
|
||||
* why this required the project owner's explicit go-ahead to touch
|
||||
* (decomp-verified original code, not an ABI/pointer-width artifact). */
|
||||
|
||||
sp8C.f[0] = sp9C->m[0][0] * sp54;
|
||||
sp8C.f[1] = sp9C->m[0][1] * sp54;
|
||||
|
||||
@@ -393,6 +393,17 @@ bool fileGetIsCheatUnlocked(save_data *save, s32 cheat)
|
||||
s32 bits;
|
||||
|
||||
#ifdef PORT
|
||||
/* D225 (dev-tooling QoL): both mission-select gating (file.c) and the
|
||||
* cheat-menu availability check (front.c frontCheckIfCheatIsUnlocked)
|
||||
* route through this single, side-effect-free bitfield read -- a debug
|
||||
* env toggle here unlocks both in one place. Default off, no save-file
|
||||
* mutation (query-time override only, cannot corrupt or leak into a
|
||||
* shipped save). */
|
||||
{
|
||||
static int s_unlockAll = -1;
|
||||
if (s_unlockAll < 0) s_unlockAll = getenv("GE_DEBUG_UNLOCKALL") != NULL;
|
||||
if (s_unlockAll) return TRUE;
|
||||
}
|
||||
/* D301 (github #87): callers (front.c mode-select cheat rows, file.c
|
||||
* briefing cheat check) pass fileGetSaveForFoldernum(selected_folder_num)
|
||||
* straight through with no NULL check. On real N64 that lookup can
|
||||
|
||||
@@ -359,6 +359,29 @@ void sndHandleEvent(ALSndPlayer *sndp, ALSndpEvent *event) {
|
||||
OSIntMask d285Mask = osSetIntMask(OS_IM_NONE);
|
||||
ALSoundState *iterState = (ALSoundState *) D_800243E4.node.prev;
|
||||
|
||||
/* D305 (Steam Deck SIGSEGV, v0.3.0 pre-release, FAULT ADDR 0x62 --
|
||||
* same exact signature as D285/M-140, reproduced AFTER the M-141
|
||||
* lock fix was already live-verified crash-free on real hardware
|
||||
* (v0.2.2 hotfix). That rules out D285's own "unprotected
|
||||
* concurrent mutation" theory as the sole cause: the lock above
|
||||
* (d285Mask) prevents another thread from emptying this list
|
||||
* mid-scan, but does nothing if the list is ALREADY empty
|
||||
* (D_800243E4.node.prev == NULL) the moment this single-threaded
|
||||
* scan starts -- which the do-while below dereferences
|
||||
* unconditionally before its own `iterState != NULL` check ever
|
||||
* runs (a do-while always executes its body once). The scan is
|
||||
* gated on `limitReached` (the 8-voice pool believed exhausted),
|
||||
* which should imply at least one live tracked node -- a NULL
|
||||
* here means the pool-exhaustion accounting and this list have
|
||||
* desynced by some still-unidentified mechanism (not proven to be
|
||||
* the user's "Game.AllUnlocked" report specifically; logged as a
|
||||
* correlation, not a confirmed cause -- see docs/dev/findings.md
|
||||
* D305/D285). Same defensive-guard shape as D255's
|
||||
* skip-and-log-instead-of-dereferencing fix: this only prevents a
|
||||
* crash in a state that was never valid to scan in the first
|
||||
* place, and changes nothing when the list is non-empty (the
|
||||
* normal, intended case). */
|
||||
if (iterState != NULL)
|
||||
do {
|
||||
/* D202/M-65 (PORT, experimental): the stock scan refuses to
|
||||
* preempt any looped or retriggering voice (0x12). That is
|
||||
@@ -394,6 +417,15 @@ void sndHandleEvent(ALSndPlayer *sndp, ALSndpEvent *event) {
|
||||
}
|
||||
iterState = (ALSoundState *) iterState->link.prev;
|
||||
} while (limitReached && iterState != NULL);
|
||||
else if (getenv("GE_D305")) {
|
||||
/* D305: log the desync state for a future occurrence --
|
||||
* confirms whether Game.AllUnlocked correlates or was
|
||||
* coincidental, and whether it recurs at all now that the
|
||||
* crash itself can't happen. */
|
||||
fprintf(stderr,
|
||||
"D305: sndHandleEvent preempt-scan found D_800243E4 empty "
|
||||
"while limitReached was set -- pool/list desync, scan skipped\n");
|
||||
}
|
||||
osSetIntMask(d285Mask);
|
||||
|
||||
if (!limitReached) {
|
||||
|
||||
Reference in new issue
Block a user