diff --git a/docs/dev/findings-index.csv b/docs/dev/findings-index.csv index 8d9af1c0..357a1ec4 100644 --- a/docs/dev/findings-index.csv +++ b/docs/dev/findings-index.csv @@ -133,7 +133,7 @@ D165,FIXED,"Front-end menu cursor doesn't feel like a mouse pointer (M-31, user 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 — fix wired, headless reload clean (2026-09-23); bug NOT reproduced even with the fix off (GE_D235_OFF)." D236,CLOSED,"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).","CLOSED (M-198, pass 26, 425237f9; probes stripped 0503d335):" -D237,OPEN,"QoL ask (M-106, user QA):",OPEN — not designed; port-only +D237,FIXED,"QoL ask (M-106, user QA):","FIXED (2026-09-23): category headers + 5 more settings (SkipIntro, Show FPS, pad invert/deadzone/trigger); wheel scroll already existed." D238,OPEN,"QoL ask (M-106, user QA):",OPEN — not designed; port-only (port/src/input.c + config.c) D239,CLOSED,"QoL ask (M-106, user QA):",CLOSED — executed in the v0.2.0 prep pass (port-only)… D240,OPEN,"Player gunshot SFX timing/cadence reportedly does not match the N64 original's rate (M-106, user QA report).","OPEN — not root-caused; static call-site tracing only, no runtime verification possible in this environment." diff --git a/docs/dev/findings.md b/docs/dev/findings.md index 9e4e977f..b3119f2f 100644 --- a/docs/dev/findings.md +++ b/docs/dev/findings.md @@ -540,7 +540,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | 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 — fix wired, headless reload clean (2026-09-23); bug NOT reproduced even with the fix off (GE_D235_OFF). Needs the original die → mission select → restart trigger in a playtest to close. | | 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 | **CLOSED (M-198, pass 26, `425237f9`; probes stripped `0503d335`): root cause = `TEXTURETYPE_DETAIL` tile binding — fast3d drew the detail tile (opaque IA8 noise) instead of the base image; `gfx_lod_tile_offset` now samples tile 1 when the pair differs (`Video.DetailBaseTile`, default on). Live-verified Surface 1/2 + Depot; awaiting user play-test sign-off.** Historical trail — pass 24 (2026-09-21, static) DECODED THE TREE TEXTURE** (new `tools_pc/texdecode.py` ports image.c's LOOKUP/HUFFLOOKUP/RLELOOKUP codecs). **`tex1198` LOD 0 = 64×17 RGBA5551, 44.2% opaque, and its 1-bit alpha renders as an unmistakable treeline silhouette** — pass 23's identification confirmed by decode, not inference. **Rare authored a six-level LOD pyramid that PRESERVES the silhouette and its alpha coverage (44.2% → 43.1% → 38.8%), each level a separately-authored image with its own compmethod.** Since `XLU_SURF` has no alpha test (pass 22), averaging that 0/1 alpha into fractional mip alpha would smear the minified treeline into exactly the reported band — so 'authored LOD chain replaced by driver-generated mips' is now the leading mechanism, with hard ground truth behind it. Corrects pass 23: only `tex1198` is a treeline (`tex1199` 89.4% opaque denser foliage; `tex1200`/`tex1201` 100% opaque detail textures sharing the class). Two decoder bugs found while porting (missing per-image byte-align, `image.c:1096`; a GUESSED rather than ported RLE fudge) — both produced confident wrong pictures. **Pass 25 (live) answered all three questions: (1) YES 64×17 RGBA16 intact, pool ≡ offline decode; (2) YES alpha survives (44.2% opaque); (3) NO — no code path uploads authored chains as GL mips.** New: `tex1198`–`1201` are **never drawn from an idle spawn** (zero RGBA16 imports in 4800 frames) — the spawn wall is painted by other classes (cf D276/D280); the drawn tree-class card is **`tex149` IA8 32×64 hard cutout, byte-exact import, no authored chain**. Mip mechanism located: `texGetDepthAndSize` sizes LOADBLOCKs from the header `lods` field even for single-image textures (live A/B: 2808 B vs 2048 B real data), and the port's RC2 clip + `glGenerateMipmap`/trilinear (observed live) box-filters hard 0/F alpha into fractional mips → XLU smear. Pass 26 fix directions in entry (cheapest: no mip gen for no-chain textures). | -| 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 | +| 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 | FIXED (2026-09-23): category headers + 5 more settings (SkipIntro, Show FPS, pad invert/deadzone/trigger); wheel scroll already existed. Hands-on pass owed. | | 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)… | | D240 | **Player gunshot SFX timing/cadence reportedly does not match the N64 original's rate (M-106, user QA report).** — full `## D240` entry at file tail | **OPEN — not root-caused; static call-site tracing only, no runtime verification possible in this environment.** Confidence LOW-MEDIUM that this is a *new* bug distinct from the D155/D186/D193/D204/D209 timing-keystone family (all of which gate the identical `deltaFrames→speedgraphframes→g_ClockTimer→g_GlobalTimer` chain this SFX trigger consumes).… | @@ -11333,6 +11333,8 @@ the Surface 1 spawn capture is unchanged across the strip. **Status (from §F table):** OPEN — not designed; port-only +**IMPLEMENTED (2026-09-23, v0.4.0; port-only, `port/src/optionsoverlay.c`).** Wheel scroll already existed (D304 era). Added: (1) **category headers**, a new `ROW_HEADER` kind (not config-backed, never selectable; up/down/wheel step over headers via `overlayStepSel`, clicks on headers ignored, `rowAdjust` no-ops) drawn in the title accent: DISPLAY / VIEW / MOUSE / AIM / CONTROLLER / GAME; (2) **more settings**: `Game.SkipIntro`, `Video.DisplayFPS`, `Input.PadLookInvertY`, `Input.PadDeadzone` (slider, step 500), `Input.PadTriggerPct`. `Game.NoHitFlash` was already exposed. Verified: overlay capture shows the headers and the selection starting on the first real row; no unregistered-key warnings; Windows + Linux builds clean. Owed: a hands-on pass (wheel/arrows skip headers, the new sliders feel right). + ## D238 — QoL ask (M-106, user QA): mouse sensitivity rework toward normal PC-shooter feel. **QoL ask (M-106, user QA): mouse sensitivity rework toward normal PC-shooter feel.** Three parts: (1) a *separate* RMB aim-mode sensitivity in addition to hipfire turn; (2) ideally ONE single master mouse-sensitivity setting driving both, **linked by default**, with an option to set them independently later (today `Input.MouseAimSpeed` and `Input.MouseTurnSpeed` are two independent keys, defaults 16/100 — see `input.c:188-190`); (3) up/down should feel mostly the same as left/right (`Input.MouseYScale` exists but the axis asymmetry in the gain constants `AIM_GAIN`/`MOUSE_TURN_GAIN` + smoothing path is the likely culprit). User's reference for "feels better": GEPD Mouse Injector. Lump with the overall mouse/input fixes (D194/D165/D166 lineage; SOLITAIRE natural-pitch scheme switch may subsume part of this — see D166 M-87 note). diff --git a/port/src/optionsoverlay.c b/port/src/optionsoverlay.c index 44c9f217..fef1ca04 100644 --- a/port/src/optionsoverlay.c +++ b/port/src/optionsoverlay.c @@ -63,7 +63,8 @@ extern s16 viGetY(void); /* ------------------------------------------------------------------------ */ -enum { ROW_TOGGLE, ROW_SLIDER, ROW_ENUM, ROW_MSAA, ROW_RES, ROW_ACTION, ROW_FPSCAP }; +enum { ROW_TOGGLE, ROW_SLIDER, ROW_ENUM, ROW_MSAA, ROW_RES, ROW_ACTION, ROW_FPSCAP, + ROW_HEADER /* D237: non-selectable category label */ }; static const char *const kOnOff[] = { "OFF", "ON", NULL }; static const char *const kTexFilter[] = { "NEAREST", "BILINEAR", "3-POINT", NULL }; @@ -119,6 +120,7 @@ struct Row { }; static struct Row rows[] = { + { "__HdrDisplay", "DISPLAY", ROW_HEADER, 0, NULL, 0, 0, 0, 0,0,0,0,0 }, { "Video.Fullscreen", "Fullscreen", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 }, { "__Resolution", "Resolution", ROW_RES, 0, NULL, 0, 0, 0, 0,0,0,0,0 }, { "Video.VSync", "VSync", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 }, @@ -126,6 +128,7 @@ static struct Row rows[] = { { "Video.MSAA", "MSAA", ROW_MSAA, 0, NULL, 1, 0, 0, 0,0,0,0,0 }, { "Video.TextureFilter", "Texture filter", ROW_ENUM, 1, kTexFilter, 0, 0, 0, 0,0,0,0,0 }, { "Video.Anisotropy", "Anisotropic", ROW_SLIDER, 1, NULL, 0, 0, 0, 0,0,0,0,0 }, + { "__HdrView", "VIEW", ROW_HEADER, 0, NULL, 0, 0, 0, 0,0,0,0,0 }, { "Video.FovScale", "FOV scale %", ROW_SLIDER, 5, NULL, 0, 0, 0, 0,0,0,0,0 }, /* D334: native widescreen (world projected at the window aspect, Hor+). * While on, "Widescreen auto FOV" has no effect (it was the stretch-era @@ -166,6 +169,7 @@ static struct Row rows[] = { /* { "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 }, */ + { "__HdrMouse", "MOUSE / AIM", ROW_HEADER, 0, NULL, 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 }, /* Input.PdMouseAim (findings D332): the Perfect Dark port's mouse-aim @@ -179,6 +183,10 @@ static struct Row rows[] = { * PD DAMP = the PD model above, CENTRED = mouse turns the camera and the * crosshair stays centred. Input.PdMouseAim=1 migrates to PD DAMP. */ { "Input.AimStyle", "RMB aim style", ROW_ENUM, 1, kAimStyle, 0, 0, 0, 0,0,0,0,0 }, + { "__HdrPad", "CONTROLLER", ROW_HEADER, 0, NULL, 0, 0, 0, 0,0,0,0,0 }, + { "Input.PadLookInvertY", "Pad invert look Y",ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 }, + { "Input.PadDeadzone", "Stick deadzone", ROW_SLIDER, 500, NULL, 0, 0, 0, 0,0,0,0,0 }, + { "Input.PadTriggerPct", "Trigger threshold %",ROW_SLIDER,1, NULL, 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 * scales explosion shake alone; it never touches the always-on walking @@ -188,6 +196,9 @@ static struct Row rows[] = { * hook stay in place. */ /* D232: the community "no damage flash" toggle (suppresses the red/green * hit-flash overlay in bondview2). */ + { "__HdrGame", "GAME", ROW_HEADER, 0, NULL, 0, 0, 0, 0,0,0,0,0 }, + { "Game.SkipIntro", "Skip intro", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 }, + { "Video.DisplayFPS", "Show FPS", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 }, { "Game.NoHitFlash", "No hit flash", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 }, /* D216/Game.SkipIntro: user report (v0.2.1 testing) that it breaks audio * -- pulled from the menu until root-caused. Not exposed to players; the @@ -323,6 +334,28 @@ static int overlayRowAtY(double oy) } /* Rebuild the visible-row list and keep the selection in range. */ +/* D237: move a visible-list position by dir (+1/-1) to the next row that + * is not a category header; stays put at the ends. */ +static int overlayStepSel(int pos, int dir) +{ + int p = pos; + for (;;) { + int q = p + dir; + if (q < 0 || q >= s_visN) { + /* nothing selectable that way: if we started on a header, try + * the other direction once so we never rest on one */ + if (rows[s_visIdx[p]].kind == ROW_HEADER && dir > 0) { + return overlayStepSel(pos, -1); + } + return (rows[s_visIdx[p]].kind == ROW_HEADER) ? pos : p; + } + p = q; + if (rows[s_visIdx[p]].kind != ROW_HEADER) { + return p; + } + } +} + static void overlayUpdateVisible(void) { s_visN = 0; @@ -340,6 +373,11 @@ static void overlayUpdateVisible(void) if (s_sel >= s_visN) { s_sel = s_visN - 1; } + /* D237: never rest on a category header -- step to the nearest row + * below it (or above, at the end of the list). */ + if (rows[s_visIdx[s_sel]].kind == ROW_HEADER) { + s_sel = overlayStepSel(s_sel, +1); + } } /* Keep the selected row on screen: shift the window ONLY when the selection @@ -421,7 +459,8 @@ static void overlayInit(void) configForEachOption(resolveCb, NULL); for (int i = 0; i < NUM_ROWS; i++) { - if (rows[i].kind == ROW_RES || rows[i].kind == ROW_ACTION) { + if (rows[i].kind == ROW_RES || rows[i].kind == ROW_ACTION || + rows[i].kind == ROW_HEADER) { rows[i].found = 1; /* not config-backed */ continue; } @@ -560,7 +599,7 @@ static void rowSet(struct Row *r, double v) static void rowAdjust(struct Row *r, int dir) { - if (!r->found) { + if (!r->found || r->kind == ROW_HEADER) { /* D237: headers have no value */ return; } double v = rowGet(r); @@ -649,11 +688,9 @@ void optionsOverlayScroll(int dir) return; } overlayUpdateVisible(); - s_sel += (dir > 0) ? -1 : 1; /* wheel-up -> move up the list */ - /* Clamp at both ends like a normal PC settings list -- wrapping made the - * selection "repeat" from the far edge, which read as a duplicate. */ - if (s_sel < 0) s_sel = 0; - if (s_sel >= s_visN) s_sel = s_visN - 1; + /* wheel-up -> move up the list; clamps at both ends like a normal PC + * settings list (wrapping read as a duplicate) and skips D237 headers. */ + s_sel = overlayStepSel(s_sel, (dir > 0) ? -1 : 1); } /* Set a slider row from an overlay-space x inside its value bar, snapped to @@ -744,8 +781,8 @@ void optionsOverlayHandleInput(void) prevStart = startNow; /* ---- keyboard / D-pad nav (clamped at the ends; scroll follows) ---- */ - if (up && !prevUp && s_sel > 0) s_sel--; - if (dn && !prevDn && s_sel < s_visN - 1) s_sel++; + if (up && !prevUp) s_sel = overlayStepSel(s_sel, -1); /* skips headers */ + if (dn && !prevDn) s_sel = overlayStepSel(s_sel, +1); overlayUpdateScroll(); if (lf && !prevLf) rowAdjust(&rows[s_visIdx[s_sel]], -1); if (rt && !prevRt) rowAdjust(&rows[s_visIdx[s_sel]], +1); @@ -781,7 +818,7 @@ void optionsOverlayHandleInput(void) optionsOverlayToggle(); /* close + configSave */ return; } - if (hoverVis >= 0) { + if (hoverVis >= 0 && rows[s_visIdx[hoverVis]].kind != ROW_HEADER) { s_sel = hoverVis; /* explicit click -> select */ overlayUpdateScroll(); if (ox >= bx0) { @@ -803,7 +840,8 @@ void optionsOverlayHandleInput(void) dragRow = -1; } /* right press in the value column: cycle back / decrement */ - if (rmb && !prevRmb && hoverVis >= 0 && !onClose && ox >= bx0) { + if (rmb && !prevRmb && hoverVis >= 0 && !onClose && ox >= bx0 && + rows[s_visIdx[hoverVis]].kind != ROW_HEADER) { s_sel = hoverVis; overlayUpdateScroll(); rowAdjust(&rows[s_visIdx[hoverVis]], -1); @@ -999,6 +1037,10 @@ Gfx *optionsOverlayEmit(void) u32 col = (p == s_sel) ? 0xffffffff : 0xc0c0c8ff; char val[48]; + if (r->kind == ROW_HEADER) { /* D237 category label, no value */ + gdl = drawText(gdl, OV_X0, rowY, (char *)r->label, 0xffe040ff); + continue; + } gdl = drawText(gdl, OV_LABEL_X, rowY, (char *)r->label, r->found ? col : 0x808080ff); if (!r->found) {