mirror of
https://github.com/jkdansereau/goldeneye-pc-port.git
synced 2026-10-23 15:52:42 +02:00
Merge remote-tracking branch 'origin/main' into docs/m42-graphics-honesty
# Conflicts: # docs/dev/findings.md
This commit is contained in:
commit
7d90aca7d3
14 files changed
+1388
-26
No files matched your search
+180
-2
@@ -401,6 +401,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 |
|
||||
@@ -4756,6 +4757,126 @@ at the terrain**, so a no-input capture there does not frame the sky. `-level_29
|
||||
(Streets, night) top-of-frame reads dim `(29,24,22)` not pure black — needs a
|
||||
visual check to distinguish "dark night sky drawn" from "partial".
|
||||
|
||||
### D176(a) — M-43 UPDATE: full stream decode + two implementation paths (static analysis)
|
||||
|
||||
The M-37 scratch note (`D176a-sky-rootcause.md`) was never committed. This is the
|
||||
recovered, complete spec, re-derived from `src/game/sky.c` and `include/PR/gbi.h`.
|
||||
No build, no run — analysis only. **Implementation is still owed and is a full
|
||||
session; this entry exists so the next pass starts from the format, not a
|
||||
grep.**
|
||||
|
||||
**What the stream actually is.** `skyRenderTri` (`sky.c:1480`, textured tris),
|
||||
`skyRenderFull` (`sky.c:1976`, textured quads = 2 tris) and the untextured
|
||||
`G_TRI_FILL` path emit a **verbatim N64 RDP triangle command**, byte-for-byte,
|
||||
chopped into 32-bit halves and carried as `gImmp1(G_RDPHALF_1 / _CONT / _2, word)`
|
||||
pairs. GE's modified RSP ucode (`rsp/graphics/gmain.s`) does nothing clever with
|
||||
them — it copies the words into a DMEM scratch buffer and DMAs the assembled
|
||||
command straight to the RDP as a `G_TRI_FILL` (0xc8) or `G_TRI_SHADE_TXTR` (0xce)
|
||||
edge-walked triangle. So "decode the stream" == "own an RDP triangle rasteriser
|
||||
with edge + shade + S/T/W coefficient planes." fast3d has no such thing (it is an
|
||||
RSP-level vertex/`gSP*Triangle` interpreter); `pd_port` has the identical no-op.
|
||||
|
||||
**Word-by-word layout** (all values are S15.16 or int/frac-split 16.16 unless
|
||||
noted; `sub_GAME_7F094298(f)` = clamp ±32767.9 then `(s32)(f * 65536)`, i.e. a
|
||||
float→S15.16 fixed convert):
|
||||
|
||||
1. *Edge header* — 2 words:
|
||||
- `w0 = (opcode<<24) | (backface ? 0x00800000 : 0) | (s32)YL`
|
||||
where opcode is `G_TRI_SHADE_TXTR` or `G_TRI_FILL`, backface = `sp444 < 0`
|
||||
(signed double area after the 3-way `unk2c` vertex sort), `YL = sp47c->unk2c`
|
||||
(lowest vertex, screen-Y × 4, subpixel).
|
||||
- `w1 = ((s32)YM << 16) | (s32)YL_mid` → actually `(s32)sp480->unk2c << 16 |
|
||||
(s32)sp484->unk2c` = `YM<<16 | YH` (mid, high). N64 order is YL/YM/YH; here
|
||||
the code sorts so `sp484 ≤ sp480 ≤ sp47c` on `unk2c`, then emits
|
||||
hi=sp484, mid=sp480, lo=sp47c.
|
||||
2. *Edge slopes* — 3 word-pairs, `(X.16.16 , dXdy.16.16)` for the L, M, H edges:
|
||||
- `(7F094298(sp480->unk28 * 0.25), 7F094298(sp384))` — XL, DxLDy
|
||||
- `(7F094298(sp410), 7F094298(sp394))` — XH, DxHDy
|
||||
- `(7F094298(sp408), 7F094298(sp38c))` — XM, DxMDy
|
||||
`sp384/38c/394` are the edge inverse-slopes (`dx/dy`) clamped to ±1877;
|
||||
`sp408/sp410` are the H/M edge X starting values back-stepped to the YH
|
||||
scanline (`sp37c` = sub-pixel fraction of `sp484->unk2c * 0.25`).
|
||||
`*0.25` because `unk28/unk2c` are stored ×4.
|
||||
**`skyRenderTri` returns here if `!textured`** (G_TRI_FILL: header + edges
|
||||
only).
|
||||
3. *Shade coefficients* (textured only) — 8 word-pairs = the RDP shade DMEM
|
||||
block: RGBA base, DrDx/DgDx/DbDx/DaDx, DrDe.., DrDy.. each as an int-parts
|
||||
word then a frac-parts word (`(v & 0xffff0000) | (next & 0xffff0000) >> 16`
|
||||
packs two ints; the matching `<<16 | (next & 0xffff)` packs the fracs). Values:
|
||||
`sp210[0..3]` = colour at origin, `sp290[0..3]` = d/dx, `sp2b0[0..3]` = d/dy,
|
||||
`sp230[0..3]` = d/de, all run through `7F094298`. Colours are the per-vertex
|
||||
`SkyRelated38.rgba` barycentrically solved to a plane via `sp440`
|
||||
(= 1/doubled-area) and the `sp3c8..sp3d4` edge deltas.
|
||||
4. *Texture coefficients* (textured only) — 8 word-pairs, same int/frac packing,
|
||||
for S, T, W: `sp210[4..6]`, `sp290[4..6]`, `sp2b0[4..6]`, `sp230[4..6]`, each
|
||||
scaled by `sp190` (an LOD/overflow clamp: `1/max(perspective-corrected
|
||||
gradient magnitude / 1024, 1)`). The final pair is emitted with
|
||||
`G_RDPHALF_2` (not `_CONT`) — **that is the stream terminator.**
|
||||
|
||||
Total data words: **2 + 6 + 16 + 16 = 40** for `G_TRI_SHADE_TXTR` (20 RDPHALF
|
||||
pairs), **2 + 6 = 8** for `G_TRI_FILL`. The 2-word "header" is exactly the raw
|
||||
N64 RDP triangle command's word0 (`[cmd 8b][backface 1b @23]...[YL 14b]`) +
|
||||
word1 (`[YM 14b][YH 14b]`) — matches the canonical hardware format; the shade
|
||||
and texture blocks are the standard 16-word int-halves-then-frac-halves RDP
|
||||
coefficient layout (cross-checked M-43 against `include/PR/gbi.h` immediate-word
|
||||
packing conventions — the low-level RDP tri macros themselves are RSP-internal
|
||||
and not in this tree's `gbi.h`).
|
||||
|
||||
`skyRenderFull` is the same but solves the plane over 4 corner verts and emits
|
||||
the quad as two RDP tris sharing the coefficient blocks; its own header pair also
|
||||
ends the first tri and a second header starts the second.
|
||||
|
||||
**Vertex data available upstream** (before the DL): `sub_GAME_7F097388`
|
||||
(`sky.c:1398`) fully projects each sky corner and writes `SkyRelated38`:
|
||||
`unk28` = screen X ×4, `unk2c` = screen Y ×4 (minus `WaterConcavity*4`),
|
||||
`unk30` = screen Z (0..0x7fff), `unk34` = 1/w, `unk20/unk24` = S/T, `rgba` =
|
||||
per-vertex colour. **This is a complete screen-space vertex.** The projection is
|
||||
pure float math off endian-clean matrices — not the bug, and reusable as-is.
|
||||
|
||||
---
|
||||
|
||||
**Path A — RDP triangle rasteriser in `port/fast3d/gfx_pc.cpp` (port-only, "correct").**
|
||||
At `case G_RDPHALF_*` (`gfx_pc.cpp:~2901`): accumulate words into a small buffer;
|
||||
when a `G_RDPHALF_1` carries a `0xc8`/`0xce` opcode start a command, close on
|
||||
`G_RDPHALF_2`. Then either (a) feed a real edge-walk rasteriser writing into the
|
||||
current render target with the active combiner/tile, or (b) **invert the plane
|
||||
equations** — you have YH/YM/YL, the three edge X-at-Y and slopes, so recover the
|
||||
3 screen (x,y); evaluate the RGBA and STW planes at those 3 points to get 3
|
||||
`LoadedVertex` (already-projected: set `.x/.y` from screen coords mapped to
|
||||
NDC-ish like the `gDPTextureRectangle` path, `.z` from a fixed sky depth, `.w`
|
||||
from `unk34`), then hand to the existing `gfx_sp_tri` / GL path with a forced
|
||||
"2D, no model matrix" flag. (b) is a few hundred lines and reuses the whole
|
||||
existing combiner/texture pipeline; (a) means a new software rasteriser. Prefer
|
||||
(b). Cost: ~1 session, genuinely new code, verifiable headless.
|
||||
|
||||
**Path B — swap the emit in `src/game/sky.c` behind `#ifdef PORT` ("cheap").**
|
||||
`skyRenderTri` / `skyRenderFull` already hold 3–4 fully-projected screen-space
|
||||
`SkyRelated38` verts. Under `#ifdef PORT`, skip the whole RDPHALF block and emit
|
||||
a normal `gSPVertex` of screen-space `Vtx` (XYZ from `unk28/2c/30` ÷4, ST from
|
||||
`unk20/24`, RGBA from the struct) + `gSP1Triangle` / `gSP2Triangles`, with a
|
||||
`G_TEXTURE`/combiner setup mirroring what the RDP path implied
|
||||
(`SHADE,ENV,TEXEL0,ENV`, `texSelect(&skywaterimages[SkyImageId], …)` already runs
|
||||
just before). fast3d already eats screen-space verts (that is the front-end / HUD
|
||||
path). ~30–60 lines, no new fast3d code. **Cost: a few hours.** Tension with the
|
||||
"no game-logic edits" rule — but this is a pure N64-RSP-idiom substitution (same
|
||||
class as the `G_TRI4` and dynamic-lighting `#ifdef PORT`s already in `src/`), it
|
||||
changes no behaviour on N64 (`#else` keeps the stream verbatim), and it is
|
||||
exactly the "if a game file seems to need a behavioural change, the fix belongs
|
||||
in `port/`" *narrow* exception for a hardware idiom that cannot be isolated in
|
||||
`port/` without reimplementing the RDP. Document as D176(a) in §F if taken.
|
||||
|
||||
**Recommendation:** Path B first — it lights up every cloud-sky level
|
||||
(Surface `-level_36`, Statue `-level_22`, Frigate `-level_29`, Dam exterior) for
|
||||
a few hours' work and de-risks Path A by giving a visual ground truth. Path A
|
||||
stays the "right" long-term answer if Path B's screen-space verts show
|
||||
seams/precision artefacts vs. the RDP's subpixel edge walk. Either way the
|
||||
verification target is **`-level_22` (Statue, night) frame ~360, no input** — the
|
||||
M-42 clean headless black-sky repro.
|
||||
|
||||
**Cheap first step (unchanged, still not done):** headless `-level_22` /
|
||||
`-level_29` — confirm also black, proving it is the shared emit path and not a
|
||||
Surface-only asset.
|
||||
|
||||
---
|
||||
|
||||
## D177 — Ladders non-functional: `count`/`rooms` land in the high half of a widened pointer (M-36)
|
||||
@@ -4797,8 +4918,9 @@ Touched: `src/game/stan.c` (macro + `stanCheckLinkedSpecialTile` +
|
||||
All `#ifdef PORT`, N64 path kept verbatim under `#else`. Candidate root-cause
|
||||
came from an M-36 subagent; parts 1/3 were its work, part 2 (the local size)
|
||||
found on review. **Verified:** build links clean; `-level_09` framediff 3/3;
|
||||
`-level_36` (Surface, has mandatory ladders) boots crash-free. **Interactive
|
||||
climb test owed** — headless can't drive the climb input.
|
||||
`-level_36` (Surface, has mandatory ladders) boots crash-free.
|
||||
**Interactive climb test — DONE (M-37): user confirmed climbing works
|
||||
in-game.** Merged to `main` via PR #5 (`955349db`).
|
||||
|
||||
porting-notes.md §C (pointer-width in ROM/record structs).
|
||||
|
||||
@@ -5075,3 +5197,59 @@ golden + a Streets/Dam/Archives load.
|
||||
Docs only: this subsection + the §F index rows (D88.4 status, new D132
|
||||
row). No code changed; the proposed diff above is not applied. No temp
|
||||
probe scripts left.
|
||||
|
||||
## D185 — fresh `d69_emit.py` run yields a game-crashing `data/pccg-<region>/` (M-38)
|
||||
|
||||
**Status: OPEN — alpha-release blocker.** (Add to the §F index.)
|
||||
|
||||
**Symptom.** `python tools_pc/d69_emit.py ntsc-final` in this repo writes
|
||||
`data/pccg-ntsc-final/pccg.bin` = 3346895 B (52 sidecars, 1856-B manifest),
|
||||
prints `ALL CHECKS PASSED`, and `-level_09` / `-level_20` then segfault before
|
||||
frame 1 (`EXCEPTION: 0xc0000005`, faulting return address is an ASCII
|
||||
model-name string). The pre-migration working sidecar
|
||||
(`C:/Users/james/Source/Repos/007/data/pccg-ntsc-final/pccg.bin`, 3604378 B,
|
||||
2421-B manifest) drops into this tree and `-level_09` runs crash-free.
|
||||
|
||||
**Isolation done (M-38).**
|
||||
- Not the romdata M-38 change: baseline (stashed) crashes identically.
|
||||
- Not the build: same exe, only the `data/pccg-ntsc-final/` bytes swapped.
|
||||
- `tools_pc/d69_emit.py` is byte-identical between the public repo and the
|
||||
pre-migration repo `007` (only a doc-path comment differs).
|
||||
- The migration commit `52539d10 "Prepare for public release"` normalised the
|
||||
committed d69 inputs from CRLF to LF: `scripts/filelist.u.csv` (−812 B, one
|
||||
CR/line) and `assets/obseg/file_resource_table.inc.c`.
|
||||
- **Restoring CR to both inputs did NOT restore the 3604378-B output** → line
|
||||
endings are not the (whole) cause. Some other part of the d69 input closure
|
||||
regressed in the history rewrite, or the working sidecar predates a real
|
||||
d69/input change and 3346895 is a separate second bug.
|
||||
- `d43_emit.py` is unaffected (its `find_row` tries `name`, `name[1:]`,
|
||||
`name[:-1]`, `name[1:-1]` — tolerant of a mangled basename; `d69`'s
|
||||
`fl_by_base.get(basename)` is exact).
|
||||
|
||||
**Impact.** `docs/building.md` tells users to run `d69_emit.py`; M-38's
|
||||
`prepare-assets.py` does the same. Both currently produce a broken game on a
|
||||
clean checkout. The committed `data/` dirs (carried from pre-migration) hide it.
|
||||
|
||||
**Next.** Diff the sidecar *name* set: pre-migration `manifest.csv` vs a fresh
|
||||
run's — find which bg/stan entries d69 now drops, trace through `find_row` →
|
||||
`file_resource_table.inc.c` → `filelist.u.csv`. Fix so a fresh run reproduces
|
||||
3604378 B / 2421-B manifest; re-verify `-level_09`/`-20` crash-free. Do NOT
|
||||
regenerate `data/pccg-ntsc-final/` before the fix (it holds the working copy).
|
||||
|
||||
### D185 — RESOLVED (M-38): not a d69 regression, a missing d88 step
|
||||
|
||||
`d88_emit.py` **appends** the 21 `Usetup*Z` per-level stage-setup files
|
||||
(object placement, AI opcode streams, pads/nav) to the same
|
||||
`data/pccg-<region>/pccg.bin` + `manifest.csv` that `d69_emit.py` writes
|
||||
(52 bg/stan rows → 74 after d88). The M-38 `prepare-assets.py` first shipped
|
||||
ran only d43 + d69, so every level loaded with no setup data → instant
|
||||
segfault. The "working" pre-migration sidecar was simply a complete
|
||||
d43+d69+d88 output; nothing regressed in d69 or its inputs.
|
||||
|
||||
Fix (commit `4491db3f`): `prepare-assets.py` runs d43 → d69 → `d88 --regen`;
|
||||
`bundle-win.sh` vendors `d88_emit.py` + its only local import
|
||||
`d88_propdefs.py`. Verified: fresh 3-script run from an assembled bundle is
|
||||
byte-identical to the known-good `pccg.bin` (3604378 B) / `pcmodels.bin`;
|
||||
`-level_09` + `-level_20` boot crash-free. The CRLF→LF observation on
|
||||
`filelist.u.csv` / `file_resource_table.inc.c` is real but a red herring for
|
||||
this crash (d69 tolerates it; output unchanged).
|
||||
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -54,6 +54,10 @@ void inputNotifyClick(void);
|
||||
int inputReleaseCapture(void);
|
||||
int inputMouseCaptureActive(void);
|
||||
|
||||
/* F10 options overlay: force the OS cursor free + visible while the overlay
|
||||
* owns the mouse. Safe to call every poll. */
|
||||
void inputSuspendForOverlay(void);
|
||||
|
||||
/* Queue a mouse-wheel weapon-cycle input (one short A-button press). Sign is
|
||||
* ignored -- GE only cycles forward on a bare A edge. */
|
||||
void inputPostWheel(int notches);
|
||||
|
||||
@@ -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 */
|
||||
@@ -48,6 +48,20 @@ 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);
|
||||
|
||||
/* F10 options overlay -> window/fullscreen changes. The overlay input handler
|
||||
* runs on the scheduler thread; SDL window ops must run on the thread that
|
||||
* created the window, so these only post a request that videoPumpEvents()
|
||||
* (host thread) applies. The Get* helpers are read-only and thread-safe. */
|
||||
void videoRequestWindowSize(int w, int h);
|
||||
void videoRequestFullscreen(int on);
|
||||
void videoGetWindowSize(int *w, int *h);
|
||||
void videoGetDesktopSize(int *w, int *h);
|
||||
int videoIsFullscreen(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);
|
||||
|
||||
@@ -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++;
|
||||
|
||||
@@ -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);
|
||||
@@ -866,6 +878,22 @@ int inputReleaseCapture(void)
|
||||
|
||||
int inputMouseCaptureActive(void) { return mouseCaptureMode && !mouseGrabbed; }
|
||||
|
||||
/* F10 options overlay: while it owns controller 0 the inputComputePad poll
|
||||
* early-returns before reconcileGrab()/applyCursorVisibility(), so whatever
|
||||
* grab state was live when F10 was pressed (relative mode + hidden cursor in a
|
||||
* stage) would persist and the mouse UI would be unusable. Force the cursor
|
||||
* free + visible every poll; the normal reconcile resumes once the overlay
|
||||
* closes and the early-return no longer fires. */
|
||||
void inputSuspendForOverlay(void)
|
||||
{
|
||||
if (mouseGrabbed) {
|
||||
mouseGrabbed = 0;
|
||||
SDL_SetRelativeMouseMode(SDL_FALSE);
|
||||
mouseDX = mouseDY = 0.0;
|
||||
}
|
||||
SDL_ShowCursor(SDL_ENABLE);
|
||||
}
|
||||
|
||||
void inputPostWheel(int notches)
|
||||
{
|
||||
if (notches < 0) notches = -notches; /* both directions cycle forward */
|
||||
|
||||
@@ -0,0 +1,626 @@
|
||||
/*
|
||||
* 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>
|
||||
/* gbi.h's gDP* DL macros use _SHIFTL/_SHIFTR but do not define them -- game TUs
|
||||
* get them from <ultra64.h>/<PR/mbi.h>, which also drags in N64 OS headers that
|
||||
* shadow libc here. Define the two pure macros locally (verbatim from mbi.h) so
|
||||
* this stays a plain port TU. Without them GCC/ld fails "undefined reference to
|
||||
* _SHIFTL" (MinGW's chain happens to provide it). */
|
||||
#ifndef _SHIFTL
|
||||
#define _SHIFTL(v, s, w) ((u32)(((u32)(v) & ((0x01 << (w)) - 1)) << (s)))
|
||||
#define _SHIFTR(v, s, w) ((u32)(((u32)(v) >> (s)) & ((0x01 << (w)) - 1)))
|
||||
#endif
|
||||
#include <PR/gbi.h>
|
||||
|
||||
#include "platform.h"
|
||||
#include "system.h"
|
||||
#include "config.h"
|
||||
#include "video.h"
|
||||
#include "input.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, ROW_RES };
|
||||
|
||||
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 };
|
||||
|
||||
/* Windowed-mode resolution presets. Filtered at init to those that fit the
|
||||
* desktop; the Resolution row cycles the surviving list. */
|
||||
static const int kResList[][2] = {
|
||||
{ 640, 480 }, { 800, 600 }, { 960, 720 }, { 1024, 768 },
|
||||
{ 1152, 864 }, { 1280, 720 }, { 1280, 800 }, { 1280, 960 },
|
||||
{ 1366, 768 }, { 1440, 900 }, { 1600, 900 }, { 1600, 1200 },
|
||||
{ 1680, 1050 }, { 1920, 1080 }, { 1920, 1200 }, { 2560, 1440 },
|
||||
{ 3200, 1800 }, { 3840, 2160 },
|
||||
};
|
||||
#define NUM_RES ((int)(sizeof(kResList) / sizeof(kResList[0])))
|
||||
static int s_resFit[NUM_RES]; /* indices into kResList that fit the desktop */
|
||||
static int s_resFitN = 0;
|
||||
static int s_resSel = 0; /* index into s_resFit */
|
||||
|
||||
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.Fullscreen", "Fullscreen", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
|
||||
{ "__Resolution", "Resolution", ROW_RES, 0, NULL, 0, 0, 0, 0,0,0,0,0 },
|
||||
{ "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;
|
||||
|
||||
/* Layout (game 2D pixel space = viGetX() x viGetY(), ~320x240). Shared by the
|
||||
* 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) */
|
||||
#define OV_ROW_Y(i) (OV_TOP + ((i) + OV_HDR) * OV_LINE)
|
||||
#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 void sliderBarSpan(s32 *x0, s32 *x1)
|
||||
{
|
||||
*x0 = OV_BAR_X;
|
||||
*x1 = OV_RIGHT - OV_NUM_W;
|
||||
if (*x1 < *x0 + 16) {
|
||||
*x1 = *x0 + 16;
|
||||
}
|
||||
}
|
||||
|
||||
/* Row index the given overlay-space y falls in, or -1. */
|
||||
static int overlayRowAtY(double oy)
|
||||
{
|
||||
for (int i = 0; i < NUM_ROWS; i++) {
|
||||
double top = OV_ROW_Y(i) - 3;
|
||||
if (oy >= top && oy < top + OV_LINE) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
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].kind == ROW_RES) {
|
||||
rows[i].found = 1; /* not config-backed; driven via video.c */
|
||||
continue;
|
||||
}
|
||||
if (!rows[i].found) {
|
||||
sysLogPrintf(LOG_WARNING, "optionsoverlay: option '%s' not registered",
|
||||
rows[i].key);
|
||||
}
|
||||
}
|
||||
|
||||
/* Build the windowed-resolution preset list: presets that fit the desktop,
|
||||
* plus the current window size snapped to the nearest surviving entry. */
|
||||
{
|
||||
int dw = 1920, dh = 1080;
|
||||
videoGetDesktopSize(&dw, &dh);
|
||||
s_resFitN = 0;
|
||||
for (int i = 0; i < NUM_RES; i++) {
|
||||
if (kResList[i][0] <= dw && kResList[i][1] <= dh) {
|
||||
s_resFit[s_resFitN++] = i;
|
||||
}
|
||||
}
|
||||
if (s_resFitN == 0) {
|
||||
s_resFit[s_resFitN++] = 0;
|
||||
}
|
||||
int cw = 0, ch = 0;
|
||||
videoGetWindowSize(&cw, &ch);
|
||||
long best = -1;
|
||||
for (int k = 0; k < s_resFitN; k++) {
|
||||
int i = s_resFit[k];
|
||||
long d = labs((long)kResList[i][0] - cw) +
|
||||
labs((long)kResList[i][1] - ch);
|
||||
if (best < 0 || d < best) { best = d; s_resSel = k; }
|
||||
}
|
||||
}
|
||||
|
||||
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 (strcmp(r->key, "Video.Fullscreen") == 0) {
|
||||
videoRequestFullscreen((int)lround(v));
|
||||
} else 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;
|
||||
}
|
||||
case ROW_RES: {
|
||||
if (s_resFitN <= 0 || videoIsFullscreen()) {
|
||||
break; /* resolution is windowed-only */
|
||||
}
|
||||
s_resSel += (dir >= 0) ? 1 : -1;
|
||||
if (s_resSel < 0) s_resSel = s_resFitN - 1;
|
||||
if (s_resSel >= s_resFitN) s_resSel = 0;
|
||||
int i = s_resFit[s_resSel];
|
||||
videoRequestWindowSize(kResList[i][0], kResList[i][1]);
|
||||
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;
|
||||
}
|
||||
|
||||
/* Set a slider row from an overlay-space x inside its value bar, snapped to
|
||||
* the row's step. */
|
||||
static void sliderSetFromX(struct Row *r, double ox)
|
||||
{
|
||||
double lo = rowLo(r), hi = rowHi(r);
|
||||
if (hi <= lo) {
|
||||
return;
|
||||
}
|
||||
s32 bx0, bx1;
|
||||
sliderBarSpan(&bx0, &bx1);
|
||||
double f = (ox - bx0) / (double)(bx1 - bx0);
|
||||
if (f < 0) f = 0;
|
||||
if (f > 1) f = 1;
|
||||
double v = lo + f * (hi - lo);
|
||||
double step = (r->step > 0.0) ? r->step : 1.0;
|
||||
v = lround(v / step) * step;
|
||||
rowSet(r, v);
|
||||
}
|
||||
|
||||
void optionsOverlayHandleInput(void)
|
||||
{
|
||||
static int prevUp, prevDn, prevLf, prevRt, prevLmb, prevRmb;
|
||||
static int dragRow = -1;
|
||||
|
||||
if (!s_open) {
|
||||
prevUp = prevDn = prevLf = prevRt = prevLmb = prevRmb = 0;
|
||||
dragRow = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
/* The overlay owns the mouse while it is open: force the OS cursor free +
|
||||
* visible (a stage poll would otherwise leave it locked/hidden). */
|
||||
inputSuspendForOverlay();
|
||||
|
||||
const Uint8 *ks = SDL_GetKeyboardState(NULL);
|
||||
int mx = 0, my = 0;
|
||||
Uint32 mb = SDL_GetMouseState(&mx, &my);
|
||||
int lmb = (mb & SDL_BUTTON(SDL_BUTTON_LEFT)) ? 1 : 0;
|
||||
int rmb = (mb & SDL_BUTTON(SDL_BUTTON_RIGHT)) ? 1 : 0;
|
||||
|
||||
int up = ks[SDL_SCANCODE_UP] || ks[SDL_SCANCODE_KP_8];
|
||||
int dn = ks[SDL_SCANCODE_DOWN] || ks[SDL_SCANCODE_KP_2];
|
||||
int lf = ks[SDL_SCANCODE_LEFT] || ks[SDL_SCANCODE_KP_4];
|
||||
int rt = ks[SDL_SCANCODE_RIGHT] || ks[SDL_SCANCODE_KP_6]
|
||||
|| ks[SDL_SCANCODE_RETURN] || ks[SDL_SCANCODE_KP_ENTER];
|
||||
|
||||
/* ---- keyboard / D-pad nav ---- */
|
||||
if (up && !prevUp) s_sel = (s_sel + NUM_ROWS - 1) % NUM_ROWS;
|
||||
if (dn && !prevDn) s_sel = (s_sel + 1) % NUM_ROWS;
|
||||
if (lf && !prevLf) rowAdjust(&rows[s_sel], -1);
|
||||
if (rt && !prevRt) rowAdjust(&rows[s_sel], +1);
|
||||
|
||||
/* ---- mouse ---- */
|
||||
int ww = 0, wh = 0;
|
||||
videoGetWindowSize(&ww, &wh);
|
||||
if (ww > 0 && wh > 0) {
|
||||
double ox = (double)mx * (double)viGetX() / ww;
|
||||
double oy = (double)my * (double)viGetY() / wh;
|
||||
int hoverRow = overlayRowAtY(oy);
|
||||
int onClose = overlayInCloseBox(ox, oy);
|
||||
|
||||
if (hoverRow >= 0 && !onClose) {
|
||||
s_sel = hoverRow; /* hover-to-highlight */
|
||||
}
|
||||
|
||||
/* 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);
|
||||
if (lmb && !prevLmb) {
|
||||
if (onClose) {
|
||||
optionsOverlayToggle(); /* close + configSave */
|
||||
return;
|
||||
}
|
||||
if (hoverRow >= 0 && ox >= bx0) {
|
||||
struct Row *r = &rows[hoverRow];
|
||||
if (r->kind == ROW_SLIDER && r->found) {
|
||||
sliderSetFromX(r, ox);
|
||||
dragRow = hoverRow;
|
||||
} else {
|
||||
rowAdjust(r, +1); /* toggle / cycle forward */
|
||||
}
|
||||
}
|
||||
}
|
||||
/* drag a slider */
|
||||
if (lmb && dragRow >= 0 && rows[dragRow].kind == ROW_SLIDER) {
|
||||
sliderSetFromX(&rows[dragRow], ox);
|
||||
}
|
||||
if (!lmb) {
|
||||
dragRow = -1;
|
||||
}
|
||||
/* right press in the value column: cycle back / decrement */
|
||||
if (rmb && !prevRmb && hoverRow >= 0 && !onClose && ox >= bx0) {
|
||||
rowAdjust(&rows[hoverRow], -1);
|
||||
}
|
||||
}
|
||||
|
||||
prevUp = up; prevDn = dn; prevLf = lf; prevRt = rt;
|
||||
prevLmb = lmb; prevRmb = rmb;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
#define OV_BUF_CMDS 8192
|
||||
static Gfx s_buf[OV_BUF_CMDS];
|
||||
|
||||
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_RES) {
|
||||
if (videoIsFullscreen()) {
|
||||
snprintf(out, n, "(fullscreen)");
|
||||
} else if (s_resFitN <= 0) {
|
||||
snprintf(out, n, "n/a");
|
||||
} else {
|
||||
int i = s_resFit[s_resSel];
|
||||
snprintf(out, n, "%d x %d", kResList[i][0], kResList[i][1]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
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, "OFF");
|
||||
return;
|
||||
}
|
||||
snprintf(out, n, "%d", (int)lround(v));
|
||||
}
|
||||
|
||||
static Gfx *drawText(Gfx *gdl, s32 x, s32 y, const char *str, u32 colour)
|
||||
{
|
||||
s32 px = x, py = y;
|
||||
/* width/height are the on-screen CLIP rect textRenderGlyph tests against
|
||||
* (clipX=start x, clipY=start y, +clipWidth/+clipHeight), NOT the text's
|
||||
* own measured size -- passing the measured w/h clipped every glyph out
|
||||
* (baseline+height > measured h => nothing drawn). Match the game: pass
|
||||
* the full 2D viewport, like bondview2.c's debug-text path. */
|
||||
return textRender(gdl, &px, &py, (char *)str, ptrFontBankGothicChars,
|
||||
ptrFontBankGothic, colour, viGetX(), viGetY(), 0, 0);
|
||||
}
|
||||
|
||||
static s32 measureText(const char *str)
|
||||
{
|
||||
s32 h = 0, w = 0;
|
||||
textMeasure(&h, &w, (char *)str, ptrFontBankGothicChars, ptrFontBankGothic, 0);
|
||||
return w;
|
||||
}
|
||||
|
||||
/* Right-aligned: the string ends at xr. */
|
||||
static Gfx *drawTextR(Gfx *gdl, s32 xr, s32 y, const char *str, u32 colour)
|
||||
{
|
||||
return drawText(gdl, xr - measureText(str), y, str, colour);
|
||||
}
|
||||
|
||||
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 right = OV_RIGHT;
|
||||
const s32 panelTop = OV_TOP - 9;
|
||||
const s32 panelBottom = OV_ROW_Y(NUM_ROWS - 1) + OV_LINE / 2 + 3;
|
||||
s32 bx0, bx1;
|
||||
sliderBarSpan(&bx0, &bx1);
|
||||
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 - 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 */
|
||||
|
||||
for (int i = 0; i < NUM_ROWS; i++) {
|
||||
s32 rowY = OV_ROW_Y(i);
|
||||
if (i == s_sel) {
|
||||
gdl = fillRect(gdl, OV_X0 - 4, rowY - 3, W - (OV_X0 - 4),
|
||||
rowY + OV_LINE - 4, 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 + 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);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 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, "click value / drag / arrows",
|
||||
0x8890a0ff); /* hint line */
|
||||
gdl = drawText(gdl, (OV_CB_X0 + OV_CB_X1) / 2 - measureText("X") / 2,
|
||||
OV_TOP, "X", 0xffffffff); /* close glyph */
|
||||
|
||||
for (int i = 0; i < NUM_ROWS; i++) {
|
||||
s32 rowY = OV_ROW_Y(i);
|
||||
u32 col = (i == s_sel) ? 0xffffffff : 0xc0c0c8ff;
|
||||
char val[48];
|
||||
|
||||
gdl = drawText(gdl, OV_LABEL_X, rowY, (char *)rows[i].label,
|
||||
rows[i].found ? col : 0x808080ff);
|
||||
if (!rows[i].found) {
|
||||
gdl = drawTextR(gdl, right, rowY, "(n/a)", 0x808080ff);
|
||||
continue;
|
||||
}
|
||||
|
||||
valueText(&rows[i], val, sizeof(val));
|
||||
if (rows[i].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);
|
||||
} else {
|
||||
gdl = drawTextR(gdl, right, rowY, val, col);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
+141
-16
@@ -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,110 @@ 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;
|
||||
}
|
||||
|
||||
/* --- F10 overlay: window / fullscreen changes, deferred to the host thread ---
|
||||
* optionsOverlayHandleInput() runs on the scheduler thread; SDL_SetWindowSize /
|
||||
* SDL_SetWindowFullscreen pump the Win32 message loop and must run on the
|
||||
* window's creating thread. The overlay posts a request here; the host-thread
|
||||
* event pump drains it in videoDrainWindowRequests(). */
|
||||
static volatile int winReqKind = 0; /* 0 none, 1 resize, 2 fullscreen */
|
||||
static volatile int winReqA = 0, winReqB = 0;
|
||||
|
||||
void videoRequestWindowSize(int w, int h)
|
||||
{
|
||||
winReqA = w; winReqB = h; winReqKind = 1;
|
||||
}
|
||||
|
||||
void videoRequestFullscreen(int on)
|
||||
{
|
||||
winReqA = on ? 1 : 0; winReqKind = 2;
|
||||
}
|
||||
|
||||
void videoGetWindowSize(int *w, int *h)
|
||||
{
|
||||
uint32_t ww = 0, hh = 0; int32_t x = 0, y = 0;
|
||||
if (initDone && wmAPI && wmAPI->get_dimensions) {
|
||||
wmAPI->get_dimensions(&ww, &hh, &x, &y);
|
||||
}
|
||||
if (w) *w = (int)ww;
|
||||
if (h) *h = (int)hh;
|
||||
}
|
||||
|
||||
void videoGetDesktopSize(int *w, int *h)
|
||||
{
|
||||
SDL_DisplayMode m;
|
||||
memset(&m, 0, sizeof(m));
|
||||
if (SDL_GetDesktopDisplayMode(0, &m) != 0 || m.w <= 0 || m.h <= 0) {
|
||||
m.w = 1920; m.h = 1080;
|
||||
}
|
||||
if (w) *w = m.w;
|
||||
if (h) *h = m.h;
|
||||
}
|
||||
|
||||
int videoIsFullscreen(void)
|
||||
{
|
||||
return (initDone && wmAPI && wmAPI->get_fullscreen_state)
|
||||
? (wmAPI->get_fullscreen_state() ? 1 : 0) : 0;
|
||||
}
|
||||
|
||||
static void videoDrainWindowRequests(void)
|
||||
{
|
||||
int kind = winReqKind;
|
||||
if (!kind || !wmAPI) {
|
||||
winReqKind = 0;
|
||||
return;
|
||||
}
|
||||
winReqKind = 0;
|
||||
|
||||
if (kind == 1) {
|
||||
int w = winReqA, h = winReqB;
|
||||
int32_t px = 100, py = 100;
|
||||
if (wmAPI->get_fullscreen_state && wmAPI->get_fullscreen_state()) {
|
||||
if (wmAPI->set_fullscreen) wmAPI->set_fullscreen(false);
|
||||
cfgFullscreen = 0;
|
||||
}
|
||||
if (wmAPI->get_centered_positions) {
|
||||
wmAPI->get_centered_positions(w, h, &px, &py);
|
||||
}
|
||||
if (wmAPI->set_dimensions) {
|
||||
wmAPI->set_dimensions((uint32_t)w, (uint32_t)h, px, py);
|
||||
}
|
||||
gfx_sdl_update_cached_size();
|
||||
cfgWinW = w; cfgWinH = h;
|
||||
sysLogPrintf(LOG_INFO, "video: window -> %dx%d", w, h);
|
||||
} else if (kind == 2) {
|
||||
int on = winReqA;
|
||||
if (wmAPI->set_fullscreen) wmAPI->set_fullscreen(on != 0);
|
||||
gfx_sdl_update_cached_size();
|
||||
cfgFullscreen = on ? 1 : 0;
|
||||
sysLogPrintf(LOG_INFO, "video: fullscreen %s", on ? "on" : "off");
|
||||
}
|
||||
}
|
||||
|
||||
static u32 frames = 0;
|
||||
/* Set by the host event pump (F12), consumed on the render thread in
|
||||
* videoEndFrame where a GL context is current. */
|
||||
@@ -165,16 +270,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
|
||||
@@ -209,6 +305,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();
|
||||
}
|
||||
|
||||
@@ -227,6 +334,11 @@ void videoPumpEvents(void)
|
||||
if (!initDone) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Apply any window/fullscreen change the F10 overlay posted from the
|
||||
* scheduler thread (must run here, on the window's creating thread). */
|
||||
videoDrainWindowRequests();
|
||||
|
||||
SDL_Event ev;
|
||||
while (SDL_PollEvent(&ev)) {
|
||||
switch (ev.type) {
|
||||
@@ -246,20 +358,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:
|
||||
|
||||
@@ -78,6 +78,27 @@ for l in SDL2 zlib gcc-libs libwinpthread mingw-w64; do
|
||||
[ -d "$MINGW/share/licenses/$l" ] && cp -r "$MINGW/share/licenses/$l" "$OUT/licenses/$l"
|
||||
done
|
||||
|
||||
# --- asset-prep tool -------------------------------------------------
|
||||
# The port needs two ROM-derived directories (data/pcmodels-<region>/ and
|
||||
# data/pccg-<region>/) that we cannot ship. prepare-assets.py regenerates
|
||||
# them from the user's own ROM using only the Python standard library. It is
|
||||
# assembled fresh from the tree here so it can never drift from the emit
|
||||
# scripts. The vendored inputs are decomp layout/symbol metadata (.inc.c /
|
||||
# .csv), NOT game assets (no textures, audio, models, levels, or text).
|
||||
PREP="$OUT/prepare-assets"
|
||||
mkdir -p "$PREP/vendor/scripts" "$PREP/vendor/assets/obseg"
|
||||
cp tools_pc/dist/prepare-assets/prepare-assets.py "$PREP/"
|
||||
cp tools_pc/d43_emit.py tools_pc/d69_emit.py "$PREP/"
|
||||
cp tools_pc/d88_emit.py tools_pc/d88_propdefs.py "$PREP/" # d88 = per-level stage setup (+ its only local import)
|
||||
cp scripts/filelist.u.csv "$PREP/vendor/scripts/"
|
||||
cp assets/obseg/file_resource_table.inc.c "$PREP/vendor/assets/obseg/"
|
||||
# d43_emit.py os.walk()s assets/ for every *modelFileHeader.inc.c — preserve paths.
|
||||
( cd . && find assets -iname 'modelfileheader.inc.c' -print0 \
|
||||
| xargs -0 -I{} cp --parents {} "$PREP/vendor/" )
|
||||
nmh="$(find "$PREP/vendor/assets" -iname 'modelfileheader.inc.c' | wc -l)"
|
||||
echo " + prepare-assets/ (emit scripts + $nmh model headers)"
|
||||
[ "$nmh" -gt 400 ] || { echo "error: prepare-assets vendored only $nmh model headers — expected ~512" >&2; exit 1; }
|
||||
|
||||
# --- guard: no ROM / game data snuck in --------------------------------
|
||||
if find "$OUT" -type f \( -iname '*.z64' -o -iname '*.n64' -o -iname '*.v64' \) | grep -q .; then
|
||||
echo "error: bundle contains a ROM image — aborting" >&2
|
||||
|
||||
Vendored
+19
-8
@@ -28,20 +28,31 @@ NTSC-U (US):
|
||||
|
||||
Only the US ROM is supported in this alpha.
|
||||
|
||||
## 2. Run
|
||||
## 2. Set up (one time)
|
||||
|
||||
1. Unzip this folder anywhere.
|
||||
2. Make a `data` folder next to `ge007.x86_64.exe`.
|
||||
3. Copy your ROM into it, named exactly: `data\ge007.ntsc-final.z64`
|
||||
4. Run `ge007.x86_64.exe` **from this folder**.
|
||||
4. Prepare the ROM-derived data (needs Python 3.8+ on your PATH):
|
||||
|
||||
Configuration is written to `ge007.ini` on first run.
|
||||
```
|
||||
python prepare-assets\prepare-assets.py
|
||||
```
|
||||
|
||||
> If the game exits immediately reporting missing data, your ROM's assets need
|
||||
> the one-time extraction step described under "Building" in the project README
|
||||
> — the alpha binary expects the same asset layout a source build produces.
|
||||
This reads your ROM and writes `data\pcmodels-ntsc-final\` and
|
||||
`data\pccg-ntsc-final\` next to the exe. It uses only the Python standard
|
||||
library — no compiler or N64 toolchain — and takes a few seconds. Run it
|
||||
again only if you replace the ROM.
|
||||
|
||||
## 3. Controls (defaults)
|
||||
## 3. Run
|
||||
|
||||
Run `ge007.x86_64.exe` **from this folder**. Configuration is written to
|
||||
`ge007.ini` on first run.
|
||||
|
||||
> If the game exits immediately reporting missing data, re-run step 2.4 above
|
||||
> and check it printed `ALL CHECKS PASSED` for both passes.
|
||||
|
||||
## 4. Controls (defaults)
|
||||
|
||||
| Action | Keyboard / mouse |
|
||||
|---|---|
|
||||
@@ -55,7 +66,7 @@ Configuration is written to `ge007.ini` on first run.
|
||||
|
||||
Mouse sensitivity and Y-inversion are in the `[Input]` section of `ge007.ini`.
|
||||
|
||||
## 4. Legal
|
||||
## 5. Legal
|
||||
|
||||
Non-commercial fan preservation / research project. **Not** affiliated with,
|
||||
endorsed by, or sponsored by Nintendo, Rare, Microsoft, MGM, Danjaq, or EON
|
||||
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
#!/usr/bin/env python3
|
||||
"""One-time asset preparation for the GoldenEye 007 PC port (alpha).
|
||||
|
||||
The port loads PC-layout model / display-list / collision / nav data from two
|
||||
directories that are derived from a *GoldenEye* ROM and therefore are NOT
|
||||
shipped in this package:
|
||||
|
||||
data/pcmodels-<region>/ (pcmodels.bin + manifest.csv)
|
||||
data/pccg-<region>/ (pccg.bin + manifest.csv)
|
||||
|
||||
This script regenerates them from a ROM you legally own. It calls only the
|
||||
Python standard library -- no compiler, no N64 toolchain, no network. The
|
||||
conversion is deterministic: the same ROM always produces byte-identical
|
||||
output.
|
||||
|
||||
Usage (from the folder this script lives in, or anywhere):
|
||||
|
||||
python prepare-assets.py [--rom PATH] [--out DIR] [--keep-temp]
|
||||
|
||||
By default it looks for the ROM at <bundle>/data/*.z64 (and a few other
|
||||
obvious spots), verifies its SHA-1 to pick the region, and writes the two
|
||||
directories under <bundle>/data/ , where <bundle> is the folder that
|
||||
contains this "prepare-assets" folder (i.e. next to ge007.x86_64.exe).
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
VENDOR = HERE / "vendor"
|
||||
|
||||
# SHA-1 of each supported retail ROM (big-endian .z64) -> region id the
|
||||
# emit scripts expect. Mirrors ge007.{u,e,j}.sha1 in the source tree.
|
||||
ROM_SHA1 = {
|
||||
"abe01e4aeb033b6c0836819f549c791b26cfde83": "ntsc-final", # NTSC-U (US)
|
||||
"167c3c433dec1f1eb921736f7d53fac8cb45ee31": "pal-final", # PAL (EU)
|
||||
"2a5dade32f7fad6c73c659d2026994632c1b3174": "jpn-final", # NTSC-J (JP)
|
||||
}
|
||||
|
||||
# (script, args, produces) — run in order. d88 has no output of its own: it
|
||||
# appends the 21 per-level stage-setup files to the pccg dir d69 just made,
|
||||
# and --regen makes that idempotent.
|
||||
EMIT_STEPS = (
|
||||
("d43_emit.py", (), "pcmodels-{region}/pcmodels.bin"),
|
||||
("d69_emit.py", (), "pccg-{region}/pccg.bin"),
|
||||
("d88_emit.py", ("--regen",), "pccg-{region}/pccg.bin"),
|
||||
)
|
||||
OUTPUT_DIRS = ("pcmodels-{region}", "pccg-{region}")
|
||||
|
||||
|
||||
def die(msg):
|
||||
print(f"\nprepare-assets: error: {msg}\n", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def sha1_of(path):
|
||||
h = hashlib.sha1()
|
||||
with open(path, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(1 << 20), b""):
|
||||
h.update(chunk)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def find_rom(explicit, bundle_root):
|
||||
if explicit:
|
||||
p = Path(explicit).expanduser()
|
||||
if not p.is_file():
|
||||
die(f"--rom {p} : not a file")
|
||||
return p
|
||||
search = []
|
||||
for base in (bundle_root, bundle_root / "data", HERE, HERE / "vendor" / "data"):
|
||||
search += sorted(base.glob("*.z64")) + sorted(base.glob("*.n64")) + sorted(base.glob("*.v64"))
|
||||
# de-dup, keep order
|
||||
seen, cand = set(), []
|
||||
for p in search:
|
||||
if p.resolve() not in seen:
|
||||
seen.add(p.resolve())
|
||||
cand.append(p)
|
||||
if not cand:
|
||||
die("no .z64 ROM found. Put your GoldenEye ROM at "
|
||||
f"{bundle_root / 'data'} (create the 'data' folder) or pass --rom PATH.")
|
||||
if len(cand) > 1:
|
||||
print("prepare-assets: multiple ROM candidates found:")
|
||||
for p in cand:
|
||||
print(f" {p}")
|
||||
print("prepare-assets: using the first; pass --rom PATH to choose.")
|
||||
return cand[0]
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="Generate the GoldenEye PC port's ROM-derived asset dirs.")
|
||||
ap.add_argument("--rom", help="path to your .z64 ROM (default: auto-detect near the bundle)")
|
||||
ap.add_argument("--out", help="bundle root to write data/ into (default: the folder holding prepare-assets)")
|
||||
ap.add_argument("--keep-temp", action="store_true", help="keep the temporary ROM copy under vendor/data/")
|
||||
args = ap.parse_args()
|
||||
|
||||
if not VENDOR.is_dir():
|
||||
die(f"missing {VENDOR} -- this script must stay inside the 'prepare-assets' folder from the zip.")
|
||||
for script, _extra, _produces in EMIT_STEPS:
|
||||
if not (HERE / script).is_file():
|
||||
die(f"missing {HERE / script}")
|
||||
if not (HERE / "d88_propdefs.py").is_file():
|
||||
die(f"missing {HERE / 'd88_propdefs.py'} (imported by d88_emit.py)")
|
||||
|
||||
bundle_root = Path(args.out).expanduser().resolve() if args.out else HERE.parent
|
||||
|
||||
rom = find_rom(args.rom, bundle_root)
|
||||
print(f"prepare-assets: ROM {rom}")
|
||||
digest = sha1_of(rom)
|
||||
region = ROM_SHA1.get(digest)
|
||||
if region is None:
|
||||
die(f"ROM SHA-1 {digest} is not a recognised retail GoldenEye 007 image.\n"
|
||||
" Supported: NTSC-U / PAL / NTSC-J, big-endian .z64.\n"
|
||||
" A byte-swapped (.n64/.v64) or overdumped ROM will not work -- convert to .z64 first.")
|
||||
print(f"prepare-assets: SHA-1 {digest} -> region '{region}'")
|
||||
|
||||
# The emit scripts assume CWD is a tree with scripts/ assets/ data/ .
|
||||
work = VENDOR
|
||||
(work / "data").mkdir(exist_ok=True)
|
||||
staged_rom = work / "data" / f"ge007.{region}.z64"
|
||||
if not (staged_rom.exists() and staged_rom.stat().st_size == rom.stat().st_size):
|
||||
shutil.copy2(rom, staged_rom)
|
||||
|
||||
# Fresh start so a re-run is deterministic (d69 recreates its dir; d88
|
||||
# --regen edits it in place).
|
||||
for d in OUTPUT_DIRS:
|
||||
p = work / "data" / d.format(region=region)
|
||||
if p.exists():
|
||||
shutil.rmtree(p)
|
||||
|
||||
for script, extra, produces in EMIT_STEPS:
|
||||
argv = [sys.executable, str(HERE / script), region, *extra]
|
||||
print(f"prepare-assets: running {' '.join(argv[1:])} ...")
|
||||
r = subprocess.run(argv, cwd=work)
|
||||
if r.returncode != 0:
|
||||
die(f"{script} exited {r.returncode}")
|
||||
want = work / "data" / produces.format(region=region)
|
||||
if not want.is_file():
|
||||
die(f"{script} did not produce {want}")
|
||||
|
||||
dest_data = bundle_root / "data"
|
||||
dest_data.mkdir(parents=True, exist_ok=True)
|
||||
for d in OUTPUT_DIRS:
|
||||
src = work / "data" / d.format(region=region)
|
||||
dest = dest_data / d.format(region=region)
|
||||
if dest.resolve() == src.resolve():
|
||||
continue
|
||||
if dest.exists():
|
||||
shutil.rmtree(dest)
|
||||
shutil.move(str(src), str(dest))
|
||||
n = sum(1 for _ in dest.rglob("*") if _.is_file())
|
||||
print(f"prepare-assets: wrote {dest} ({n} files)")
|
||||
|
||||
if not args.keep_temp and staged_rom.exists() and staged_rom.resolve() != rom.resolve():
|
||||
staged_rom.unlink()
|
||||
|
||||
print("\nprepare-assets: done. You can now run ge007.x86_64.exe from the bundle folder.")
|
||||
print(f" (Your ROM must also be at {dest_data / f'ge007.{region}.z64'} for the game itself.)")
|
||||
if region != "ntsc-final":
|
||||
print(" NOTE: only the NTSC-U ROM is validated in this alpha; "
|
||||
f"'{region}' is experimental.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user