checkpoint: F10 GE-style refresh + distance defaults retune, D360-D370 series (local WIP; D370 pending user acceptance) — pre-wave baseline for the v0.4.0 modern-options wave

This commit is contained in:
James Dansereau committed 2026-09-27 16:18:20 -04:00
1 parent d8971fded0
commit c2c6fbdd68
10 files changed
+1048 -278

No files matched your search

+129
View File
@@ -0,0 +1,129 @@
# F10 visual refresh — GoldenEye dossier styling (proposal)
Status: **superseded by D363 after user playtest** — the dossier/paper
concept below was implemented as D362, then replaced with a dark
watch-green/Bank Gothic version. This is the historical proposal for a
port-layer F10 overlay proposal, not a replacement for GE's pause watch or the
front-end PC Options screen. Reference surfaces: `src/game/front.c`
`constructor_menu15_cheat`/`frontSetupMenuBackground`, the existing
`port/src/frontoptions.c` cheat-list-inspired options screen, and GE's in-stage
watch. Current overlay: `port/src/optionsoverlay.c` `optionsOverlayEmit`,
`overlayRowAtY`, `sliderBarSpan`, `maxVisibleRows`.
## 1. Direction and boundaries
Make F10 read as an **MI6 dossier laid over the game scene**, not a generic
SDL debug panel: dark translucent scrim, warm paper/ivory content card,
black Zurich-style menu text, red for active options/slider fills and an
occasional muted gold title/accent. The front-end PC Options already uses
GE's actual 3D folder and Zurich text; use it as the visual reference, not
as a display-list dependency. In-stage F10 must remain an inexpensive flat
2D overlay: no loading a 3D wallet model, new ROM assets, front-end init
functions or `src/game` modifications. Preserve the current two-level
category -> settings navigation and the shared settings/commit API.
The initial port-only implementation lives in `optionsOverlayEmit()` and
uses one shared `OvLayout` for both 320x240 stage and 440x330 front canvases.
Headless captures live under `scratch/d362-test/` (not distributed). Manual
visual review on a bright stage and controller/mouse feel-check remain owed.
## 2. Concrete visual hierarchy
- **Backdrop/card:** keep an adjustable translucent dim over the scene;
replace the opaque blue-black rectangle with a near-opaque ivory panel
(subtle border and one offset shadow). Do not mimic the actual folder
texture unless a later, measured prototype proves the simple card
insufficient. Preserve legible contrast against both dark and bright
levels, including safe-area crop and widescreen pillarboxes.
- **Masthead:** small gold/black `MI6 / OPTIONS` or `PC OPTIONS` heading,
fine black rule (a restrained `CLASSIFIED` stamp can be added later if
measured text fits) rather than a blue title/instruction block. Keep the
category name on the **same
page above its first setting**, especially INPUT over Mouse sensitivity.
- **Root:** numbered `1. Input` ... `5. Video`, GE's dossier/menu numbering
and title case as on `frontoptions.c`; a narrow translucent dark selection
box behind the focused item. Category order and labels stay unchanged in
the shared row table; casing is presentation-only.
- **Section:** a compact `PREVIOUS`/Back tab, title and optional
`(Profile N)` annotation, then two columns: label at left, value at right.
Unselected values in black, active/toggled values in GE's dark red
(`0xA00000FF` is the cheat-screen reference). Slider tracks are thin
charcoal with a red fill and an obvious knob; show the numerical value
outside the bar. A mixed-scope `(per profile)` marker must remain legible
without colliding with the value column; measure it and, if necessary,
use a small second line rather than silently removing the distinction.
- **Actions:** Reset retains its two-press/three-second confirmation and
displays `CONFIRM` in red only while armed. Quit remains visually distinct
from resets (and retains its existing activation semantics). Back/Close
affordances should match the actual click targets, not become decoration.
- **Footer:** one quiet, short line for mouse/arrows/controller, with an
explicit `ESC: Back / F10: Close` cue. No newly invented glyphs or fonts
that may be missing from the game's text bank. FPS readout is independent.
Suggested layout sketch (not pixel positions):
```
MI6 / PC OPTIONS CLOSE
-----------------------------------------------
PREVIOUS GRAPHICS
Anti-aliasing 2x
Texture filter Bilinear
Draw distance [------|----] 50/100
...
Reset to defaults [ENTER]
ESC Back / F10 Close
```
## 3. Implementation boundaries
1. Extract a port-only layout helper for fonts, palette,
panel rectangles and spacing; preserve `rows[]`, `rowAdjust`, persistence,
file scoping, reset logic and input bindings. Use GE's loaded Zurich Bold
font for body *only after* validating it exists on both stage and front
contexts; Bank Gothic can remain a fallback/header font. Text clipping
must use the whole viewport as `textRender` expects.
2. Replace only the fill/text passes in `optionsOverlayEmit()` first. Emit
fills before text as currently, reset GBI state explicitly and keep
`OV_BUF_CMDS`/DL size bounds. Avoid mutable texture uploads or new assets.
3. One shared geometry calculation must drive panel/card positions,
`maxVisibleRows`, row Y, slider-bar span, close/back hitboxes, and
`overlayRowAtY`. In particular, don't change visual row spacing without
updating hit-testing (D304/D316), and don't use naive OS-window pixel
mapping instead of `gfx_get_ui_screen_rect` (D316/D335). When a page
scrolls, keep its section title visible; a clipped row cannot be clicked.
4. Use measured text widths to reserve space for values, `Confirm`,
`(Profile N)` and restart markers. At 320x240, allow scrolling/short
contextual hints, not overlapping text or tiny glyphs. At 440x330 and
ultrawide, centre or constrain the card rather than stretching the
columns across the entire viewport.
5. Make any GE-like UI sound effects opt-in to a *separate* follow-up: F10
runs in controller polling, and D361 prohibits game save/audio calls
from that thread. A cosmetic refresh must not reintroduce that lockup.
## 4. Acceptance gates
- Compare screenshots with the existing front Options/cheat dossier and the
in-stage watch; user reviews the live palette/layout before closing the task.
- Native 320x240, front-end 440x330, windowed 4:3, 16:9 and 21:9,
fullscreen, safe-area crop on/off: INPUT title visible; no text clipping,
unreadable values, wrong hitboxes, phantom/duplicated bottom rows or
overflow in the display list. Check graphics's longest labels and AUDIO's
per-profile markers with and without a valid file.
- Mouse click/drag/wheel, arrows and D-pad/stick, ESC back, F10/Start/X
close, focused/armed reset, and quit action all behave **exactly** as
before. F10 Audio on file select still commits via D361's game-thread
queue; test both fresh and existing EEPROM without altering the user's
save (backup/restore for probes).
- With overlay closed, append no panel DL and retain existing golden frame
hashes (unless FPS display is explicitly enabled). Run `./build-pc.sh
ntsc-final`, link/duplicate-symbol checks, front-end + stage smoke,
`GE_WSPROBE_F10FRONT` and targeted live playtest. Log the result in
`docs/dev/findings.md` after implementation, not in advance.
## 5. Explicit non-goals
No fullscreen 3D folder in gameplay, changes to the N64 watch, new config
keys, row wording/value changes, save-format changes, controller remapping,
new persistent assets, or `src/game` behavioral edits. The front-end PC
Options screen is a **visual reference**, not part of the first styling
patch; harmonizing its tiny decorative details is a later optional pass.
+31 -6
View File
@@ -73,6 +73,12 @@ Sequencing follows M-139's R → Q → F order, split into four phases below.
| D314/D315 | F10 flicker (user-deprioritized) + main-vs-in-game menu scaling | D315 deferred from v0.3.0; bigger design question — lands naturally with the overlay work. |
| D238 | Mouse sensitivity rework (master sens, RMB aim sens, Y-gain parity) | Partly superseded: aim modes folded (D337, N64 default via PD integration; Centred (PC) opt-in), Aim range PC/N64 (D338). Remaining: sensitivity feel tuning. |
| D239 | Click-to-lock parity: delete always-grab mode 0 + config key | Small; closes D192 by deletion. |
| GEPD controls preset | New opt-in key-layout option ("Controls: FPS (default) / GEPD") reproducing the 1964 GEPD scheme: Q=A (accept/next-weapon), E=Use (B), **R=dedicated reload**, **Ctrl=crouch** (hold or toggle mode); M1=fire / M2=aim / wheel=weapon-cycle / arrows=stick all already exist. Implementation = D214 bind-string presets (`Input.Layout` enum writing preset strings into `g_bindStr`, per-user ini override preserved) + two new port-layer actions (IA_RELOAD emits **B** — reload is the B button in GE: `lv.c:796` `bond_pressed_reload_activate()` → `attempt_reload_item_in_hand()`, so a dedicated key needs no ROM hack and zero game-code changes; IA_CROUCH emits B held — crouch is B, same as the pad layout `input.c:1477–1481` — or a latched toggle). **Wrinkle (decided 2026-09-27): the preset leaves LeanLeft unbound** (its default `Q`, `input.c:885`, would otherwise make GEPD's Q=Action emit A+L; 1964 has no lean key, so the faithful layout drops it — rebindable via per-key override; FPS layout keeps Q). Small, `input.c` + overlay row, OFF default. See `GEPORT-REFERENCE-DEEPDIVE.md` §7 and `MODERN-OPTIONS-PLAN.md` (M3). |
| Modern options wave (parity set, 2026-09-27) | One opt-in "Modern" section in the F10/PC options page (`GE.` ini namespace, unknown keys ignored, all rows **default-off / N64-original**): (a) **crosshair on/off + color** (N64 default: always-on white; tint/hide at the HUD draw site); (b) **low-end perf preset** (FpsCap 30 / AA off / render-scale — the D339/#92 follow-up); (c) **GE+ gameplay toggles, clean subset**: disable knockback, disable
hitstun, damage sound, endless death cam (damage flash already shipped
as D232 `Game.NoHitFlash`) — each a small `#ifdef PORT` hook point;
rule-2 flag per feature, port-side default OFF; see `GEPORT-REFERENCE-DEEPDIVE.md`
§2.1 for the full upstream list); (d) **AllUnlocked preservation proof**: verify untoggling keeps progress (D257 read-time patch design should already guarantee it — evidence, not code). (F12 screenshot → PNG dropped from this wave 2026-09-27 — the dev PPM dump stays, the PNG upgrade is parked.) Total ≈ 2 sessions; lands in Phase 3 as one wave. |
| D226 | Player-adjustable HUD scale (ammo counter, pickup text, dialogue) | **DONE 2026-09-24 (`30720354`)**: Game.HudScale 75–150% (user-verified), fast3d rect scaling about anchors, dialogue/bottom width-capped. |
| Rumble (D224) | Wire `osMotor*` → `SDL_GameControllerRumble` (PD precedent exists) | Deprioritized M-87; pick up if gamepad support is in scope for v0.4.0 — user call. |
| Per-pad tuning / hot-plug | PD incremental hot-plug + remembered seats; deadzone/scale/rumble per pad | Medium; real-controller users. |
@@ -190,15 +196,25 @@ sweep after each landing).
5. Long-idle soak test (both platforms) as the stability acceptance bar.
### Phase 3 — Improvements / QoL (only after Phases 1–2 are green)
Order by user-visible value:
1. **Native widescreen** (letterbox/center instead of stretch) — the biggest
remaining "feels wrong" item; decide the CenterHUD G-class sign-off first.
2. **F10 overlay wave: D237 (wheel + categories + more settings) + D314/D315.**
Order by user-visible value (2026-09-27 refresh — several rows since DONE, marked):
1. ~~**Native widescreen**~~ — **DONE 2026-09-23** (D334/D335; playtest sign-off owed).
2. ~~**F10 overlay wave: D237 + D314/D315**~~ — D237 **DONE 2026-09-23**; D314/D315 still open (user-deprioritized; lands with further overlay work).
3. **Input feel: D238 sensitivity rework, D239 delete always-grab, window
HiDpi/center; rumble + per-pad tuning if gamepad is in scope (user call).**
4. **D226 HUD scale** (F10 slider candidate alongside FovScale).
4. ~~**D226 HUD scale**~~ — **DONE 2026-09-24**.
5. Unlocked FPS: re-measure Phase 0 against the post-D248/D250 baseline and
decide whether v0.4.0 or v0.5.0.
6. **Modern options wave** (Bucket C "parity set": crosshair on/off+color,
low-end perf preset, GE+ gameplay toggles subset, AllUnlocked
preservation proof, GEPD key-layout preset — F12 screenshot dropped
2026-09-27) — one opt-in
section, all rows default-off/N64-original. ≈ 2 sessions total (damage
flash was already D232; F12 screenshot dropped 2026-09-27); the
user-facing answer to "does it have the modern options". Per-item
hook points, effort, and rule-2 sign-off list: local
`MODERN-OPTIONS-PLAN.md` (M1–M7, M6 parked; zero sign-off: M1/M3/M5/M7,
one each: M2 + M4's four toggles — M4.3 is the shipped D232 NoHitFlash —
batched review).
### Phase 4 — Release ritual
1. `./build-pc.sh ntsc-final` clean; `/linkcheck`; full 21-level
@@ -457,9 +473,18 @@ Checklist beyond the normal play:
you see it, then share the `D303:` log lines from around that moment —
each line carries the RDP state of a candidate quad and will identify the
spurious draw call's source.
18. **AllUnlocked preservation (M7)**: with the new tree — enable
AllUnlocked, play two missions, toggle it OFF in the Watch options,
continue a third mission: objectives/saves must persist (D257
read-time patch design; evidence for the findings entry — failure is
its own D3xx). Also the modern-toggles A/Bs when M2/M4 land (see
`MODERN-OPTIONS-PLAN.md` per-item verification).
## Conventions reminder
- One findings entry per investigation; next labels after **D322**.
- One findings entry per investigation; the index tail moves constantly —
it ended at **D370** as of 2026-09-27 (the F10 drag entry), so new
labels start **D371+** (the old "after D322" note is long stale; always
confirm the tail of `findings-index.csv` before filing).
- Fidelity fixes: port-layer or documented A1/ABI exception only; genuine
behavioral differences need the rule-2 sign-off (`docs/dev-process.md`).
- Every subagent reads `docs/porting-notes.md` first and appends to it.
+11
View File
@@ -236,3 +236,14 @@ D355,FIXED,F10/options Music slider inaudible in-stage:,"FIXED (port-only, port/
D356,FIXED,"Settings regroup: Turok-style pages renamed to functional sections (INPUT/GAMEPLAY/GRAPHICS/AUDIO/VIDEO) with PD-style ""(File N)"" scoping annotations, the redu…","FIXED (port-only, port/src/optionsoverlay.{h,c} + port/src/watchsettings.{h,c} + port/src/frontoptions.c; plan docs/dev/D356-SETTINGS-REGROUP-PLAN.md):"
D358,FIXED,D356 level-1 overlap:,"FIXED (port-only, port/src/frontoptions.c):"
D359,PARTIAL,"Settings UI file terminology settled on the end-user term ""Profile"":","LANDED (port-only, frontoptions.c + optionsoverlay.c + comments):"
D360,OPEN,F10 INPUT heading scrolled away; F10 navigation and distance defaults (2026-09-27).,IMPLEMENTED (port-only):
D361,FIXED,Main-menu F10 Audio freezes the game:,"FIXED (port-only, port/src/watchsettings.c + F10 probe in optionsoverlay.c):"
D362,CLOSED,F10 GoldenEye dossier-style visual refresh (2026-09-27).,CLOSED (superseded by D363 after user playtest):
D363,FIXED,F10 return to a dark overlay with the N64 watch's green/Bank Gothic visual language (2026-09-27).,"FIXED (palette/font user-approved, 2026-09-27); D364 follows up on profile tags, click alignment and slider calibration."
D364,FIXED,Settings UI feedback:,FIXED (user mostly accepted; D365 follows up on annotation size and click targets).
D365,OPEN,F10 annotation hierarchy and visible mouse change targets (2026-09-27).,IMPLEMENTED (optionsoverlay.c):
D366,PARTIAL,Input calibration label:,PARTIAL: label changed in both UIs; numeric parity with external GEPD bundle needs its actual settings before changing input defaults.
D367,?,F10 full-width control boxes misaligned with their contents (2026-09-27).,SUPERSEDED by D368 after user playtest:
D368,?,Simplify F10 click targets:,"INPUT RULE KEPT; tinted strips user-rejected, removed in D369."
D369,FIXED,Remove F10 row boxes/strips entirely (2026-09-27).,FIXED (awaiting live acceptance):
D370,PARTIAL,F10 distance/volume drags hurt frame rate (2026-09-27).,PARTIAL (user live test):
1 # GENERATED by tools_pc/gen_findings_index.py -- do not hand-edit.
236 D356
237 D358
238 D359
239 D360
240 D361
241 D362
242 D363
243 D364
244 D365
245 D366
246 D367
247 D368
248 D369
249 D370
+312
View File
@@ -644,6 +644,17 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
| D356 | **Settings regroup: Turok-style pages renamed to functional sections (INPUT/GAMEPLAY/GRAPHICS/AUDIO/VIDEO) with PD-style "(File N)" scoping annotations, the redundant N64-watch rows (`Bond.Look`, `Bond.AimControl`) off the menu surface, and a real per-section Reset-to-defaults (two-step arm->confirm, watch rows to BLANKSAVEDATA via the D352 commit path, ini rows to the port's C initializers; `__Resolution` a documented exclusion) replacing the Turok reset rows the PC build never honored (2026-09-27).** — full `## D356` entry at file tail | FIXED (port-only, `port/src/optionsoverlay.{h,c}` + `port/src/watchsettings.{h,c}` + `port/src/frontoptions.c`; plan `docs/dev/D356-SETTINGS-REGROUP-PLAN.md`): 5 sections all below MAX_PROWS, hidden rows D181/D216-style (keys + watch paths stay live), edge-triggered reset with 3 s arm window + maintain/clear disarm, GE_WSPROBE_RESET front + in-stage probes headless-verified (all sections failures=0, scope isolation OK, saved bytes 0xFF), GE_WSPROBE/GE_WSPROBE_FRONT regressions clean. |
| D358 | **D356 level-1 overlap: the front options save-file row (item 0) painted under the first content row, and `s_hl` (item-numbered) was indexed straight into `s_rowIdx` (content-numbered) -- wrong-row adjusts, a dead last row, a one-past read, wrong highlight (2026-09-27).** — full `## D358` entry at file tail | FIXED (port-only, `port/src/frontoptions.c`): content rows draw at `rowY(k+1)`; all `s_hl` -> `s_rowIdx` uses take `s_hl-1`; A-press guards `s_hl >= 1 && s_hl <= s_rowN`; hit-test/cursor untouched (already item-numbered). Companion: "(save)" tag -> "(profile)". |
| D359 | **Settings UI file terminology settled on the end-user term "Profile": the screen mixed five terms ("Save file"/"File N"/"(profile)"/"Select file"/"(no file)"), and the manual's "game file" reads as the program's own files in modern PC usage (2026-09-27).** — full `## D359` entry at file tail | LANDED (port-only, `frontoptions.c` + `optionsoverlay.c` + comments): top row `Profile`/`N`, annotation `(Profile N)`/`(none)`, per-file row tag `(per profile)`, F10 value `Select a profile`; identifiers/ini keys unchanged (display-layer only). Manual "game file" -> "profile" sync TODO (port-owned doc, not in repo). |
| D360 | **F10 INPUT heading scrolled away; F10 navigation and distance defaults (2026-09-27).** — full `## D360` entry at file tail | IMPLEMENTED (port-only): F10 root category list + per-section Back; distance sliders normalized to 0–100, default 50/100 (250% authored distance), 2x MSAA default. Compiled and linked to a separate test binary (live game locked main executable); manual playtest owed. |
| D361 | **Main-menu F10 Audio freezes the game: EEPROM save from joyPoll deadlocks waiting for its own poll ack (2026-09-27).** — full `## D361` entry at file tail | FIXED (port-only, `port/src/watchsettings.c` + F10 probe in `optionsoverlay.c`): queue front F10 save edits to game thread; actual poll-thread Music+FX commits tested with existing and fresh EEPROM, orderly quit, user's ini/save restored. |
| D362 | **F10 GoldenEye dossier-style visual refresh (2026-09-27).** — full `## D362` entry at file tail | CLOSED (superseded by D363 after user playtest): paper/Zurich/gold look rejected. The pinned section heading and shared layout/hit geometry remain. |
| D363 | **F10 return to a dark overlay with the N64 watch's green/Bank Gothic visual language (2026-09-27).** — full `## D363` entry at file tail | FIXED (palette/font user-approved, 2026-09-27); D364 follows up on profile tags, click alignment and slider calibration. |
| D364 | **Settings UI feedback: replace P legend; F10 slider click band misses drawn bar; input defaults visually off-centre (2026-09-27).** — full `## D364` entry at file tail | FIXED (user mostly accepted; D365 follows up on annotation size and click targets). |
| D365 | **F10 annotation hierarchy and visible mouse change targets (2026-09-27).** — full `## D365` entry at file tail | IMPLEMENTED (`optionsoverlay.c`): dim Bank Gothic profile annotation (not oversized Zurich); `CONTROLS:` Zurich heading, smaller Bank Gothic instructions; boxed right-hand controls + hover and matching bounded click x. NTSC build/link/stage capture pass; live click assessment owed. |
| D366 | **Input calibration label: call 50 the default, not original (2026-09-27).** — full `## D366` entry at file tail | PARTIAL: label changed in both UIs; numeric parity with external GEPD bundle needs its actual settings before changing input defaults. Built, stage tested in D367; user playtest owed. |
| D367 | **F10 full-width control boxes misaligned with their contents (2026-09-27).** — full `## D367` entry at file tail | SUPERSEDED by D368 after user playtest: per-value boxes remained confusing. |
| D368 | **Simplify F10 click targets: setting name selects, the rest changes (2026-09-27).** — full `## D368` entry at file tail | INPUT RULE KEPT; tinted strips user-rejected, removed in D369. |
| D369 | **Remove F10 row boxes/strips entirely (2026-09-27).** — full `## D369` entry at file tail | FIXED (awaiting live acceptance): no row backgrounds, control rectangles or hover paint; selected setting distinguished by brighter text, slider tracks remain. Input rule from D368 unchanged. |
| D370 | **F10 distance/volume drags hurt frame rate (2026-09-27).** — full `## D370` entry at file tail | PARTIAL (user live test): mostly improved; rapid spam can still lower FPS. Unchanged-detent/audio and Draw/LOD reapply fixed; other image sliders still request full video pass (live log shows frequent FOV passes). No further churn without focused repro. |
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
@@ -13997,3 +14008,304 @@ display-layer concern only (principle 3).
**TODO:** update the instruction manual's "game file" to "profile"
(port-owned doc, not in this repo).
## D360 — F10 category navigation and modern-display distance defaults (2026-09-27)
**Reported:** F10's INPUT header was offscreen above Mouse sensitivity when
scrolling; the long flat list was hard to navigate. Draw/LOD distance auto
switches obscured the controls and N64-scale pop-in looked like a port bug.
4x default MSAA was too expensive for low-end hardware.
**Fix (port-only):** F10 opens on a five-category list, and entering a
category shows its header (with Back) and settings together. Keyboard,
controller, mouse and wheel all use the same per-page list. ESC backs out of
a section, then closes from the category list; F10/Start/X still close.
Front-end PC Options already had category pages and shares the row table.
Removed AutoFov controls from both menus (ini keys remain for legacy files),
and exposed both distance sliders at all times. Stored values remain percent
of authored distance; the UI displays 0–100, where 50/100 maps to 250%.
Fresh and SteamOS defaults are 250% draw and LOD distance, 2x MSAA. Reset
uses the same new defaults. Existing ini settings are preserved; old LOD
values below the menu minimum (100%) can still be edited via ini. Note:
these higher distances increase visible geometry/pool demand; test on
open levels and low-end GPUs before claiming performance parity.
**Verification:** `git diff --check` clean. `build-pc.sh ntsc-final`
initially failed because GCC could not create temporary files in
`C:\Windows\`; configuring C/C++ with `-pipe` built all 248 objects.
The existing executable was locked by a running `ge007.x86_64` process,
so the same linker response file was linked to
`scratch/cc-tmp/ge007-check.exe` successfully. No live game was interrupted.
Manual mouse/pad/ESC, reset, and 50/100 rendering/perf checks are owed.
## D361 — F10 Audio on the main menu locks up: self-deadlock in EEPROM poll gate (2026-09-27)
**User repro:** fresh v0.4.0 playtest, change an AUDIO slider from F10
on the main menu → game locks (not merely an inaudible volume change).
**Exact chain:** F10 input is processed inside `inputComputePad()` called
by `osContStartReadData()` in `joyPoll()` (`src/joy.c:500`). D354 routed
front-context watch edits through `watchSettingsSet()` → `saveFrontField()`
on that same poll thread. Both `fileGamePakProbe()` and `fileWriteSave()` call
`joyGamePakProbe/LongWrite()` → `joyDisablePoll()` → blocking receive on
`g_ContDisablePollReceiveMessageQueue`. Only `joyPoll()` can send the
acknowledgement (`src/joy.c:434`), but it is itself blocked inside the
F10 edit: deterministic self-deadlock. D354's claim that one settings
surface meant this thread's writes were safe was incorrect; serialization
between editors does not make the controller-poll handshake re-entrant.
**Fix (port-only):** front F10 writes now coalesce in a small locked
folder/field queue. `watchSettingsGameTick()` drains commits on the game
thread before its non-stage early return and runs `saveFrontField()` there,
with no lock held across EEPROM I/O. The on-screen value is staged before
enqueue for immediate feedback, then cleared only after the matching commit.
The separate stage queue and the direct front options screen path retain
their prior behavior. `GE_WSPROBE_F10FRONT=1` is an opt-in regression probe
called from F10's actual controller-poll input path at file select (once,
after a valid save is available), committing Music+FX to 4096.
**Verified:** NTSC incremental build/link succeeded. Native Windows test
with `GE_OPTIONSOVERLAY=1`, `Game.SkipIntro=1` and the poll-thread probe
logged the queue plus both Music/FX field persist events and exited via
`GE_QUITFRAME=700` (with existing EEPROM **and** absent EEPROM on boot).
The pre-existing game-thread `GE_WSPROBE_FRONT=120` front-path regression
also persisted Music 255→32 and quit normally. The user's ini and EEPROM
were copied aside and restored byte-identically after each run. Manual
mouse slider/section-reset and stage transition checks remain owed.
## D362 — F10 GoldenEye dossier-style visual refresh (2026-09-27)
**Request:** restyle F10 to match GoldenEye, following
`docs/dev/F10-GE-STYLE-PLAN.md`. Existing front Options already uses the
in-game dossier; in-stage F10 needs a lighter port-owned 2D equivalent.
**Implementation (port-only):** replace the blue/black panel with a dark
scene scrim and warm paper card/shadow/rules, Zurich Bold menu ink (Bank
Gothic fallback during early font loading), MI6 gold masthead, dark-red
active values and slider fills/knobs, numbered GE-style category labels,
faint black selected-row highlights. The active section's Back/title/Profile
band stays pinned while content scrolls, so INPUT never disappears above
Mouse sensitivity. A short `P` gutter marker and footer legend retain
per-profile scoping without the old `(per profile)` label/value collision
at 320px. One `OvLayout` drives render, max rows, slider/drag coordinates,
mouse row-hit bands and close box. No config, input binding, save format,
N64 game-code, or F10 audio queue changes. `GE_OPTIONSOVERLAY=2..6`
optionally auto-opens a particular section for deterministic screenshots
(`=1` remains the category root).
**Verification:** NTSC compile/link clean; native-Windows `GE_PCDUMP`
front-end root and GRAPHICS capture plus 320x240 stage GRAPHICS capture
(no crash/overflow; orderly `GE_QUITFRAME` exit). Captures:
`scratch/d362-test/` (local, not distributed). User ini and EEPROM restored
byte-identically after each run. A final tiny title/footer polish rebuild
compiled, but the running playtest held `build-pc/ge007.x86_64.exe` open;
linked identical objects to `scratch/d362-test/ge007-d362.exe`, and the
local `scratch/playtest_v040.bat` installed that binary on the next
closed-game launch (guards against a concurrent game process; installed exe
size/timestamp match the staged build). Live human
review of palette, label fit, mouse clicks/drag/wheel, controller focus,
back/close, longest GRAPHICS labels and the F10 Audio regression is owed.
**User follow-up:** dossier/paper styling rejected; see D363 for the dark,
watch-green successor. D362 is retained as the design/iteration record.
## D363 — F10 returns to a dark overlay, restyled after GE's green watch (2026-09-27)
**User feedback on D362:** "make it more like how it was but use the watch
style as a reference — green, that other font etc." The watch's actual
options pages (`src/game/options.c:3524+`, `:3825+`) use Bank Gothic and
mint/bright green ink (`0xA0FFA0F0` selected, muted green unselected).
The paper card, Zurich Bold, gold, red, numbered categories and dossier
branding from D362 were not the desired direction.
**Fix (port-only):** restore the original F10's simple dark translucent
panel and full-screen dim; use the existing Bank Gothic font for all rows,
subdued green for ordinary text and bright watch green for selection,
active values and slider knobs/fill. The `PC OPTIONS` title and close `X`
replace MI6/CLOSE; category names return to their uppercase row labels.
Keep D360's two-level navigation and D362's shared layout/hit geometry,
scrolling and pinned section heading; no setting behavior or save changes.
The mixed-scope profile marker stays short to avoid 320px overlap.
**Verification:** NTSC build/link clean. `GE_OPTIONSOVERLAY=4`,
`GE_PCDUMP=160-160` with native Windows `GE_QUITFRAME=170` passed in both
front/menu and `-level_09` stage contexts at 640x480: dark panel and green
pixels present (front/stage captures in `scratch/d363-test/`), no errors,
orderly exit. User ini and EEPROM restored byte-identically after both.
Live review of colour, text, click/drag/wheel, controller, ESC/close and
F10 Audio remains owed.
**User follow-up:** palette/font approved, but P shorthand, low-contrast
legend, slider mouse hit position and off-centre input defaults need work;
see D364.
## D364 — Profile scope, F10 slider hitbox and calibrated input displays (2026-09-27)
**Reported:** user likes D363's palette but not `P` / `P = PROFILE`;
controls legend reads like setting text; clicking the visible F10 slider
requires aiming above the bar; Input defaults such as deadzone and trigger
show ~23% rather than a visually centred 50/100 or 5/10.
**Diagnosis:** the F10 bar was at `rowY + 10..12` (knob `+8..14`), but
`overlayRowAtY` assigns the row only within `rowY - 8..+8`: the bar/knob
landed in the NEXT row's hit band. The input values were physically correct
(raw `MouseSensitivity=100` of 1..500; `PadDeadzone=7000` of 0..30000,
~21% full-stick travel; `PadTriggerPct=23` of 1..99), but the UI treated
them as linear full-range sliders rather than calibrated defaults. AUDIO's
`Bond.Music`/`Bond.FX` rows were watch-backed but lacked `.saveScoped`, so
the promised `(Profile N)` title was missing despite every Audio row being
per-profile.
**Fix (port-only):** move F10 slider bar to `rowY+3..5` and knob to
`rowY+1..7`, both entirely inside the row's `-8..+8` click band. The one
shared slider-bar geometry still drives dragging. Give the control legend
its own dark inset strip with muted-grey Zurich text (Bank Gothic remains
for SETTINGS); likewise use a separate font/colour for scope notes. Remove
`P`/legend. Tag each saved Gameplay toggle `(profile)` beside its label
when measured space permits; Audio is entirely per-profile, so its title
now carries `(Profile N)` (both UIs) and no per-row tags. The front dossier
also suppresses redundant inline Audio tags. Input's three calibrated
sliders now display 0–100, mapping each *unchanged* raw default to 50/100
via a two-segment monotonic map preserving raw minimum and maximum; bars,
mouse drags and the shared front options screen use that same map. A
`(50 = original)` note explains the scale. Other sliders (audio %, texture
filter x, FOV/HUD %) keep their physical units and authored defaults.
Reset still writes the original raw defaults, config/ini values are unchanged,
and input sensitivity/deadzone/trigger behavior is untouched.
**Verification:** NTSC build/link clean. Native-Windows `GE_OPTIONSOVERLAY=2`
+ `-level_09` stage screenshot at 640x480 and orderly `GE_QUITFRAME=180`
pass, with the user ini/EEPROM restored byte-identically after the run.
Mouse click/drag on real 4:3/16:9 front/stage, reset and visible tooltip
fit still need an interactive user pass. User accepted most of D364 but
requested smaller profile annotation, a `Controls:` heading with smaller
instructions, and a visible mouse-change target; see D365.
## D365 — F10 annotations and bounded mouse-change controls (2026-09-27)
**Reported:** `(profile)` is visibly larger than its setting label; the
Controls legend should read `Controls:` with smaller following instructions;
it isn't visually clear that a label click *selects* but the right column
*changes* a setting.
**Fix:** Bank Gothic (the smaller watch font already used for row labels)
now renders dim profile tags and section scope notes instead of the larger
Zurich Bold. The footer's `CONTROLS:` heading keeps Zurich Bold, but its
instructions use smaller, dim Bank Gothic, including `BOX: CHANGE` within
sections. Draw subtle green-bordered dark control boxes around every section
row's right-hand interactive area, brightening the hovered box without
moving keyboard selection. Restrict left/right mouse changes to the same
bounded x region; label clicks still select only. Coordinates are mapped
through `gfx_get_ui_screen_rect` for hover as for input, preserving the
safe-area crop behavior (D316). No game logic or saved data touched.
**Verification:** NTSC compile/link and 640x480 `GE_OPTIONSOVERLAY=2`
`-level_09` frame-160 capture with clean frame-180 quit; ini and EEPROM
restored byte-identically after testing. During compilation the user's
existing game process held the exe, so it was first linked to a staged
scratch path rather than overwritten; final build linked directly after
that process exited. Mouse hover/click in a live window is still owed.
## D366 — Input slider calibration: “default,” not “original” (2026-09-27)
**Request:** user prefers the GEPD bundle as the standard for input and
wants the `(50 = original)` note to read `(50 = default)`.
**Fix:** changed both settings UIs' Input-header text to `(50 = default)`.
`calibratedDefault()` still maps the port's current *real* defaults
(`MouseSensitivity=100`, `PadDeadzone=7000`, `PadTriggerPct=23`) to 50/100,
and `kResetDefaults` agrees with `input.c`. No live config or save edited.
**Caveat:** GEPD's emulator bundle is an external reference; this checkout
has a stub in `reference/mouse-injector/` and its `GEPD-INPUT-PLAN.md` records
its camera aim formulas, but neither has an authoritative bundle controller
config for all three numbers. The port's 38/50 per-mode mouse speeds were
user-calibrated for its own FOV. Don't claim numeric parity with the GEPD
bundle or re-tune default input feel without the user's bundle configuration.
**Verification:** NTSC compile and staged link succeed. The active user's
playtest held `build-pc/ge007.x86_64.exe`, so the new binary was linked to
`scratch/d366-test/` and the local v040 launcher installs it only after the
running game exits. No stage smoke test while the user's save is active. The user later exited;
D367 includes the subsequent native stage smoke test of both changes.
## D367 — F10 control frames aligned with their actual clickable contents (2026-09-27)
**Reported:** D365's full-width rectangles do not align with their text
contents. Root cause: every row used a fixed `[barX0, right-7)` box despite
right-aligned toggles/enum text near `right-12`, slider bars ending much
earlier at `barX1`, and reset actions having their button in the *label*
column (empty value until armed). This advertised empty space as clickable
and left the reset's real target unframed.
**Fix:** `overlayControlSpan()` now calculates the actual button region per
row: tight around slider tracks, around measured right-aligned values, or
around the reset label; non-interactive/unavailable slider rows get no box.
Both mouse buttons and hover use that same geometry, so moving the mouse
only highlights a genuinely clickable area. The narrower border fits the
Bank Gothic text's vertical footprint. Label clicks on ordinary settings
still only select; clicking the framed reset label invokes its existing
two-step confirmation. No game logic or config/default value changes.
**Verification:** NTSC compile/link passed, native 640x480
`GE_OPTIONSOVERLAY=2 -level_09` stage capture and orderly frame-180 exit;
ini/EEPROM restored byte-for-byte. Mouse click/hover and confirmation remain
to be feel-checked live. User's immediate playtest found remaining boxes
wrong; replaced by the simpler D368 rule.
## D368 — F10: name selects, right-of-name changes (2026-09-27)
**User decision:** stop trying to frame each differently sized value; all
of the row except the setting name should change it. D367's boxed controls
were still misleading in a live test.
**Fix (port-only):** `overlayControlSpan` now starts after the measured
Bank Gothic setting name (capped at its label-column clipping edge) and
extends to the panel's right edge. It is used by mouse hover, left/right
click and a single subtly tinted strip with a thin left divider; the
per-value borders are gone. Clicking a name still focuses without changing.
A slider click on its track positions/drags; clicking elsewhere to the right
of its name changes it by one step instead of snapping to the minimum or
maximum. Reset is an action, so its whole row activates the existing
confirmation. The footer now says `RIGHT: CHANGE`. No config/save or game
logic changes. NTSC build/link passes; live mouse check still owed.
## D369 — Remove F10 per-row paint (2026-09-27)
User still saw boxes in D368 because the tinted per-row change strips and
selected-row rectangles were still painted. Removed **all** of those from
`optionsOverlayEmit()` (including hover paint). Selected text remains
brighter, sliders retain only their track and knob, and the existing dark
panel/Controls footer remain. Mouse hit behavior from D368 is unchanged.
NTSC compile and staged link pass; a running playtest still holds the old
exe, so the local launcher will install `scratch/d369-test/ge007-d369.exe`
only after the game exits. No save/ini touched.
## D370 — Avoid redundant work during F10 slider drags (2026-09-27)
**Reported:** dragging Draw/LOD distance, Music or FX volume drops FPS.
**Diagnosis:** `sliderSetFromX` runs on every controller poll while LMB is
held, including repeated polls over the *same snapped value*. `rowSetCommit`
then re-enqueues watch audio updates and, for *any* `Video.*` key, requests a
`videoStartFrame` live-config pass: `set_swap_interval`, FPS cap, texture
filter, image state and a log line. Draw/LOD multipliers are already read
straight from their config pointers by the render path, so no such pass is
needed. The active playtest log has hundreds of `video: live config applied`
lines and multiple watch-setting persists; log alone does not isolate all
of those calls to one knob. Physically increasing Draw/LOD can independently
increase rendering load, which this fix will not hide.
**Fix (port-only `optionsoverlay.c`):** early-return before live-apply/queue
when a snapped value has not changed (including queued watch settings); held
mouse polls no longer repeat the same audio setter or GL state work. Retain
slider-release `watchSettingsCommit` so a drag persists exactly once. Exempt
only `Video.DrawDistance` and `Video.LodDistance` from
`videoRequestLiveConfig`; all other Video keys retain their old live-apply
semantics. Audio updates still preview live for genuinely changed detents.
No user config/save or game code edited. NTSC compile + staged link passed;
active Windows game initially held the executable; the local launcher
installed the staged binary on the user's next safe launch (now running).
Live FPS retest (ideally compare dragging
Music/FX while paused vs simply raising Draw/LOD values) owed.
**User playtest:** "it can still drop it down if you spam it but mostly good."
Residual not isolated: the latest playtest log shows repeated full video
config passes while changing FOV (not Draw/LOD) and several watch EEPROM
persists; sustained high Draw/LOD also increases actual GPU work. Parked as
polish until a focused repro identifies which slider/spam pattern dominates.
+2
View File
@@ -35,6 +35,8 @@ extern "C" {
/* Toggle open/closed. On the closing edge the config is saved. */
void optionsOverlayToggle(void);
/* ESC: back to categories, or close from the category list (host-thread request). */
void optionsOverlayBack(void);
/* 1 while the overlay is on screen. */
int optionsOverlayIsOpen(void);
+7 -4
View File
@@ -498,6 +498,8 @@ Gfx *frontOptionsMenuDraw(Gfx *DL)
if (f < 0) snprintf(note, sizeof(note), "(none)");
else snprintf(note, sizeof(note), "(Profile %d)", f + 1);
DL = ink(DL, ROW_X + titleW + 8, TITLE_Y, note, INK_DIM);
} else if (strcmp(optionsRowLabel(s_pageHdr[s_page]), "INPUT") == 0) {
DL = ink(DL, ROW_X + titleW + 8, TITLE_Y, "(50 = default)", INK_DIM);
}
/* D356: the top profile row (item 0, not in the row table; the label
@@ -534,10 +536,11 @@ Gfx *frontOptionsMenuDraw(Gfx *DL)
}
DL = ink(DL, ROW_X, y, label, INK);
/* D357: per-file rows in the mixed-scope section carry a dim
* "(per profile)" tag -- "stored separately in each profile";
* the section title says which one is active. */
if (optionsRowIsSaveScoped(i)) {
/* Only GAMEPLAY mixes ini and profile options. AUDIO is wholly
* profile-scoped and already has (Profile N) in its title; an
* inline tag there would collide with the volume slider. */
if (optionsRowIsSaveScoped(i) &&
strcmp(optionsRowLabel(s_pageHdr[s_page]), "GAMEPLAY") == 0) {
DL = ink(DL, ROW_X + measureW(label) + 4, y, "(per profile)\n", INK_DIM);
}
+1 -1
View File
@@ -121,7 +121,7 @@ int main(int argc, char **argv)
}
/* 1. Platform + config + filesystem. Steam Deck / SteamOS: seed the
* first-run preset (native 1280x800 fullscreen, MSAA 4, longer draw/LOD
* first-run preset (native 1280x800 fullscreen, MSAA 2, midpoint draw/LOD
* distances) before the load so a missing ini saves these values; an
* existing ini always wins. */
if (getenv("STEAMOS")) {
+473 -235
View File
@@ -8,9 +8,9 @@
*
* The panel adapts to whatever 2D space it is drawn in (320x240 in-game vs
* 440x330 on front-end screens -- viSetXY differs) and scrolls when the row
* list outgrows the viewport (wheel / arrows at the edges). Cyclic rows
* (MSAA, texture filter, resolution, toggles) wrap in both directions; the
* manual % rows (draw/LOD distance) are hidden while their "auto" toggle is on.
* list outgrows the viewport (wheel / arrows at the edges). F10 opens at a
* category list; each category has its own short page and Back row. Cyclic
* rows (MSAA, texture filter, resolution, toggles) wrap in both directions.
*
* Text + fill helpers are the game's own (textRender / microcode_constructor /
* gDPFillRectangle) reached by extern -- same pattern input.c uses to read
@@ -66,6 +66,8 @@ struct font;
struct fontchar;
extern struct font *ptrFontBankGothic;
extern struct fontchar *ptrFontBankGothicChars;
extern struct font *ptrFontZurichBold;
extern struct fontchar *ptrFontZurichBoldChars;
extern Gfx *microcode_constructor(Gfx *gdl);
extern Gfx *textRender(Gfx *gdl, s32 *x, s32 *y, char *text, struct fontchar *chars,
struct font *font, u32 colour, s32 width, s32 height,
@@ -134,8 +136,8 @@ struct Row {
void *ptr;
double cfgMin, cfgMax;
/* Hidden while the option named here is nonzero (a manual % row
* disappears while its "auto" toggle is on). Resolved to hidePtr at init. */
/* Optional conditional row visibility (e.g. Aim range while centred).
* Resolved to hidePtr at init. */
const char *hiddenIfOn;
int *hidePtr;
@@ -167,7 +169,7 @@ static struct Row rows[] = {
* watchSettingsActiveFolder(). All rows use designated initializers
* (D351 class). */
{ .key="__HdrInput", .label="INPUT", .kind=ROW_HEADER },
{ .key="Input.MouseSensitivity", .label="Mouse sensitivity", .kind=ROW_SLIDER, .step=5 },
{ .key="Input.MouseSensitivity", .label="Mouse sensitivity", .kind=ROW_SLIDER, .step=5 }, /* calibrated UI midpoint = raw 100 */
{ .key="Input.MouseInvertY", .label="Invert look (mouse)", .kind=ROW_TOGGLE, .step=1, .names=kOnOff },
/* Input.PdMouseAim (findings D332): the Perfect Dark port's mouse-aim
* model. The port only accumulates the mouse; the game's own crosshair
@@ -186,8 +188,8 @@ static struct Row rows[] = {
* style is CENTRED (PC), where the crosshair doesn't travel. */
{ .key="Input.AimRange", .label="Aim range", .kind=ROW_ENUM, .step=1, .names=kAimRange, .hiddenIfOn="Input.AimMode" },
{ .key="Input.PadLookInvertY", .label="Invert look (controller)", .kind=ROW_TOGGLE, .step=1, .names=kOnOff },
{ .key="Input.PadDeadzone", .label="Stick deadzone", .kind=ROW_SLIDER, .step=500, .unit="%", .dispDiv=300 },
{ .key="Input.PadTriggerPct", .label="Trigger threshold", .kind=ROW_SLIDER, .step=1, .unit="%" },
{ .key="Input.PadDeadzone", .label="Stick deadzone", .kind=ROW_SLIDER, .step=500 },
{ .key="Input.PadTriggerPct", .label="Trigger threshold", .kind=ROW_SLIDER, .step=1 },
{ .key="__ResetInput", .label="Reset to defaults", .kind=ROW_ACTION },
{ .key="__HdrGameplay", .label="GAMEPLAY", .kind=ROW_HEADER },
@@ -247,10 +249,10 @@ static struct Row rows[] = {
{ .key="Video.NativeWidescreen", .label="Native widescreen", .kind=ROW_TOGGLE, .step=1, .names=kOnOff },
{ .key="Video.WidescreenAuto", .label="Widescreen auto FOV", .kind=ROW_TOGGLE, .step=1, .names=kOnOff },
{ .key="Video.SafeAreaCrop", .label="Crop overscan", .kind=ROW_TOGGLE, .step=1, .names=kOnOff },
{ .key="Video.DrawDistance", .label="Draw distance", .kind=ROW_SLIDER, .step=25, .hiddenIfOn="Video.DrawDistanceAutoFov", .unit="%" },
{ .key="Video.DrawDistanceAutoFov", .label="Draw dist. follows FOV", .kind=ROW_TOGGLE, .step=1, .names=kOnOff },
{ .key="Video.LodDistance", .label="LOD distance", .kind=ROW_SLIDER, .step=25, .hiddenIfOn="Video.LodDistanceAutoFov", .unit="%" },
{ .key="Video.LodDistanceAutoFov", .label="LOD dist. follows FOV", .kind=ROW_TOGGLE, .step=1, .names=kOnOff },
/* 100..400% of authored distance; 250% is the midpoint (50/100).
* Legacy AutoFov ini keys remain supported but no longer hide sliders. */
{ .key="Video.DrawDistance", .label="Draw distance", .kind=ROW_SLIDER, .step=25, .uiMin=100, .uiMax=400 },
{ .key="Video.LodDistance", .label="LOD distance", .kind=ROW_SLIDER, .step=25, .uiMin=100, .uiMax=400 },
/* D304: the per-mode aim/turn-sensitivity sliders and the "Link" toggle
* that papered over their decoupling risk were pulled from the menu
* (user feedback after the D304 widening); the ONE master
@@ -275,9 +277,9 @@ static struct Row rows[] = {
* A port-level master volume (M3) will join here as Turok's
* Master/Sound/Music trio. */
{ .key="Bond.Music", .label="Music volume", .kind=ROW_SLIDER, .step=128,
.uiMax=32767, .cfgMax=32767, .found=1, .unit="%", .dispDiv=328 },
.uiMax=32767, .cfgMax=32767, .found=1, .unit="%", .dispDiv=328, .saveScoped=1 },
{ .key="Bond.FX", .label="FX volume", .kind=ROW_SLIDER, .step=128,
.uiMax=32767, .cfgMax=32767, .found=1, .unit="%", .dispDiv=328 },
.uiMax=32767, .cfgMax=32767, .found=1, .unit="%", .dispDiv=328, .saveScoped=1 },
{ .key="__ResetAudio", .label="Reset to defaults", .kind=ROW_ACTION },
/* D353's DISPLAY section renamed VIDEO (the presentation knobs), with
@@ -300,20 +302,19 @@ static struct Row rows[] = {
static int s_inited = 0;
static volatile int s_open = 0;
static int s_sel = 0; /* selection, index into s_visIdx (visible list) */
static int s_section = -1; /* -1 = category list; otherwise rows[] header index */
static SDL_atomic_t s_backPending; /* ESC is received on the host thread */
/* Visible-row list: rows whose hiddenIfOn option is nonzero are omitted
* (manual % rows hide while their auto toggle is on). Rebuilt every frame in
* overlayUpdateVisible(); s_scroll is the first visible-list entry drawn. */
/* Visible-row list: category headers on the root page, or the active
* section's Back header and content (including conditional rows). Built
* on the scheduler thread; s_scroll is the first displayed entry. */
static int s_visIdx[NUM_ROWS];
static int s_visN = 0;
static int s_scroll = 0;
static SDL_atomic_t s_wheelPending; /* D314: host-thread wheel notches */
/* D213: optional on-screen FPS readout (PD parity: Video.DisplayFPS). Drawn
* top-right whenever enabled, independent of the F10 panel. Config-only knob
* (kept off the 13-row panel, which is at its layout limit) -- matches PD,
* whose DisplayFPS is also file-only. Default 0 => emit path unchanged =>
* golden dumps byte-identical. */
/* D213: optional on-screen FPS readout (PD parity: Video.DisplayFPS).
* Drawn top-right whenever enabled, independent of the F10 panel. */
static int s_showFps = 0;
static char s_fpsText[16] = "";
@@ -347,106 +348,94 @@ static void fpsTick(void)
* emit path and the mouse hit-testing in optionsOverlayHandleInput().
* BankGothic caps are ~9 units tall here, so rows need ~16 units of pitch and
* values are right-aligned to the panel edge to survive the wide font. */
#define OV_X0 20
#define OV_LABEL_X 28
#define OV_TOP 12
#define OV_LINE 15 /* row pitch (13 rows must fit ~240) */
#define OV_HDR 2 /* header rows above row 0 (title+hint) */
/* i is a VISIBLE-POSITION (already scroll-adjusted by the caller). */
#define OV_ROW_Y(i) (OV_TOP + ((i) + OV_HDR) * OV_LINE)
#define OV_TOP 8
#define OV_LINE 16
/* Dark 2D watch-style overlay; the in-stage watch's 3D model is not loaded
* here. All rendering AND hit-testing take coordinates from this layout;
* 320x240 stages and 440x330 front screens share the same geometry. */
struct OvLayout {
s32 left, right, top, sectionY, contentY, bottom, footerY;
s32 labelX, valueR, barX0, barX1, maxRows;
};
/* How many rows fit between the header and the bottom edge of whatever 2D
* space we are in right now (320x240 in-game, 440x330 on front-end screens --
* viSetXY differs, see src/game/front.c). More rows than this => scroll. */
static int maxVisibleRows(void)
static struct OvLayout overlayLayout(void)
{
int n = (viGetY() - 6 - OV_TOP - OV_HDR * OV_LINE) / OV_LINE;
if (n < 4) {
n = 4;
}
return n;
struct OvLayout o;
s32 w = viGetX(), h = viGetY();
s32 cardW = w - 16;
if (cardW > 376) cardW = 376;
o.left = (w - cardW) / 2;
o.right = o.left + cardW;
o.top = OV_TOP;
o.sectionY = o.top + 31;
o.contentY = o.top + 49;
o.labelX = o.left + 18;
o.valueR = o.right - 12;
o.barX1 = o.right - 65;
o.barX0 = o.barX1 - 74;
o.maxRows = (h - 6 - o.contentY - 24) / OV_LINE;
if (o.maxRows < 4) o.maxRows = 4;
/* Root has five categories. A section's title/Back sits in its own
* pinned band; only the content rows consume this scrollable area. */
int contentN = s_visN - (s_section >= 0 ? 1 : 0);
int drawn = contentN < o.maxRows ? contentN : o.maxRows;
o.bottom = o.contentY + drawn * OV_LINE + 24;
if (o.bottom > h - 6) o.bottom = h - 6;
o.footerY = o.bottom - 15;
return o;
}
#define OV_RIGHT (viGetX() - OV_X0) /* right edge for right-aligned text */
#define OV_NUM_W 36 /* reserved width for a slider's number */
#define OV_BAR_X 150
/* Slider fill bar span in overlay space (shared by emit + hit-testing). */
static int maxVisibleRows(void) { return overlayLayout().maxRows; }
/* Shared with slider dragging and its render path. */
static void sliderBarSpan(s32 *x0, s32 *x1)
{
*x0 = OV_BAR_X;
*x1 = OV_RIGHT - OV_NUM_W;
if (*x1 < *x0 + 16) {
*x1 = *x0 + 16;
}
struct OvLayout o = overlayLayout();
*x0 = o.barX0;
*x1 = o.barX1;
}
/* 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.
*
* D316 fix: the band used to be [Y-3, Y+12) -- 3 units above OV_ROW_Y but
* 12 below it, a systematic downward bias baked in independent of the
* mouse-mapping bug above. Centered here on OV_ROW_Y so a borderline click
* no longer favors the row below. */
/* Visible position or -1. Match the rendered [s_scroll, pLast] window
* exactly (D304: never hit the invisible row below Quit) and centre the
* hit band on the drawn row (D316). The section title has its own pinned
* hit band and is always the selectable Back row, even while scrolling. */
static int overlayRowAtY(double oy)
{
int maxV = maxVisibleRows();
int pLast = (s_visN - s_scroll < maxV) ? s_visN - 1 : s_scroll + maxV - 1;
struct OvLayout o = overlayLayout();
if (s_section >= 0 && oy >= o.sectionY - OV_LINE / 2 &&
oy < o.sectionY + OV_LINE / 2) return 0; /* pinned Back/title */
int pLast = s_scroll + o.maxRows - 1;
if (pLast >= s_visN) pLast = s_visN - 1;
for (int p = s_scroll; p <= pLast; p++) {
double top = OV_ROW_Y(p - s_scroll) - OV_LINE / 2;
if (oy >= top && oy < top + OV_LINE) {
return p;
}
double top = o.contentY + (p - s_scroll) * OV_LINE - OV_LINE / 2;
if (oy >= top && oy < top + OV_LINE) return p;
}
return -1;
}
/* 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. */
/* Move through the current category list or section (including its Back row). */
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;
}
}
int q = pos + dir;
return (q < 0 || q >= s_visN) ? pos : q;
}
static void overlayUpdateVisible(void)
{
s_visN = 0;
for (int i = 0; i < NUM_ROWS; i++) {
/* The file chooser belongs to the GE front-end screen, not F10.
* F10 in-stage always targets the active Bond file. */
if (rows[i].kind != ROW_BOND_FILE &&
!(rows[i].hidePtr && *rows[i].hidePtr != 0)) {
s_visIdx[s_visN++] = i;
/* Category list: only headers. Section: its header serves as Back,
* followed by its settings. The file chooser belongs to the front. */
if (s_section < 0) {
if (rows[i].kind == ROW_HEADER) s_visIdx[s_visN++] = i;
} else if (i == s_section) {
s_visIdx[s_visN++] = i; /* Back */
} else if (i > s_section) {
if (rows[i].kind == ROW_HEADER) break;
if (rows[i].kind != ROW_BOND_FILE &&
!(rows[i].hidePtr && *rows[i].hidePtr))
s_visIdx[s_visN++] = i;
}
}
if (s_visN == 0) { /* cannot happen (toggles have no hide source) */
@@ -458,11 +447,6 @@ 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
@@ -482,32 +466,27 @@ static void overlayUpdateVisible(void)
static void overlayUpdateScroll(void)
{
int maxV = maxVisibleRows();
if (s_visN <= maxV) {
s_scroll = 0;
int first = s_section >= 0 ? 1 : 0; /* section header stays pinned */
int count = s_visN - first;
if (count <= maxV) {
s_scroll = first;
return;
}
if (s_sel < s_scroll) {
if (s_scroll < first) s_scroll = first;
if (s_sel >= first && 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;
}
if (s_scroll > s_visN - maxV) {
s_scroll = s_visN - maxV;
}
if (s_scroll > s_visN - maxV) s_scroll = s_visN - maxV;
}
/* The close box brackets the title row at the panel's right edge. */
#define OV_CB_X0 (OV_RIGHT - 14)
#define OV_CB_X1 (OV_RIGHT + 7)
#define OV_CB_Y0 (OV_TOP - 3)
#define OV_CB_Y1 (OV_TOP + 12)
static int overlayInCloseBox(double ox, double oy)
{
return ox >= OV_CB_X0 && ox <= OV_CB_X1 &&
oy >= OV_CB_Y0 && oy <= OV_CB_Y1;
struct OvLayout o = overlayLayout();
return ox >= o.right - 40 && ox <= o.right - 5 &&
oy >= o.top + 3 && oy <= o.top + 22;
}
/* ------------------------------------------------------------------------ */
@@ -611,7 +590,22 @@ static void overlayInit(void)
const char *e = getenv("GE_OPTIONSOVERLAY");
if (e && atoi(e) != 0) {
s_open = 1;
sysLogPrintf(LOG_INFO, "optionsoverlay: auto-opened (GE_OPTIONSOVERLAY)");
/* Diagnostic screenshots: 1 = category root, 2..6 = the five
* sections. This is only a boot probe; normal F10 always opens
* at the category root. */
int page = atoi(e) - 2;
if (page >= 0 && page < 5) {
int found = 0;
for (int i = 0; i < NUM_ROWS; i++) {
if (rows[i].kind == ROW_HEADER && found++ == page) {
s_section = i;
s_sel = 1;
break;
}
}
overlayUpdateVisible();
}
sysLogPrintf(LOG_INFO, "optionsoverlay: auto-opened (GE_OPTIONSOVERLAY=%s)", e);
}
}
@@ -639,6 +633,40 @@ static double rowHi(const struct Row *r)
return (r->uiMin != r->uiMax) ? r->uiMax : r->cfgMax;
}
/* Player-facing calibrated input controls: the original raw defaults are
* deliberate (100% mouse speed, 7000/30000 stick deadzone, 23% trigger
* travel). Show each as 50/100, without changing input.c, ini ranges,
* reset values, or either endpoint. Both UIs and F10 hit/drag use the same
* piecewise mapping, so the shown number always matches the filled bar. */
static double calibratedDefault(const struct Row *r)
{
if (!strcmp(r->key, "Input.MouseSensitivity")) return 100.0;
if (!strcmp(r->key, "Input.PadDeadzone")) return 7000.0;
if (!strcmp(r->key, "Input.PadTriggerPct")) return 23.0;
return -1.0;
}
static double rowFractionAt(const struct Row *r, double v)
{
double lo = rowLo(r), hi = rowHi(r), def = calibratedDefault(r);
if (hi <= lo) return 0.0;
if (v < lo) v = lo;
if (v > hi) v = hi;
if (def > lo && def < hi)
return v <= def ? 0.5 * (v - lo) / (def - lo)
: 0.5 + 0.5 * (v - def) / (hi - def);
return (v - lo) / (hi - lo);
}
static double rowValueAtFraction(const struct Row *r, double f)
{
double lo = rowLo(r), hi = rowHi(r), def = calibratedDefault(r);
if (f < 0.0) f = 0.0;
if (f > 1.0) f = 1.0;
if (def > lo && def < hi)
return f <= 0.5 ? lo + (def - lo) * f * 2.0
: def + (hi - def) * (f - 0.5) * 2.0;
return lo + (hi - lo) * f;
}
static struct Row *rowByKey(const char *key)
{
for (int i = 0; i < NUM_ROWS; i++) {
@@ -662,13 +690,26 @@ static void rowSetCommit(struct Row *r, double v, int commit)
}
int field = watchSettingsFieldForKey(r->key);
if (field >= 0) {
watchSettingsSet(field, (int)lround(v), commit);
int want = (int)lround(v);
/* Held drags poll far more often than the mouse moves to a new
* slider detent. Don't enqueue another audio update for the same
* value; release still commits via watchSettingsCommit(). */
if (watchSettingsRead(field) != want)
watchSettingsSet(field, want, commit);
return;
}
switch (r->type) {
case CONFIG_OPT_INT: *(int *)r->ptr = (int)lround(v); break;
case CONFIG_OPT_UINT: *(unsigned int *)r->ptr = (unsigned int)(v < 0 ? 0 : lround(v)); break;
case CONFIG_OPT_FLOAT: *(float *)r->ptr = (float)v; break;
case CONFIG_OPT_INT:
if (*(int *)r->ptr == (int)lround(v)) return;
*(int *)r->ptr = (int)lround(v); break;
case CONFIG_OPT_UINT: {
unsigned int want = (unsigned int)(v < 0 ? 0 : lround(v));
if (*(unsigned int *)r->ptr == want) return;
*(unsigned int *)r->ptr = want; break;
}
case CONFIG_OPT_FLOAT:
if (*(float *)r->ptr == (float)v) return;
*(float *)r->ptr = (float)v; break;
default: return;
}
@@ -676,7 +717,12 @@ static void rowSetCommit(struct Row *r, double v, int commit)
* is read straight off the pointer by its owner every frame/poll. */
if (strcmp(r->key, "Video.Fullscreen") == 0) {
videoRequestFullscreen((int)lround(v));
} else if (strncmp(r->key, "Video.", 6) == 0 && !r->restart) {
} else if (strncmp(r->key, "Video.", 6) == 0 && !r->restart &&
strcmp(r->key, "Video.DrawDistance") != 0 &&
strcmp(r->key, "Video.LodDistance") != 0) {
/* These two distance knobs are read directly by the render path.
* Reapplying VSync/texture/GL image state every drag detent only
* stalls frames; they do not need videoStartFrame's config pass. */
videoRequestLiveConfig();
}
@@ -826,17 +872,15 @@ static const struct { const char *key; double def; } kResetDefaults[] = {
{ "Input.PadDeadzone", 7000 },/* = STICK_DEADZONE (7000) */
{ "Input.PadTriggerPct", 23 }, /* = 23 */
/* GRAPHICS (port/src/video.c initializers) */
{ "Video.MSAA", 4 }, /* = 4 */
{ "Video.MSAA", 2 }, /* = 2 */
{ "Video.TextureFilter", 1 }, /* = 1 (bilinear) */
{ "Video.Anisotropy", 4 }, /* = 4 */
{ "Video.FovScale", 100 }, /* = 100 */
{ "Video.NativeWidescreen", 1 }, /* = 1 */
{ "Video.WidescreenAuto", 1 }, /* = 1 */
{ "Video.SafeAreaCrop", 1 }, /* = 1 */
{ "Video.DrawDistance", 150 }, /* = 150 (D218) */
{ "Video.DrawDistanceAutoFov", 1 }, /* = 1 (D218) */
{ "Video.LodDistance", 150 }, /* = 150 (D249) */
{ "Video.LodDistanceAutoFov", 0 }, /* = 0 (standalone perf lever) */
{ "Video.DrawDistance", 250 }, /* midpoint: 50/100 */
{ "Video.LodDistance", 250 }, /* midpoint: 50/100 */
/* GAMEPLAY ini rows (port/src/video.c initializers) */
{ "Game.SkipIntro", 0 }, /* = 0 */
{ "Game.NoHitFlash", 0 }, /* = 0 */
@@ -857,8 +901,8 @@ static double kResetDefault(const char *key)
}
/* The section's DECLARED row range (its header through the next header) --
* NOT the visible list, so conditionally hidden rows (the manual draw/LOD %
* sliders while their auto-FOV toggles are on) are reset too; their live
* NOT the visible list, so conditionally hidden rows (e.g. Aim range while
* centred) are reset too; their live
* values are written through the same ptr/rowSet path, visibility
* irrelevant. Per row, its OWN scope is reset: watch rows -> the selected
* file's BLANKSAVEDATA values through the normal commit path (front: direct
@@ -950,6 +994,11 @@ void optionsOverlayToggle(void)
if (pending > 0) watchSettingsCommit(pending - 1);
}
s_open = !s_open;
if (s_open) {
s_section = -1;
s_sel = s_scroll = 0;
}
SDL_AtomicSet(&s_backPending, 0);
sysLogPrintf(LOG_INFO, "optionsoverlay: %s", s_open ? "opened" : "closed");
if (!s_open) {
optionsResetClear(); /* D356: closing the overlay disarms a pending reset */
@@ -957,6 +1006,12 @@ void optionsOverlayToggle(void)
}
}
void optionsOverlayBack(void)
{
/* Called from SDL's host event pump; navigation is scheduler-owned. */
if (s_open) SDL_AtomicSet(&s_backPending, 1);
}
int optionsOverlayIsOpen(void)
{
if (!s_inited) {
@@ -1006,7 +1061,7 @@ static void sliderSetFromX(struct Row *r, double ox)
double f = (ox - bx0) / (double)(bx1 - bx0);
if (f < 0) f = 0;
if (f > 1) f = 1;
double v = lo + f * (hi - lo);
double v = rowValueAtFraction(r, f);
double step = (r->step > 0.0) ? r->step : 1.0;
v = lround(v / step) * step;
rowSetCommit(r, v, 0); /* slider drag: save only on release */
@@ -1021,6 +1076,10 @@ static void sliderSetFromX(struct Row *r, double ox)
* see D302: a hot-path getenv() regression cost a real bug before). */
static int s_d314Enabled = -1; /* -1 = not yet resolved */
/* Shared by paint and mouse input: the area right of a setting's name
* changes it; its name only selects it. Reset actions use the whole row. */
static int overlayControlSpan(int i, s32 *x0, s32 *x1);
void optionsOverlayHandleInput(void)
{
static int prevUp, prevDn, prevLf, prevRt, prevLmb, prevRmb;
@@ -1049,11 +1108,42 @@ void optionsOverlayHandleInput(void)
* visible (a stage poll would otherwise leave it locked/hidden). */
inputSuspendForOverlay();
/* D361: regression probe on the ACTUAL joyPoll/F10 thread (not the
* game-thread GE_WSPROBE_FRONT hook). A front-end write used to block
* forever in joyDisablePoll waiting for this very poll to acknowledge it.
* Opt-in only, once after the file-select save becomes available. */
{
static int probe = -1, readyTicks = 0;
if (probe < 0) probe = getenv("GE_WSPROBE_F10FRONT") ? 1 : 0;
if (probe == 1 && current_menu == MENU_FILE_SELECT && watchSettingsAvailable() &&
++readyTicks >= 120) {
probe = 2;
sysLogPrintf(LOG_INFO, "wsf10front: queue Music+FX from controller poll");
watchSettingsSet(WATCH_SETTING_MUSIC, 4096, 1);
watchSettingsSet(WATCH_SETTING_FX, 4096, 1);
}
}
/* D356: a pending reset arm expires after 3 s or when the selection
* leaves the armed row; the overlay's selection is s_visIdx[s_sel]. */
optionsResetMaintain(s_visIdx[s_sel]);
overlayUpdateVisible();
if (SDL_AtomicSet(&s_backPending, 0)) {
if (s_section >= 0) {
int hdr = s_section;
s_section = -1;
overlayUpdateVisible();
s_sel = 0;
while (s_sel + 1 < s_visN && s_visIdx[s_sel] != hdr) s_sel++;
s_scroll = 0;
optionsResetClear();
} else {
optionsOverlayToggle();
return;
}
}
optionsResetMaintain(s_section < 0 ? -1 : s_visIdx[s_sel]);
overlayUpdateVisible(); /* % rows may have appeared/vanished (auto toggles) */
/* % rows may have appeared/vanished after a setting change. */
overlayApplyWheel(); /* D314: wheel notches queued by the host thread */
overlayUpdateScroll();
@@ -1123,14 +1213,31 @@ void optionsOverlayHandleInput(void)
if (dir != 0) {
adjDir = dir;
adjTimer = 18;
rowAdjust(&rows[s_visIdx[s_sel]], dir);
if (s_section < 0) {
s_section = s_visIdx[s_sel];
s_sel = 1;
s_scroll = 0;
overlayUpdateVisible();
adjDir = 0;
} else if (s_sel == 0) {
int hdr = s_section;
s_section = -1;
overlayUpdateVisible();
s_sel = 0;
while (s_sel + 1 < s_visN && s_visIdx[s_sel] != hdr) s_sel++;
s_scroll = 0;
optionsResetClear();
adjDir = 0;
} else {
rowAdjust(&rows[s_visIdx[s_sel]], dir);
}
} else if ((rt || lf) && adjDir != 0) {
if (--adjTimer <= 0) {
adjTimer = 4;
/* D356: reset actions are edge-triggered -- the held-repeat
* re-fires adjust rows but must never re-fire a reset (a held
* key could arm AND confirm, or commit repeatedly). */
if (!isResetRow(&rows[s_visIdx[s_sel]]))
if (s_section >= 0 && s_sel != 0 && !isResetRow(&rows[s_visIdx[s_sel]]))
rowAdjust(&rows[s_visIdx[s_sel]], adjDir);
}
} else {
@@ -1159,28 +1266,38 @@ void optionsOverlayHandleInput(void)
* new row back into the cursor and made the bottom twitch/echo).
* Selection moves only by click, wheel, or arrows. */
/* left press. A click in the label column only focuses the row; a
* click in the value/control column (>= the bar-span start) changes
* it -- so clicking to select a toggle doesn't also flip it. */
s32 bx0, bx1;
sliderBarSpan(&bx0, &bx1);
/* Clicking the name selects; anywhere to its right changes the
* setting. Reset actions use the whole row. */
if (lmb && !prevLmb) {
if (onClose) {
optionsOverlayToggle(); /* close + configSave */
return;
}
if (hoverVis >= 0 && rows[s_visIdx[hoverVis]].kind != ROW_HEADER) {
if (hoverVis >= 0) {
s_sel = hoverVis; /* explicit click -> select */
overlayUpdateScroll();
if (ox >= bx0) {
struct Row *r = &rows[s_visIdx[hoverVis]];
if (r->kind == ROW_SLIDER && r->found) {
sliderSetFromX(r, ox);
s_dragRow = s_visIdx[hoverVis];
int field = watchSettingsFieldForKey(r->key);
SDL_AtomicSet(&s_dragWatchField, field + 1);
} else {
rowAdjust(r, +1); /* toggle / cycle forward (wraps) */
if (rows[s_visIdx[hoverVis]].kind == ROW_HEADER) {
int hdr = s_visIdx[hoverVis];
if (s_section < 0) { s_section = hdr; s_sel = 1; }
else { s_section = -1; s_sel = 0; optionsResetClear(); }
s_scroll = 0;
overlayUpdateVisible();
if (s_section < 0)
while (s_sel + 1 < s_visN && s_visIdx[s_sel] != hdr) s_sel++;
} else {
int i = s_visIdx[hoverVis];
s32 x0, x1;
if (overlayControlSpan(i, &x0, &x1) && ox >= x0 && ox < x1) {
struct Row *r = &rows[i];
if (r->kind == ROW_SLIDER && r->found &&
ox >= overlayLayout().barX0 && ox <= overlayLayout().barX1) {
sliderSetFromX(r, ox);
s_dragRow = i;
int field = watchSettingsFieldForKey(r->key);
SDL_AtomicSet(&s_dragWatchField, field + 1);
} else {
rowAdjust(r, +1); /* toggle / cycle forward (wraps) */
}
}
}
}
@@ -1197,12 +1314,16 @@ void optionsOverlayHandleInput(void)
s_dragRow = -1;
SDL_AtomicSet(&s_dragWatchField, 0);
}
/* right press in the value column: cycle back / decrement */
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);
/* Right press in the same change region: cycle back. */
if (rmb && !prevRmb && hoverVis >= 0 && !onClose && s_section >= 0) {
int i = s_visIdx[hoverVis];
s32 x0, x1;
if (!isResetRow(&rows[i]) && overlayControlSpan(i, &x0, &x1) &&
ox >= x0 && ox < x1) {
s_sel = hoverVis;
overlayUpdateScroll();
rowAdjust(&rows[i], -1);
}
}
}
@@ -1284,6 +1405,19 @@ static void valueText(int i, char *out, int n)
else snprintf(out, n, "%d FPS", fps);
return;
}
if (calibratedDefault(r) >= 0.0) {
snprintf(out, n, "%d/100", (int)lround(rowFractionAt(r, v) * 100.0));
return;
}
/* Distance sliders are normalized to a 0..100 UI while retaining the
* original percent-based ini format (250% = 50/100). */
if (strcmp(r->key, "Video.DrawDistance") == 0 ||
strcmp(r->key, "Video.LodDistance") == 0) {
int normalized = (int)lround((v - 100.0) / 3.0);
if (normalized < 0) normalized = 0; /* legacy LOD ini < 100 */
snprintf(out, n, "%d/100", normalized);
return;
}
/* D346: integer sliders -- optional raw->display divide + unit suffix. */
if (r->dispDiv > 0) {
v = (double)(int)lround(v / (double)r->dispDiv);
@@ -1320,6 +1454,104 @@ static Gfx *drawTextR(Gfx *gdl, s32 xr, s32 y, const char *str, u32 colour)
return drawText(gdl, xr - measureText(str), y, str, colour);
}
/* The N64 watch pages use Bank Gothic with a dim/bright green palette.
* Reuse that same loaded font here, without invoking watch rendering. */
static struct font *bodyFont(void) { return ptrFontBankGothic; }
static struct fontchar *bodyChars(void) { return ptrFontBankGothicChars; }
static s32 bodyWidth(const char *str)
{
s32 h = 0, w = 0;
textMeasure(&h, &w, (char *)str, bodyChars(), bodyFont(), 0);
return w;
}
static Gfx *drawBody(Gfx *gdl, s32 x, s32 y, const char *str, u32 colour)
{
s32 px = x, py = y;
return textRender(gdl, &px, &py, (char *)str, bodyChars(), bodyFont(),
colour, viGetX(), viGetY(), 0, 0);
}
static Gfx *drawBodyR(Gfx *gdl, s32 xr, s32 y, const char *str, u32 colour)
{
return drawBody(gdl, xr - bodyWidth(str), y, str, colour);
}
/* No per-value boxes: the full strip after the rendered setting name is
* active, even if there's space between its name and its bar/value. Its
* boundary is capped at the label's clipping limit for long names. */
static int overlayControlSpan(int i, s32 *x0, s32 *x1)
{
struct OvLayout o = overlayLayout();
const struct Row *r = &rows[i];
char val[64];
if (r->kind == ROW_HEADER || (r->kind == ROW_SLIDER && !r->found))
return 0;
*x1 = o.right - 7;
if (isResetRow(r)) {
*x0 = o.left + 7; /* reset is an action, not a setting name */
} else {
valueText(i, val, sizeof(val));
s32 limit = r->kind == ROW_SLIDER ? o.barX0 :
o.valueR - bodyWidth(val) - 7;
if (r->restart) limit = o.barX0 - 6;
*x0 = o.labelX + bodyWidth(r->label) + 4;
if (*x0 > limit) *x0 = limit;
}
return *x1 > *x0;
}
/* In the watch-style panel, instructions/scope annotations are deliberately
* a second font and cool-grey ink, not another setting in green Gothic. */
static struct font *metaFont(void)
{
return ptrFontZurichBold && ptrFontZurichBoldChars ? ptrFontZurichBold : bodyFont();
}
static struct fontchar *metaChars(void)
{
return ptrFontZurichBold && ptrFontZurichBoldChars ? ptrFontZurichBoldChars : bodyChars();
}
static s32 metaWidth(const char *str)
{
s32 h = 0, w = 0;
textMeasure(&h, &w, (char *)str, metaChars(), metaFont(), 0);
return w;
}
static Gfx *drawMeta(Gfx *gdl, s32 x, s32 y, const char *str, u32 colour)
{
s32 px = x, py = y;
return textRender(gdl, &px, &py, (char *)str, metaChars(), metaFont(),
colour, viGetX(), viGetY(), 0, 0);
}
/* Label column may be narrow at 320x240. Never paint over a slider/value. */
static int sectionIsMixedScope(int header)
{
if (header < 0) return 0;
int scoped = 0, global = 0;
for (int i = header + 1; i < NUM_ROWS && rows[i].kind != ROW_HEADER; i++) {
if (rows[i].kind == ROW_ACTION) continue;
if (rows[i].saveScoped) scoped = 1;
else global = 1;
}
return scoped && global;
}
static Gfx *drawBodyFit(Gfx *gdl, s32 x, s32 y, s32 maxW, const char *str, u32 colour)
{
char out[80];
snprintf(out, sizeof(out), "%s", str);
if (maxW < 12) return gdl;
if (bodyWidth(out) > maxW) {
int n = (int)strlen(out);
while (n > 1) {
out[--n] = 0;
if (n + 2 < (int)sizeof(out)) {
out[n] = '.'; out[n + 1] = '.'; out[n + 2] = 0;
}
if (bodyWidth(out) <= maxW) break;
out[n] = 0;
}
}
return drawBody(gdl, x, y, out, colour);
}
Gfx *optionsOverlayEmit(void)
{
if (!s_inited) {
@@ -1352,26 +1584,10 @@ Gfx *optionsOverlayEmit(void)
const s32 W = viGetX();
const s32 H = viGetY();
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).
* 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 - 1
: s_scroll + maxV - 1;
const s32 panelBottom = OV_ROW_Y(pLast) + OV_LINE / 2 + 3;
s32 bx0, bx1;
sliderBarSpan(&bx0, &bx1);
struct OvLayout o = overlayLayout();
int pLast = s_scroll + o.maxRows - 1;
if (pLast >= s_visN) pLast = s_visN - 1;
const s32 bx0 = o.barX0, bx1 = o.barX1;
Gfx *gdl = s_buf;
gDPPipeSync(gdl++);
@@ -1379,84 +1595,107 @@ Gfx *optionsOverlayEmit(void)
gDPSetTexturePersp(gdl++, G_TP_NONE);
gDPSetScissor(gdl++, G_SC_NON_INTERLACE, 0, 0, W, H);
/* ---- pass 1: all fills (G_CC_PRIMITIVE) ---- */
gdl = fillRect(gdl, 0, 0, W, H, 0, 0, 0, 150); /* dim */
gdl = fillRect(gdl, OV_X0 - 8, panelTop, W - (OV_X0 - 8), panelBottom,
8, 10, 24, 210); /* panel */
gdl = fillRect(gdl, OV_CB_X0, OV_CB_Y0, OV_CB_X1, OV_CB_Y1,
150, 40, 40, 235); /* close */
/* Dark glass like the previous F10 overlay; green ink and highlights
* follow GE's watch options (options.c's Bank Gothic/0xA0FFA0F0).
* No dossier paper, gold, 3D watch model or new assets. */
gdl = fillRect(gdl, 0, 0, W, H, 0, 0, 0, 150);
gdl = fillRect(gdl, o.left, o.top, o.right, o.bottom,
5, 17, 13, 225);
gdl = fillRect(gdl, o.left + 8, o.top + 25, o.right - 8, o.top + 26,
56, 135, 73, 195);
/* Help is a separate control strip, not another setting row. */
gdl = fillRect(gdl, o.left + 3, o.footerY - 5, o.right - 3, o.bottom - 2,
1, 9, 7, 235);
gdl = fillRect(gdl, o.left + 8, o.footerY - 5, o.right - 8, o.footerY - 4,
55, 102, 68, 170);
/* No row panels or control boxes. Selection is the brighter text in
* pass 2; only a slider's actual track/knob gets a background. */
for (int p = s_scroll; p <= pLast; p++) {
const struct Row *r = &rows[s_visIdx[p]];
s32 rowY = OV_ROW_Y(p - s_scroll);
if (p == s_sel) {
gdl = fillRect(gdl, OV_X0 - 4, rowY - 3, W - (OV_X0 - 4),
rowY + OV_LINE - 4, 40, 46, 96, 220);
}
s32 rowY = o.contentY + (p - s_scroll) * OV_LINE;
if (r->kind == ROW_SLIDER && r->found) {
double lo = rowLo(r), hi = rowHi(r);
double f = (hi > lo) ? (rowGet(r) - lo) / (hi - lo) : 0.0;
if (f < 0) f = 0; if (f > 1) f = 1;
s32 by = rowY + 3;
gdl = fillRect(gdl, bx0, by, bx1, by + 5, 60, 60, 70, 220);
gdl = fillRect(gdl, bx0, by, bx0 + (s32)((bx1 - bx0) * f), by + 5,
210, 200, 90, 255);
double f = rowFractionAt(r, rowGet(r));
s32 by = rowY + 3; /* INSIDE the row's hit band, not the next row */
gdl = fillRect(gdl, bx0, by, bx1, by + 2, 31, 68, 44, 205);
gdl = fillRect(gdl, bx0, by, bx0 + (s32)((bx1 - bx0) * f), by + 2,
59, 200, 87, 255);
s32 knob = bx0 + (s32)((bx1 - bx0) * f);
gdl = fillRect(gdl, knob - 1, by - 2, knob + 2, by + 4,
160, 255, 160, 255);
}
}
/* ---- pass 2: text ---- */
gdl = microcode_constructor(gdl);
gdl = drawText(gdl, OV_X0, OV_TOP, "PC OPTIONS", 0xffe040ff);
gdl = drawText(gdl, OV_X0, OV_TOP + OV_LINE,
"select: scroll/click, change: mouse/arrows",
0x8890a0ff); /* hint line */
gdl = drawText(gdl, (OV_CB_X0 + OV_CB_X1) / 2 - measureText("X") / 2,
OV_TOP, "X", 0xffffffff); /* close glyph */
gdl = drawBody(gdl, o.left + 10, o.top + 8, "PC OPTIONS", 0xa0ffa0ff);
gdl = drawBodyR(gdl, o.right - 14, o.top + 8, "X", 0xa0ffa0ff);
if (s_section < 0) {
gdl = drawMeta(gdl, o.labelX, o.sectionY, "SELECT A CATEGORY", 0x829e91ff);
} else {
const struct Row *hdr = &rows[s_section];
gdl = drawBody(gdl, o.left + 12, o.sectionY, "< BACK", 0xa0ffa0ff);
s32 titleX = o.left + 80;
gdl = drawBody(gdl, titleX, o.sectionY, hdr->label, 0xa0ffa0ff);
if (hdr->saveScoped) {
int f = watchSettingsActiveFolder();
char note[24];
if (f < 0) snprintf(note, sizeof(note), "(none)");
else snprintf(note, sizeof(note), "(Profile %d)", f + 1);
gdl = drawBody(gdl, titleX + bodyWidth(hdr->label) + 5,
o.sectionY, note, 0x829e91ff);
} else if (!strcmp(hdr->key, "__HdrInput")) {
gdl = drawBody(gdl, titleX + bodyWidth(hdr->label) + 5,
o.sectionY, "(50 = default)", 0x829e91ff);
}
}
for (int p = s_scroll; p <= pLast; p++) {
const struct Row *r = &rows[s_visIdx[p]];
s32 rowY = OV_ROW_Y(p - s_scroll);
u32 col = (p == s_sel) ? 0xffffffff : 0xc0c0c8ff;
s32 rowY = o.contentY + (p - s_scroll) * OV_LINE;
u32 ink = !r->found ? 0x498053ff :
p == s_sel ? 0xa0ffa0ff : 0x66ca77ff;
char val[48];
if (r->kind == ROW_HEADER) { /* D237 category label, no value */
gdl = drawText(gdl, OV_X0, rowY, (char *)r->label, 0xffe040ff);
/* D356: sections that contain per-file rows carry the active
* file in the header ("GAMEPLAY (File 1)"), PD-style -- in-stage
* the active file, front end the resolved front target. */
if (r->saveScoped) {
int f = watchSettingsActiveFolder();
char note[16];
if (f < 0) snprintf(note, sizeof(note), "(none)");
else snprintf(note, sizeof(note), "(Profile %d)", f + 1);
gdl = drawText(gdl, OV_X0 + measureText(r->label) + 6, rowY, note, 0x909090ff);
}
if (s_section < 0) {
gdl = drawBody(gdl, o.labelX, rowY, r->label, ink);
continue;
}
gdl = drawText(gdl, OV_LABEL_X, rowY, (char *)r->label,
r->found ? col : 0x808080ff);
/* D357: per-file rows in the mixed-scope section carry a dim
* "(per profile)" tag -- "stored separately in each profile";
* the section title says which one is active. */
if (r->saveScoped && r->kind != ROW_HEADER) {
gdl = drawText(gdl, OV_LABEL_X + measureText(r->label) + 5, rowY,
"(per profile)", 0x909090ff);
valueText(s_visIdx[p], val, sizeof(val));
s32 valueW = bodyWidth(val);
s32 labelEnd = r->kind == ROW_SLIDER ? bx0 : o.valueR - valueW - 7;
if (r->restart) labelEnd = bx0 - 6;
gdl = drawBodyFit(gdl, o.labelX, rowY, labelEnd - o.labelX,
r->label, ink);
/* Only GAMEPLAY mixes profile and global values. Its four profile
* toggles have space for a real, subdued word; AUDIO is wholly
* profile-scoped and says so in its section title. */
if (r->saveScoped && sectionIsMixedScope(s_section)) {
s32 tagX = o.labelX + bodyWidth(r->label) + 4;
if (tagX + bodyWidth("(profile)") < labelEnd - 2)
gdl = drawBody(gdl, tagX, rowY, "(profile)", 0x829e91ff);
}
if (!r->found) {
gdl = drawTextR(gdl, right, rowY, "(n/a)", 0x808080ff);
gdl = drawBodyR(gdl, o.valueR, rowY, "(n/a)", 0x498053ff);
continue;
}
valueText(s_visIdx[p], val, sizeof(val));
if (r->restart) {
/* value left of the bar span, "(restart)" pinned to the edge */
gdl = drawText(gdl, bx0, rowY, val, col);
gdl = drawTextR(gdl, right, rowY, "(restart)", 0x909090ff);
gdl = drawBody(gdl, bx0, rowY, val, ink);
gdl = drawBodyR(gdl, o.valueR, rowY, "(restart)", 0x498053ff);
} else {
gdl = drawTextR(gdl, right, rowY, val, col);
u32 valueInk = (strcmp(val, "On") == 0 ||
(isResetRow(r) && strcmp(val, "Confirm") == 0))
? 0xa0ffa0ff : ink;
gdl = drawBodyR(gdl, o.valueR, rowY, val, valueInk);
}
}
gdl = drawMeta(gdl, o.left + 11, o.footerY, "CONTROLS:", 0xa6baaaff);
gdl = drawBody(gdl, o.left + 16 + metaWidth("CONTROLS:"), o.footerY,
s_section < 0 ? "ENTER SELECT F10 CLOSE" :
"RIGHT: CHANGE ESC BACK F10 CLOSE", 0x829e91ff);
if (s_visN - (s_section >= 0 ? 1 : 0) > o.maxRows)
gdl = drawBodyR(gdl, o.right - 10, o.sectionY,
s_scroll > (s_section >= 0 ? 1 : 0) ? "^ v" : "v",
0x80d58bff);
gDPPipeSync(gdl++);
gSPEndDisplayList(gdl++);
@@ -1541,8 +1780,7 @@ double optionsRowFraction(int i)
if (!r || rowHi(r) <= rowLo(r)) {
return 0.0;
}
double f = (rowGet(r) - rowLo(r)) / (rowHi(r) - rowLo(r));
return f < 0.0 ? 0.0 : (f > 1.0 ? 1.0 : f);
return rowFractionAt(r, rowGet(r));
}
/* D356: raw value (the fraction accessors are bar geometry only); the
@@ -1562,7 +1800,7 @@ void optionsRowSetFraction(int i, double f)
if (f < 0.0) f = 0.0;
if (f > 1.0) f = 1.0;
double step = (r->step > 0.0) ? r->step : 1.0;
double v = rowLo(r) + f * (rowHi(r) - rowLo(r));
double v = rowValueAtFraction(r, f);
rowSetCommit(r, lround(v / step) * step, 0);
}
+21 -22
View File
@@ -51,12 +51,12 @@ static struct GfxRenderingAPI *renderingAPI;
static int initDone = 0;
/*
* [Video] ge007.ini knobs. Every default reproduces the previously-hardcoded
* behaviour, so a fresh config or a missing [Video] section changes nothing.
* [Video] ge007.ini knobs. Draw/LOD and MSAA defaults favour a clean picture
* at modern resolutions without requiring 4x anti-aliasing.
*/
static int cfgVSync = 1; /* swap interval: 0 = off, 1 = on */
static int cfgFpsCap = 60; /* frame cap in fps; 0 = uncapped (vsync); menu only exposes 30/60 */
static int cfgMSAA = 4; /* 1/2/4/8 samples; default 4 (modern ports ship AA on; snaps down to the highest supported level) */
static int cfgMSAA = 2; /* 1/2/4/8 samples; 2x default is lighter on low-end GPUs */
static int cfgTexFilter = 1; /* 0 = nearest, 1 = bilinear (default), 2 = N64 3-point + trilinear */
static int cfgFixMipTex = 1; /* RC2: clip mip-contaminated texture uploads to base height */
static int cfgDetailBaseTile = 1; /* D236: TEXTURETYPE_DETAIL -> sample the base image, not the detail tile */
@@ -65,10 +65,10 @@ static int cfgFovScale = 100; /* D211: percent of the original vertical FOV
static int cfgWidescreenAuto = 1; /* WIDESCREEN-FOV-PLAN Phase 4: auto-scale vertical FOV by window aspect ratio; on by default, no-op at 4:3 */
static int cfgNativeWidescreen = 1; /* D334 (WIDESCREEN-FOV-PLAN Phase 2): project the world at the real window aspect (Hor+); no-op at 4:3 */
static int cfgHudScale = 100; /* D226: HUD text/ammo scale %, 100 = original (no emission) */
static int cfgDrawDistance = 150; /* D218: percent of the level's authored far-clip/fog distance. Default raised 100->150 for v0.2.0: at the authored N64 distance, props visibly fade in just before they become visible on modern displays (Dam alarms / wall switches); 150 is the value the Steam Deck preset playtest-validated. 100 = unchanged N64. */
static int cfgDrawDistanceAutoFov = 1; /* D218: couple draw distance to Video.FovScale unless DrawDistance is set explicitly */
static int cfgLodDistance = 150; /* D249: percent scale on the geometry/model LOD-swap distance. Default raised 100->150 for v0.2.0 (same pop-in family as DrawDistance: LOD-swapped props like Dam's alarms/wall switches faded in at range); 150 matches the Steam Deck preset. 100 = unchanged N64. */
static int cfgLodDistanceAutoFov = 0; /* off by default -- unlike DrawDistance, this is meant as a standalone perf lever, not something that should silently get more expensive as FovScale widens */
static int cfgDrawDistance = 250; /* % of authored far clip; 250% is the UI's 50/100 midpoint */
static int cfgDrawDistanceAutoFov = 0; /* legacy ini option, no longer exposed in the menu */
static int cfgLodDistance = 250; /* % of authored geometry LOD distance; 50/100 in the UI */
static int cfgLodDistanceAutoFov = 0; /* legacy ini option, no longer exposed in the menu */
static int cfgAniso = 4; /* D212: anisotropic filtering samples; 4 = the value fast3d already applied (no visual delta at default) */
static int cfgSafeAreaCrop = 1; /* crop the N64 TV-overscan safe-area margin (visible as black top/bottom bars on PC) instead of showing it; on by default */
static int cfgFullscreen = 0; /* 0 = windowed, 1 = borderless fullscreen */
@@ -200,7 +200,7 @@ f32 portScaleFovY(f32 fovy, s32 isTitleScreen)
* both the far clip plane and the fog-saturation distance (levels are tuned
* for the stock ~60deg FOV), plus the character/prop fog-visibility-fade
* cutoff (src/game/propobj.c chrobjFogVisRangeRelated/sub_GAME_7F054C58).
* Video.DrawDistanceAutoFov (default on) couples the multiplier to
* Video.DrawDistanceAutoFov (legacy ini option, default off) couples the multiplier to
* Video.FovScale so a wider FOV doesn't clip its own newly visible far
* geometry -- or fade NPCs out early -- against distances tuned for the
* narrower original view (D218's "blue artifacting"; the M-121 live
@@ -213,7 +213,7 @@ f32 portScaleFovY(f32 fovy, s32 isTitleScreen)
* glow" on far Dam tunnel geometry, so auto coupling is now 4x FovScale)
* to give headroom (max portFovScale is 1.5 at Video.FovScale's registered
* ceiling of 150, so 4x tops out at 6.0x -- ceiling raised to match).
* Identity (1.0f) at both defaults, bit-for-bit no-op. NOTE: end-to-end
* Identity (1.0f) at DrawDistance=100 with auto-FOV off. NOTE: end-to-end
* re-check of fogLoadCurrentEnvironment (bgfog.c) found the fog RAMP
* itself (not just the far-clip cutoff) already scales correctly with
* this multiplier -- g_ScaledFarFogIntensity/scaled_far_fog_dist both
@@ -244,14 +244,14 @@ f32 portDrawDistanceMultiplier(void)
* "closer", so this function returns the INVERSE of the requested percent:
* Video.LodDistance=200 (keep full detail twice as far, more cost) ->
* 0.5x on the distance term; =50 (drop to lower detail twice as soon, less
* cost -- the perf lever) -> 2.0x. Video.LodDistanceAutoFov (default OFF,
* unlike DrawDistanceAutoFov) can couple it to Video.FovScale the same
* cost -- the perf lever) -> 2.0x. Video.LodDistanceAutoFov (legacy ini
* option, default OFF) can couple it to Video.FovScale the same
* direction as draw distance if ever wanted; off by default so this stays a
* standalone dial and doesn't quietly add cost as FovScale widens. Clamped
* to a [0.25, 4.0] distance multiplier (== effective LodDistance 25-400%) --
* far outside that band either does nothing visible (LOD never triggers) or
* thrashes every frame. Identity (1.0f) at the Video.LodDistance=100
* default, bit-for-bit no-op. */
* thrashes every frame. Identity (1.0f) at Video.LodDistance=100
* with auto-FOV off. */
f32 portLodDistanceMultiplier(void)
{
f32 pct;
@@ -376,10 +376,9 @@ PD_CONSTRUCTOR static void videoConfigInit(void)
* first-run path saves these values to disk and every later launch reads the
* file (user changes via F10 win outright); if an ini already exists its
* values overwrite everything here. So this is a first-launch preset only.
* 1280x800 is the Deck's native panel resolution; MSAA 4 + VSync is
* comfortable headroom for the A11 GPU; DrawDistance/LodDistance at 150%
* because the authored N64 fade distances read "things pop in just before
* you can see them" on a sharp 7" close-up panel (e.g. the Dam lock).
* 1280x800 is the Deck's native panel resolution; 2x MSAA + VSync keep
* GPU cost modest. Draw/LOD distances use the standard midpoint (250%)
* to avoid obvious N64-era pop-in on modern displays.
* Note: the panel is 16:10 and the game renders 4:3, so this stretches
* uniformly like any non-4:3 window today (letterboxing is the parked
* WIDESCREEN-FOV-PLAN). */
@@ -389,9 +388,9 @@ void videoApplySteamOSDefaults(void)
cfgWinW = 1280;
cfgWinH = 800;
cfgVSync = 1;
cfgMSAA = 4;
cfgDrawDistance = 150;
cfgLodDistance = 150;
cfgMSAA = 2;
cfgDrawDistance = 250;
cfgLodDistance = 250;
}
/* Set by videoRequestLiveConfig() (F10 overlay, host thread); consumed on the
@@ -742,12 +741,12 @@ void videoPumpEvents(void)
optionsOverlayToggle();
}
} else if (ev.key.keysym.sym == SDLK_ESCAPE && !ev.key.repeat) {
/* Overlay open: ESC closes it (and is swallowed). Otherwise
/* Overlay open: ESC backs out of a category, then closes (swallowed). Otherwise
* WI-1: in click-to-lock mode ESC frees the captured cursor
* (and is swallowed); else it falls through to input.c where
* it feeds the N64 B button (D145). */
if (optionsOverlayIsOpen()) {
optionsOverlayToggle();
optionsOverlayBack();
} else {
inputReleaseCapture();
}
+61 -10
View File
@@ -67,12 +67,18 @@ static int staged[WATCH_SETTING_COUNT];
static int stagedField = -1, stagedFolder = -1;
static SDL_SpinLock uiLock;
/* Bounded producer (F10 scheduler) -> consumer (game thread) queue. Snapshot
* is also guarded; no game/watch reads from F10 render or input paths. */
/* Bounded producer (F10 controller poll) -> consumer (game thread) queues.
* Snapshot is also guarded; F10 input/render never call GE save routines. */
struct Command { int folder, field, value, commit; };
#define CMD_CAP 128
static struct Command cmds[CMD_CAP];
static int head, count;
/* Front-end F10 also runs on the controller poll thread. fileWriteSave()
* calls joyDisablePoll() and waits for that SAME thread to acknowledge it:
* writing here deadlocks. Keep front commits separate from stage commands;
* the game-thread tick drains them without touching the controller thread. */
static struct Command frontCmds[CMD_CAP];
static int frontCount;
static int snapshot[WATCH_SETTING_COUNT];
static int snapFolder = -1;
static SDL_SpinLock lock;
@@ -159,8 +165,8 @@ int watchSettingsActiveFolder(void)
return validSave(f) ? f : -1;
}
/* D356: queued field commands awaiting the game thread (0 in front
* contexts -- nothing is queued there). Drives the in-stage reset probe's
/* D356: stage commands awaiting the game thread (front F10 uses a separate
* queue). Drives the in-stage reset probe's
* dispatch check (plan §5.8). */
int watchSettingsQueueCount(void)
{
@@ -344,14 +350,39 @@ void watchSettingsSet(enum WatchSettingField field, int value, int commit)
if (!stageActive()) {
int f = frontFolder();
SDL_AtomicLock(&uiLock);
if (commit) {
stagedField = -1;
SDL_AtomicUnlock(&uiLock);
saveFrontField(f, field, value);
} else {
stagedField = field; stagedFolder = f; staged[field] = value;
if (!optionsOverlayIsOpen()) {
/* The front options screen and dev probes run on the game thread. */
if (commit) stagedField = -1;
else { stagedField = field; stagedFolder = f; staged[field] = value; }
SDL_AtomicUnlock(&uiLock);
if (commit) saveFrontField(f, field, value);
return;
}
/* F10 is polled from joyPoll() via osContStartReadData. Even a
* fileGamePakProbe here deadlocks: joyDisablePoll waits for joyPoll's
* acknowledgement. Stage the value for UI feedback and enqueue ONLY
* under a short lock; the game thread does the EEPROM I/O. */
stagedField = field; stagedFolder = f; staged[field] = value;
SDL_AtomicUnlock(&uiLock);
SDL_AtomicLock(&lock);
int i;
for (i = 0; i < frontCount; i++) {
if (frontCmds[i].folder == f && frontCmds[i].field == (int)field) break;
}
if (i == frontCount && frontCount >= CMD_CAP) {
SDL_AtomicUnlock(&lock);
sysLogPrintf(LOG_WARNING, "watchsettings: front command queue full; edit not applied");
return;
}
if (i == frontCount) {
frontCount++;
frontCmds[i].commit = 0;
}
frontCmds[i].folder = f;
frontCmds[i].field = field;
frontCmds[i].value = value;
frontCmds[i].commit = commit || frontCmds[i].commit;
SDL_AtomicUnlock(&lock);
return;
}
SDL_AtomicLock(&lock);
@@ -446,6 +477,26 @@ static void applyValue(enum WatchSettingField field, int value)
void watchSettingsGameTick(void)
{
/* Drain F10 front-end commits on the game thread. This MUST run before
* the early return for non-stage contexts below. Never hold the queue
* lock through fileWriteSave/joyDisablePoll: joyPoll must stay runnable. */
struct Command frontBatch[CMD_CAP];
int nf;
SDL_AtomicLock(&lock);
nf = frontCount;
memcpy(frontBatch, frontCmds, (size_t)nf * sizeof(frontBatch[0]));
frontCount = 0;
SDL_AtomicUnlock(&lock);
for (int i = 0; i < nf; i++) {
struct Command *c = &frontBatch[i];
if (c->commit) saveFrontField(c->folder, (enum WatchSettingField)c->field, c->value);
SDL_AtomicLock(&uiLock);
if (stagedFolder == c->folder && stagedField == c->field &&
staged[c->field] == c->value && c->commit)
stagedField = -1;
SDL_AtomicUnlock(&uiLock);
}
/* D356: the GE_WSPROBE_RESET env gate drives the real reset UI dispatch
* (arm -> confirm -> handler, no bypass) through optionsoverlay.c's
* probe hooks. Each phase waits for its context to be stable (~120