port: D184 — F10 port-layer options overlay (approach C)

New port/src/optionsoverlay.c + port/include/optionsoverlay.h: an
immediate-mode options overlay drawn as its own fast3d 2D display list,
appended after the game DL. Implements docs/dev/OPTIONS-MENU-PLAN.md §2/§3.

- config.c/h: configForEachOption() + configSetOptionMeta() label/step/enum
  side table; CONFIG_OPT_* enum. config.c stays free of specific-key knowledge.
- gfx_pc.cpp gfx_run(): one-line hook runs optionsOverlayEmit() after the game
  DL, before gfx_flush. Returns NULL when closed -> zero bytes appended, golden
  dumps byte-identical.
- video.c: F10 toggles (videoPumpEvents, next to F12); ESC closes; wheel
  scrolls; videoRequestLiveConfig()/liveCfgDirty re-applies VSync/FpsCap/
  TextureFilter at frame start.
- input.c: while open, controller 0 returns a neutral pad and nav
  (arrows/enter/wheel/mouse) routes to optionsOverlayHandleInput() — mirrors
  the D180 WI-1 swallow pattern.
- v1 rows: VSync, FpsCap, MSAA(restart), TextureFilter, MouseAimSpeed,
  MouseTurnSpeed, MouseInvertY, MouseCaptureMode, ScreenShakeIntensity.
  Live-apply where possible; configSave() on close.
- GE_OPTIONSOVERLAY=1 auto-opens at boot (env-gated diagnostic, left in).

Port-layer only, no src/ edits, no #ifdef PORT in game code. Build links
241/241. Runtime framediff/soak/eyeball owed on an interactive machine
(this headless env kills the GUI process early and writes 0-byte PPMs).
This commit is contained in:
James Dansereau committed 2026-09-02 19:03:37 -04:00
1 parent 7e2e371a0a
commit 2f5d7f9063
10 files changed
+687 -16

No files matched your search

+1
View File
@@ -400,6 +400,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
| D179 | **Packaged build crashes after the logos — the D43/D69 model + bg sidecars are ROM-derived and absent from any build that has no ROM (M-34, alpha-release QA).** The first `goldeneye-pc-port-*-win64.zip` from CI (bundle-win.sh) shows the Rare/Nintendo logos (compiled-in `assets/rarewarelogo.c`) then AVs at `0x6b157a88`/`0x70157a88` — symbolised: `modelPromoteNodeOffsetsToPointers` (`model.c`) ← `load_object_fill_header` (`objecthandler_2.c`) ← first prop/item model load. Root cause is **not** a code regression: the port loads PC-layout model geometry from `data/pcmodels-<region>/{pcmodels.bin,manifest.csv}` and stage bg/stan from `data/pccg-<region>/{pccg.bin,manifest.csv}`, both produced offline by `tools_pc/d43_emit.py` / `d69_emit.py` from the ROM (see `port/src/pcmodels.c` / `pccg.c`; `pcmodelsReserveSize` logs *"pcmodels.bin not found — model loads will fail"* and returns 0). CI has no ROM so it never runs the emit scripts, and the two `data/` dirs are (correctly) gitignored ROM-derived game data — **cannot be committed or shipped** (converted Nintendo/Rare geometry, DLs, collision, stan nav; = distributing assets). A local build with those dirs present runs clean (verified M-34: 1741 frames, `romdataInit ... mapped at 0x10000000`, `pcmodels: 512 sidecars`, `pccg: 73 sidecars`, no crash — so `0x10000000` is *not* the problem here). The `docs/building.md` sidecar-gen step was also missing entirely. **Fix = release-side, backlogged** (`docs/BACKLOG.md` → "Alpha release"): bundle the emit scripts + their committed inputs as a pure-stdlib-Python asset-prep tool the user runs once against their own ROM (~5 MB output, no MIPS toolchain). Secondary/latent: the fixed-address `VirtualAlloc((LPVOID)0x10000000)` in `romdata.c` has no working fallback (`"using heap copy — direct ROM reads will fail"` then limps into the same crash) — didn't bite M-34 but will on a machine where something occupies `0x10000000`; harden separately (reserve earliest in `main`, or retry low bases and derive all segment math from the base obtained). | Diagnosed, not fixed — release packaging gap + latent `romdata.c` fallback |
| D180 | **Native-PC input pass (M-XX QoL run, `qol/native-pc-input-menu`, port-only).** Three parts, all config-gated with default = prior behaviour: **(WI-1) `Input.MouseCaptureMode`** (0 = legacy always-grab; 1 = Quake-style click-to-lock) in `port/src/input.c` + `port/src/video.c` — in mode 1 the OS cursor is free until a click lands in the game window (`SDL_MOUSEBUTTONDOWN` → `inputNotifyClick`), and ESC / focus-loss / entering any front-end menu (`current_menu != MENU_RUN_STAGE`) frees it again; re-entering a stage while still "armed" re-locks so unpausing needs no click. `reconcileGrab()` runs once per controller-0 poll. Mouse buttons are suppressed from the game while the cursor is free in-stage (no phantom fire). Controller paths untouched. **(WI-2) absolute-cursor menu tracking** — when capture mode is on and the cursor is free in a menu, the D165/D169 pointer P-controller takes its target from the real cursor's absolute window position mapped onto the live virtual front-end rect (`getPlayer_c_screen*`), giving true 1:1 tracking instead of the relative-delta estimator. Relative path unchanged for legacy/grabbed. **(B3) `Input.MouseAimSpeed` default 25 → 16** (aim mode still overshot at 25 per the backlog). | LANDED, port-only, no `#ifdef PORT`. Build 241/241, `-level_09` framediff 3/3 within threshold (unregressed), `GE_STARTMENU` menu boot crash-free. **Feel-checks owed** (headless can't drive the mouse): click-to-lock ergonomics, absolute menu tracking, the new aim-speed default. |
| D181 | **`Game.ScreenShakeIntensity` — first route-(b) `src/` gameplay-cosmetic hook (M-XX QoL run).** `src/fr.c viShake()` gains a single `#ifdef PORT` line — `intensity *= portScreenShakeScale;` (extern `f32`, defined + `configRegisterFloat`'d 0.0–10.0 in `port/src/video.c`) — before the existing clamp. Default `1.0f` ⇒ exact no-op, every headless golden dump byte-unaffected; `0.0` disables explosion/effect screen shake, up to `10.0` exaggerates it. N64 build (no `-DPORT`) keeps the original line verbatim. Precedent per `docs/BACKLOG.md` "Fun features → screen-shake intensity slider" and AGENTS.md #2's documented-exception path. Same policy class as a future FOV hook. | LANDED behind config, default = original. Build green. |
| D184 | **F10 port-layer options overlay — the approach-(C) surface from `OPTIONS-MENU-PLAN.md` (M-37).** New `port/src/optionsoverlay.c` + `port/include/optionsoverlay.h`: an immediate-mode overlay drawn as its own fast3d 2D DL, appended after the game DL in `gfx_pc.cpp gfx_run()` (`optionsOverlayEmit()` → NULL when closed ⇒ **zero bytes appended, golden dumps byte-identical**). F10 toggles (`video.c videoPumpEvents`, next to F12); while open, `input.c inputComputePad(0)` returns a neutral pad and routes arrows/enter/wheel/mouse-buttons to `optionsOverlayHandleInput()` (mirrors the WI-1/D180 swallow). ESC closes; `configSave()` on close. `config.c` gains `configForEachOption()` + a `configSetOptionMeta()` label/step/enum side table (no per-key knowledge in config.c). Live knobs (VSync/FpsCap/TextureFilter via `videoRequestLiveConfig()`; mouse + screen-shake via the registered pointer) apply immediately; MSAA tagged "(restart)". v1 rows per plan §3: VSync, FpsCap, MSAA, TextureFilter, MouseAimSpeed, MouseTurnSpeed, MouseInvertY, MouseCaptureMode, ScreenShakeIntensity. | **LANDED, port-only, no `#ifdef PORT` in `src/`.** Build links 241/241. **Runtime verification NOT run this session** — the headless env kills the GUI process after ~8 s and `gfx_opengl_dump_bound_fbo` writes 0-byte PPMs here, so framediff / 60 s soak / overlay-frame eyeball are all owed on an interactive machine. Overlay-closed no-op is code-evident (emit returns NULL before touching the DL buffer). |
| D85 | room primary/secondary DL → `texLoadFromGdl` garbage | OPEN (safety-netted; widen/pool fixes landed, geometry now renders) |
| D86 · D87 | modelInitRwData truncated ptr · attract-demo BE `ramromfilestructure` | resolved |
| D88.1–D88.3 · D88.5–D88.6 | `Usetup*Z` header + sub-table width/endian conversion (`d88_emit.py`) | resolved |
+16
View File
@@ -387,6 +387,22 @@ through a converter or a runtime bswap fixup reads scrambled.
## D. N64 hardware idioms fast3d does not emulate
- **Appending a port-owned 2D overlay to the game frame: hook inside
`gfx_run()`, not `videoSubmitCommands`.** `gfx_run()` (`gfx_pc.cpp`) is
monolithic — it does `start_frame` → `gfx_run_dl` → `gfx_flush` →
`end_frame` → `swap_buffers_begin` in one call, and the game DL is executed
from `osSpTaskStartGo` (`libultra.c`), *not* through `videoSubmitCommands`
(which is dead). So a second `gfx_run()` per frame is not possible (it swaps
buffers). The clean seam is a one-line C hook between `gfx_run_dl(commands)`
and `gfx_flush()` that runs a second `gfx_run_dl()` on a port-built DL when
non-NULL (D184, F10 options overlay). The overlay DL is self-sufficient:
`gDPPipeSync` + `gDPSetCycleType(G_CYC_1CYCLE)` + `gDPSetTexturePersp(G_TP_NONE)`
+ a full-screen `gDPSetScissor` + `microcode_constructor()` (the game's own
2D combiner/rendermode prologue), then `gDPFillRectangle` / `textRender`
(game symbols, extern'd like `input.c` externs `current_menu` — UI, not
logic). 2D pixel space is `viGetX()` x `viGetY()`. Return NULL when the
overlay is closed so the frame is byte-for-byte unchanged.
- Z buffer cleared by pointing the colour image at it + fill-rect → does
nothing in fast3d; must emit `G_CLEAR_DEPTH_EXT` (D105).
- LOD / detail mip tiles: fast3d fabricates a crop when detail textures
+12
View File
@@ -3068,6 +3068,12 @@ extern "C" void gfx_start_frame(void) {
uint32_t num_dls = 0;
/* F10 port-layer options overlay (port/src/optionsoverlay.c). Returns a
* self-contained 2D display list to draw on top of the game's frame, or NULL
* when the overlay is closed -- in which case nothing is appended and the
* frame is byte-identical to before (golden dumps unaffected). */
extern "C" Gfx* optionsOverlayEmit(void);
extern "C" void gfx_run(Gfx* commands) {
++num_dls;
gfx_sp_reset();
@@ -3091,6 +3097,12 @@ extern "C" void gfx_run(Gfx* commands) {
rendering_state.viewport = {};
rendering_state.scissor = {};
gfx_run_dl(commands);
{
Gfx* overlay = optionsOverlayEmit();
if (overlay != nullptr) {
gfx_run_dl(overlay);
}
}
gfx_flush();
gfxFramebuffer = 0;
+33
View File
@@ -33,6 +33,39 @@ void configRegisterString(const char *key, char *value, int bufSize);
const char *configGetFrameDump(void); /* "lo-hi[:step]" or NULL if unset */
int configGetInputLog(void);
/* ------------------------------------------------------------------------
* Option enumeration (for the F10 port-layer options overlay).
*
* configForEachOption() walks every registered option; configSetOptionMeta()
* attaches display metadata (label / adjust step / enum-value names) keyed by
* the dotted key. Meta is an optional side table -- config.c stays ignorant of
* which specific keys exist; the overlay populates it at init time.
* ---------------------------------------------------------------------- */
enum {
CONFIG_OPT_INT = 0,
CONFIG_OPT_UINT,
CONFIG_OPT_FLOAT,
CONFIG_OPT_STR,
};
/* key : dotted option key
* type : CONFIG_OPT_*
* ptr : &int / &unsigned / &float / char* (the live variable)
* min,max : registered clamp bounds (min==max -> unclamped)
* step : adjust step from the meta side table (0 if none)
* label : display label from the meta side table (NULL if none)
* enumNames: NULL-terminated value-name list from the meta side table (or NULL)
* ctx : opaque pointer passed straight through
*/
typedef void (*ConfigOptionCb)(const char *key, int type, void *ptr,
double min, double max, double step,
const char *label, const char *const *enumNames,
void *ctx);
void configForEachOption(ConfigOptionCb cb, void *ctx);
void configSetOptionMeta(const char *key, const char *label, double step,
const char *const *enumNames);
#ifdef __cplusplus
}
#endif
+50
View File
@@ -0,0 +1,50 @@
#ifndef PORT_OPTIONSOVERLAY_H
#define PORT_OPTIONSOVERLAY_H
/*
* F10 in-game options overlay (approach C from docs/dev/OPTIONS-MENU-PLAN.md).
*
* A self-contained port-layer immediate-mode overlay: it draws its own 2D
* display list on top of the game's frame (appended in fast3d's gfx_run,
* after the game DL) and handles its own keyboard/mouse nav. It edits the
* port-owned config.c options directly, so changes apply live for the live
* ones. When closed it renders NOTHING (zero bytes appended) -- golden dumps
* stay byte-identical.
*
* Hooks:
* video.c videoPumpEvents : F10 -> optionsOverlayToggle(); ESC closes;
* wheel -> optionsOverlayScroll()
* input.c inputComputePad : controller 0 swallowed while open;
* nav routed to optionsOverlayHandleInput()
* gfx_pc.cpp gfx_run : optionsOverlayEmit() appended after the game DL
*/
#include <PR/ultratypes.h>
#include <PR/gbi.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Toggle open/closed. On the closing edge the config is saved. */
void optionsOverlayToggle(void);
/* 1 while the overlay is on screen. */
int optionsOverlayIsOpen(void);
/* Called from inputComputePad(0) while open: reads SDL keyboard edges and
* drives the cursor / value adjustments. */
void optionsOverlayHandleInput(void);
/* Mouse-wheel notch -> move the selection (host event pump). */
void optionsOverlayScroll(int dir);
/* Build the overlay's 2D display list for this frame, or return NULL when the
* overlay is closed. Called by fast3d after running the game DL. */
Gfx *optionsOverlayEmit(void);
#ifdef __cplusplus
}
#endif
#endif /* PORT_OPTIONSOVERLAY_H */
+4
View File
@@ -48,6 +48,10 @@ void videoResetTextureCache(void);
/* Current FPS (measured). */
float videoGetFPS(void);
/* Re-apply the live-tunable [Video] knobs (VSync / FpsCap / TextureFilter) on
* the next frame start. Called by the F10 options overlay after an edit. */
void videoRequestLiveConfig(void);
/* Snapshot live window geometry into the config vars (call before configSave
* on a clean exit). No-op if the window isn't up. */
void videoSaveWindowState(void);
+75
View File
@@ -109,6 +109,81 @@ void configRegisterString(const char *key, char *value, int bufSize)
o->bufSize = bufSize;
}
/* ------------------------------------------------------------------------
* Option metadata side table + enumeration (F10 options overlay). config.c
* gains no knowledge of specific keys: the overlay calls configSetOptionMeta()
* for each row it draws, and configForEachOption() joins that against the
* registered options.
* ---------------------------------------------------------------------- */
struct OptionMeta {
char key[64];
const char *label;
double step;
const char *const *enumNames;
};
static struct OptionMeta metaOpts[MAX_OPTIONS];
static int numMetaOpts = 0;
static const struct OptionMeta *findMeta(const char *key)
{
for (int i = 0; i < numMetaOpts; i++) {
if (strcmp(metaOpts[i].key, key) == 0) {
return &metaOpts[i];
}
}
return NULL;
}
void configSetOptionMeta(const char *key, const char *label, double step,
const char *const *enumNames)
{
struct OptionMeta *m = NULL;
for (int i = 0; i < numMetaOpts; i++) {
if (strcmp(metaOpts[i].key, key) == 0) {
m = &metaOpts[i];
break;
}
}
if (!m) {
if (numMetaOpts >= MAX_OPTIONS) return;
m = &metaOpts[numMetaOpts++];
strncpy(m->key, key, sizeof(m->key) - 1);
m->key[sizeof(m->key) - 1] = 0;
}
m->label = label;
m->step = step;
m->enumNames = enumNames;
}
void configForEachOption(ConfigOptionCb cb, void *ctx)
{
if (!cb) return;
for (int i = 0; i < numIntOpts; i++) {
const struct OptionMeta *m = findMeta(intOpts[i].key);
cb(intOpts[i].key, CONFIG_OPT_INT, intOpts[i].value,
(double)intOpts[i].min, (double)intOpts[i].max,
m ? m->step : 0.0, m ? m->label : NULL, m ? m->enumNames : NULL, ctx);
}
for (int i = 0; i < numUIntOpts; i++) {
const struct OptionMeta *m = findMeta(uintOpts[i].key);
cb(uintOpts[i].key, CONFIG_OPT_UINT, uintOpts[i].value,
(double)uintOpts[i].min, (double)uintOpts[i].max,
m ? m->step : 0.0, m ? m->label : NULL, m ? m->enumNames : NULL, ctx);
}
for (int i = 0; i < numFloatOpts; i++) {
const struct OptionMeta *m = findMeta(floatOpts[i].key);
cb(floatOpts[i].key, CONFIG_OPT_FLOAT, floatOpts[i].value,
(double)floatOpts[i].min, (double)floatOpts[i].max,
m ? m->step : 0.0, m ? m->label : NULL, m ? m->enumNames : NULL, ctx);
}
for (int i = 0; i < numStrOpts; i++) {
const struct OptionMeta *m = findMeta(strOpts[i].key);
cb(strOpts[i].key, CONFIG_OPT_STR, strOpts[i].value,
0.0, 0.0, m ? m->step : 0.0, m ? m->label : NULL,
m ? m->enumNames : NULL, ctx);
}
}
static int parseInt(const char *s)
{
while (*s == ' ' || *s == '\t') s++;
+12
View File
@@ -79,6 +79,7 @@
#include "system.h"
#include "config.h"
#include "input.h"
#include "optionsoverlay.h"
/* N64 button bits (from PR/os.h -- duplicated here to avoid pulling os.h,
* whose `u8 errno;` field collides with <errno.h>'s macro). */
@@ -500,6 +501,17 @@ unsigned inputComputePad(int idx, signed char *stick_x, signed char *stick_y)
return 0;
}
/* F10 options overlay: while it is open, controller 0 is fully swallowed
* (neutral pad, no stick) and the nav keys / wheel drive the overlay
* instead. Mirrors the WI-1 "cursor free in a stage -> withhold input"
* pattern. Controllers 1-3 are untouched. */
if (idx == 0 && optionsOverlayIsOpen()) {
optionsOverlayHandleInput();
if (stick_x) *stick_x = 0;
if (stick_y) *stick_y = 0;
return 0;
}
/* ---- keyboard + mouse: controller 0 only ---- */
if (idx == 0) {
const Uint8 *ks = SDL_GetKeyboardState(NULL);
+427
View File
@@ -0,0 +1,427 @@
/*
* F10 in-game options overlay -- approach (C) from docs/dev/OPTIONS-MENU-PLAN.md.
*
* Port-layer only. No src/ menu code is touched: the overlay draws its own
* fast3d 2D display list (appended after the game DL in gfx_run) and edits the
* port-owned config.c variables directly. Live knobs apply immediately; the
* two that need an FBO/window rebuild (MSAA, Fullscreen) are tagged "(restart)".
*
* Text + fill helpers are the game's own (textRender / microcode_constructor /
* gDPFillRectangle) reached by extern -- same pattern input.c uses to read
* current_menu / cursor_h_pos. This is a rendering/UI view, not a logic change.
*
* Diagnostic: set GE_OPTIONSOVERLAY=1 to auto-open at boot (headless layout
* check). Env-gated, harmless when unset.
*/
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <SDL.h>
#include <PR/ultratypes.h>
#include <PR/gbi.h>
#include "platform.h"
#include "system.h"
#include "config.h"
#include "video.h"
#include "optionsoverlay.h"
/* ---- game symbols (rendering/UI only; see input.c for the same pattern) ---- */
struct font;
struct fontchar;
extern struct font *ptrFontBankGothic;
extern struct fontchar *ptrFontBankGothicChars;
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,
u32 yOffset, s32 lineheight);
extern void textMeasure(s32 *textheight, s32 *textwidth, char *text,
struct fontchar *chars, struct font *font, s32 lineheight);
extern s16 viGetX(void);
extern s16 viGetY(void);
/* ------------------------------------------------------------------------ */
enum { ROW_TOGGLE, ROW_SLIDER, ROW_ENUM, ROW_MSAA };
static const char *const kOnOff[] = { "OFF", "ON", NULL };
static const char *const kTexFilter[] = { "NEAREST", "BILINEAR", "3-POINT", NULL };
static const char *const kCapture[] = { "ALWAYS GRAB", "CLICK-TO-LOCK", NULL };
static const int kMsaaSeq[] = { 1, 2, 4, 8 };
struct Row {
const char *key;
const char *label;
int kind;
double step;
const char *const *names; /* ROW_TOGGLE / ROW_ENUM value names */
int restart; /* value change needs a restart */
double uiMin, uiMax; /* 0,0 -> use the registered clamp */
/* resolved from config.c at init */
int found;
int type; /* CONFIG_OPT_* */
void *ptr;
double cfgMin, cfgMax;
};
static struct Row rows[] = {
{ "Video.VSync", "VSync", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
{ "Video.FpsCap", "Frame cap", ROW_SLIDER, 10, NULL, 0, 0, 360, 0,0,0,0,0 },
{ "Video.MSAA", "MSAA", ROW_MSAA, 0, NULL, 1, 0, 0, 0,0,0,0,0 },
{ "Video.TextureFilter", "Texture filter", ROW_ENUM, 1, kTexFilter, 0, 0, 0, 0,0,0,0,0 },
{ "Input.MouseAimSpeed", "Mouse aim speed", ROW_SLIDER, 1, NULL, 0, 0, 100, 0,0,0,0,0 },
{ "Input.MouseTurnSpeed", "Mouse turn speed", ROW_SLIDER, 1, NULL, 0, 0, 100, 0,0,0,0,0 },
{ "Input.MouseInvertY", "Mouse invert Y", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
{ "Input.MouseCaptureMode", "Mouse capture", ROW_TOGGLE, 1, kCapture, 0, 0, 0, 0,0,0,0,0 },
{ "Game.ScreenShakeIntensity","Screen shake", ROW_SLIDER, 0.25, NULL, 0, 0, 3, 0,0,0,0,0 },
};
#define NUM_ROWS ((int)(sizeof(rows) / sizeof(rows[0])))
static int s_inited = 0;
static volatile int s_open = 0;
static int s_sel = 0;
/* ------------------------------------------------------------------------ */
static void resolveCb(const char *key, int type, void *ptr, double min, double max,
double step, const char *label, const char *const *names,
void *ctx)
{
(void)step; (void)label; (void)names; (void)ctx;
for (int i = 0; i < NUM_ROWS; i++) {
if (strcmp(rows[i].key, key) == 0) {
rows[i].found = 1;
rows[i].type = type;
rows[i].ptr = ptr;
rows[i].cfgMin = min;
rows[i].cfgMax = max;
return;
}
}
}
static void overlayInit(void)
{
if (s_inited) {
return;
}
s_inited = 1;
/* Publish display metadata so config.c / future consumers can see it,
* without config.c knowing any specific key. */
for (int i = 0; i < NUM_ROWS; i++) {
configSetOptionMeta(rows[i].key, rows[i].label, rows[i].step, rows[i].names);
}
configForEachOption(resolveCb, NULL);
for (int i = 0; i < NUM_ROWS; i++) {
if (!rows[i].found) {
sysLogPrintf(LOG_WARNING, "optionsoverlay: option '%s' not registered",
rows[i].key);
}
}
const char *e = getenv("GE_OPTIONSOVERLAY");
if (e && atoi(e) != 0) {
s_open = 1;
sysLogPrintf(LOG_INFO, "optionsoverlay: auto-opened (GE_OPTIONSOVERLAY)");
}
}
static double rowGet(const struct Row *r)
{
if (!r->found || !r->ptr) {
return 0.0;
}
switch (r->type) {
case CONFIG_OPT_INT: return (double)*(int *)r->ptr;
case CONFIG_OPT_UINT: return (double)*(unsigned int *)r->ptr;
case CONFIG_OPT_FLOAT: return (double)*(float *)r->ptr;
default: return 0.0;
}
}
static double rowLo(const struct Row *r)
{
return (r->uiMin != r->uiMax) ? r->uiMin : r->cfgMin;
}
static double rowHi(const struct Row *r)
{
return (r->uiMin != r->uiMax) ? r->uiMax : r->cfgMax;
}
static void rowSet(struct Row *r, double v)
{
double lo = rowLo(r), hi = rowHi(r);
if (lo != hi) {
if (v < lo) v = lo;
if (v > hi) v = hi;
}
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;
default: return;
}
/* Live-apply the video knobs that need a fast3d/SDL call. Everything else
* is read straight off the pointer by its owner every frame/poll. */
if (strncmp(r->key, "Video.", 6) == 0 && !r->restart) {
videoRequestLiveConfig();
}
}
static void rowAdjust(struct Row *r, int dir)
{
if (!r->found) {
return;
}
double v = rowGet(r);
switch (r->kind) {
case ROW_TOGGLE:
rowSet(r, (v != 0.0) ? 0.0 : 1.0);
break;
case ROW_MSAA: {
int idx = 0;
for (int i = 0; i < 4; i++) {
if (kMsaaSeq[i] == (int)lround(v)) idx = i;
}
idx += dir;
if (idx < 0) idx = 0;
if (idx > 3) idx = 3;
rowSet(r, (double)kMsaaSeq[idx]);
break;
}
case ROW_ENUM: {
double lo = r->cfgMin, hi = r->cfgMax;
v += dir;
if (v < lo) v = hi;
if (v > hi) v = lo;
rowSet(r, v);
break;
}
default: /* ROW_SLIDER */
rowSet(r, v + dir * r->step);
break;
}
}
/* ------------------------------------------------------------------------ */
void optionsOverlayToggle(void)
{
overlayInit();
s_open = !s_open;
sysLogPrintf(LOG_INFO, "optionsoverlay: %s", s_open ? "opened" : "closed");
if (!s_open) {
configSave();
}
}
int optionsOverlayIsOpen(void)
{
if (!s_inited) {
overlayInit();
}
return s_open;
}
void optionsOverlayScroll(int dir)
{
if (!s_open || dir == 0) {
return;
}
s_sel += (dir > 0) ? -1 : 1; /* wheel-up -> move up the list */
if (s_sel < 0) s_sel = NUM_ROWS - 1;
if (s_sel >= NUM_ROWS) s_sel = 0;
}
void optionsOverlayHandleInput(void)
{
static int prev[6]; /* UP DOWN LEFT RIGHT RETURN MOUSE */
if (!s_open) {
memset(prev, 0, sizeof(prev));
return;
}
const Uint8 *ks = SDL_GetKeyboardState(NULL);
Uint32 mb = SDL_GetMouseState(NULL, NULL);
int cur[6];
cur[0] = ks[SDL_SCANCODE_UP] || ks[SDL_SCANCODE_KP_8];
cur[1] = ks[SDL_SCANCODE_DOWN] || ks[SDL_SCANCODE_KP_2];
cur[2] = ks[SDL_SCANCODE_LEFT] || ks[SDL_SCANCODE_KP_4];
cur[3] = ks[SDL_SCANCODE_RIGHT] || ks[SDL_SCANCODE_KP_6]
|| ks[SDL_SCANCODE_RETURN] || ks[SDL_SCANCODE_KP_ENTER];
cur[4] = 0; /* reserved */
cur[5] = (mb & SDL_BUTTON(SDL_BUTTON_LEFT)) ? 1 : 0;
int rmb = (mb & SDL_BUTTON(SDL_BUTTON_RIGHT)) ? 1 : 0;
if (cur[0] && !prev[0]) {
s_sel = (s_sel + NUM_ROWS - 1) % NUM_ROWS;
}
if (cur[1] && !prev[1]) {
s_sel = (s_sel + 1) % NUM_ROWS;
}
if (cur[2] && !prev[2]) {
rowAdjust(&rows[s_sel], -1);
}
if ((cur[3] && !prev[3]) || (cur[5] && !prev[5])) {
rowAdjust(&rows[s_sel], +1);
}
static int prevRmb = 0;
if (rmb && !prevRmb) {
rowAdjust(&rows[s_sel], -1);
}
prevRmb = rmb;
memcpy(prev, cur, sizeof(prev));
}
/* ------------------------------------------------------------------------ */
#define OV_BUF_CMDS 8192
static Gfx s_buf[OV_BUF_CMDS];
/* Layout (game 2D pixel space = viGetX() x viGetY(), ~320x240). */
#define OV_X0 26
#define OV_LABEL_X 34
#define OV_VALUE_X 176
#define OV_BAR_X 176
#define OV_BAR_W 84
#define OV_NUM_X 266
#define OV_TOP 24
#define OV_LINE 14
static Gfx *fillRect(Gfx *gdl, s32 x0, s32 y0, s32 x1, s32 y1,
u8 r, u8 g, u8 b, u8 a)
{
gDPSetRenderMode(gdl++, G_RM_XLU_SURF, G_RM_XLU_SURF2);
gDPSetCombineMode(gdl++, G_CC_PRIMITIVE, G_CC_PRIMITIVE);
gDPSetPrimColor(gdl++, 0, 0, r, g, b, a);
gDPFillRectangle(gdl++, x0, y0, x1, y1);
return gdl;
}
static void valueText(const struct Row *r, char *out, int n)
{
double v = rowGet(r);
if ((r->kind == ROW_TOGGLE || r->kind == ROW_ENUM) && r->names) {
int idx = (int)lround(v);
int cnt = 0;
while (r->names[cnt]) cnt++;
if (idx >= 0 && idx < cnt) {
snprintf(out, n, "%s", r->names[idx]);
return;
}
}
if (r->kind == ROW_MSAA) {
if ((int)lround(v) <= 1) snprintf(out, n, "OFF");
else snprintf(out, n, "%dx", (int)lround(v));
return;
}
if (r->kind == ROW_SLIDER && r->type == CONFIG_OPT_FLOAT) {
snprintf(out, n, "%.2f", v);
return;
}
if (r->kind == ROW_SLIDER && (int)lround(v) == 0 &&
strcmp(r->key, "Video.FpsCap") == 0) {
snprintf(out, n, "UNCAPPED");
return;
}
snprintf(out, n, "%d", (int)lround(v));
}
static Gfx *drawText(Gfx *gdl, s32 x, s32 y, const char *str, u32 colour)
{
s32 w = 0, h = 0;
s32 px = x, py = y;
textMeasure(&h, &w, (char *)str, ptrFontBankGothicChars, ptrFontBankGothic, 10);
return textRender(gdl, &px, &py, (char *)str, ptrFontBankGothicChars,
ptrFontBankGothic, colour, w, h, 0, 10);
}
Gfx *optionsOverlayEmit(void)
{
if (!s_inited) {
overlayInit();
}
if (!s_open) {
return NULL; /* nothing appended -> golden dumps byte-identical */
}
const s32 W = viGetX();
const s32 H = viGetY();
const s32 panelBottom = OV_TOP + (NUM_ROWS + 3) * OV_LINE;
Gfx *gdl = s_buf;
gDPPipeSync(gdl++);
gDPSetCycleType(gdl++, G_CYC_1CYCLE);
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 - 6, OV_TOP - 8, W - (OV_X0 - 6), panelBottom,
8, 10, 24, 205); /* panel */
for (int i = 0; i < NUM_ROWS; i++) {
s32 rowY = OV_TOP + (i + 2) * OV_LINE;
if (i == s_sel) {
gdl = fillRect(gdl, OV_X0 - 3, rowY - 2, W - (OV_X0 - 3), rowY + OV_LINE - 3,
40, 46, 96, 220);
}
if (rows[i].kind == ROW_SLIDER && rows[i].found) {
double lo = rowLo(&rows[i]), hi = rowHi(&rows[i]);
double f = (hi > lo) ? (rowGet(&rows[i]) - lo) / (hi - lo) : 0.0;
if (f < 0) f = 0; if (f > 1) f = 1;
s32 by = rowY + 2;
gdl = fillRect(gdl, OV_BAR_X, by, OV_BAR_X + OV_BAR_W, by + 6,
60, 60, 70, 220);
gdl = fillRect(gdl, OV_BAR_X, by, OV_BAR_X + (s32)(OV_BAR_W * f), by + 6,
210, 200, 90, 255);
}
}
/* ---- pass 2: text ---- */
gdl = microcode_constructor(gdl);
gdl = drawText(gdl, OV_X0, OV_TOP, "PC OPTIONS", 0xffe040ff);
gdl = drawText(gdl, OV_X0 + 96, OV_TOP, "F10 close arrows adjust", 0x9090a0ff);
for (int i = 0; i < NUM_ROWS; i++) {
s32 rowY = OV_TOP + (i + 2) * OV_LINE;
u32 col = (i == s_sel) ? 0xffffffff : 0xc0c0c8ff;
char val[48];
if (!rows[i].found) {
gdl = drawText(gdl, OV_LABEL_X, rowY, (char *)rows[i].label, 0x808080ff);
gdl = drawText(gdl, OV_VALUE_X, rowY, "(n/a)", 0x808080ff);
continue;
}
gdl = drawText(gdl, OV_LABEL_X, rowY, (char *)rows[i].label, col);
valueText(&rows[i], val, sizeof(val));
if (rows[i].kind == ROW_SLIDER) {
gdl = drawText(gdl, OV_NUM_X, rowY, val, col);
} else {
gdl = drawText(gdl, OV_VALUE_X, rowY, val, col);
}
if (rows[i].restart) {
gdl = drawText(gdl, OV_VALUE_X + 46, rowY, "(restart)", 0x909090ff);
}
}
gDPPipeSync(gdl++);
gSPEndDisplayList(gdl++);
if ((gdl - s_buf) > OV_BUF_CMDS) {
sysLogPrintf(LOG_ERROR, "optionsoverlay: DL overflow (%d)", (int)(gdl - s_buf));
}
return s_buf;
}
+57 -16
View File
@@ -28,6 +28,7 @@
#include "config.h"
#include "video.h"
#include "input.h"
#include "optionsoverlay.h"
#include "../fast3d/gfx_api.h"
#include "../fast3d/gfx_sdl.h"
@@ -96,6 +97,31 @@ PD_CONSTRUCTOR static void videoConfigInit(void)
configRegisterInt("Window.Maximized", &cfgWinMax, 0, 1);
}
/* Set by videoRequestLiveConfig() (F10 overlay, host thread); consumed on the
* scheduler thread in videoStartFrame() where the GL context is bound. */
static volatile int liveCfgDirty = 0;
static void videoApplyTexFilter(void)
{
if (cfgTexFilter >= 2) {
gfx_set_texture_filter(FILTER_THREE_POINT);
gfx_set_mipmap_filter(MIPMAP_LINEAR);
} else if (cfgTexFilter == 1) {
gfx_set_texture_filter(FILTER_LINEAR);
gfx_set_mipmap_filter(MIPMAP_LINEAR);
} else {
gfx_set_texture_filter(FILTER_NONE);
gfx_set_mipmap_filter(MIPMAP_NEAREST);
}
}
/* Re-apply the live-tunable [Video] knobs (VSync / FpsCap / TextureFilter).
* MSAA and Fullscreen are FBO/window rebuilds -> "(restart)" in the overlay. */
void videoRequestLiveConfig(void)
{
liveCfgDirty = 1;
}
static u32 frames = 0;
/* Set by the host event pump (F12), consumed on the render thread in
* videoEndFrame where a GL context is current. */
@@ -153,16 +179,7 @@ int videoInit(void)
gfx_set_fix_mip_textures(cfgFixMipTex);
gfx_set_wrap_fix(cfgWrapFix);
if (cfgTexFilter >= 2) {
gfx_set_texture_filter(FILTER_THREE_POINT);
gfx_set_mipmap_filter(MIPMAP_LINEAR);
} else if (cfgTexFilter == 1) {
gfx_set_texture_filter(FILTER_LINEAR);
gfx_set_mipmap_filter(MIPMAP_LINEAR);
} else {
gfx_set_texture_filter(FILTER_NONE);
gfx_set_mipmap_filter(MIPMAP_NEAREST);
}
videoApplyTexFilter();
/* The GL context is currently current on this (host main) thread, but all
* rendering happens on the game's scheduler thread. WGL only allows a
@@ -195,6 +212,17 @@ void videoStartFrame(void)
/* Rendering runs on the game's scheduler thread; the GL context was
* created on the host main thread. */
gfx_sdl_make_context_current();
if (liveCfgDirty) {
liveCfgDirty = 0;
wmAPI->set_swap_interval(cfgVSync ? 1 : 0);
gfx_set_target_fps(cfgFpsCap); /* 0 = uncapped */
videoApplyTexFilter();
sysLogPrintf(LOG_INFO, "video: live config applied "
"(vsync=%d fpscap=%d texfilter=%d)",
cfgVSync, cfgFpsCap, cfgTexFilter);
}
gfx_start_frame();
}
@@ -232,20 +260,33 @@ void videoPumpEvents(void)
exit(0);
} else if (ev.key.keysym.sym == SDLK_F12 && !ev.key.repeat) {
screenshotReq = 1;
} else if (ev.key.keysym.sym == SDLK_F10 && !ev.key.repeat) {
optionsOverlayToggle(); /* F10: port-layer options overlay */
} else if (ev.key.keysym.sym == SDLK_ESCAPE && !ev.key.repeat) {
/* WI-1: in click-to-lock mode ESC frees the captured cursor
* (and is swallowed); otherwise it falls through to input.c
* where it feeds the N64 B button (D145). */
inputReleaseCapture();
/* Overlay open: ESC closes it (and is 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();
} else {
inputReleaseCapture();
}
}
break;
case SDL_MOUSEBUTTONDOWN:
/* WI-1: a click in the window (re)locks the cursor in
* click-to-lock mode; a no-op otherwise. */
inputNotifyClick();
if (!optionsOverlayIsOpen()) {
inputNotifyClick();
}
break;
case SDL_MOUSEWHEEL:
inputPostWheel(ev.wheel.y); /* weapon cycle */
if (optionsOverlayIsOpen()) {
optionsOverlayScroll(ev.wheel.y); /* move the selection */
} else {
inputPostWheel(ev.wheel.y); /* weapon cycle */
}
break;
case SDL_CONTROLLERDEVICEADDED:
case SDL_CONTROLLERDEVICEREMOVED: