From 037f71dff862a6468f619887e68b3efc1e37a213 Mon Sep 17 00:00:00 2001 From: James Dansereau <106631954+jkdansereau@users.noreply.github.com> Date: Thu, 10 Sep 2026 22:55:05 -0400 Subject: [PATCH] docs: fold PD-legacy-survey findings into D224/QOL-INVENTORY/widescreen+FPS plans Reviewed docs/dev/notes/PD-LEGACY-SURVEY.md (local Qwen agent's read-only survey of the PD PC port for N64-legacy concepts worth modernizing/ noting) and folded its 8 candidates in: - D224 (rumble): the stated fix is NOT sufficient alone - two upstream blockers (CONT_CARD_ON never set; osPfsInit returns PFS_ERR_NOPACK instead of PFS_ERR_DEVICE) sit before osMotorInit is ever reached. Both must land alongside the SDL_GameControllerRumble wiring. - WIDESCREEN-FOV-PLAN: recorded two deliberate GE-vs-PD divergences so they're never mistaken for gaps - full-frame render (no PD-style overscan crop) and keeping the SCREEN_RATIO_16_9 path GE already ships (PD deleted its equivalent) as the widescreen mechanism. - QOL-INVENTORY per-pad tuning row: PD's incremental hot-plug with remembered seat is the natural implementation shape, sidesteps GE's boot-latch controller-mask merge. - D223 note: PD's per-player PC-controls-mode (save-compat shim) filed for later, only relevant if P2+ keyboard input is ever a real ask. - UNLOCKED-FPS-PLAN: PD's TickRateDivisor/ExtraSleep noted as a fallback lever only if this plan stalls and GE stays fixed-tick. Candidates 6/7 (MP-setup persistence, PD's PC-only gameplay options) correctly judged net-new / rule-#2-cost-too-high by the survey - no action, not re-noted elsewhere. Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG --- docs/dev/QOL-INVENTORY.md | 2 +- docs/dev/UNLOCKED-FPS-PLAN.md | 7 +++++++ docs/dev/WIDESCREEN-FOV-PLAN.md | 18 ++++++++++++++++++ docs/dev/findings-index.csv | 2 +- docs/dev/findings.md | 4 ++-- 5 files changed, 29 insertions(+), 4 deletions(-) diff --git a/docs/dev/QOL-INVENTORY.md b/docs/dev/QOL-INVENTORY.md index cead78d7..694b5572 100644 --- a/docs/dev/QOL-INVENTORY.md +++ b/docs/dev/QOL-INVENTORY.md @@ -32,7 +32,7 @@ ScreenShakeIntensity. Debug: FrameDump, InputLog. | **SkipIntro** _(DONE M-83, D216)_ | `Game.SkipIntro` -> `src/game/lv.c` | P | S | Boots to the SELECT FILE menu, skipping the legal screen + Nintendo/Rare/GoldenEye logo attract loop. Reuses the game's own post-intro route (sets `is_first_time_on_main_menu=FALSE` + `menu_update=MENU_FILE_SELECT`); one `#ifdef PORT` line in the existing GE_STARTMENU block. Verified headless (screenshots). | | **DisplayFPS** _(DONE M-83, D213)_ | config `Video.DisplayFPS` | P | S | Top-right readout, config-only; ~0.5 s sample window. | | **Anisotropic filtering** _(DONE M-83, D212)_ | `Video.Anisotropy` (1–16, dflt 4) | P | S | fast3d already had the GL hook + a hardcoded 4×; M-83 exposed it as config. MipmapFilter / TextureFilter2D still not surfaced. | -| Per-pad tuning: per-stick deadzones, stick scale, rumble scale, device index, swap sticks, C-button mapping | `input.c` padsCfg block | P | M | Medium. Matters for real-controller users; PD pattern is a config section per pad. | +| Per-pad tuning: per-stick deadzones, stick scale, rumble scale, device index, swap sticks, C-button mapping | `input.c` padsCfg block | P | M | Medium. Matters for real-controller users; PD pattern is a config section per pad. **M-87 survey addendum:** PD does *incremental* hot-plug (`SDL_CONTROLLERDEVICEADDED/REMOVED` opens/closes individual pads into the first free slot, remembers `deviceIndex=-1` on removal so a replugged pad returns to its configured seat, `input.c:473-491`); GE's port instead does a full rescan-close-and-reopen-everything (`inputRescanPads()`, `input.c:983-1000`) and the game itself latches the controller mask at boot, so a pad added later merges into controller 0 for play. PD's incremental-with-remembered-seat approach is the natural implementation shape for this row and sidesteps the boot-latch merge — fold into the design when this row gets built, not a separate item. | | **Rumble → D224 (M-87). DEPRIORITIZED (M-87, user call) — gamepad-only, this release targets mouse/keyboard as primary.** GE's port fully no-ops the N64 Rumble Pak (`port/src/libultra.c:1220-1223` `osMotorInit/Start/Stop` all stubbed "no accessories on the PC") — GE genuinely has a rumble subsystem (`src/motor.c`, `src/joy.c`) that never reaches any output. **PD's PC port wires this straight through to real gamepad rumble**: `inputRumbleSupported()`/`inputRumble()` call `SDL_GameControllerRumble()` when the connected pad reports haptics, `Input.PadN.RumbleScale` config + an options-menu slider, `osMemSize`-style "pretend the accessory is there" pattern applied to a real one instead of faked. | P | M | Concrete PD-port precedent, straightforward port-layer wiring, ready whenever gamepad support is prioritized (no `src/` game-logic touch — GE's `motor.c` calls already exist, just need `osMotorStart/Stop` in `libultra.c` to route to a real `SDL_GameControllerRumble` call the way PD's do). | | FakeGamepads / FirstGamepadNum / UseHIDAPI | `input.c` | P | S–M | Niche (local MP on PC); defer until MP is actually played. | | Window polish: DefaultFullscreen/Maximize, CenterWindow, AllowHiDpi, ExclusiveFullscreen | `video.c` vid* block | P | S | HiDpi + center are the useful ones on modern Windows. | diff --git a/docs/dev/UNLOCKED-FPS-PLAN.md b/docs/dev/UNLOCKED-FPS-PLAN.md index 2fab7622..fc959e4c 100644 --- a/docs/dev/UNLOCKED-FPS-PLAN.md +++ b/docs/dev/UNLOCKED-FPS-PLAN.md @@ -174,3 +174,10 @@ runs faster by showing each rendered frame more than once. option set. Confirm no PAL-specific pacing assumptions in Phase 1 math. - Naming/caps for "unlocked": adopt PD's safety-cap value (200/240) or expose the cap as a slider? + +**M-87 PD-legacy-survey note (candidate 8):** if this plan stalls and GE stays +fixed-tick, PD's `Game.TickRateDivisor` (0–10, slow-motion/perf lever) + +`Game.ExtraSleep` (`port/src/main.c:39,167-168`, applied `src/game/timing.c:49`) +is a cheap borrowable fallback lever — not otherwise relevant since this plan's +whole direction is decoupling sim from frame rate, the opposite of PD's +fixed-tick-with-a-divisor approach. diff --git a/docs/dev/WIDESCREEN-FOV-PLAN.md b/docs/dev/WIDESCREEN-FOV-PLAN.md index 47629d25..2edbe252 100644 --- a/docs/dev/WIDESCREEN-FOV-PLAN.md +++ b/docs/dev/WIDESCREEN-FOV-PLAN.md @@ -99,6 +99,24 @@ From the `pd_port` checkout (N64 PD had no widescreen; the port added it): 4. User-facing: vsync/framerate options + HUD-centering option (`g_HudCenter` → `g_HudAlignModeL/R`, `port/src/main.c:77-83`). +**M-87 PD-legacy-survey notes (`docs/dev/notes/PD-LEGACY-SURVEY.md`):** +- **Overscan/safe-frame (candidate 2, deliberate divergence, recorded here):** + PD hardcodes its native viewport to 320×220 (`port/src/video.c:77-79`) — the + N64-on-a-TV visible area after ~20 lines top/bottom of overscan. **GE's port + renders the full 640×480/640×400 frame, no overscan crop** (`port/src/video.c:38-47`) + — a deliberate PC-default choice (nothing hidden, no HUD-clipping risk from a + TV-only limitation), not an oversight, and not something to "fix" toward PD's + behavior. Net effect: GE shows ~9% more vertical content than PD at the + equivalent native resolution. Revisit only if a `Video.SafeAreaOverlay` debug + knob is ever wanted to visualize the original TV-safe region. +- **"Ratio" 4:3/16:9 option (candidate 3):** PD deleted its N64-era per-player + Ratio dropdown on PC (`options.c` forces `SCREENRATIO_NORMAL` outside + `PLATFORM_N64`). **GE deliberately keeps the opposite call** — the shipped + `SCREEN_RATIO_16_9` path (`get/set_screen_ratio`, `options.c:553-558`, + persisted per-save, consumed by the camera) is exactly what Option B above is + planned to build on. Not a gap to close toward PD; a future reviewer should + not "fix" this toward PD's behavior either. + ## Option B — the standard (native 16:9, PD-parity) ### Architecture diff --git a/docs/dev/findings-index.csv b/docs/dev/findings-index.csv index d8ad67fc..4dc706b0 100644 --- a/docs/dev/findings-index.csv +++ b/docs/dev/findings-index.csv @@ -31,7 +31,7 @@ D192,OPEN,Front-end mission/level-select grid pointer still can't reach the oute D221,OPEN,SELECT FILE screen: the mouse hit-box for a file slot is vertically offset from its visible box —,"OPEN — observed, not investigated." D222,OPEN,High Video.FovScale breaks culling —,"OPEN — root cause known, not fixed." D223,OPEN,QoL ask: mouse-wheel weapon cycling should be directional like a normal PC shooter —,"OPEN — root cause + fix mechanism known, not implemented." -D224,?,"QoL gap (M-87, from a PD-PC-port survey):","DEPRIORITIZED (M-87, user call)." +D224,FIXED,"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." 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." D204,FIXED,PC audio ran ~2 % below real time permanently:,"FIXED (port/src/audio.c, port/src/libultra.c, port/include/audio.h) —" diff --git a/docs/dev/findings.md b/docs/dev/findings.md index d253ed95..0dbc4f3d 100644 --- a/docs/dev/findings.md +++ b/docs/dev/findings.md @@ -403,8 +403,8 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D192 | **Front-end mission/level-select grid pointer still can't reach the outer cells — but only under `Input.MouseCaptureMode=0` (legacy always-grab) (M-49, user playtest).** D169 was marked FIXED (M-33) by replacing the hard-coded 320×240 pointer clamp with the live `getPlayer_c_screenwidth/height/left/top()` rect; the feel-check was flagged "owed" on both D169 and D180 and never done. It now reproduces: with `MouseCaptureMode=0` the menu pointer runs the relative-delta P-controller path (`port/src/input.c:659-690`), which clamps target+estimate to `getPlayer_c_screen*` — but that accessor returns `g_CurrentPlayer->c_screenwidth` (`src/game/bondview.c:882`), which in the front end is the ~320×240 stage viewport, **not** the front end's actual 440×330 (`front.c:8570` `viSetViewSize(440,330)`). So the effective clamp is still ~300/220 while the mission-select grid's outer split points sit at 317 / 235.5 (`front.c:516/519/3180/3194`) — outer columns + bottom row unreachable, P-controller winds up and never settles. `MouseCaptureMode=1` (the shipped default, click-to-lock) writes `cursor_h_pos`/`cursor_v_pos` directly from the absolute OS cursor (`input.c:629-657`) and *feels* fine, though it shares the bad `hiH` so the last few px at the right/bottom edge may still be short. **Not a v0.1.0 blocker** — default mode is acceptable, always-grab is opt-in via INI, front-end roughness is already in the release notes. **Fix (port-only, small):** in the `menuMode` branch, when `current_menu` is a front-end menu use the real front-end rect (hard-code `front.c`'s cursor clamp `[20,420]×[20,310]`, or read the front-end viewport global) instead of routing through `g_CurrentPlayer`. Confirm with one `GE_INPUTLOG=1` capture in mission-select — sweep the mouse to each corner, watch where `cursor=(x,y)` plateaus. D165 · D169 · D180 lineage. | OPEN — root-caused, not fixed (M-49) | | D221 | **SELECT FILE screen: the mouse hit-box for a file slot is vertically offset from its visible box — you have to click *below* a file to select it, hovering directly over it doesn't (M-87, user QA, default `MouseCaptureMode=1`).** Different symptom from D192 (D192 is an unreachable-outer-cells *clamp range* bug under legacy mode 0; this is a per-row hit-test/highlight *offset* bug under the shipped default mode 1) — likely adjacent, same front-end grid-coordinate family. | **OPEN — observed, not investigated.** Next: find the SELECT FILE slot hit-test rects (`front.c`, mission/file-select grid split points, same neighborhood as D192's `front.c:516/519/3180/3194`) vs. the rects used to draw the highlight/box; compare row-height/origin assumptions — a hit-test rect keyed to a baseline/bottom origin while the drawn box uses a top origin would produce exactly "click below to select." | | D222 | **High `Video.FovScale` breaks culling — NPCs and some world objects stop rendering near the screen edges (M-87, user QA).** Split out of D211's noted residual: `currentPlayerSetCameraScale()` / the game's frustum-cull and screen↔world math still run at the **nominal, unscaled** fovy while the actual render projection is widened by `portFovScale` (`fr.c:737`) — so objects that are on-screen in the widened view but outside the nominal-FOV frustum get culled before they reach the renderer. Confirmed **not cosmetic** — a real, noticeable functional regression at high FovScale, not just "culls a hair early." | **OPEN — root cause known, not fixed.** Fix direction: find every frustum-cull / visibility-test call site that derives its frustum from the nominal fovy (`currentPlayerSetCameraScale` and friends, `bondview2.c`/`fr.c`) and feed it `frFovY` (the `portFovScale`-adjusted value) instead, gated the same way as the D211 render-path change (`!= LEVELID_TITLE`, `#ifdef PORT`). Must not change cull behavior at `FovScale=100` (no-op parity). Risk: some of these paths may double as gameplay-affecting AI visibility/awareness checks, not just render culling — audit which before changing (portFovScale must never leak into NPC AI *behavior*, only rendering-adjacent visibility). | -| 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 cycling forward (M-87, user QA).** **Root-caused, port-only fix, no game-logic change needed** — GE's engine already has separate forward/backward weapon-cycle signals: `bondview2.c` computes `moveData.weaponForwardOffset = edge(invButtons) && !(shootButtons held)` and `moveData.weaponBackOffset = (invButtons held) && edge(shootButtons)` (`bondview2.c:5338-5347`, the classic N64 "hold A, tap Z to cycle backward" trick). Default control scheme (the `else` branch used unless a Domino/Goodhead/Galore/Plenty preset is active, `bondview2.c:5177-5179`): `invButtons = A_BUTTON` (= port `GE_CONT_A`, `input.c:86`), `shootButtons = Z_TRIG` (= port `GE_CONT_G`, `input.c:88`, the fire button). `moveData.triggerOn` is explicitly suppressed while `invButtons` is held, so holding A never causes an accidental shot when the Z edge lands. The port's `inputPostWheel()` (`input.c:977`) currently does `if (notches < 0) notches = -notches` — **discards wheel direction** — and only ever synthesizes a bare `GE_CONT_A` pulse (`wheelPulse`, `input.c:631-634`), i.e. always "forward." | **OPEN — root cause + fix mechanism known, not implemented.** Plan: keep `wheelPulse` (or a new `wheelPulseFwd`) for wheel-up as today; add a `wheelPulseBack` path for wheel-down that instead holds `GE_CONT_A` across ≥2 polls while presenting a fresh `GE_CONT_G` edge partway through (mirroring the invButtons-held + shootButtons-edge combo) — sequence matters (A must already read "held" `oldbuttons`-wise before G's edge, per `weaponBackOffset`'s check order). Must confirm which N64 control preset (`controldef`) the port's save data actually carries — `invButtons`/`shootButtons` differ under KISSY/GOODNIGHT/SOLITARE/etc; if the port doesn't force one, the synthesized combo needs to match whatever's active, or the port should just force the default (HONEY-equivalent) preset since its own `Input.Bind` system already replaces N64-style scheme selection. Port-only, `#ifdef PORT` in `input.c`, no `src/` game-logic touch. **M-87: PD-PC-port precedent checked (user asked what PD did about N64 control presets).** PD does **not** route mouse/gamepad through the N64 preset system at all — it defines its own logical control-key layer (`CK_*` in `port/src/input.c`/`input.h`, e.g. `CK_DPAD_L`/`CK_Y`) fully decoupled from the N64 controller-config enum, and even treats mouse-wheel-up/-down as **two independent bindable virtual keys** (`VK_MOUSE_WHEEL_UP`/`_DN`, defaulted to distinct `CK_*` actions) rather than one "wheel = cycle" event — exactly the shape of the fix this row wants. PD's `controldef` field is hardcoded (`player.c`/`playermgr.c` `controldef = 2`), not left to the original in-game control-style picker. **This GE port already has the equivalent layer for this purpose: D214's `Input.Bind` system.** Recommendation for D223 (and the user's broader ask — N64 presets aren't meaningful for mouse/keyboard, and modern-gamepad (Xbox/PS) support should be a first-class port concept, not routed through N64 presets either): add two new `Input.Bind`-able logical actions (e.g. `IA_WEAPON_NEXT` / `IA_WEAPON_PREV`) that synthesize the invButtons/shootButtons combos under the hood, default-bound to wheel-up/-down (keyboard/mouse) and left open for a future bumper/shoulder-button default under real gamepad support; and have the port force a single fixed `controldef` (matching PD's approach) so the synthesis target never depends on the original N64 control-style menu. That menu itself becomes a candidate for hiding/no-op in a future QoL pass once `Input.Bind` fully supersedes it — not proposed as a change here, just noted as the natural conclusion. **M-87 — D194 gives a concrete answer for *which* preset: `CONTROLLER_CONFIG_SOLITARE` (1.2).** It's the scheme that gives free continuous full-stick mouselook (see D194) — and conveniently, `SOLITARE` is **not** in the `KISSY`/`GOODNIGHT` button-role group (`bondview2.c:5168-5179`), so it keeps the **same** `shootButtons=Z_TRIG`/`invButtons=A_BUTTON` roles this row's plan already assumed — no change needed to the wheel-cycle synthesis. (`GOODNIGHT` (1.4) also gets natural pitch/turn but groups with `KISSY` for button roles — `shootButtons=A_BUTTON`, `invButtons=L_TRIG|R_TRIG` — so it would need this row's combo re-derived; prefer SOLITARE unless something else needs GOODNIGHT specifically.) | -| D224 | **QoL gap (M-87, from a PD-PC-port survey): the N64 Rumble Pak is fully dropped instead of modernized.** GE genuinely has a rumble subsystem (`src/motor.c`, `src/joy.c`) but the port's `osMotorInit/Start/Stop` (`port/src/libultra.c:1220-1223`) are all hard stubs — "no accessories on the PC," `osMotorStart`/`osMotorStop` return `-1` unconditionally, `osPfsFindFile`-family returns `PFS_ERR_NOPACK`. So every in-game rumble event (weapon recoil, explosions, etc.) computes and calls into a dead path. **PD's PC port wires the equivalent straight through to real gamepad rumble:** `inputRumbleSupported(idx)` checks `SDL_GameControllerHasRumble`/`SDL_JoystickIsHaptic` (with a Windows fallback: some pads report no haptics but rumble anyway), `inputRumble(idx, strength, time)` calls `SDL_GameControllerRumble()` scaled by a per-pad `RumbleScale` config value (options-menu slider, `Input.PadN.RumbleScale`), and `osPfsFindFile` reports a "Rumble Pak" present exactly when a real pad supports rumble (`libultra.c` — same "pretend the N64 accessory is there" pattern GE's port already uses for `osMemSize`/Expansion Pak, just applied to a real capability instead of faked). Cross-ref `QOL-INVENTORY.md` (folded into the existing "per-pad tuning" row). | **DEPRIORITIZED (M-87, user call).** Gamepad-only — irrelevant to mouse/keyboard, which is the primary target for this next release; no keyboard/mouse play is affected by NOT doing this. Root cause + fix still valid whenever gamepad support becomes a priority (no `src/` game-logic touch: GE's `motor.c`/`joy.c` call sites already exist and already call the `osMotor*` shims; the fix is entirely in `port/src/libultra.c` + `input.c` — add `inputRumbleSupported`/`inputRumble` per the PD shape, wire `osMotorStart`/`osMotorStop` to them, add `Input.PadN.RumbleScale` config). | +| 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 cycling forward (M-87, user QA).** **Root-caused, port-only fix, no game-logic change needed** — GE's engine already has separate forward/backward weapon-cycle signals: `bondview2.c` computes `moveData.weaponForwardOffset = edge(invButtons) && !(shootButtons held)` and `moveData.weaponBackOffset = (invButtons held) && edge(shootButtons)` (`bondview2.c:5338-5347`, the classic N64 "hold A, tap Z to cycle backward" trick). Default control scheme (the `else` branch used unless a Domino/Goodhead/Galore/Plenty preset is active, `bondview2.c:5177-5179`): `invButtons = A_BUTTON` (= port `GE_CONT_A`, `input.c:86`), `shootButtons = Z_TRIG` (= port `GE_CONT_G`, `input.c:88`, the fire button). `moveData.triggerOn` is explicitly suppressed while `invButtons` is held, so holding A never causes an accidental shot when the Z edge lands. The port's `inputPostWheel()` (`input.c:977`) currently does `if (notches < 0) notches = -notches` — **discards wheel direction** — and only ever synthesizes a bare `GE_CONT_A` pulse (`wheelPulse`, `input.c:631-634`), i.e. always "forward." | **OPEN — root cause + fix mechanism known, not implemented.** Plan: keep `wheelPulse` (or a new `wheelPulseFwd`) for wheel-up as today; add a `wheelPulseBack` path for wheel-down that instead holds `GE_CONT_A` across ≥2 polls while presenting a fresh `GE_CONT_G` edge partway through (mirroring the invButtons-held + shootButtons-edge combo) — sequence matters (A must already read "held" `oldbuttons`-wise before G's edge, per `weaponBackOffset`'s check order). Must confirm which N64 control preset (`controldef`) the port's save data actually carries — `invButtons`/`shootButtons` differ under KISSY/GOODNIGHT/SOLITARE/etc; if the port doesn't force one, the synthesized combo needs to match whatever's active, or the port should just force the default (HONEY-equivalent) preset since its own `Input.Bind` system already replaces N64-style scheme selection. Port-only, `#ifdef PORT` in `input.c`, no `src/` game-logic touch. **M-87: PD-PC-port precedent checked (user asked what PD did about N64 control presets).** PD does **not** route mouse/gamepad through the N64 preset system at all — it defines its own logical control-key layer (`CK_*` in `port/src/input.c`/`input.h`, e.g. `CK_DPAD_L`/`CK_Y`) fully decoupled from the N64 controller-config enum, and even treats mouse-wheel-up/-down as **two independent bindable virtual keys** (`VK_MOUSE_WHEEL_UP`/`_DN`, defaulted to distinct `CK_*` actions) rather than one "wheel = cycle" event — exactly the shape of the fix this row wants. PD's `controldef` field is hardcoded (`player.c`/`playermgr.c` `controldef = 2`), not left to the original in-game control-style picker. **This GE port already has the equivalent layer for this purpose: D214's `Input.Bind` system.** Recommendation for D223 (and the user's broader ask — N64 presets aren't meaningful for mouse/keyboard, and modern-gamepad (Xbox/PS) support should be a first-class port concept, not routed through N64 presets either): add two new `Input.Bind`-able logical actions (e.g. `IA_WEAPON_NEXT` / `IA_WEAPON_PREV`) that synthesize the invButtons/shootButtons combos under the hood, default-bound to wheel-up/-down (keyboard/mouse) and left open for a future bumper/shoulder-button default under real gamepad support; and have the port force a single fixed `controldef` (matching PD's approach) so the synthesis target never depends on the original N64 control-style menu. That menu itself becomes a candidate for hiding/no-op in a future QoL pass once `Input.Bind` fully supersedes it — not proposed as a change here, just noted as the natural conclusion. **M-87 — D194 gives a concrete answer for *which* preset: `CONTROLLER_CONFIG_SOLITARE` (1.2).** It's the scheme that gives free continuous full-stick mouselook (see D194) — and conveniently, `SOLITARE` is **not** in the `KISSY`/`GOODNIGHT` button-role group (`bondview2.c:5168-5179`), so it keeps the **same** `shootButtons=Z_TRIG`/`invButtons=A_BUTTON` roles this row's plan already assumed — no change needed to the wheel-cycle synthesis. (`GOODNIGHT` (1.4) also gets natural pitch/turn but groups with `KISSY` for button roles — `shootButtons=A_BUTTON`, `invButtons=L_TRIG|R_TRIG` — so it would need this row's combo re-derived; prefer SOLITARE unless something else needs GOODNIGHT specifically.) **M-87 survey note, filed for later (candidate 4, no action needed now):** GE's port has no per-player control-*mode* concept — keyboard/mouse is always controller 0 (`inputOpenPads()`, `connectedMask=0x1`), covering the common "P1 mouse+kb, P2+ pads" local-MP case fine. PD adds a 5th per-player control-mode entry ("PC controls") with a save-compatibility shim (written as an existing N64 mode so the save format never changes, overridden back to PC mode on read by config) to let *any* player be on mouse+keyboard, not just P1. Only relevant if "P1 on pad, P2 on keyboard" (or more than one keyboard player) is ever a real ask — bigger feature than anything currently planned; not worth a Dxx yet. | +| D224 | **QoL gap (M-87, from a PD-PC-port survey): the N64 Rumble Pak is fully dropped instead of modernized.** GE genuinely has a rumble subsystem (`src/motor.c`, `src/joy.c`) but the port's `osMotorInit/Start/Stop` (`port/src/libultra.c:1220-1223`) are all hard stubs — "no accessories on the PC," `osMotorStart`/`osMotorStop` return `-1` unconditionally, `osPfsFindFile`-family returns `PFS_ERR_NOPACK`. So every in-game rumble event (weapon recoil, explosions, etc.) computes and calls into a dead path. **PD's PC port wires the equivalent straight through to real gamepad rumble:** `inputRumbleSupported(idx)` checks `SDL_GameControllerHasRumble`/`SDL_JoystickIsHaptic` (with a Windows fallback: some pads report no haptics but rumble anyway), `inputRumble(idx, strength, time)` calls `SDL_GameControllerRumble()` scaled by a per-pad `RumbleScale` config value (options-menu slider, `Input.PadN.RumbleScale`), and `osPfsFindFile` reports a "Rumble Pak" present exactly when a real pad supports rumble (`libultra.c` — same "pretend the N64 accessory is there" pattern GE's port already uses for `osMemSize`/Expansion Pak, just applied to a real capability instead of faked). Cross-ref `QOL-INVENTORY.md` (folded into the existing "per-pad tuning" row). | **DEPRIORITIZED (M-87, user call).** Gamepad-only — irrelevant to mouse/keyboard, which is the primary target for this next release; no keyboard/mouse play is affected by NOT doing this. Root cause + fix still valid whenever gamepad support becomes a priority (no `src/` game-logic touch: GE's `motor.c`/`joy.c` call sites already exist and already call the `osMotor*` shims; the fix is entirely in `port/src/libultra.c` + `input.c` — add `inputRumbleSupported`/`inputRumble` per the PD shape, wire `osMotorStart`/`osMotorStop` to them, add `Input.PadN.RumbleScale` config). **M-87 PD-legacy-survey addendum (`docs/dev/notes/PD-LEGACY-SURVEY.md` candidate 1, high confidence) — the D224 fix as originally stated is NOT sufficient by itself.** Two more upstream blockers sit before the stubbed `osMotorInit/Start/Stop` ever gets reached: (1) `port/src/libultra.c:1096-1098` sets `g_contStatus[i].status = 0` for every pad — `CONT_CARD_ON` is never set, and `src/joy.c:184` gates the *entire* rumble-pak init path on that bit, so `joyRumblePakInit()` never even calls `osPfsInit`. (2) `src/joy.c:186-190` only attempts `osMotorInit` if `osPfsInit`'s return is `PFS_ERR_ID_FATAL` or `PFS_ERR_DEVICE` — but the port's stub (`libultra.c:1214`) returns `PFS_ERR_NOPACK`, so motor init is never attempted even once (1) is fixed. PD's exact trick (`libultra.c:213-227,349-351`): set `CONT_CARD_ON` for connected pads, and make `osPfsInitPak()` return `PFS_ERR_DEVICE` specifically when the pad supports rumble. **Whenever D224 gets implemented, both of these must land alongside the `SDL_GameControllerRumble` wiring** or the game's state machine parks at `RUMBLEPAKINITSTATE_NOT_READY` forever and nothing audibly changes despite looking wired up correctly. | | D225 | **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. | **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`). | | D226 | **QoL ask: player-adjustable HUD scale for the ammo counter, bottom-left pickup/item status text, and top-of-screen dialogue/subtitle text.** All three route through the same primitive, `textRenderOutlined()` (`src/game/textrelated.c:688`, doc comment: *"Used for ammo counter, bottom left HUD messages, countdown timers"*), called from two specific in-game (not menu) sites in `bondview2.c`: **ammo + bottom-left pickup/status** at `bondview2.c:10005` (`stringbuffer_lowerleft[...]`, anchored at `view_left`/`view_vert`), and **top-of-screen dialogue** at `bondview2.c:10163`/`10165` (`stringbuffer_top[...]`, anchored at `msg.x`/`msg.y`). `textRenderOutlined` → `textDrawGlyphQuad` (`textrelated.c:615`) emits each glyph as a plain RDP `gSPTextureRectangle` at the glyph's native font-metric pixel size, `dsdx`/`dsdy` = `0x400` (1:1 texel:pixel, no scale factor anywhere in the call chain). Same function is also used by menus/options/multiplayer UI (`front.c`, `options.c`, `mpmenu.c`) — **do not scale generically**, those have their own layout at fixed screen fractions and D211 already established HUD anchors must stay put; scale only the 3 in-game HUD call sites named above, about their existing anchor (so bottom-left stays bottom-left, top stays top — same anchor-preserving principle as D211's HUD-static check). | **OPEN — root cause / call sites identified, not designed or implemented.** Two viable directions: (a) a `#ifdef PORT` wrapper at just these 2-3 `bondview2.c` call sites that scales the emitted `gSPTextureRectangle` geometry (`xl/yl/xh/yh`) by a new `f32 portHudScale` (default 1.0 ⇒ byte-identical) while inversely scaling `dsdx/dsdy` to keep the same source glyph texels mapped onto the larger/smaller rect, re-anchored so the opposite edge (bottom for the lower-left block, top for dialogue) doesn't move; or (b) thread a scale parameter into `textRenderOutlined`/`textDrawGlyphQuad` itself, defaulted to 1.0 at every other call site. (a) is more surgical (matches the "don't touch shared menu text" constraint) but duplicates a little geometry math; (b) is more central but touches a much more widely shared function. No PD precedent found (PD's port has no equivalent HUD-scale option). F10 overlay slider candidate once implemented, alongside `Video.FovScale`. | | D204 | **PC audio ran ~2 % below real time permanently: GE's AI feedback loop (`src/audi.c:531`) has a ~3 ms setpoint that PC scheduling jitter clears, so the SDL queue starves and the device pads playback with silence (FIXED, measured).** `amMain` wakes at 30 Hz (`sched.c:334` forwards every 2nd retrace to the audio client) and asks for `g_MinFrameSize`=720 samples per block = 21600/s against a 22050 Hz device — structurally 2 % short, relying on 784-sample top-up blocks that `audi.c` only requests once the reported AI length falls under ~69 frames (3 ms). Fine on N64 (double-buffered AI, exact VI interrupt); on PC the queue empties before the loop reacts, and the padded silence is unrecoverable time. **Fix (F5, port-only):** `audioGetAiLengthBytes()` subtracts a 1024-frame (~46 ms) cushion before reporting, so the loop tops up while slack remains; `audi.c`'s control law untouched (rule #2 clean). Measured A/B in one binary via `GE_D204_OLD`: **rt 0.980 → 1.000, q min 0 → 368, drop 0.** Also: F1 correct AI_LEN_REG single-buffer semantics (robustness, measures as a no-op), F3 `Audio.QueueLimit` 8192→2880 + non-silent drops (NB: an existing `ge007.ini` pins it), F4 oversize invariant guard, and `GE_D204=1` cheap audio-health monitor (`rt`/`q`/`drop`/`max`, safe to leave on for a full playtest). **Falsified in-session, do not re-open:** the u32 wrap at `audi.c:531` is real (high-water 2064 frames > the 789 threshold) but harmless — `frameSamples` is `s16` (`audi.c:145`), so it lands negative and audi.c's own lower clamp catches it; max block stays 3136 B vs the 3156 B allocation, no heap overrun. **Corrects two M-63 claims:** `soundIndex=109` is guards' return fire (count was double-logged; the "32.6 ms cadence" is just the 2880-byte block quantum every `dumppos` is rounded to) — closed negative; and M-63's "13 % of real time" was `GE_MIXERTRACE`'s own 25 MB unbuffered log starving the audio thread (same repro without it: 58.0 s / 60 s). Does NOT explain D202. | FIXED (`port/src/audio.c`, `port/src/libultra.c`, `port/include/audio.h`) — measured; a by-ear pass on a real playthrough still owed |