diff --git a/.github/release-notes.md b/.github/release-notes.md index cd0b9de2..a8a24101 100644 --- a/.github/release-notes.md +++ b/.github/release-notes.md @@ -55,8 +55,8 @@ frame cadence. Fixed at the root; the game now holds a rock-stable 60 fps ### Rebuilt options (F10 overlay + front-end PC Options) -- Both options screens are reorganized into functional sections — **Display, - Input, Gameplay, HUD, Bond file** — each with its own *Reset to +- Both options screens are reorganized into functional sections — **Input, + Gameplay, HUD, Graphics, Audio, Display** — each with its own *Reset to defaults*; per-profile settings scoping; and the old "save file" wording is now **profile**. (D353–D356) - The **F10 in-game overlay** was refreshed GE-style: category headers, @@ -65,7 +65,9 @@ frame cadence. Fixed at the root; the game now holds a rock-stable 60 fps **Invert look / Southpaw / Deadzone / Trigger threshold**. (D237/D345– D347/D360–D370) - The front end gained a **PC Options screen** beside the file-select bar, - and save-file **Copy/Erase** moved into the bottom bar. (D343) + and save-file **Copy/Erase** moved into the bottom bar. Long sections + (Input) page within the screen with a (1/2) marker in the title, so every + row stays on screen and stays reachable. (D343/D406) - Watch-only settings — **Auto-aim**, **Look ahead** and the rest — are now editable from the PC options and **persist correctly** (this closes the "auto-aim won't stay on" report, issue #103). (D330/D350/D352/D354) diff --git a/docs/dev/findings-index.csv b/docs/dev/findings-index.csv index 7ee7df55..d59eb1b3 100644 --- a/docs/dev/findings-index.csv +++ b/docs/dev/findings-index.csv @@ -282,3 +282,4 @@ D402,RESOLVED,First clean Linux (glibc / SteamOS) build of release/v0.4.0 broke D403,RESOLVED,Deck v0.4.0-pre test:,"RESOLVED (2026-09-27, user-confirmed on the Deck:" D404,RESOLVED,"""Aim style: Centred (PC)"" (Input.AimMode=1, D333/D337) has no effect when aiming with a gamepad; the row's own doc comment over-promises (""applies to every aim…","RESOLVED (2026-09-28, port-layer only):" D405,CLOSED,"fast3d/OpenGL: MSAA above the driver max black-screens; two glBindFramebuffer calls pass an index where a GL name is required (PR #106, italoarruda, 2026-09-25…","CLOSED (2026-09-28). Merged from PR #106 (code file only; security-reviewed clean - port-layer GL state fixes, no external input, no allocations)." +D406,OPEN,front-end PC Options screen:,"OPEN (implemented 2026-09-28, port-layer only):" diff --git a/docs/dev/findings.md b/docs/dev/findings.md index c09b6e6e..9da9db9b 100644 --- a/docs/dev/findings.md +++ b/docs/dev/findings.md @@ -690,6 +690,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D403 | **Deck v0.4.0-pre test: Nintendo logo + copyright/legal page render broken, broader graphics "not reflecting new updates" — stale 0.3.0-era sidecars seeded into the bundle (2026-09-27).** Full `## D403` entry at file tail. | RESOLVED (2026-09-27, user-confirmed on the Deck: a stale-sidecar **test artifact**, never a code regression): the Deck bundle was seeded with `pcmodels`/`pccg` sidecars from the `0.3.0-local` install (data dated 2026-09-15), which predate the D75/M-197 embedded-blob raw-pixel fix (commit `425237f9`, 2026-09-21) and the D303 muzzle-vertex converter fix (2026-09-26) → the `P*` front-end logo/legal textures come out broken while the in-exe fast3d fixes (D397 gun-barrel/file-select) render correctly. Disambiguation performed (2026-09-27): `data/pcmodels-ntsc-final` + `data/pccg-ntsc-final` regenerated from the Python-source converter (`prepare-assets.py` + the current `d43/d69/d88_emit.py`, `ALL CHECKS PASSED`) on the NTSC ROM and pushed to the Deck (sha256-verified), preserving the user's `ge007.eep`/`ge007.ini`. **RESOLVED (user-confirmed on the Deck, 2026-09-27): the front-end logo / copyright / legal page now render correctly — confirming this was the stale-sidecar test artifact, not a code regression.** | | D404 | **"Aim style: Centred (PC)" (`Input.AimMode=1`, D333/D337) has no effect when aiming with a gamepad; the row's own doc comment over-promises ("applies to every aim input … pad trigger") but the centred path is mouse-only (Steam Deck, 2026-09-27).** Full `## D404` entry at file tail. | RESOLVED (2026-09-28, port-layer only): the toggle now genuinely works on a pad. First logged as a communication bug and closed by relabel ("Aim style (mouse)"), then reopened on user challenge the same day: the pad path emits stick *deflection* into the game's crosshair integrator, so the pad was actually always in the **N64** model (a flick travels the crosshair; edge-scroll + spring-back only made a hold *feel* direct), and the "stick has no crosshair to travel" rationale was wrong. Fix: new `padDirectCompute()` in `input.c` (the pad twin of `hipDirectCompute`'s CENTRED branch -- same dead/watch/pause/cutscene gates, minus the mouse-grab gate; revised same day to the N64 path's verbatim quadratic curve -- `sign(v)\u00b7clamp(defl/70,\u00b11)\u00b2 \u00b7 (fov/60) \u00b7 3.5 deg/poll` (bondview2.c canNaturalTurn/canNaturalPitch), full-stick parity with the N64 path, pending retest on the refreshed build); the natural-pitch pad branch routes CENTRED through it and keeps N64's classic deflection behaviour (also the default, so no pad default changes). Labels reverted to device-neutral ("Aim style" / "Aim range"). **Steam Deck (2026-09-27/28): "centered aim is broken on deck too" -- fixed for real; the first-pass linear gain was measured as a dramatic sensitivity loss (2026-09-28) and replaced with the N64 path's verbatim quadratic curve (fov/60 base, 3.5 deg/poll top rate); retest on the refreshed build: user "good". RESOLVED.** | | D405 | **fast3d/OpenGL: MSAA above the driver max black-screens; two `glBindFramebuffer` calls pass an index where a GL name is required** (PR #106, italoarruda, 2026-09-25; label rebased from the PR's D323, which collides with our D323 LOD finding) — full `## D405` entry at file tail | **CLOSED (2026-09-28).** Merged from PR #106 (code file only; security-reviewed clean - port-layer GL state fixes, no external input, no allocations). `resolve_msaa_color_buffer` clamps `msaa_level` to `GL_MAX_SAMPLES`; the resolve/copy paths bind `current_framebuffer ? framebuffers[i].fbo : 0` (the default framebuffer is GL name 0). Both Windows (opengl32) and Linux use this backend. Post-merge sweep 21/21 PASS + MSAA=16 boot check render-verified; link ritual clean. | +| D406 | **front-end PC Options screen: long sections (INPUT, 17 visible rows) run off the bottom of the 440x330 paper, and rows past the silent 14-row page cap (Crouch mode, Reset to defaults, Bindings…) are unreachable from the front screen** (user report 2026-09-28, v0.4.0 pre-push review) — full `## D406` entry at file tail | OPEN (implemented 2026-09-28, port-layer only): in-section pagination in `frontoptions.c` (13 rows/page fit the paper; the vertical stepper crosses page boundaries; title carries a (p/N) marker; the F10 overlay is untouched). Verification owed on the rebuilt binary (user re-check of the front screen). | Phase 2 replaced the Phase-1 demo loop with the real `mainproc()` on real OS @@ -15479,3 +15480,15 @@ issue, README-disclosed; Nintendo logo + legal page are clean.** Release decisio M7/D387 ships as the loudly-documented EXPERIMENTAL option (user: the warning is sufficient); D258 ships NTSC-only (PAL/JP repair deferred post-v0.4); D224 rumble validated on the Deck and included; D281 keeps the shipped mitigation + disclosure, zero-saves root cause moves to v0.5.0. + +## D406 — front-end PC Options: long sections (INPUT) run off the bottom of the paper; rows past the silent page cap are unreachable (user report 2026-09-28, during the v0.4.0 pre-push review) + +**Report:** on the main-menu PC Options screen (MENU_PC_OPTIONS, `port/src/frontoptions.c`), the INPUT section "has too many settings and they go off the bottom of the screen." + +**Root cause (geometry, two stacked miscalculations):** the 440x330 paper ends at y=330. `rowY()` puts content row k at `ROW_Y0 + (k+2)*ROW_DY` (the D356 top profile row takes slot 1). D346b sized the 18px pitch for 15 rows *without* a profile row; D356 then added the profile row (cap became `MAX_PROWS-1 = 14`), which pushed the 14th content row to y = 65+15*18 = **335 — 5px below the paper's bottom edge** (text + highlight box mostly off-screen). Meanwhile the v0.4.0 Wave-A additions grew INPUT to **17 visible rows** (mouse H/V, invert-mouse, aim style, aim range, invert-pad, deadzone L/R, pad look-sens X/Y, look smoothing, southpaw, trigger threshold, vibration, crouch mode, reset, Bindings…), so rows 15–17 (Crouch mode, Reset to defaults, **Bindings…**) were dropped by `buildPages()`' silent cap — logged as a warning, but **unreachable from the front screen** (key rebinding was only reachable via the F10 overlay). + +**Fix (port-layer only, `frontoptions.c`; no `src/` edits, F10 overlay untouched — it scrolls its own window, D345–D347):** in-section pagination. `buildPages()` now gathers all visible rows of the active section and pages them at `ROWS_PER_PAGE = 13` (the largest page that fits: last row at y = 65+14*18 = 317, +~10px text stays inside 330). The discrete vertical stepper crosses page boundaries — stepping past the last visible row turns the page (cursor lands on the next page's first row; the reverse at the top), single-page sections still clamp as before — and the section title carries a `(p/N)` marker when it spans pages (e.g. "Input (1/2)"). `s_pageno` resets on every section/nested-page entry and back-navigation, and is re-clamped on each `buildPages()` so rows that auto-hide (aim range, auto-FOV) cannot strand the cursor past the end. + +**Verification:** rebuild + user re-check of the front screen owed on the refreshed build (INPUT shows 13 rows + "Input (1/2)", stepping down turns to page 2 with Crouch mode / Reset / Bindings…; `GE_FRONTNAVLOG` logs the page number per step; `GE_FRONTOPTIONS_SECTION=INPUT` logs the gathered row count). + +**Status:** OPEN (implemented 2026-09-28, pre-push; verification pending rebuild). diff --git a/port/src/frontoptions.c b/port/src/frontoptions.c index 88de0b1e..e16df069 100644 --- a/port/src/frontoptions.c +++ b/port/src/frontoptions.c @@ -94,6 +94,18 @@ extern struct rectbbox folder_option_ERASE_bound; /* front.c:439 */ #define MAX_PAGES 8 #define MAX_PROWS 15 /* D346b: 12 -> 15 for the merged Video section (see ROW_DY) */ +/* D406: content rows per page. The 440x330 paper ends at y=330; content row k + * sits at y = ROW_Y0 + (k+2)*ROW_DY (the D356 profile row takes slot 1), so a + * page fits while ROW_Y0 + (CAP+1)*ROW_DY + ~10px of text stays <= 330: + * CAP = 13 at ROW_DY 18. D346b's 18px pitch was sized for 15 rows with no + * profile row; D356's profile row plus the v0.4.0 Wave-A INPUT rows (17) + * pushed the 14th row to y=335 -- off the bottom of the paper -- and rows + * 15-17 (Crouch mode, Reset to defaults, Bindings...) were dropped by the + * old silent page cap, unreachable from the front screen. Sections that + * outgrow one page now page: the vertical stepper crosses page boundaries + * and the title carries a (p/N) marker. The F10 overlay is unaffected + * (it scrolls its own window, D345-D347). */ +#define ROWS_PER_PAGE 13 static const char kLabel[] = "PC Options"; /* ASCII only: issue #87 / D295 */ static const char kLabelNL[] = "PC Options\n"; /* height measure only, D400 */ @@ -101,6 +113,8 @@ static const char kLabelNL[] = "PC Options\n"; /* height measure only, D400 */ /* ---- screen state (game thread) ---- */ static int s_level = 0; /* 0 = categories, 1 = category, 2/3 = nested pages */ static int s_page = 0; /* current root category */ +static int s_pageno = 0; /* D406: page within the active section */ +static int s_rowTotal = 0; /* D406: visible rows in the active section */ static int s_subHeader = -1; /* rows[] header index for the nested page */ static int s_hl = -1; /* highlighted row / section, -1 = none */ static int s_dragRow = -1; /* global row index being dragged */ @@ -113,6 +127,7 @@ static int s_pageHdr[MAX_PAGES]; static int s_pageN = 0; static int s_rowIdx[MAX_PROWS]; static int s_rowN = 0; +#define MAX_SECT_ROWS 48 /* D406: per-section gather buffer (INPUT = 17) */ /* ------------------------------------------------------------------------ */ @@ -178,26 +193,26 @@ static void buildPages(void) s_page = 0; } s_rowN = 0; + s_rowTotal = 0; if (s_pageN > 0) { - int overflow = 0; - /* D356: the level-1 page also carries the top save-file row, so the - * content rows get one less line. */ - int cap = MAX_PROWS - 1; + /* D406: gather every visible row of the active section, then page it + * (ROWS_PER_PAGE fits the paper, see above). The old code capped at + * MAX_PROWS-1 and silently dropped the rest -- a warning, no access. + * s_pageno is re-clamped on every build so rows that auto-hide + * (aim range, auto-FOV) cannot strand the cursor past the end. */ + int tmp[MAX_SECT_ROWS]; + int tn = 0; for (int i = activeHeader() + 1; i < n && !optionsRowIsHeader(i); i++) { if (!optionsRowIsShown(i)) continue; - if (s_rowN < cap) { - s_rowIdx[s_rowN++] = i; - } else { - overflow++; - } + if (tn < MAX_SECT_ROWS) tmp[tn++] = i; } - /* D353: the old code stopped at MAX_PROWS silently; a section that - * outgrew the page (the D346b VIDEO section did, at exactly 15) lost - * rows with no trace. */ - if (overflow) - sysLogPrintf(LOG_WARNING, "frontoptions: section '%s' has %d row(s) past the page cap (%d); last visible: '%s'", - optionsRowLabel(activeHeader()), overflow, cap, - optionsRowLabel(s_rowIdx[cap - 1])); + s_rowTotal = tn; + int pages = (tn + ROWS_PER_PAGE - 1) / ROWS_PER_PAGE; + if (pages < 1) pages = 1; + if (s_pageno > pages - 1) s_pageno = pages - 1; + int off = s_pageno * ROWS_PER_PAGE; + for (int k = 0; k < ROWS_PER_PAGE && off + k < tn; k++) + s_rowIdx[s_rowN++] = tmp[off + k]; } } @@ -258,6 +273,7 @@ void frontOptionsMenuInit(void) s_level = 0; s_page = 0; + s_pageno = 0; /* D406 */ s_subHeader = -1; s_hl = -1; s_dragRow = -1; @@ -278,6 +294,7 @@ void frontOptionsMenuInit(void) for (int p = 0; p < s_pageN; p++) { if (s_pageHdr[p] == root) { s_page = p; + s_pageno = 0; s_level = depth; s_subHeader = depth > 1 ? i : -1; buildPages(); @@ -306,6 +323,7 @@ static void goBack(void) if (s_level >= 2) { int child = s_subHeader; int parent = optionsRowHeaderParent(child); + s_pageno = 0; /* D406: every fresh page view starts at page 1 */ if (parent == s_pageHdr[s_page]) { s_level = 1; s_subHeader = -1; @@ -324,6 +342,7 @@ static void goBack(void) } if (s_level == 1) { s_level = 0; + s_pageno = 0; /* D406 */ cursorToItem(s_page); sysLogPrintf(LOG_INFO, "frontoptions: back to section list"); return; @@ -374,6 +393,7 @@ void frontOptionsMenuInterface(void) playSfx(DOOR_METAL_CLOSE2_SFX); s_page = s_hl; s_level = 1; + s_pageno = 0; /* D406 */ sysLogPrintf(LOG_INFO, "frontoptions: section %d", s_page + 1); buildPages(); cursorToItem(0); @@ -396,6 +416,7 @@ void frontOptionsMenuInterface(void) optionsResetClear(); s_subHeader = child; s_level++; + s_pageno = 0; /* D406 */ buildPages(); sysLogPrintf(LOG_INFO, "frontoptions: opened %s (depth %d)", optionsRowLabel(child), s_level); @@ -510,14 +531,29 @@ void frontOptionsMenuInterface(void) int n = itemCount(); if (n > 0) { int q = (s_hl < 0 ? 0 : s_hl) + vdir; - if (q < 0) q = 0; - if (q >= n) q = n - 1; + /* D406: stepping past the end of the visible page turns + * the page (cursor lands on the first row of the next + * page, or the last row of the previous one); with one + * page the ends simply clamp, as before. */ + if (q >= n && s_pageno * ROWS_PER_PAGE + s_rowN < s_rowTotal) { + s_pageno++; + buildPages(); + q = 1; + } else if (q < 0 && s_pageno > 0) { + s_pageno--; + buildPages(); + q = itemCount() - 1; + } else { + if (q < 0) q = 0; + if (q >= n) q = n - 1; + } if (q != s_hl) { if (getenv("GE_FRONTNAVLOG")) - sysLogPrintf(LOG_INFO, "frontnav: %s -> item %d%s%s", + sysLogPrintf(LOG_INFO, "frontnav: %s -> item %d%s%s (page %d)", s_level ? optionsRowLabel(activeHeader()) : "categories", q, s_level && q > 0 ? " " : "", - s_level && q > 0 ? optionsRowLabel(s_rowIdx[q - 1]) : ""); + s_level && q > 0 ? optionsRowLabel(s_rowIdx[q - 1]) : "", + s_level ? s_pageno + 1 : 0); s_hl = q; cursorToItemRaw(q); } @@ -559,8 +595,14 @@ Gfx *frontOptionsMenuDraw(Gfx *DL) DL = ink(DL, SEC_X, rowY(k), name, INK); } } else { - char title[32]; + char title[48]; titleCase(optionsRowLabel(activeHeader()), title, sizeof(title)); + /* D406: multi-page sections mark the page in the title. */ + if (s_rowTotal > ROWS_PER_PAGE) { + int pages = (s_rowTotal + ROWS_PER_PAGE - 1) / ROWS_PER_PAGE; + snprintf(title + strlen(title), sizeof(title) - strlen(title), + " (%d/%d)", s_pageno + 1, pages); + } int titleW = measureW(title); strcat(title, "\n"); DL = ink(DL, ROW_X, TITLE_Y, title, INK); @@ -718,6 +760,19 @@ Gfx *optionsFileSelectLabel(Gfx *gdl) /* Same centring front.c uses for Copy/Erase (front.c:2779/2791). */ y = LABEL_CY - (h / 2); + /* D406: test hook -- enter the screen without a mouse click so headless + * runs (GE_INPUTSCRIPT D-pad stepping + GE_FRONTNAVLOG / diagnostics) + * can exercise it. Diagnostic-only: never active without the env. */ + if (getenv("GE_FRONTOPTIONS_AUTO") && menu_update == MENU_INVALID && + folder_selected_for_deletion < 0) { + static int autoEntered = 0; + if (!autoEntered) { + autoEntered = 1; + playSfx(DOOR_LOCK_SFX); + frontChangeMenu(MENU_PC_OPTIONS, FALSE); + } + } + hot = !optionsOverlayIsOpen() && menu_update == MENU_INVALID && folder_selected_for_deletion < 0