mirror of
https://github.com/jkdansereau/goldeneye-pc-port.git
synced 2026-10-23 15:52:42 +02:00
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:
1 parent
d8971fded0
commit
c2c6fbdd68
10 files changed
+1036
-266
No files matched your search
@@ -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
@@ -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.
|
||||
@@ -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):
|
||||
|
@@ -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.
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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")) {
|
||||
|
||||
+463
-225
@@ -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,105 +348,93 @@ 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. */
|
||||
/* 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 != 0)) {
|
||||
!(rows[i].hidePtr && *rows[i].hidePtr))
|
||||
s_visIdx[s_visN++] = i;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
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,24 +1266,33 @@ 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) {
|
||||
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 = s_visIdx[hoverVis];
|
||||
s_dragRow = i;
|
||||
int field = watchSettingsFieldForKey(r->key);
|
||||
SDL_AtomicSet(&s_dragWatchField, field + 1);
|
||||
} else {
|
||||
@@ -1185,6 +1301,7 @@ void optionsOverlayHandleInput(void)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/* drag a slider */
|
||||
if (lmb && s_dragRow >= 0 && rows[s_dragRow].kind == ROW_SLIDER) {
|
||||
sliderSetFromX(&rows[s_dragRow], ox);
|
||||
@@ -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) {
|
||||
/* 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[s_visIdx[hoverVis]], -1);
|
||||
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
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
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);
|
||||
saveFrontField(f, field, value);
|
||||
} else {
|
||||
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
|
||||
|
||||
Reference in new issue
Block a user